Browse Source

Merge pull request #3117 from nocodb/fix/gui-v2-table-rename

fix(gui-v2): table rename
pull/3298/head
աɨռɢӄաօռɢ 2 years ago committed by GitHub
parent
commit
344a05eece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  2. 51
      packages/nc-gui-v2/components/dlg/TableRename.vue
  3. 2
      packages/nc-gui-v2/composables/useProject.ts
  4. 15
      packages/nc-gui-v2/composables/useTable.ts
  5. 3
      packages/nc-gui/components/ProjectTreeView.vue
  6. 5
      packages/nocodb-sdk/src/lib/Api.ts
  7. 81
      packages/nocodb/src/lib/meta/api/tableApis.ts
  8. 16
      packages/nocodb/src/lib/models/Model.ts
  9. 6
      scripts/cypress/integration/common/1a_table_operations.js
  10. 8
      scripts/sdk/swagger.json

26
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -19,7 +19,7 @@ const inputEl = ref<HTMLInputElement>()
const { addTab } = useTabs() const { addTab } = useTabs()
const { loadTables } = useProject() const { loadTables, isMysql, isMssql, isPg } = useProject()
const { table, createTable, generateUniqueTitle, tables, project } = useTable(async (table) => { const { table, createTable, generateUniqueTitle, tables, project } = useTable(async (table) => {
await loadTables() await loadTables()
@ -39,7 +39,29 @@ const validateDuplicateAlias = (v: string) => (tables.value || []).every((t) =>
const validators = computed(() => { const validators = computed(() => {
return { return {
title: [validateTableName, validateDuplicateAlias], title: [
validateTableName,
validateDuplicateAlias,
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql) {
tableNameLengthLimit = 64
} else if (isPg) {
tableNameLengthLimit = 63
} else if (isMssql) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
}
resolve()
})
},
},
],
table_name: [validateTableName], table_name: [validateTableName],
} }
}) })

51
packages/nc-gui-v2/components/dlg/TableRename.vue

@ -24,8 +24,7 @@ const dialogShow = computed({
}) })
const { updateTab } = useTabs() const { updateTab } = useTabs()
const { loadTables } = useProject() const { loadTables, tables, project, isMysql, isMssql, isPg } = useProject()
const { tables } = useProject()
const inputEl = $ref<any>() const inputEl = $ref<any>()
let loading = $ref(false) let loading = $ref(false)
@ -38,18 +37,39 @@ const validators = computed(() => {
title: [ title: [
validateTableName, validateTableName,
{ {
validator: (rule: any, value: any, callback: (errMsg?: string) => void) => { validator: (rule: any, value: any) => {
if (/^\s+|\s+$/.test(value)) { return new Promise<void>((resolve, reject) => {
callback('Leading or trailing whitespace not allowed in table name') let tableNameLengthLimit = 255
} if (isMysql) {
if ( tableNameLengthLimit = 64
!(tables?.value || []).every( } else if (isPg) {
(t) => t.id === tableMeta.id || t.table_name.toLowerCase() !== (value || '').toLowerCase(), tableNameLengthLimit = 63
) } else if (isMssql) {
) { tableNameLengthLimit = 128
callback('Duplicate table alias') }
} const projectPrefix = project?.value?.prefix || ''
callback() if ((projectPrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
}
resolve()
})
},
},
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (/^\s+|\s+$/.test(value)) {
return reject(new Error('Leading or trailing whitespace not allowed in table name'))
}
if (
!(tables?.value || []).every(
(t) => t.id === tableMeta.id || t.table_name.toLowerCase() !== (value || '').toLowerCase(),
)
) {
return reject(new Error('Duplicate table alias'))
}
resolve()
})
}, },
}, },
], ],
@ -71,7 +91,8 @@ const renameTable = async () => {
loading = true loading = true
try { try {
await $api.dbTable.update(tableMeta?.id as string, { await $api.dbTable.update(tableMeta?.id as string, {
title: formState.title, project_id: tableMeta?.project_id,
table_name: formState.title,
}) })
dialogShow.value = false dialogShow.value = false
loadTables() loadTables()

2
packages/nc-gui-v2/composables/useProject.ts

@ -20,6 +20,7 @@ export function useProject(projectId?: MaybeRef<string>) {
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '') const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType)) const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg') const isPg = computed(() => projectBaseType === 'pg')
const sqlUi = computed( const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>, () => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
@ -82,6 +83,7 @@ export function useProject(projectId?: MaybeRef<string>) {
loadProject, loadProject,
loadTables, loadTables,
isMysql, isMysql,
isMssql,
isPg, isPg,
sqlUi, sqlUi,
isSharedBase, isSharedBase,

15
packages/nc-gui-v2/composables/useTable.ts

@ -30,12 +30,15 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
return table.columns.includes(col.column_name) return table.columns.includes(col.column_name)
}) })
const tableMeta = await $api.dbTable.create(project?.value?.id as string, { try {
...table, const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
columns, ...table,
}) columns,
})
onTableCreate?.(tableMeta) onTableCreate?.(tableMeta)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
} }
watch( watch(

3
packages/nc-gui/components/ProjectTreeView.vue

@ -1431,7 +1431,8 @@ export default {
let item = cookie; let item = cookie;
try { try {
await this.$api.dbTable.update(item.id, { await this.$api.dbTable.update(item.id, {
title, project_id: this.projectId,
table_name: title,
}); });
} catch (e) { } catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000); this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000);

5
packages/nocodb-sdk/src/lib/Api.ts

@ -121,6 +121,7 @@ export interface TableType {
columns?: ColumnType[]; columns?: ColumnType[];
columnsById?: object; columnsById?: object;
slug?: string; slug?: string;
project_id?: string;
} }
export interface ViewType { export interface ViewType {
@ -169,7 +170,7 @@ export interface TableReqType {
deleted?: boolean; deleted?: boolean;
order?: number; order?: number;
mm?: boolean; mm?: boolean;
columns?: ColumnType[]; columns: ColumnType[];
} }
export interface TableListType { export interface TableListType {
@ -1487,7 +1488,7 @@ export class Api<
*/ */
update: ( update: (
tableId: string, tableId: string,
data: { title?: string }, data: { table_name?: string; project_id?: string },
params: RequestParams = {} params: RequestParams = {}
) => ) =>
this.request<any, any>({ this.request<any, any>({

81
packages/nocodb/src/lib/meta/api/tableApis.ts

@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express';
import Model from '../../models/Model'; import Model from '../../models/Model';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { Tele } from 'nc-help'; import { Tele } from 'nc-help';
import DOMPurify from 'isomorphic-dompurify';
import { import {
AuditOperationSubTypes, AuditOperationSubTypes,
AuditOperationTypes, AuditOperationTypes,
@ -102,6 +103,8 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
} }
} }
req.body.table_name = DOMPurify.sanitize(req.body.table_name);
// validate table name // validate table name
if (/^\s+|\s+$/.test(req.body.table_name)) { if (/^\s+|\s+$/.test(req.body.table_name)) {
NcError.badRequest( NcError.badRequest(
@ -217,18 +220,86 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
export async function tableUpdate(req: Request<any, any>, res) { export async function tableUpdate(req: Request<any, any>, res) {
const model = await Model.get(req.params.tableId); const model = await Model.get(req.params.tableId);
const project = await Project.getWithInfo(req.body.project_id);
const base = project.bases[0];
if (!req.body.table_name) {
NcError.badRequest(
'Missing table name `table_name` property in request body'
);
}
if (project.prefix) {
if (!req.body.table_name.startsWith(project.prefix)) {
req.body.table_name = `${project.prefix}${req.body.table_name}`;
}
}
req.body.table_name = DOMPurify.sanitize(req.body.table_name);
// validate table name
if (/^\s+|\s+$/.test(req.body.table_name)) {
NcError.badRequest(
'Leading or trailing whitespace not allowed in table names'
);
}
if (
!(await Model.checkTitleAvailable({
table_name: req.body.table_name,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table name');
}
if (!req.body.title) {
req.body.title = getTableNameAlias(
req.body.table_name,
project.prefix,
base
);
}
if ( if (
!(await Model.checkAliasAvailable({ !(await Model.checkAliasAvailable({
title: req.body.title, title: req.body.title,
project_id: model.project_id, project_id: project.id,
base_id: model.base_id, base_id: base.id,
exclude_id: req.params.tableId,
})) }))
) { ) {
NcError.badRequest('Duplicate table name'); NcError.badRequest('Duplicate table alias');
} }
await Model.updateAlias(req.params.tableId, req.body.title); const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.clientType;
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
if (req.body.table_name.length > tableNameLengthLimit) {
NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`);
}
await Model.updateAliasAndTableName(
req.params.tableId,
req.body.title,
req.body.table_name
);
await sqlMgr.sqlOpPlus(base, 'tableRename', {
...req.body,
tn: req.body.table_name,
tn_old: model.table_name,
});
Tele.emit('evt', { evt_type: 'table:updated' }); Tele.emit('evt', { evt_type: 'table:updated' });

16
packages/nocodb/src/lib/models/Model.ts

@ -412,14 +412,25 @@ export default class Model implements TableType {
return insertObj; return insertObj;
} }
static async updateAlias(tableId, title: string, ncMeta = Noco.ncMeta) { static async updateAliasAndTableName(
if (!title) NcError.badRequest("Missing 'title' property in body"); tableId,
title: string,
table_name: string,
ncMeta = Noco.ncMeta
) {
if (!title) {
NcError.badRequest("Missing 'title' property in body");
}
if (!table_name) {
NcError.badRequest("Missing 'table_name' property in body");
}
// get existing cache // get existing cache
const key = `${CacheScope.MODEL}:${tableId}`; const key = `${CacheScope.MODEL}:${tableId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
// update alias // update alias
if (o) { if (o) {
o.title = title; o.title = title;
o.table_name = table_name;
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o);
} }
@ -430,6 +441,7 @@ export default class Model implements TableType {
MetaTable.MODELS, MetaTable.MODELS,
{ {
title, title,
table_name,
}, },
tableId tableId
); );

6
scripts/cypress/integration/common/1a_table_operations.js

@ -76,6 +76,9 @@ export const genTest = (apiType, dbType) => {
cy.closeTableTab("CityX"); cy.closeTableTab("CityX");
// revert re-name operation to not impact rest of test suite
cy.renameTable("CityX", "City");
// 4. verify linked contents in other table // 4. verify linked contents in other table
// 4a. Address table, has many field // 4a. Address table, has many field
cy.openTableTab("Address", 25); cy.openTableTab("Address", 25);
@ -97,9 +100,6 @@ export const genTest = (apiType, dbType) => {
.contains("Kabul") .contains("Kabul")
.should("exist"); .should("exist");
cy.closeTableTab("Country"); cy.closeTableTab("Country");
// revert re-name operation to not impact rest of test suite
cy.renameTable("CityX", "City");
}); });
}); });
}; };

8
scripts/sdk/swagger.json

@ -1309,7 +1309,10 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"title": { "table_name": {
"type": "string"
},
"project_id": {
"type": "string" "type": "string"
} }
} }
@ -6050,6 +6053,9 @@
}, },
"slug": { "slug": {
"type": "string" "type": "string"
},
"project_id": {
"type": "string"
} }
}, },
"required": [ "required": [

Loading…
Cancel
Save