mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
committed by
GitHub
25 changed files with 645 additions and 1216 deletions
@ -1,43 +1,359 @@ |
|||||||
import { OrgUserRoles } from 'nocodb-sdk'; |
import { UITypes } from 'nocodb-sdk'; |
||||||
import { NC_APP_SETTINGS } from '../constants'; |
|
||||||
import Store from '../models/Store'; |
|
||||||
import { MetaTable } from '../utils/globals'; |
import { MetaTable } from '../utils/globals'; |
||||||
|
import Column from '../models/Column'; |
||||||
|
import Filter from '../models/Filter'; |
||||||
|
import Project from '../models/Project'; |
||||||
|
import type { MetaService } from '../meta/meta.service'; |
||||||
import type { NcUpgraderCtx } from './NcUpgrader'; |
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
import type { SelectOptionsType } from 'nocodb-sdk'; |
||||||
|
|
||||||
/** Upgrader for upgrading roles */ |
// as of 0.104.3, almost all filter operators are available to all column types
|
||||||
export default async function ({ ncMeta }: NcUpgraderCtx) { |
// while some of them aren't supposed to be shown
|
||||||
const users = await ncMeta.metaList2(null, null, MetaTable.USERS); |
// this upgrader is to remove those unsupported filters / migrate to the correct filter
|
||||||
|
|
||||||
for (const user of users) { |
// Change Summary:
|
||||||
user.roles = user.roles |
// - Text-based columns:
|
||||||
.split(',') |
// - remove `>`, `<`, `>=`, `<=`
|
||||||
.map((r) => { |
// - Numeric-based / SingleSelect columns:
|
||||||
// update old role names with new roles
|
// - remove `like`
|
||||||
if (r === 'user') { |
// - migrate `null`, and `empty` to `blank`
|
||||||
return OrgUserRoles.CREATOR; |
// - Checkbox columns:
|
||||||
} else if (r === 'user-new') { |
// - remove `equal`
|
||||||
return OrgUserRoles.VIEWER; |
// - migrate `empty` and `null` to `notchecked`
|
||||||
} |
// - MultiSelect columns:
|
||||||
return r; |
// - remove `like`
|
||||||
}) |
// - migrate `equal`, `null`, `empty`
|
||||||
.join(','); |
// - Attachment columns:
|
||||||
await ncMeta.metaUpdate( |
// - remove `>`, `<`, `>=`, `<=`, `equal`
|
||||||
null, |
// - migrate `empty`, `null` to `blank`
|
||||||
null, |
// - LTAR columns:
|
||||||
MetaTable.USERS, |
// - remove `>`, `<`, `>=`, `<=`
|
||||||
{ roles: user.roles }, |
// - migrate `empty`, `null` to `blank`
|
||||||
user.id, |
// - Lookup columns:
|
||||||
|
// - migrate `empty`, `null` to `blank`
|
||||||
|
// - Duration columns:
|
||||||
|
// - remove `like`
|
||||||
|
// - migrate `empty`, `null` to `blank`
|
||||||
|
|
||||||
|
const removeEqualFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
// remove `is equal`, `is not equal`
|
||||||
|
if (['eq', 'neq'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const removeArithmeticFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
// remove `>`, `<`, `>=`, `<=`
|
||||||
|
if (['gt', 'lt', 'gte', 'lte'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const removeLikeFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
// remove `is like`, `is not like`
|
||||||
|
if (['like', 'nlike'].includes(filter.comparison_op)) { |
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const migrateNullAndEmptyToBlankFilters = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||||
|
// migrate to blank
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'blank', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||||
|
// migrate to not blank
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notblank', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
const migrateMultiSelectEq = async (filter, col: Column, ncMeta) => { |
||||||
|
// only allow eq / neq
|
||||||
|
if (!['eq', 'neq'].includes(filter.comparison_op)) return; |
||||||
|
// if there is no value -> delete this filter
|
||||||
|
if (!filter.value) { |
||||||
|
return await Filter.delete(filter.id, ncMeta); |
||||||
|
} |
||||||
|
// options inputted from users
|
||||||
|
const options = filter.value.split(','); |
||||||
|
// retrieve the possible col options
|
||||||
|
const colOptions = (await col.getColOptions()) as SelectOptionsType; |
||||||
|
// only include valid options as the input value becomes dropdown type now
|
||||||
|
const validOptions = []; |
||||||
|
for (const option of options) { |
||||||
|
if (colOptions.options.includes(option)) { |
||||||
|
validOptions.push(option); |
||||||
|
} |
||||||
|
} |
||||||
|
const newFilterValue = validOptions.join(','); |
||||||
|
// if all inputted options are invalid -> delete this filter
|
||||||
|
if (!newFilterValue) { |
||||||
|
return await Filter.delete(filter.id, ncMeta); |
||||||
|
} |
||||||
|
const actions = []; |
||||||
|
if (filter.comparison_op === 'eq') { |
||||||
|
// migrate to `contains all of`
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'anyof', |
||||||
|
value: newFilterValue, |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else if (filter.comparison_op === 'neq') { |
||||||
|
// migrate to `doesn't contain all of`
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'nanyof', |
||||||
|
value: newFilterValue, |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
return await Promise.all(actions); |
||||||
|
}; |
||||||
|
|
||||||
|
const migrateToCheckboxFilter = (filter, ncMeta) => { |
||||||
|
const actions = []; |
||||||
|
const possibleTrueValues = ['true', 'True', '1', 'T', 'Y']; |
||||||
|
const possibleFalseValues = ['false', 'False', '0', 'F', 'N']; |
||||||
|
if (['empty', 'null'].includes(filter.comparison_op)) { |
||||||
|
// migrate to not checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notchecked', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else if (['notempty', 'notnull'].includes(filter.comparison_op)) { |
||||||
|
// migrate to checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'checked', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
); |
); |
||||||
|
} else if (filter.comparison_op === 'eq') { |
||||||
|
if (possibleTrueValues.includes(filter.value)) { |
||||||
|
// migrate to checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'checked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else if (possibleFalseValues.includes(filter.value)) { |
||||||
|
// migrate to notchecked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notchecked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else { |
||||||
|
// invalid value - good to delete
|
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
} else if (filter.comparison_op === 'neq') { |
||||||
|
if (possibleFalseValues.includes(filter.value)) { |
||||||
|
// migrate to checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'checked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else if (possibleTrueValues.includes(filter.value)) { |
||||||
|
// migrate to not checked
|
||||||
|
actions.push( |
||||||
|
Filter.update( |
||||||
|
filter.id, |
||||||
|
{ |
||||||
|
comparison_op: 'notchecked', |
||||||
|
value: '', |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
|
); |
||||||
|
} else { |
||||||
|
// invalid value - good to delete
|
||||||
|
actions.push(Filter.delete(filter.id, ncMeta)); |
||||||
|
} |
||||||
|
} |
||||||
|
return actions; |
||||||
|
}; |
||||||
|
|
||||||
|
async function migrateFilters(ncMeta: MetaService) { |
||||||
|
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||||
|
for (const filter of filters) { |
||||||
|
if (!filter.fk_column_id || filter.is_group) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
const col = await Column.get({ colId: filter.fk_column_id }, ncMeta); |
||||||
|
if ( |
||||||
|
[ |
||||||
|
UITypes.SingleLineText, |
||||||
|
UITypes.LongText, |
||||||
|
UITypes.PhoneNumber, |
||||||
|
UITypes.Email, |
||||||
|
UITypes.URL, |
||||||
|
].includes(col.uidt) |
||||||
|
) { |
||||||
|
await Promise.all(removeArithmeticFilters(filter, ncMeta)); |
||||||
|
} else if ( |
||||||
|
[ |
||||||
|
// numeric fields
|
||||||
|
UITypes.Duration, |
||||||
|
UITypes.Currency, |
||||||
|
UITypes.Percent, |
||||||
|
UITypes.Number, |
||||||
|
UITypes.Decimal, |
||||||
|
UITypes.Rating, |
||||||
|
UITypes.Rollup, |
||||||
|
// select fields
|
||||||
|
UITypes.SingleSelect, |
||||||
|
].includes(col.uidt) |
||||||
|
) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.Checkbox) { |
||||||
|
await Promise.all(migrateToCheckboxFilter(filter, ncMeta)); |
||||||
|
} else if (col.uidt === UITypes.MultiSelect) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
await migrateMultiSelectEq(filter, col, ncMeta); |
||||||
|
} else if (col.uidt === UITypes.Attachment) { |
||||||
|
await Promise.all([ |
||||||
|
...removeArithmeticFilters(filter, ncMeta), |
||||||
|
...removeEqualFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.LinkToAnotherRecord) { |
||||||
|
await Promise.all([ |
||||||
|
...removeArithmeticFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.Lookup) { |
||||||
|
await Promise.all([ |
||||||
|
...removeArithmeticFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} else if (col.uidt === UITypes.Duration) { |
||||||
|
await Promise.all([ |
||||||
|
...removeLikeFilters(filter, ncMeta), |
||||||
|
...migrateNullAndEmptyToBlankFilters(filter, ncMeta), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function updateProjectMeta(ncMeta: MetaService) { |
||||||
|
const projectHasEmptyOrFilters: Record<string, boolean> = {}; |
||||||
|
|
||||||
|
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
||||||
|
|
||||||
|
const actions = []; |
||||||
|
|
||||||
|
for (const filter of filters) { |
||||||
|
if ( |
||||||
|
['notempty', 'notnull', 'empty', 'null'].includes(filter.comparison_op) |
||||||
|
) { |
||||||
|
projectHasEmptyOrFilters[filter.project_id] = true; |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
// set invite only signup if user have environment variable set
|
const projects = await ncMeta.metaList2(null, null, MetaTable.PROJECT); |
||||||
if (process.env.NC_INVITE_ONLY_SIGNUP) { |
|
||||||
await Store.saveOrUpdate( |
const defaultProjectMeta = { |
||||||
{ |
showNullAndEmptyInFilter: false, |
||||||
value: '{ "invite_only_signup": true }', |
}; |
||||||
key: NC_APP_SETTINGS, |
|
||||||
}, |
for (const project of projects) { |
||||||
ncMeta, |
const oldProjectMeta = project.meta; |
||||||
|
let newProjectMeta = defaultProjectMeta; |
||||||
|
try { |
||||||
|
newProjectMeta = |
||||||
|
(typeof oldProjectMeta === 'string' |
||||||
|
? JSON.parse(oldProjectMeta) |
||||||
|
: oldProjectMeta) ?? defaultProjectMeta; |
||||||
|
} catch {} |
||||||
|
|
||||||
|
newProjectMeta = { |
||||||
|
...newProjectMeta, |
||||||
|
showNullAndEmptyInFilter: projectHasEmptyOrFilters[project.id] ?? false, |
||||||
|
}; |
||||||
|
|
||||||
|
actions.push( |
||||||
|
Project.update( |
||||||
|
project.id, |
||||||
|
{ |
||||||
|
meta: JSON.stringify(newProjectMeta), |
||||||
|
}, |
||||||
|
ncMeta, |
||||||
|
), |
||||||
); |
); |
||||||
} |
} |
||||||
|
await Promise.all(actions); |
||||||
|
} |
||||||
|
|
||||||
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
|
// fix the existing filter behaviours or
|
||||||
|
// migrate `null` or `empty` filters to `blank`
|
||||||
|
await migrateFilters(ncMeta); |
||||||
|
// enrich `showNullAndEmptyInFilter` in project meta
|
||||||
|
// if there is empty / null filters in existing projects,
|
||||||
|
// then set `showNullAndEmptyInFilter` to true
|
||||||
|
// else set to false
|
||||||
|
await updateProjectMeta(ncMeta); |
||||||
} |
} |
||||||
|
@ -1,164 +0,0 @@ |
|||||||
import { RelationTypes, UITypes } from 'nocodb-sdk'; |
|
||||||
import NocoCache from '../cache/NocoCache'; |
|
||||||
import { MetaTable } from '../meta/meta.service'; |
|
||||||
import { Base } from '../models'; |
|
||||||
import NcConnectionMgrv2 from '../utils/common/NcConnectionMgrv2'; |
|
||||||
import { CacheGetType, CacheScope } from '../utils/globals'; |
|
||||||
import { Model } from '../models'; |
|
||||||
import type { LinkToAnotherRecordColumn } from '../models'; |
|
||||||
import type { MetaService } from '../meta/meta.service'; |
|
||||||
import type { NcUpgraderCtx } from './NcUpgrader'; |
|
||||||
|
|
||||||
// An upgrader for upgrading LTAR relations in XCDB bases
|
|
||||||
// it will delete all the foreign keys and create a new index
|
|
||||||
// and treat all the LTAR as virtual
|
|
||||||
|
|
||||||
async function upgradeModelRelations({ |
|
||||||
model, |
|
||||||
relations, |
|
||||||
ncMeta, |
|
||||||
sqlClient, |
|
||||||
}: { |
|
||||||
ncMeta: MetaService; |
|
||||||
model: Model; |
|
||||||
sqlClient: ReturnType< |
|
||||||
(typeof NcConnectionMgrv2)['getSqlClient'] |
|
||||||
> extends Promise<infer U> |
|
||||||
? U |
|
||||||
: ReturnType<(typeof NcConnectionMgrv2)['getSqlClient']>; |
|
||||||
relations: { |
|
||||||
tn: string; |
|
||||||
rtn: string; |
|
||||||
cn: string; |
|
||||||
rcn: string; |
|
||||||
}[]; |
|
||||||
}) { |
|
||||||
// Iterate over each column and upgrade LTAR
|
|
||||||
for (const column of await model.getColumns(ncMeta)) { |
|
||||||
if (column.uidt !== UITypes.LinkToAnotherRecord) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
|
|
||||||
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>( |
|
||||||
ncMeta, |
|
||||||
); |
|
||||||
|
|
||||||
switch (colOptions.type) { |
|
||||||
case RelationTypes.HAS_MANY: |
|
||||||
{ |
|
||||||
// skip if virtual
|
|
||||||
if (colOptions.virtual) { |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
const parentCol = await colOptions.getParentColumn(ncMeta); |
|
||||||
const childCol = await colOptions.getChildColumn(ncMeta); |
|
||||||
|
|
||||||
const parentModel = await parentCol.getModel(ncMeta); |
|
||||||
const childModel = await childCol.getModel(ncMeta); |
|
||||||
|
|
||||||
// delete the foreign key constraint if exists
|
|
||||||
const relation = relations.find((r) => { |
|
||||||
return ( |
|
||||||
parentCol.column_name === r.rcn && |
|
||||||
childCol.column_name === r.cn && |
|
||||||
parentModel.table_name === r.rtn && |
|
||||||
childModel.table_name === r.tn |
|
||||||
); |
|
||||||
}); |
|
||||||
|
|
||||||
// delete the relation
|
|
||||||
if (relation) { |
|
||||||
await sqlClient.relationDelete({ |
|
||||||
parentColumn: relation.rcn, |
|
||||||
childColumn: relation.cn, |
|
||||||
parentTable: relation.rtn, |
|
||||||
childTable: relation.tn, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// skip postgres since we were already creating the index while creating the relation
|
|
||||||
if (ncMeta.knex.clientType() !== 'pg') { |
|
||||||
// create a new index for the column
|
|
||||||
const indexArgs = { |
|
||||||
columns: [relation.cn], |
|
||||||
tn: relation.tn, |
|
||||||
non_unique: true, |
|
||||||
}; |
|
||||||
await sqlClient.indexCreate(indexArgs); |
|
||||||
} |
|
||||||
} |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
// update the relation as virtual
|
|
||||||
await ncMeta.metaUpdate( |
|
||||||
null, |
|
||||||
null, |
|
||||||
MetaTable.COL_RELATIONS, |
|
||||||
{ virtual: true }, |
|
||||||
colOptions.id, |
|
||||||
); |
|
||||||
|
|
||||||
// update the cache as well
|
|
||||||
const cachedData = await NocoCache.get( |
|
||||||
`${CacheScope.COL_RELATION}:${colOptions.fk_column_id}`, |
|
||||||
CacheGetType.TYPE_OBJECT, |
|
||||||
); |
|
||||||
if (cachedData) { |
|
||||||
cachedData.virtual = true; |
|
||||||
await NocoCache.set( |
|
||||||
`${CacheScope.COL_RELATION}:${colOptions.fk_column_id}`, |
|
||||||
cachedData, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// An upgrader for upgrading any existing relation in xcdb
|
|
||||||
async function upgradeBaseRelations({ |
|
||||||
ncMeta, |
|
||||||
base, |
|
||||||
}: { |
|
||||||
ncMeta: MetaService; |
|
||||||
base: any; |
|
||||||
}) { |
|
||||||
// const sqlMgr = ProjectMgrv2.getSqlMgr({ id: base.project_id }, ncMeta);
|
|
||||||
|
|
||||||
const sqlClient = await NcConnectionMgrv2.getSqlClient(base, ncMeta.knex); |
|
||||||
|
|
||||||
// get all relations
|
|
||||||
const relations = (await sqlClient.relationListAll())?.data?.list; |
|
||||||
|
|
||||||
// get models for the base
|
|
||||||
const models = await ncMeta.metaList2(null, base.id, MetaTable.MODELS); |
|
||||||
|
|
||||||
// get all columns and filter out relations and upgrade
|
|
||||||
for (const model of models) { |
|
||||||
await upgradeModelRelations({ |
|
||||||
ncMeta, |
|
||||||
model: new Model(model), |
|
||||||
sqlClient, |
|
||||||
relations, |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// database to virtual relation and create an index for it
|
|
||||||
export default async function ({ ncMeta }: NcUpgraderCtx) { |
|
||||||
// get all xcdb bases
|
|
||||||
const bases = await ncMeta.metaList2(null, null, MetaTable.BASES, { |
|
||||||
condition: { |
|
||||||
is_meta: 1, |
|
||||||
}, |
|
||||||
orderBy: {}, |
|
||||||
}); |
|
||||||
|
|
||||||
// iterate and upgrade each base
|
|
||||||
for (const base of bases) { |
|
||||||
await upgradeBaseRelations({ |
|
||||||
ncMeta, |
|
||||||
base: new Base(base), |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
import { Api } from 'nocodb-sdk'; |
|
||||||
let api: Api; |
|
||||||
async function createXcdb(token?: string) { |
|
||||||
api = new Api({ |
|
||||||
baseURL: `http://localhost:8080/`, |
|
||||||
headers: { |
|
||||||
'xc-auth': token, |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
const projectList = await api.project.list(); |
|
||||||
for (const project of projectList.list) { |
|
||||||
// delete project with title 'xcdb' if it exists
|
|
||||||
if (project.title === 'xcdb') { |
|
||||||
await api.project.delete(project.id); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const project = await api.project.create({ title: 'xcdb' }); |
|
||||||
return project; |
|
||||||
} |
|
||||||
|
|
||||||
async function deleteXcdb(token?: string) { |
|
||||||
api = new Api({ |
|
||||||
baseURL: `http://localhost:8080/`, |
|
||||||
headers: { |
|
||||||
'xc-auth': token, |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
const projectList = await api.project.list(); |
|
||||||
for (const project of projectList.list) { |
|
||||||
// delete project with title 'xcdb' if it exists
|
|
||||||
if (project.title === 'xcdb') { |
|
||||||
await api.project.delete(project.id); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export { createXcdb, deleteXcdb }; |
|
@ -1,324 +0,0 @@ |
|||||||
/* |
|
||||||
* |
|
||||||
* Meta projects, additional provision for deleting of rows, columns and tables with Link to another record field type |
|
||||||
* |
|
||||||
* Pre-requisite: |
|
||||||
* TableA <hm> TableB <hm> TableC |
|
||||||
* TableA <mm> TableD <mm> TableE |
|
||||||
* TableA <hm> TableA : self relation |
|
||||||
* TableA <mm> TableA : self relation |
|
||||||
* Insert some records in TableA, TableB, TableC, TableD, TableE, add some links between them |
|
||||||
* |
|
||||||
* |
|
||||||
* Tests: |
|
||||||
* 1. Delete a row from TableA : Verify record status in adjacent tables |
|
||||||
* 2. Delete hm link from TableA to TableB : Verify record status in adjacent tables |
|
||||||
* 3. Delete mm link from TableA to TableD : Verify record status in adjacent tables |
|
||||||
* 4. Delete a self link column from TableA : Verify |
|
||||||
* 5. Delete TableA : Verify record status in adjacent tables |
|
||||||
* |
|
||||||
*/ |
|
||||||
|
|
||||||
import { test } from '@playwright/test'; |
|
||||||
import setup from '../../setup'; |
|
||||||
import { Api, UITypes } from 'nocodb-sdk'; |
|
||||||
import { DashboardPage } from '../../pages/Dashboard'; |
|
||||||
import { GridPage } from '../../pages/Dashboard/Grid'; |
|
||||||
import { createXcdb, deleteXcdb } from '../../setup/xcdbProject'; |
|
||||||
import { ProjectsPage } from '../../pages/ProjectsPage'; |
|
||||||
import { isSqlite } from '../../setup/db'; |
|
||||||
let api: Api<any>; |
|
||||||
const recordCount = 10; |
|
||||||
|
|
||||||
// serial as all projects end up creating xcdb using same name
|
|
||||||
// fix me : use worker ID logic for creating unique project name
|
|
||||||
test.describe.serial('Test table', () => { |
|
||||||
let context: any; |
|
||||||
let dashboard: DashboardPage; |
|
||||||
let grid: GridPage; |
|
||||||
const tables = []; |
|
||||||
|
|
||||||
test.afterEach(async () => { |
|
||||||
try { |
|
||||||
if (context) { |
|
||||||
await deleteXcdb(context.token); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
console.log(e); |
|
||||||
} |
|
||||||
|
|
||||||
// reset tables array
|
|
||||||
tables.length = 0; |
|
||||||
}); |
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => { |
|
||||||
context = await setup({ page, isEmptyProject: true }); |
|
||||||
dashboard = new DashboardPage(page, context.project); |
|
||||||
grid = dashboard.grid; |
|
||||||
|
|
||||||
// create a new xcdb project
|
|
||||||
const xcdb = await createXcdb(context.token); |
|
||||||
await dashboard.clickHome(); |
|
||||||
const projectsPage = new ProjectsPage(dashboard.rootPage); |
|
||||||
await projectsPage.openProject({ title: 'xcdb', withoutPrefix: true }); |
|
||||||
|
|
||||||
api = new Api({ |
|
||||||
baseURL: `http://localhost:8080/`, |
|
||||||
headers: { |
|
||||||
'xc-auth': context.token, |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
const columns = [ |
|
||||||
{ |
|
||||||
column_name: 'Id', |
|
||||||
title: 'Id', |
|
||||||
uidt: UITypes.ID, |
|
||||||
}, |
|
||||||
{ |
|
||||||
column_name: 'Title', |
|
||||||
title: 'Title', |
|
||||||
uidt: UITypes.SingleLineText, |
|
||||||
pv: true, |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
const rows = []; |
|
||||||
for (let i = 0; i < recordCount * 10; i++) { |
|
||||||
rows.push({ |
|
||||||
Id: i + 1, |
|
||||||
Title: `${i + 1}`, |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) { |
|
||||||
const table = await api.base.tableCreate(xcdb.id, xcdb.bases?.[0].id, { |
|
||||||
table_name: `Table${i}`, |
|
||||||
title: `Table${i}`, |
|
||||||
columns: columns, |
|
||||||
}); |
|
||||||
tables.push(table); |
|
||||||
await api.dbTableRow.bulkCreate('noco', xcdb.id, tables[i].id, rows); |
|
||||||
} |
|
||||||
|
|
||||||
// Create links
|
|
||||||
// TableA <hm> TableB <hm> TableC
|
|
||||||
await api.dbTableColumn.create(tables[0].id, { |
|
||||||
uidt: UITypes.LinkToAnotherRecord, |
|
||||||
title: `TableA:hm:TableB`, |
|
||||||
column_name: `TableA:hm:TableB`, |
|
||||||
parentId: tables[0].id, |
|
||||||
childId: tables[1].id, |
|
||||||
type: 'hm', |
|
||||||
}); |
|
||||||
await api.dbTableColumn.create(tables[1].id, { |
|
||||||
uidt: UITypes.LinkToAnotherRecord, |
|
||||||
title: `TableB:hm:TableC`, |
|
||||||
column_name: `TableB:hm:TableC`, |
|
||||||
parentId: tables[1].id, |
|
||||||
childId: tables[2].id, |
|
||||||
type: 'hm', |
|
||||||
}); |
|
||||||
|
|
||||||
// TableA <mm> TableD <mm> TableE
|
|
||||||
await api.dbTableColumn.create(tables[0].id, { |
|
||||||
uidt: UITypes.LinkToAnotherRecord, |
|
||||||
title: `TableA:mm:TableD`, |
|
||||||
column_name: `TableA:mm:TableD`, |
|
||||||
parentId: tables[0].id, |
|
||||||
childId: tables[3].id, |
|
||||||
type: 'mm', |
|
||||||
}); |
|
||||||
await api.dbTableColumn.create(tables[3].id, { |
|
||||||
uidt: UITypes.LinkToAnotherRecord, |
|
||||||
title: `TableD:mm:TableE`, |
|
||||||
column_name: `TableD:mm:TableE`, |
|
||||||
parentId: tables[3].id, |
|
||||||
childId: tables[4].id, |
|
||||||
type: 'mm', |
|
||||||
}); |
|
||||||
|
|
||||||
// TableA <hm> TableA : self relation
|
|
||||||
await api.dbTableColumn.create(tables[0].id, { |
|
||||||
uidt: UITypes.LinkToAnotherRecord, |
|
||||||
title: `TableA:hm:TableA`, |
|
||||||
column_name: `TableA:hm:TableA`, |
|
||||||
parentId: tables[0].id, |
|
||||||
childId: tables[0].id, |
|
||||||
type: 'hm', |
|
||||||
}); |
|
||||||
|
|
||||||
// TableA <mm> TableA : self relation
|
|
||||||
await api.dbTableColumn.create(tables[0].id, { |
|
||||||
uidt: UITypes.LinkToAnotherRecord, |
|
||||||
title: `TableA:mm:TableA`, |
|
||||||
column_name: `TableA:mm:TableA`, |
|
||||||
parentId: tables[0].id, |
|
||||||
childId: tables[0].id, |
|
||||||
type: 'mm', |
|
||||||
}); |
|
||||||
|
|
||||||
// Add links
|
|
||||||
// TableA <hm> TableB <hm> TableC
|
|
||||||
// Link every record in tableA to 3 records in tableB
|
|
||||||
for (let i = 1; i <= recordCount; i++) { |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableB', `${i * 3 - 2}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableB', `${i * 3 - 1}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableB', `${i * 3 - 0}`); |
|
||||||
} |
|
||||||
// Link every record in tableB to 3 records in tableC
|
|
||||||
for (let i = 1; i <= recordCount; i++) { |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[1].id, i, 'hm', 'TableB:hm:TableC', `${i * 3 - 2}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[1].id, i, 'hm', 'TableB:hm:TableC', `${i * 3 - 1}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[1].id, i, 'hm', 'TableB:hm:TableC', `${i * 3 - 0}`); |
|
||||||
} |
|
||||||
|
|
||||||
// TableA <mm> TableD <mm> TableE
|
|
||||||
// Link every record in tableA to 5 records in tableD
|
|
||||||
for (let i = 1; i <= recordCount; i++) { |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 1}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 2}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 3}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableD', `${i + 4}`); |
|
||||||
} |
|
||||||
// Link every record in tableD to 5 records in tableE
|
|
||||||
for (let i = 1; i <= recordCount; i++) { |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 1}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 2}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 3}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[3].id, i, 'mm', 'TableD:mm:TableE', `${i + 4}`); |
|
||||||
} |
|
||||||
|
|
||||||
// TableA <hm> TableA : self relation
|
|
||||||
// Link every record in tableA to 3 records in tableA
|
|
||||||
for (let i = 1; i <= recordCount; i++) { |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableA', `${i * 3 - 2}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableA', `${i * 3 - 1}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'hm', 'TableA:hm:TableA', `${i * 3 - 0}`); |
|
||||||
} |
|
||||||
|
|
||||||
// TableA <mm> TableA : self relation
|
|
||||||
// Link every record in tableA to 5 records in tableA
|
|
||||||
for (let i = 1; i <= recordCount; i++) { |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 1}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 2}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 3}`); |
|
||||||
await api.dbTableRow.nestedAdd('noco', xcdb.id, tables[0].id, i, 'mm', 'TableA:mm:TableA', `${i + 4}`); |
|
||||||
} |
|
||||||
|
|
||||||
// refresh page
|
|
||||||
await page.reload(); |
|
||||||
}); |
|
||||||
|
|
||||||
test('Delete record - single, over UI', async () => { |
|
||||||
await dashboard.treeView.openTable({ title: 'Table0' }); |
|
||||||
await grid.deleteRow(0); |
|
||||||
|
|
||||||
// verify row count
|
|
||||||
await dashboard.grid.verifyTotalRowCount({ count: 99 }); |
|
||||||
|
|
||||||
// verify row count in all tables
|
|
||||||
for (let i = 1; i <= 4; i++) { |
|
||||||
await dashboard.treeView.openTable({ title: `Table${i}` }); |
|
||||||
await dashboard.grid.verifyTotalRowCount({ count: 100 }); |
|
||||||
} |
|
||||||
|
|
||||||
// has-many removal verification
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table1' }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0', count: 0, value: [] }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0', count: 0, value: [] }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0', count: 0, value: [] }); |
|
||||||
|
|
||||||
// many-many removal verification
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table3' }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ index: 0, columnHeader: 'Table0 List', count: 0, value: [] }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ index: 1, columnHeader: 'Table0 List', count: 1, value: ['2'] }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ index: 2, columnHeader: 'Table0 List', count: 2, value: ['2', '3'] }); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ |
|
||||||
index: 3, |
|
||||||
columnHeader: 'Table0 List', |
|
||||||
count: 3, |
|
||||||
value: ['2', '3', '4'], |
|
||||||
}); |
|
||||||
await dashboard.grid.cell.verifyVirtualCell({ |
|
||||||
index: 4, |
|
||||||
columnHeader: 'Table0 List', |
|
||||||
count: 4, |
|
||||||
value: ['2', '3', '4', '5'], |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
test('Delete record - bulk, over UI', async () => { |
|
||||||
await dashboard.treeView.openTable({ title: 'Table0' }); |
|
||||||
await grid.selectRow(0); |
|
||||||
await grid.selectRow(1); |
|
||||||
await grid.selectRow(2); |
|
||||||
await grid.deleteSelectedRows(); |
|
||||||
|
|
||||||
// verify row count
|
|
||||||
await dashboard.grid.verifyTotalRowCount({ count: 97 }); |
|
||||||
|
|
||||||
// verify row count in all tables
|
|
||||||
for (let i = 1; i <= 4; i++) { |
|
||||||
await dashboard.treeView.openTable({ title: `Table${i}` }); |
|
||||||
await dashboard.grid.verifyTotalRowCount({ count: 100 }); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
test('Delete column', async () => { |
|
||||||
// has-many
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table0' }); |
|
||||||
await dashboard.grid.column.delete({ title: 'TableA:hm:TableB' }); |
|
||||||
|
|
||||||
// verify
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table1' }); |
|
||||||
await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); |
|
||||||
await dashboard.grid.column.verify({ title: 'TableB:hm:TableC', isVisible: true }); |
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// many-many
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table0' }); |
|
||||||
await dashboard.grid.column.delete({ title: 'TableA:mm:TableD' }); |
|
||||||
|
|
||||||
// verify
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table3' }); |
|
||||||
await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false }); |
|
||||||
await dashboard.grid.column.verify({ title: 'TableD:mm:TableE', isVisible: true }); |
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// has-many self relation
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table0' }); |
|
||||||
await dashboard.grid.column.delete({ title: 'TableA:hm:TableA' }); |
|
||||||
|
|
||||||
// verify
|
|
||||||
await dashboard.grid.column.verify({ title: 'TableA:hm:TableA', isVisible: false }); |
|
||||||
await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); |
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// many-many self relation
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table0' }); |
|
||||||
await dashboard.grid.column.delete({ title: 'TableA:mm:TableA' }); |
|
||||||
|
|
||||||
// verify
|
|
||||||
await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false }); |
|
||||||
await dashboard.grid.column.verify({ title: 'TableA:mm:TableA', isVisible: false }); |
|
||||||
}); |
|
||||||
|
|
||||||
test('Delete table', async () => { |
|
||||||
await dashboard.treeView.deleteTable({ title: 'Table0' }); |
|
||||||
await dashboard.treeView.verifyTable({ title: 'Table0', exists: false }); |
|
||||||
|
|
||||||
// verify
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table1' }); |
|
||||||
await dashboard.grid.column.verify({ title: 'Table0', isVisible: false }); |
|
||||||
|
|
||||||
await dashboard.treeView.openTable({ title: 'Table3' }); |
|
||||||
await dashboard.grid.column.verify({ title: 'Table0 List', isVisible: false }); |
|
||||||
}); |
|
||||||
}); |
|
Loading…
Reference in new issue