mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
4 changed files with 115 additions and 351 deletions
@ -1,359 +1,43 @@ |
|||||||
import { UITypes } from 'nocodb-sdk'; |
import { OrgUserRoles } 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'; |
|
||||||
|
|
||||||
// as of 0.104.3, almost all filter operators are available to all column types
|
/** Upgrader for upgrading roles */ |
||||||
// while some of them aren't supposed to be shown
|
export default async function ({ ncMeta }: NcUpgraderCtx) { |
||||||
// this upgrader is to remove those unsupported filters / migrate to the correct filter
|
const users = await ncMeta.metaList2(null, null, MetaTable.USERS); |
||||||
|
|
||||||
// Change Summary:
|
|
||||||
// - Text-based columns:
|
|
||||||
// - remove `>`, `<`, `>=`, `<=`
|
|
||||||
// - Numeric-based / SingleSelect columns:
|
|
||||||
// - remove `like`
|
|
||||||
// - migrate `null`, and `empty` to `blank`
|
|
||||||
// - Checkbox columns:
|
|
||||||
// - remove `equal`
|
|
||||||
// - migrate `empty` and `null` to `notchecked`
|
|
||||||
// - MultiSelect columns:
|
|
||||||
// - remove `like`
|
|
||||||
// - migrate `equal`, `null`, `empty`
|
|
||||||
// - Attachment columns:
|
|
||||||
// - remove `>`, `<`, `>=`, `<=`, `equal`
|
|
||||||
// - migrate `empty`, `null` to `blank`
|
|
||||||
// - LTAR columns:
|
|
||||||
// - remove `>`, `<`, `>=`, `<=`
|
|
||||||
// - migrate `empty`, `null` to `blank`
|
|
||||||
// - 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) => { |
for (const user of users) { |
||||||
const actions = []; |
user.roles = user.roles |
||||||
const possibleTrueValues = ['true', 'True', '1', 'T', 'Y']; |
.split(',') |
||||||
const possibleFalseValues = ['false', 'False', '0', 'F', 'N']; |
.map((r) => { |
||||||
if (['empty', 'null'].includes(filter.comparison_op)) { |
// update old role names with new roles
|
||||||
// migrate to not checked
|
if (r === 'user') { |
||||||
actions.push( |
return OrgUserRoles.CREATOR; |
||||||
Filter.update( |
} else if (r === 'user-new') { |
||||||
filter.id, |
return OrgUserRoles.VIEWER; |
||||||
{ |
|
||||||
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') { |
return r; |
||||||
if (possibleFalseValues.includes(filter.value)) { |
}) |
||||||
// migrate to checked
|
.join(','); |
||||||
actions.push( |
await ncMeta.metaUpdate( |
||||||
Filter.update( |
null, |
||||||
filter.id, |
null, |
||||||
{ |
MetaTable.USERS, |
||||||
comparison_op: 'checked', |
{ roles: user.roles }, |
||||||
value: '', |
user.id, |
||||||
}, |
|
||||||
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) { |
// set invite only signup if user have environment variable set
|
||||||
const filters = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP); |
if (process.env.NC_INVITE_ONLY_SIGNUP) { |
||||||
for (const filter of filters) { |
await Store.saveOrUpdate( |
||||||
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; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const projects = await ncMeta.metaList2(null, null, MetaTable.PROJECT); |
|
||||||
|
|
||||||
const defaultProjectMeta = { |
|
||||||
showNullAndEmptyInFilter: false, |
|
||||||
}; |
|
||||||
|
|
||||||
for (const project of projects) { |
|
||||||
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), |
value: '{ "invite_only_signup": true }', |
||||||
|
key: NC_APP_SETTINGS, |
||||||
}, |
}, |
||||||
ncMeta, |
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); |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,78 @@ |
|||||||
|
import { RelationTypes, UITypes } from 'nocodb-sdk'; |
||||||
|
import ProjectMgrv2 from '../db/sql-mgr/v2/ProjectMgrv2'; |
||||||
|
import type SqlMgrv2 from '../db/sql-mgr/v2/SqlMgrv2'; |
||||||
|
import type { MetaService } from '../meta/meta.service'; |
||||||
|
import type { LinkToAnotherRecordColumn, Model } from '../models'; |
||||||
|
import type { NcUpgraderCtx } from './NcUpgrader'; |
||||||
|
|
||||||
|
async function upgradeModelRelations({ |
||||||
|
model, |
||||||
|
sqlMgr, |
||||||
|
ncMeta, |
||||||
|
}: { |
||||||
|
ncMeta: MetaService; |
||||||
|
model: Model; |
||||||
|
sqlMgr: SqlMgrv2; |
||||||
|
}) { |
||||||
|
// Iterate over each column and upgrade LTAR
|
||||||
|
for (const column of await model.getColumns()) { |
||||||
|
if (column.uidt !== UITypes.LinkToAnotherRecord) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const colOptions = await column.getColOptions<LinkToAnotherRecordColumn>(); |
||||||
|
|
||||||
|
switch (colOptions.type) { |
||||||
|
// case RelationTypes.MANY_TO_MANY:
|
||||||
|
//
|
||||||
|
// break;
|
||||||
|
case RelationTypes.HAS_MANY: |
||||||
|
{ |
||||||
|
// delete the foreign key constraint if exists
|
||||||
|
// create a new index for the column
|
||||||
|
} |
||||||
|
|
||||||
|
break; |
||||||
|
// case RelationTypes.BELONGS_TO:
|
||||||
|
// break;
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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); |
||||||
|
|
||||||
|
// get models for the base
|
||||||
|
const models = await ncMeta.metaList2(null, base.id, 'models'); |
||||||
|
|
||||||
|
// get all columns and filter out relations and upgrade
|
||||||
|
for (const model of models) { |
||||||
|
await upgradeModelRelations({ ncMeta, model, sqlMgr }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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, 'bases', { |
||||||
|
condition: { |
||||||
|
is_meta: 1, |
||||||
|
}, |
||||||
|
orderBy: {}, |
||||||
|
}); |
||||||
|
|
||||||
|
// iterate and upgrade each base
|
||||||
|
for (const base of bases) { |
||||||
|
await upgradeBaseRelations({ |
||||||
|
ncMeta, |
||||||
|
base, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue