Browse Source

feat: Formula edit, delete and Swagger changes

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/448/head
Pranav C 3 years ago committed by Pranav C
parent
commit
ed8dda6a7a
  1. 100
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  2. 42
      packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue
  3. 2
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue
  4. 14
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue
  5. 29
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  6. 5
      packages/nc-gui/package-lock.json
  7. 1
      packages/nc-gui/package.json
  8. 26
      packages/nocodb/src/__tests__/formula.test.ts
  9. 2
      packages/nocodb/src/interface/XcDynamicChanges.ts
  10. 6
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts
  11. 11
      packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts
  12. 2
      packages/nocodb/src/lib/noco/NcProjectBuilder.ts
  13. 22
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  14. 34
      packages/nocodb/src/lib/noco/common/helpers/addErrorOnColumnDeleteInFormula.ts
  15. 43
      packages/nocodb/src/lib/noco/common/helpers/jsepTreeToFormula.ts
  16. 37
      packages/nocodb/src/lib/noco/common/helpers/updateColumnNameInFormula.ts
  17. 68
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts
  18. 31
      packages/nocodb/src/lib/sqlMgr/code/routers/xc-ts/SwaggerXc.ts

100
packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue

@ -1,24 +1,36 @@
<template>
<v-text-field
v-model="formula.value"
dense
outlined
class="caption"
hide-details="auto"
label="Formula"
persistent-hint
hint="Available formulas are ADD, AVG, CONCAT, +, -, /"
/>
<div>
<!-- todo: autocomplete based on available functions and metadata -->
<v-text-field
v-model="formula.value"
dense
outlined
class="caption"
hide-details="auto"
label="Formula"
persistent-hint
hint="Available formulas are ADD, AVG, CONCAT, +, -, /"
:rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]"
/>
</div>
</template>
<script>
import jsep from 'jsep'
export default {
name: 'FormulaOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias', 'value'],
data: () => ({
formula: {},
formulas: ['AVERAGE()', 'COUNT()', 'COUNTA()', 'COUNTALL()', 'SUM()', 'MIN()', 'MAX()', 'AND()', 'OR()', 'TRUE()', 'FALSE()', 'NOT()', 'XOR()', 'ISERROR()', 'IF()', 'LEN()', 'MID()', 'LEFT()', 'RIGHT()', 'FIND()', 'CONCATENATE()', 'T()', 'VALUE()', 'ARRAYJOIN()', 'ARRAYUNIQUE()', 'ARRAYCOMPACT()', 'ARRAYFLATTEN()', 'ROUND()', 'ROUNDUP()', 'ROUNDDOWN()', 'INT()', 'EVEN()', 'ODD()', 'MOD()', 'LOG()', 'EXP()', 'POWER()', 'SQRT()', 'CEILING()', 'FLOOR()', 'ABS()', 'RECORD_ID()', 'CREATED_TIME()', 'ERROR()', 'BLANK()', 'YEAR()', 'MONTH()', 'DAY()', 'HOUR()', 'MINUTE()', 'SECOND()', 'TODAY()', 'NOW()', 'WORKDAY()', 'DATETIME_PARSE()', 'DATETIME_FORMAT()', 'SET_LOCALE()', 'SET_TIMEZONE()', 'DATESTR()', 'TIMESTR()', 'TONOW()', 'FROMNOW()', 'DATEADD()', 'WEEKDAY()', 'WEEKNUM()', 'DATETIME_DIFF()', 'WORKDAY_DIFF()', 'IS_BEFORE()', 'IS_SAME()', 'IS_AFTER()', 'REPLACE()', 'REPT()', 'LOWER()', 'UPPER()', 'TRIM()', 'SUBSTITUTE()', 'SEARCH()', 'SWITCH()', 'LAST_MODIFIED_TIME()', 'ENCODE_URL_COMPONENT()', 'REGEX_EXTRACT()', 'REGEX_MATCH()', 'REGEX_REPLACE()']
// formulas: ['AVERAGE()', 'COUNT()', 'COUNTA()', 'COUNTALL()', 'SUM()', 'MIN()', 'MAX()', 'AND()', 'OR()', 'TRUE()', 'FALSE()', 'NOT()', 'XOR()', 'ISERROR()', 'IF()', 'LEN()', 'MID()', 'LEFT()', 'RIGHT()', 'FIND()', 'CONCATENATE()', 'T()', 'VALUE()', 'ARRAYJOIN()', 'ARRAYUNIQUE()', 'ARRAYCOMPACT()', 'ARRAYFLATTEN()', 'ROUND()', 'ROUNDUP()', 'ROUNDDOWN()', 'INT()', 'EVEN()', 'ODD()', 'MOD()', 'LOG()', 'EXP()', 'POWER()', 'SQRT()', 'CEILING()', 'FLOOR()', 'ABS()', 'RECORD_ID()', 'CREATED_TIME()', 'ERROR()', 'BLANK()', 'YEAR()', 'MONTH()', 'DAY()', 'HOUR()', 'MINUTE()', 'SECOND()', 'TODAY()', 'NOW()', 'WORKDAY()', 'DATETIME_PARSE()', 'DATETIME_FORMAT()', 'SET_LOCALE()', 'SET_TIMEZONE()', 'DATESTR()', 'TIMESTR()', 'TONOW()', 'FROMNOW()', 'DATEADD()', 'WEEKDAY()', 'WEEKNUM()', 'DATETIME_DIFF()', 'WORKDAY_DIFF()', 'IS_BEFORE()', 'IS_SAME()', 'IS_AFTER()', 'REPLACE()', 'REPT()', 'LOWER()', 'UPPER()', 'TRIM()', 'SUBSTITUTE()', 'SEARCH()', 'SWITCH()', 'LAST_MODIFIED_TIME()', 'ENCODE_URL_COMPONENT()', 'REGEX_EXTRACT()', 'REGEX_MATCH()', 'REGEX_REPLACE()']
availableFunctions: ['AVG', 'ADD', 'CONCAT'],
availableBinOps: ['+', '-', '*', '/']
}),
created() {
this.formula = this.value ? { ...this.value } : {}
},
methods: {
async save() {
try {
@ -32,7 +44,10 @@ export default {
meta.v.push({
_cn: this.alias,
formula: this.formula
formula: {
...this.formula,
tree: jsep(this.formula.value)
}
})
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
@ -43,10 +58,69 @@ export default {
meta
}])
this.$toast.success('Formula column saved successfully').goAway(3000)
return this.$emit('saved', this.alias)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
},
async update() {
try {
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
const col = meta.v.find(c => c._cn === this.column._cn && c.formula)
Object.assign(col, {
_cn: this.alias,
formula: {
...this.formula,
tree: jsep(this.formula.value),
error: undefined
}
})
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
this.$toast.success('Formula column updated successfully').goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
}
},
// todo: validate formula based on meta
parseAndValidateFormula(formula) {
try {
const pt = jsep(formula)
const err = this.validateAgainstMeta(pt)
if (err.length) {
return err.join(', ')
}
} catch (e) {
return e.message
}
},
validateAgainstMeta(pt, arr = []) {
if (pt.type === 'CallExpression') {
if (!this.availableFunctions.includes(pt.callee.name)) {
arr.push(`'${pt.callee.name}' function is not available`)
}
pt.arguments.map(arg => this.validateAgainstMeta(arg, arr))
} else if (pt.type === 'Identifier') {
if (this.meta.columns.every(c => c.cn !== pt.name)) {
arr.push(`Column with name '${pt.name}' is not available`)
}
} else if (pt.type === 'BinaryExpression') {
if (!this.availableBinOps.includes(pt.operator)) {
arr.push(`'${pt.operator}' operation is not available`)
}
this.validateAgainstMeta(pt.left, arr)
this.validateAgainstMeta(pt.right, arr)
}
return arr
}
}
}

42
packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue

@ -34,6 +34,18 @@
outlined
/>
</v-col>
<v-col v-if="column.formula" cols="12">
<formula-options
ref="formula"
:value="column.formula"
:column="column"
:new-column="newColumn"
:nodes="nodes"
:meta="meta"
:alias="newColumn._cn"
/>
</v-col>
</v-row>
</v-container>
</v-form>
@ -41,9 +53,11 @@
</template>
<script>
import FormulaOptions from '@/components/project/spreadsheet/components/editColumn/formulaOptions'
export default {
name: 'EditVirtualColumn',
components: {},
components: { FormulaOptions },
props: {
nodes: Object,
meta: Object,
@ -71,21 +85,25 @@ export default {
},
async save() {
try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcUpdateVirtualKeyAlias', {
tn: this.nodes.tn,
oldAlias: this.column._cn,
newAlias: this.newColumn._cn
}])
this.$toast.success('Successfully updated alias').goAway(3000)
if (this.column.formula) {
await this.$refs.formula.update()
} else {
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcUpdateVirtualKeyAlias', {
tn: this.nodes.tn,
oldAlias: this.column._cn,
newAlias: this.newColumn._cn
}])
this.$toast.success('Successfully updated alias').goAway(3000)
}
} catch (e) {
console.log(e)
this.$toast.error('Failed to update column alias').goAway(3000)
}
this.$emit('saved')
this.$emit('saved', this.newColumn._cn)
this.$emit('input', false)
},

2
packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue

@ -50,7 +50,7 @@
v-for="field in fieldsOrderLoc"
>
<v-list-item
v-if="field.toLowerCase().indexOf(fieldFilter.toLowerCase()) > -1"
v-if="field && field.toLowerCase().indexOf(fieldFilter.toLowerCase()) > -1"
:key="field"
dense
>

14
packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue

@ -1,5 +1,17 @@
<template>
<div>{{ row[column._cn] }}</div>
<v-tooltip
v-if="column.formula && column.formula.error && column.formula.error.length"
bottom
color="error"
>
<template #activator="{on}">
<span class="caption" v-on="on">ERR<span class="error--text">!</span></span>
</template>
<span class=" font-weight-bold">{{ column.formula.error.join(', ') }}</span>
</v-tooltip>
<div v-else>
{{ row[column._cn] }}
</div>
</template>
<script>

29
packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue

@ -196,6 +196,8 @@ export default {
return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'`
} else if (this.column.lk) {
return `'${this.column.lk._lcn}' from '${this.column.lk._ltn}' (${this.column.lk.type})`
} else if (this.column.formula) {
return `Formula - ${this.column.formula.value}`
}
return ''
}
@ -249,9 +251,36 @@ export default {
console.log(e)
}
},
async deleteFormulaColumn() {
try {
await this.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.nodes.dbAlias,
env: this.nodes.env,
tn: this.meta.tn,
force: true
})
const meta = JSON.parse(JSON.stringify(this.$store.state.meta.metas[this.meta.tn]))
// remove formula from virtual columns
meta.v = meta.v.filter(cl => !cl.formula || cl._cn !== this.column._cn)
await this.$store.dispatch('sqlMgr/ActSqlOp', [{
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'xcModelSet', {
tn: this.nodes.tn,
meta
}])
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
console.log(e)
}
},
async deleteColumn() {
if (this.column.lk) {
await this.deleteLookupColumn()
} else if (this.column.formula) {
await this.deleteFormulaColumn()
} else {
await this.deleteRelation()
}

5
packages/nc-gui/package-lock.json generated

@ -8437,6 +8437,11 @@
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true
},
"jsep": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz",
"integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA=="
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",

1
packages/nc-gui/package.json

@ -25,6 +25,7 @@
"file-saver": "^2.0.5",
"fix-path": "^3.0.0",
"inflection": "^1.12.0",
"jsep": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1",
"monaco-editor": "^0.18.1",
"monaco-themes": "^0.2.5",

26
packages/nocodb/src/__tests__/formula.test.ts

@ -1,7 +1,7 @@
import {expect} from 'chai';
import 'mocha';
import knex from '../lib/dataMapper/lib/sql/CustomKnex';
import formulaQueryBuilder from "../lib/dataMapper/lib/sql/formulaQueryBuilder";
import formulaQueryBuilderFromString from "../lib/dataMapper/lib/sql/formulaQueryBuilderFromString";
process.env.TEST = 'test';
@ -31,24 +31,24 @@ describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
describe('Formulas', function () {
it('Simple formula', function (done) {
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexMysqlRef).toQuery()).eq('concat(`city`,\' _ \',`city_id` + `country_id`) as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexPgRef).toQuery()).eq('concat("city",\' _ \',"city_id" + "country_id") as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexMssqlRef).toQuery()).eq('concat([city],\' _ \',[city_id] + [country_id]) as a')
expect(formulaQueryBuilder("concat(city, ' _ ',city_id+country_id)", 'a',knexSqliteRef).toQuery()).eq('`city` || \' _ \' || (`city_id` + `country_id`) as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexMysqlRef).toQuery()).eq('concat(`city`,\' _ \',`city_id` + `country_id`) as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexPgRef).toQuery()).eq('concat("city",\' _ \',"city_id" + "country_id") as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexMssqlRef).toQuery()).eq('concat([city],\' _ \',[city_id] + [country_id]) as a')
expect(formulaQueryBuilderFromString("concat(city, ' _ ',city_id+country_id)", 'a',knexSqliteRef).toQuery()).eq('`city` || \' _ \' || (`city_id` + `country_id`) as a')
done()
});
it('Addition', function (done) {
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('"city_id" + "country_id" + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('[city_id] + [country_id] + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilder("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('"city_id" + "country_id" + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('[city_id] + [country_id] + 2 + 3 + 4 + 5 + 4 as a')
expect(formulaQueryBuilderFromString("ADD(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4 as a')
done()
});
it('Average', function (done) {
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('("city_id" + "country_id" + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('([city_id] + [country_id] + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilder("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMysqlRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexPgRef).toQuery()).eq('("city_id" + "country_id" + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexMssqlRef).toQuery()).eq('([city_id] + [country_id] + 2 + 3 + 4 + 5 + 4) / 7 as a')
expect(formulaQueryBuilderFromString("AVG(city_id,country_id,2,3,4,5,4)", 'a',knexSqliteRef).toQuery()).eq('(`city_id` + `country_id` + 2 + 3 + 4 + 5 + 4) / 7 as a')
done()
});
});

2
packages/nocodb/src/interface/XcDynamicChanges.ts

@ -4,7 +4,7 @@ export default interface XcDynamicChanges {
onTableDelete(tn: string): Promise<void>;
onTableRename(oldTableName: string, newTableName: string): Promise<void>;
onHandlerCodeUpdate(tn: string): Promise<void>;
onValidationUpdate(tn:string):Promise<void>;
onMetaUpdate(tn:string):Promise<void>;
}

6
packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts

@ -3,7 +3,7 @@ import _ from 'lodash';
import Validator from 'validator';
import BaseModel, {XcFilter, XcFilterWithAlias} from '../BaseModel';
import formulaQueryBuilder from "./formulaQueryBuilder";
import formulaQueryBuilder from "./formulaQueryBuilderFromString";
/**
@ -1863,8 +1863,8 @@ class BaseModelSql extends BaseModel {
protected get selectFormulas() {
return (this.virtualColumns || [])?.reduce((arr, v) => {
if (v.formula?.value) {
arr.push(formulaQueryBuilder(v.formula?.value, v._cn, this.dbDriver))
if (v.formula?.value && !v.formula?.error?.length) {
arr.push(formulaQueryBuilder(v.formula?.tree, v._cn, this.dbDriver))
}
return arr;
}, [])

11
packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilder.ts → packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts

@ -3,7 +3,14 @@ import jsep from 'jsep';
// todo: switch function based on database
export default function formulaQueryBuilder(str, alias, knex) {
export function formulaQueryBuilderFromString(str, alias, knex) {
return formulaQueryBuilder(jsep(str), alias,knex)
}
export default function formulaQueryBuilder(tree, alias, knex) {
const fn = (pt, a?, prevBinaryOp ?) => {
const colAlias = a ? ` as ${a}` : '';
if (pt.type === 'CallExpression') {
@ -63,7 +70,7 @@ export default function formulaQueryBuilder(str, alias, knex) {
return query;
}
};
return fn(jsep(str), alias)
return fn(tree, alias)
}

2
packages/nocodb/src/lib/noco/NcProjectBuilder.ts

@ -229,7 +229,7 @@ export default class NcProjectBuilder {
break;
case 'xcModelSet':
await curBuilder.onValidationUpdate(data.req.args.tn);
await curBuilder.onMetaUpdate(data.req.args.tn);
console.log(`Updated validations for table : ${data.req.args.tn}`)
break;
case 'xcUpdateVirtualKeyAlias':

22
packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts

@ -24,6 +24,8 @@ import XcCache from "../plugins/adapters/cache/XcCache";
import BaseModel from "./BaseModel";
import {XcCron} from "./XcCron";
import NcConnectionMgr from "./NcConnectionMgr";
import updateColumnNameInFormula from "./helpers/updateColumnNameInFormula";
import addErrorOnColumnDeleteInFormula from "./helpers/addErrorOnColumnDeleteInFormula";
const log = debug('nc:api:base');
@ -310,10 +312,12 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
throw new Error('`onGqlSchemaUpdate` not implemented')
}
public async onValidationUpdate(tn: string): Promise<void> {
// todo: change name to meta uodate
public async onMetaUpdate(tn: string): Promise<void> {
this.baseLog(`onValidationUpdate : '%s'`, tn);
const modelRow = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {
title: tn
title: tn,
type:'table'
});
if (!modelRow) {
@ -325,6 +329,8 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
this.baseLog(`onValidationUpdate : Generating model instance for '%s' table`, tn)
this.models[modelRow.title] = this.getBaseModel(metaObj);
XcCache.del([this.projectId, this.dbAlias, 'table', tn].join('::'));
// todo: check tableAlias changed or not
// todo:
// await this.onTableRename(tn, tn)
@ -415,8 +421,15 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
newCol.validate = oldCol.validate;
}
// column rename
if (column.cno !== column.cn) {
updateColumnNameInFormula({
virtualColumns: newMeta.v,
oldColumnName: oldCol.cn,
newColumnName: newCol.cn,
})
// todo: populate alias
newCol._cn = newCol.cn;
@ -572,6 +585,11 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}
}
addErrorOnColumnDeleteInFormula({
virtualColumns: newMeta.v,
columnName: column.cno
})
aclOper.push(async () => this.deleteColumnNameInACL(tn, column.cno));

34
packages/nocodb/src/lib/noco/common/helpers/addErrorOnColumnDeleteInFormula.ts

@ -0,0 +1,34 @@
export default function (args: {
virtualColumns,
columnName: string
}): void | boolean {
let modified = false;
const fn = (pt, virtualColumn) => {
if (pt.type === 'CallExpression') {
pt.arguments.map(arg => fn(arg, virtualColumn))
} else if (pt.type === 'Literal') {
} else if (pt.type === 'Identifier') {
if (pt.name === args.columnName) {
virtualColumn.formula.error = virtualColumn.formula.error || [];
virtualColumn.formula.error.push(`Column '${args.columnName}' was deleted`)
modified = true;
}
} else if (pt.type === 'BinaryExpression') {
fn(pt.left, virtualColumn);
fn(pt.right, virtualColumn);
}
};
if (!args.virtualColumns) {
return
}
for (const v of args.virtualColumns) {
if (!v.formula?.tree) {
continue;
}
fn(v.formula.tree, v)
}
return modified;
}

43
packages/nocodb/src/lib/noco/common/helpers/jsepTreeToFormula.ts

@ -0,0 +1,43 @@
export default function jsepTreeToFormula(node) {
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
return '(' + jsepTreeToFormula(node.left) + ' ' + node.operator + ' ' + jsepTreeToFormula(node.right) + ')'
}
if (node.type === 'UnaryExpression') {
return node.operator + jsepTreeToFormula(node.argument)
}
if (node.type === 'MemberExpression') {
return jsepTreeToFormula(node.object) + '[' + jsepTreeToFormula(node.property) + ']'
}
if (node.type === 'Identifier') {
return node.name
}
if (node.type === 'Literal') {
if (typeof node.value === 'string') {
return '"' + node.value + '"'
}
return '' + node.value
}
if (node.type === 'CallExpression') {
return jsepTreeToFormula(node.callee) + '(' + node.arguments.map(jsepTreeToFormula).join(', ') + ')'
}
if (node.type === 'ArrayExpression') {
return '[' + node.elements.map(jsepTreeToFormula).join(', ') + ']'
}
if (node.type === 'Compound') {
return node.body.map(e => jsepTreeToFormula(e)).join(' ')
}
if (node.type === 'ConditionalExpression') {
return jsepTreeToFormula(node.test) + ' ? ' + jsepTreeToFormula(node.consequent) + ' : ' + jsepTreeToFormula(node.alternate)
}
return ''
}

37
packages/nocodb/src/lib/noco/common/helpers/updateColumnNameInFormula.ts

@ -0,0 +1,37 @@
import jsepTreeToFormula from "./jsepTreeToFormula";
export default function (args: {
virtualColumns,
oldColumnName: string,
newColumnName: string,
}): void | boolean {
let modified = false;
const fn = (pt) => {
if (pt.type === 'CallExpression') {
pt.arguments.map(arg => fn(arg))
} else if (pt.type === 'Literal') {
} else if (pt.type === 'Identifier') {
if (pt.name === args.oldColumnName) {
pt.name = args.newColumnName;
modified = true;
}
} else if (pt.type === 'BinaryExpression') {
fn(pt.left);
fn(pt.right);
}
};
if (!args.virtualColumns) {
return
}
for (const v of args.virtualColumns) {
if (!v.formula?.tree) {
continue;
}
fn(v.formula.tree)
v.formula.value = jsepTreeToFormula(v.formula.tree)
}
return modified;
}

68
packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

@ -398,7 +398,13 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
this.log('xcTablesPopulate : Generating swagger apis for \'%s\' - %s', table.tn, table.type)
/* create swagger json for table */
swaggerRefs[table.tn].push(await new SwaggerXc({dir: '', ctx, filename: ''}).getObject())
swaggerRefs[table.tn].push(await new SwaggerXc({
dir: '',
ctx: {
...ctx,
v: meta.v
}, filename: ''
}).getObject())
await this.generateAndSaveAcl(table.tn, table.type);
@ -995,7 +1001,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
// NOTE: xc-meta
public async onRelationCreate(tnp: string, tnc: string, args): Promise<void> {
await super.onRelationCreate(tnp, tnc, args)
const newRelatedTableSwagger = [];
// const newRelatedTableSwagger = [];
this.log('onRelationCreate : \'%s\' ==> \'%s\'', tnp, tnc)
this.deleteRoutesForTables([tnp, tnc])
@ -1004,6 +1010,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const swaggerArr = [];
const columns = this.metas[tnp]?.columns;
const hasMany = this.extractHasManyRelationsOfTable(relations, tnp);
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnp);
// set table name alias
hasMany.forEach(r => {
@ -1011,10 +1018,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
r._tn = this.getTableNameAlias(r.tn);
})
const ctx = this.generateContextForTable(tnp, columns, relations, hasMany, []);
const ctx = this.generateContextForTable(tnp, columns, relations, hasMany, belongsTo);
const meta = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
newRelatedTableSwagger.push(new SwaggerXc({ctx}).getObject());
// update old model meta with new details
const existingModel = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {'title': tnp});
@ -1025,7 +1031,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
}
swaggerArr.push(JSON.parse(existingModel.schema));
// swaggerArr.push(JSON.parse(existingModel.schema));
if (existingModel) {
this.log(`onRelationCreate : Updating model metadata for parent table '%s'`, tnp);
// todo: persisting old table_alias and columnAlias
@ -1046,6 +1052,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
_cn: `${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`
})
swaggerArr.push(new SwaggerXc({ctx:{...ctx, v: oldMeta.v}}).getObject());
if (queryParams?.showFields) {
queryParams.showFields[`${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`] = true;
@ -1100,7 +1108,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const swaggerArr = [];
const columns = this.metas[tnc]?.columns;
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnc);
const ctx = this.generateContextForTable(tnc, columns, relations, [], belongsTo);
const hasMany = this.extractHasManyRelationsOfTable(relations, tnc);
const ctx = this.generateContextForTable(tnc, columns, relations, hasMany, belongsTo);
const meta = ModelXcMetaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getObject();
@ -1121,7 +1130,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
}
swaggerArr.push(JSON.parse(existingModel.schema))
// swaggerArr.push(JSON.parse(existingModel.schema))
if (existingModel) {
meta.belongsTo.forEach(hm => {
hm.enabled = true;
@ -1140,6 +1149,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
_cn: `${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`
})
swaggerArr.push(new SwaggerXc({ctx:{...ctx, v: oldMeta.v}}).getObject());
if (queryParams?.showFields) {
queryParams.showFields[`${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`] = true;
@ -1744,6 +1756,48 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
// add new routes
}
public async onMetaUpdate(tn: string) {
await super.onMetaUpdate(tn);
const ctx = this.generateContextForTable(
tn,
this.metas[tn].columns,
[...this.metas[tn].belongsTo, ...this.metas[tn].hasMany],
this.metas[tn].hasMany,
this.metas[tn].belongsTo
);
const swaggerDoc = await new SwaggerXc({
dir: '', ctx: {
...ctx,
v: this.metas[tn].v
}, filename: ''
}).getObject();
const meta = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {
title: tn,
type: 'table'
});
const oldSwaggerDoc = JSON.parse(meta.schema);
// keep upto 5 schema backup on table update
let previousSchemas = [oldSwaggerDoc]
if (meta.schema_previous) {
previousSchemas = [...JSON.parse(meta.schema_previous), oldSwaggerDoc].slice(-5);
}
oldSwaggerDoc.definitions = swaggerDoc.definitions;
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: JSON.stringify(oldSwaggerDoc),
schema_previous: JSON.stringify(previousSchemas)
}, {
title: tn,
type: 'table'
});
await this.onSwaggerDocUpdate(tn);
}
}

31
packages/nocodb/src/lib/sqlMgr/code/routers/xc-ts/SwaggerXc.ts

@ -84,6 +84,34 @@ class SwaggerXc extends BaseRender {
properties[column._cn] = field;
}
for (const column of (args.v || [])) {
const field: any = {};
field.readOnly = true;
let _cn = column._cn;
if (column.mm) {
field.type = 'array';
field.items = {
$ref: `#/definitions/${column.mm?._rtn}`
};
_cn = `${column.mm?._rtn}MMList`;
} else if (column.hm) {
field.type = 'array';
field.items = {
$ref: `#/definitions/${column.mm?._rtn}`
};
field.$ref = `#/definitions/${column.mm?._rtn}`
_cn = `${column.mm?._rtn}List`;
} else if (column.bt) {
field.$ref = `#/definitions/${column.mm?._tn}`
_cn = `${column.mm?._rtn}Read`;
}
properties[_cn] = field;
}
return obj;
}
@ -190,7 +218,8 @@ class SwaggerXc extends BaseRender {
{
"in": "query",
"name": "sort",
"description": "Comma separated sort fields",
"description":
"Comma separated sort fields",
"type": "string"
}

Loading…
Cancel
Save