@ -509,6 +509,72 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
// Duration column needs more that that
colBody . dtxs = '4' ;
}
if ( [ UITypes . SingleSelect , UITypes . MultiSelect ] . includes ( colBody . uidt ) ) {
const dbDriver = NcConnectionMgrv2 . get ( base ) ;
const driverType = dbDriver . clientType ( ) ;
const optionTitles = colBody . colOptions . options . map ( el = > el . title ) ;
// Handle default values
if ( colBody . cdf ) {
if ( colBody . uidt === UITypes . SingleSelect ) {
if ( ! optionTitles . includes ( colBody . cdf ) ) {
NcError . badRequest ( ` Default value ' ${ colBody . cdf } ' is not a select option. ` ) ;
}
} else {
for ( const cdf of colBody . cdf . split ( ',' ) ) {
if ( ! optionTitles . includes ( cdf ) ) {
NcError . badRequest ( ` Default value ' ${ cdf } ' is not a select option. ` ) ;
}
}
}
if ( driverType === 'pg' ) {
colBody . cdf = ` ' ${ colBody . cdf } ' ` ;
}
}
// Restrict duplicates
const titles = colBody . colOptions . options . map ( el = > el . title )
if ( titles
. some ( function ( item ) {
return titles . indexOf ( item ) !== titles . lastIndexOf ( item ) ;
} )
) {
NcError . badRequest ( 'Duplicates are not allowed!' ) ;
}
// Restrict empty options
if ( titles
. some ( function ( item ) {
return item === '' ;
} )
) {
NcError . badRequest ( 'Empty options are not allowed!' ) ;
}
// Handle empty enum/set for mysql (we restrict empty user options beforehand)
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
if ( ! colBody . colOptions . options . length && ( ! colBody . dtxp || colBody . dtxp === '' ) ) {
colBody . colOptions . options . push ( { title : '' } ) ;
}
}
if ( colBody . uidt === UITypes . SingleSelect ) {
colBody . dtxp = ( colBody . colOptions ? . options . length )
? ` ${ colBody . colOptions . options . map ( o = > ` ' ${ o . title . replace ( /'/gi , '\'\'' ) } ' ` ) . join ( ',' ) } `
: '' ;
} else if ( colBody . uidt === UITypes . MultiSelect ) {
colBody . dtxp = ( colBody . colOptions ? . options . length )
? ` ${ colBody . colOptions . options . map ( ( o ) = > {
if ( o . title . includes ( ',' ) ) {
NcError . badRequest ( 'Illegal char(\',\') for MultiSelect' ) ;
}
return ` ' ${ o . title . replace ( /'/gi , '\'\'' ) } ' ` ;
} ) . join ( ',' ) } `
: '' ;
}
}
const tableUpdateBody = {
. . . table ,
tn : table.table_name ,
@ -540,13 +606,6 @@ export async function columnAdd(req: Request, res: Response<TableType>) {
const insertedColumnMeta =
columns . find ( ( c ) = > c . cn === colBody . column_name ) || ( { } as any ) ;
if (
colBody . uidt === UITypes . SingleSelect ||
colBody . uidt === UITypes . MultiSelect
) {
insertedColumnMeta . dtxp = colBody . dtxp ;
}
await Column . insert ( {
. . . colBody ,
. . . insertedColumnMeta ,
@ -653,6 +712,299 @@ export async function columnUpdate(req: Request, res: Response<TableType>) {
NcError . notImplemented (
` Updating ${ colBody . uidt } => ${ colBody . uidt } is not implemented `
) ;
} else if (
[
UITypes . SingleSelect ,
UITypes . MultiSelect
] . includes ( colBody . uidt )
) {
colBody = getColumnPropsFromUIDT ( colBody , base ) ;
if ( colBody . uidt === UITypes . SingleSelect ) {
colBody . dtxp = ( colBody . colOptions ? . options . length )
? ` ${ colBody . colOptions . options . map ( o = > ` ' ${ o . title . replace ( /'/gi , '\'\'' ) } ' ` ) . join ( ',' ) } `
: '' ;
} else if ( colBody . uidt === UITypes . MultiSelect ) {
colBody . dtxp = ( colBody . colOptions ? . options . length )
? ` ${ colBody . colOptions . options . map ( ( o ) = > {
if ( o . title . includes ( ',' ) ) {
NcError . badRequest ( 'Illegal char(\',\') for MultiSelect' ) ;
}
return ` ' ${ o . title . replace ( /'/gi , '\'\'' ) } ' ` ;
} ) . join ( ',' ) } `
: '' ;
}
const baseModel = await Model . getBaseModelSQL ( {
id : table.id ,
dbDriver : NcConnectionMgrv2.get ( base )
} ) ;
if ( column . colOptions ? . options ) {
const supportedDrivers = [ 'mysql' , 'mysql2' , 'pg' , 'mssql' , 'sqlite3' ] ;
const dbDriver = NcConnectionMgrv2 . get ( base ) ;
const driverType = dbDriver . clientType ( ) ;
// MultiSelect to SingleSelect
if ( column . uidt === UITypes . MultiSelect && colBody . uidt === UITypes . SingleSelect ) {
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = SUBSTRING_INDEX(??, ',', 1) WHERE ?? LIKE '%,%'; ` , [ table . table_name , column . title , column . title , column . title ] ) ;
} else if ( driverType === 'pg' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = split_part(??, ',', 1); ` , [ table . table_name , column . title , column . title ] ) ;
} else if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = LEFT(cast(?? as varchar(max)), CHARINDEX(',', ??) - 1) WHERE CHARINDEX(',', ??) > 0; ` , [ table . table_name , column . title , column . title , column . title , column . title ] ) ;
} else if ( driverType === 'sqlite3' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = substr(??, 1, instr(??, ',') - 1) WHERE ?? LIKE '%,%'; ` , [ table . table_name , column . title , column . title , column . title , column . title ] ) ;
}
}
// Handle migrations
for ( const op of column . colOptions . options . filter ( el = > el . order === null ) ) {
op . title = op . title . replace ( /^'/ , '' ) . replace ( /'$/ , '' )
}
// Handle default values
if ( colBody . cdf ) {
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
} else if ( driverType === 'pg' ) {
} else if ( driverType === 'mssql' ) {
} else if ( driverType === 'sqlite3' ) {
}
}
// Restrict duplicates
const titles = colBody . colOptions . options . map ( el = > el . title )
if ( titles
. some ( function ( item ) {
return titles . indexOf ( item ) !== titles . lastIndexOf ( item ) ;
} )
) {
NcError . badRequest ( 'Duplicates are not allowed!' ) ;
}
// Restrict empty options
if ( titles
. some ( function ( item ) {
return item === '' ;
} )
) {
NcError . badRequest ( 'Empty options are not allowed!' ) ;
}
// Handle empty enum/set for mysql (we restrict empty user options beforehand)
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
if ( ! colBody . colOptions . options . length && ( ! colBody . dtxp || colBody . dtxp === '' ) ) {
colBody . dtxp = '\'\'' ;
}
}
// Handle option delete
for ( const option of column . colOptions . options . filter ( oldOp = > colBody . colOptions . options . find ( newOp = > newOp . id === oldOp . id ) ? false : true ) ) {
if ( ! supportedDrivers . includes ( driverType ) && column . uidt === UITypes . MultiSelect ) {
NcError . badRequest ( 'Your database not yet supported for this operation. Please remove option from records manually before dropping.' ) ;
}
if ( column . uidt === UITypes . SingleSelect ) {
if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = NULL WHERE ?? LIKE ? ` , [ table . table_name , column . title , column . title , option . title ] ) ;
} else {
await baseModel . bulkUpdateAll ( { where : ` ( ${ column . title } ,eq, ${ option . title } ) ` } , { [ column . title ] : null } ) ;
}
} else if ( column . uidt === UITypes . MultiSelect ) {
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), ',')) WHERE FIND_IN_SET(?, ??) ` , [ table . table_name , column . title , column . title , option . title , option . title , column . title ] ) ;
} else if ( driverType === 'pg' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = array_to_string(array_remove(string_to_array(??, ','), ?), ',') ` , [ table . table_name , column . title , column . title , option . title ] ) ;
} else if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), ','), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), ',')) - 2) ` , [ table . table_name , column . title , column . title , option . title , column . title , option . title ] ) ;
} else if ( driverType === 'sqlite3' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ','), ',') ` , [ table . table_name , column . title , column . title , option . title ] ) ;
}
}
}
let interchange = [ ] ;
// Handle option update
const old_titles = column . colOptions . options . map ( el = > el . title ) ;
for ( const option of column . colOptions . options . filter ( oldOp = > colBody . colOptions . options . find ( newOp = > newOp . id === oldOp . id && newOp . title !== oldOp . title ) ) ) {
if ( ! supportedDrivers . includes ( driverType ) && column . uidt === UITypes . MultiSelect ) {
NcError . badRequest ( 'Your database not yet supported for this operation. Please remove option from records manually before updating.' ) ;
}
let newOp = { . . . colBody . colOptions . options . find ( el = > option . id === el . id ) } ;
if ( old_titles . includes ( newOp . title ) ) {
let def_option = { . . . newOp } ;
let title_counter = 1 ;
while ( old_titles . includes ( newOp . title ) ) {
newOp . title = ` ${ def_option . title } _ ${ title_counter ++ } ` ;
}
interchange . push ( {
def_option ,
temp_title : newOp.title
} ) ;
}
// Append new option before editing
if ( ( driverType === 'mysql' || driverType === 'mysql2' ) && ( column . dt === 'enum' || column . dt === 'set' ) ) {
column . colOptions . options . push ( { title : newOp.title } ) ;
let temp_dtxp = '' ;
if ( column . uidt === UITypes . SingleSelect ) {
temp_dtxp = ( column . colOptions ? . options . length )
? ` ${ column . colOptions . options . map ( o = > ` ' ${ o . title . replace ( /'/gi , '\'\'' ) } ' ` ) . join ( ',' ) } `
: '' ;
} else if ( column . uidt === UITypes . MultiSelect ) {
temp_dtxp = ( column . colOptions ? . options . length )
? ` ${ column . colOptions . options . map ( ( o ) = > {
if ( o . title . includes ( ',' ) ) {
NcError . badRequest ( 'Illegal char(\',\') for MultiSelect' ) ;
throw new Error ( '' ) ;
}
return ` ' ${ o . title . replace ( /'/gi , '\'\'' ) } ' ` ;
} ) . join ( ',' ) } `
: '' ;
}
const tableUpdateBody = {
. . . table ,
tn : table.table_name ,
originalColumns : table.columns.map ( ( c ) = > ( {
. . . c ,
cn : c.column_name ,
cno : c.column_name ,
} ) ) ,
columns : await Promise . all (
table . columns . map ( async ( c ) = > {
if ( c . id === req . params . columnId ) {
const res = {
. . . c ,
. . . column ,
cn : column.column_name ,
cno : c.column_name ,
dtxp : temp_dtxp ,
altered : Altered.UPDATE_COLUMN ,
} ;
return Promise . resolve ( res ) ;
} else {
( c as any ) . cn = c . column_name ;
}
return Promise . resolve ( c ) ;
} )
) ,
} ;
const sqlMgr = await ProjectMgrv2 . getSqlMgr ( { id : base.project_id } ) ;
await sqlMgr . sqlOpPlus ( base , 'tableUpdate' , tableUpdateBody ) ;
await Column . update ( req . params . columnId , {
. . . column ,
} ) ;
}
if ( column . uidt === UITypes . SingleSelect ) {
if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = ? WHERE ?? LIKE ? ` , [ table . table_name , column . title , newOp . title , column . title , option . title ] ) ;
} else {
await baseModel . bulkUpdateAll ( { where : ` ( ${ column . title } ,eq, ${ option . title } ) ` } , { [ column . title ] : newOp . title } ) ;
}
} else if ( column . uidt === UITypes . MultiSelect ) {
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??) ` , [ table . table_name , column . title , column . title , option . title , newOp . title , option . title , column . title ] ) ;
} else if ( driverType === 'pg' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',') ` , [ table . table_name , column . title , column . title , option . title , newOp . title ] ) ;
} else if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2) ` , [ table . table_name , column . title , column . title , option . title , newOp . title , column . title , option . title , newOp . title ] ) ;
} else if ( driverType === 'sqlite3' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',') ` , [ table . table_name , column . title , column . title , option . title , newOp . title ] ) ;
}
}
}
for ( const ch of interchange ) {
let newOp = ch . def_option ;
if ( column . uidt === UITypes . SingleSelect ) {
if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = ? WHERE ?? LIKE ? ` , [ table . table_name , column . title , newOp . title , column . title , ch . temp_title ] ) ;
} else {
await baseModel . bulkUpdateAll ( { where : ` ( ${ column . title } ,eq, ${ ch . temp_title } ) ` } , { [ column . title ] : newOp . title } ) ;
}
} else if ( column . uidt === UITypes . MultiSelect ) {
if ( driverType === 'mysql' || driverType === 'mysql2' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = TRIM(BOTH ',' FROM REPLACE(CONCAT(',', ??, ','), CONCAT(',', ?, ','), CONCAT(',', ?, ','))) WHERE FIND_IN_SET(?, ??) ` , [ table . table_name , column . title , column . title , ch . temp_title , newOp . title , ch . temp_title , column . title ] ) ;
} else if ( driverType === 'pg' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = array_to_string(array_replace(string_to_array(??, ','), ?, ?), ',') ` , [ table . table_name , column . title , column . title , ch . temp_title , newOp . title ] ) ;
} else if ( driverType === 'mssql' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = substring(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ',')), 2, len(replace(concat(',', ??, ','), concat(',', ?, ','), concat(',', ?, ','))) - 2) ` , [ table . table_name , column . title , column . title , ch . temp_title , newOp . title , column . title , ch . temp_title , newOp . title ] ) ;
} else if ( driverType === 'sqlite3' ) {
await dbDriver . raw ( ` UPDATE ?? SET ?? = TRIM(REPLACE(',' || ?? || ',', ',' || ? || ',', ',' || ? || ','), ',') ` , [ table . table_name , column . title , column . title , ch . temp_title , newOp . title ] ) ;
}
}
}
}
const tableUpdateBody = {
. . . table ,
tn : table.table_name ,
originalColumns : table.columns.map ( ( c ) = > ( {
. . . c ,
cn : c.column_name ,
cno : c.column_name ,
} ) ) ,
columns : await Promise . all (
table . columns . map ( async ( c ) = > {
if ( c . id === req . params . columnId ) {
const res = {
. . . c ,
. . . colBody ,
cn : colBody.column_name ,
cno : c.column_name ,
altered : Altered.UPDATE_COLUMN ,
} ;
// update formula with new column name
if ( c . column_name != colBody . column_name ) {
const formulas = await Noco . ncMeta
. knex ( MetaTable . COL_FORMULA )
. where ( 'formula' , 'like' , ` % ${ c . id } % ` ) ;
if ( formulas ) {
const new_column = c ;
new_column . column_name = colBody . column_name ;
new_column . title = colBody . title ;
for ( const f of formulas ) {
// the formula with column IDs only
const formula = f . formula ;
// replace column IDs with alias to get the new formula_raw
const new_formula_raw = substituteColumnIdWithAliasInFormula (
formula ,
[ new_column ]
) ;
await FormulaColumn . update ( c . id , {
formula_raw : new_formula_raw ,
} ) ;
}
}
}
return Promise . resolve ( res ) ;
} else {
( c as any ) . cn = c . column_name ;
}
return Promise . resolve ( c ) ;
} )
) ,
} ;
const sqlMgr = await ProjectMgrv2 . getSqlMgr ( { id : base.project_id } ) ;
await sqlMgr . sqlOpPlus ( base , 'tableUpdate' , tableUpdateBody ) ;
await Column . update ( req . params . columnId , {
. . . colBody ,
} ) ;
} else {
colBody = getColumnPropsFromUIDT ( colBody , base ) ;
const tableUpdateBody = {