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. 80
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  2. 22
      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

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

@ -1,4 +1,6 @@
<template> <template>
<div>
<!-- todo: autocomplete based on available functions and metadata -->
<v-text-field <v-text-field
v-model="formula.value" v-model="formula.value"
dense dense
@ -8,17 +10,27 @@
label="Formula" label="Formula"
persistent-hint persistent-hint
hint="Available formulas are ADD, AVG, CONCAT, +, -, /" hint="Available formulas are ADD, AVG, CONCAT, +, -, /"
:rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]"
/> />
</div>
</template> </template>
<script> <script>
import jsep from 'jsep'
export default { export default {
name: 'FormulaOptions', name: 'FormulaOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'], props: ['nodes', 'column', 'meta', 'isSQLite', 'alias', 'value'],
data: () => ({ data: () => ({
formula: {}, 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: { methods: {
async save() { async save() {
try { try {
@ -32,7 +44,10 @@ export default {
meta.v.push({ meta.v.push({
_cn: this.alias, _cn: this.alias,
formula: this.formula formula: {
...this.formula,
tree: jsep(this.formula.value)
}
}) })
await this.$store.dispatch('sqlMgr/ActSqlOp', [{ await this.$store.dispatch('sqlMgr/ActSqlOp', [{
@ -43,10 +58,69 @@ export default {
meta meta
}]) }])
this.$toast.success('Formula column saved successfully').goAway(3000)
return this.$emit('saved', this.alias) return this.$emit('saved', this.alias)
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) 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
} }
} }
} }

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

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

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

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

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

@ -1,5 +1,17 @@
<template> <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> </template>
<script> <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}'` return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'`
} else if (this.column.lk) { } else if (this.column.lk) {
return `'${this.column.lk._lcn}' from '${this.column.lk._ltn}' (${this.column.lk.type})` 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 '' return ''
} }
@ -249,9 +251,36 @@ export default {
console.log(e) 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() { async deleteColumn() {
if (this.column.lk) { if (this.column.lk) {
await this.deleteLookupColumn() await this.deleteLookupColumn()
} else if (this.column.formula) {
await this.deleteFormulaColumn()
} else { } else {
await this.deleteRelation() await this.deleteRelation()
} }

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

@ -8437,6 +8437,11 @@
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"dev": true "dev": true
}, },
"jsep": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-0.4.0.tgz",
"integrity": "sha512-UDkrzhJK8hmgXeGK8WIiecc/cuW4Vnx5nnrRma7yaxK0WXlvZ4VerGrcxPzifd/CA6QdcI1hpXqr22tHKXpcQA=="
},
"jsesc": { "jsesc": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "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", "file-saver": "^2.0.5",
"fix-path": "^3.0.0", "fix-path": "^3.0.0",
"inflection": "^1.12.0", "inflection": "^1.12.0",
"jsep": "^0.4.0",
"material-design-icons-iconfont": "^5.0.1", "material-design-icons-iconfont": "^5.0.1",
"monaco-editor": "^0.18.1", "monaco-editor": "^0.18.1",
"monaco-themes": "^0.2.5", "monaco-themes": "^0.2.5",

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

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

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

@ -4,7 +4,7 @@ export default interface XcDynamicChanges {
onTableDelete(tn: string): Promise<void>; onTableDelete(tn: string): Promise<void>;
onTableRename(oldTableName: string, newTableName: string): Promise<void>; onTableRename(oldTableName: string, newTableName: string): Promise<void>;
onHandlerCodeUpdate(tn: 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 Validator from 'validator';
import BaseModel, {XcFilter, XcFilterWithAlias} from '../BaseModel'; 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() { protected get selectFormulas() {
return (this.virtualColumns || [])?.reduce((arr, v) => { return (this.virtualColumns || [])?.reduce((arr, v) => {
if (v.formula?.value) { if (v.formula?.value && !v.formula?.error?.length) {
arr.push(formulaQueryBuilder(v.formula?.value, v._cn, this.dbDriver)) arr.push(formulaQueryBuilder(v.formula?.tree, v._cn, this.dbDriver))
} }
return arr; 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 // 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 fn = (pt, a?, prevBinaryOp ?) => {
const colAlias = a ? ` as ${a}` : ''; const colAlias = a ? ` as ${a}` : '';
if (pt.type === 'CallExpression') { if (pt.type === 'CallExpression') {
@ -63,7 +70,7 @@ export default function formulaQueryBuilder(str, alias, knex) {
return query; 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; break;
case 'xcModelSet': 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}`) console.log(`Updated validations for table : ${data.req.args.tn}`)
break; break;
case 'xcUpdateVirtualKeyAlias': 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 BaseModel from "./BaseModel";
import {XcCron} from "./XcCron"; import {XcCron} from "./XcCron";
import NcConnectionMgr from "./NcConnectionMgr"; import NcConnectionMgr from "./NcConnectionMgr";
import updateColumnNameInFormula from "./helpers/updateColumnNameInFormula";
import addErrorOnColumnDeleteInFormula from "./helpers/addErrorOnColumnDeleteInFormula";
const log = debug('nc:api:base'); 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') 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); this.baseLog(`onValidationUpdate : '%s'`, tn);
const modelRow = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', { const modelRow = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {
title: tn title: tn,
type:'table'
}); });
if (!modelRow) { 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.baseLog(`onValidationUpdate : Generating model instance for '%s' table`, tn)
this.models[modelRow.title] = this.getBaseModel(metaObj); this.models[modelRow.title] = this.getBaseModel(metaObj);
XcCache.del([this.projectId, this.dbAlias, 'table', tn].join('::'));
// todo: check tableAlias changed or not // todo: check tableAlias changed or not
// todo: // todo:
// await this.onTableRename(tn, tn) // await this.onTableRename(tn, tn)
@ -415,8 +421,15 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
newCol.validate = oldCol.validate; newCol.validate = oldCol.validate;
} }
// column rename
if (column.cno !== column.cn) { if (column.cno !== column.cn) {
updateColumnNameInFormula({
virtualColumns: newMeta.v,
oldColumnName: oldCol.cn,
newColumnName: newCol.cn,
})
// todo: populate alias // todo: populate alias
newCol._cn = newCol.cn; 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)); 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) this.log('xcTablesPopulate : Generating swagger apis for \'%s\' - %s', table.tn, table.type)
/* create swagger json for table */ /* 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); await this.generateAndSaveAcl(table.tn, table.type);
@ -995,7 +1001,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
// NOTE: xc-meta // NOTE: xc-meta
public async onRelationCreate(tnp: string, tnc: string, args): Promise<void> { public async onRelationCreate(tnp: string, tnc: string, args): Promise<void> {
await super.onRelationCreate(tnp, tnc, args) await super.onRelationCreate(tnp, tnc, args)
const newRelatedTableSwagger = []; // const newRelatedTableSwagger = [];
this.log('onRelationCreate : \'%s\' ==> \'%s\'', tnp, tnc) this.log('onRelationCreate : \'%s\' ==> \'%s\'', tnp, tnc)
this.deleteRoutesForTables([tnp, tnc]) this.deleteRoutesForTables([tnp, tnc])
@ -1004,6 +1010,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const swaggerArr = []; const swaggerArr = [];
const columns = this.metas[tnp]?.columns; const columns = this.metas[tnp]?.columns;
const hasMany = this.extractHasManyRelationsOfTable(relations, tnp); const hasMany = this.extractHasManyRelationsOfTable(relations, tnp);
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnp);
// set table name alias // set table name alias
hasMany.forEach(r => { hasMany.forEach(r => {
@ -1011,10 +1018,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
r._tn = this.getTableNameAlias(r.tn); 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(); const meta = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getObject();
newRelatedTableSwagger.push(new SwaggerXc({ctx}).getObject());
// update old model meta with new details // update old model meta with new details
const existingModel = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {'title': tnp}); 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) { if (existingModel) {
this.log(`onRelationCreate : Updating model metadata for parent table '%s'`, tnp); this.log(`onRelationCreate : Updating model metadata for parent table '%s'`, tnp);
// todo: persisting old table_alias and columnAlias // todo: persisting old table_alias and columnAlias
@ -1046,6 +1052,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
_cn: `${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}` _cn: `${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`
}) })
swaggerArr.push(new SwaggerXc({ctx:{...ctx, v: oldMeta.v}}).getObject());
if (queryParams?.showFields) { if (queryParams?.showFields) {
queryParams.showFields[`${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`] = true; queryParams.showFields[`${this.getTableNameAlias(tnp)} => ${this.getTableNameAlias(tnc)}`] = true;
@ -1100,7 +1108,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const swaggerArr = []; const swaggerArr = [];
const columns = this.metas[tnc]?.columns; const columns = this.metas[tnc]?.columns;
const belongsTo = this.extractBelongsToRelationsOfTable(relations, tnc); 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(); 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) { if (existingModel) {
meta.belongsTo.forEach(hm => { meta.belongsTo.forEach(hm => {
hm.enabled = true; hm.enabled = true;
@ -1140,6 +1149,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
_cn: `${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}` _cn: `${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`
}) })
swaggerArr.push(new SwaggerXc({ctx:{...ctx, v: oldMeta.v}}).getObject());
if (queryParams?.showFields) { if (queryParams?.showFields) {
queryParams.showFields[`${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`] = true; queryParams.showFields[`${this.getTableNameAlias(tnp)} <= ${this.getTableNameAlias(tnc)}`] = true;
@ -1744,6 +1756,48 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
// add new routes // 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; 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; return obj;
} }
@ -190,7 +218,8 @@ class SwaggerXc extends BaseRender {
{ {
"in": "query", "in": "query",
"name": "sort", "name": "sort",
"description": "Comma separated sort fields", "description":
"Comma separated sort fields",
"type": "string" "type": "string"
} }

Loading…
Cancel
Save