Browse Source

feat: Formula GQL support, replace column name with alias

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
dbd97542c0
  1. 77
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  2. 6
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  3. 1
      packages/nc-gui/layouts/default.vue
  4. 10
      packages/nc-gui/plugins/ncApis/gqlApi.js
  5. 8
      packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts
  6. 10
      packages/nocodb/src/lib/noco/common/BaseModel.ts
  7. 70
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  8. 17
      packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts

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

@ -9,31 +9,30 @@
> >
<template #activator> <template #activator>
<!-- todo: autocomplete based on available functions and metadata --> <!-- todo: autocomplete based on available functions and metadata -->
<v-tooltip color="info"> <!-- <v-tooltip color="info" right>-->
<template #activator="{on}"> <!-- <template #activator="{on}">-->
<v-text-field <v-text-field
ref="input" ref="input"
v-model="formula.value" v-model="formula.value"
dense dense
outlined outlined
class="caption" class="caption"
hide-details="auto" hide-details="auto"
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)]" :rules="[v => !!v || 'Required', v => parseAndValidateFormula(v)]"
autocomplete="off" autocomplete="off"
v-on="on" @input="handleInputDeb"
@input="handleInputDeb" @keydown.down.prevent="suggestionListDown"
@keydown.down.prevent="suggestionListDown" @keydown.up.prevent="suggestionListUp"
@keydown.up.prevent="suggestionListUp" @keydown.enter.prevent="selectText"
@keydown.enter.prevent="selectText" />
/> <!-- </template>-->
</template> <!-- <span class="caption">Example: AVG(column1, column2, column3)</span>-->
<span class="caption">Example: AVG(column1, column2, column3)</span> <!-- </v-tooltip>-->
</v-tooltip>
</template> </template>
<v-list v-if="suggestion" dense max-height="50vh" class="background-color"> <v-list v-if="suggestion" ref="sugList" dense max-height="50vh" style="overflow: auto">
<v-list-item-group <v-list-item-group
v-model="selected" v-model="selected"
color="primary" color="primary"
@ -41,6 +40,7 @@
<v-list-item <v-list-item
v-for="(it,i) in suggestion" v-for="(it,i) in suggestion"
:key="i" :key="i"
ref="sugOptions"
dense dense
selectable selectable
@mousedown.prevent="appendText(it)" @mousedown.prevent="appendText(it)"
@ -73,7 +73,7 @@ export default {
// 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'], availableFunctions: ['AVG', 'ADD', 'CONCAT'],
availableBinOps: ['+', '-', '*', '/'], availableBinOps: ['+', '-', '*', '/'],
autocomplete: true, autocomplete: false,
suggestion: null, suggestion: null,
wordToComplete: '', wordToComplete: '',
selected: 0, selected: 0,
@ -83,7 +83,7 @@ export default {
suggestionsList() { suggestionsList() {
return [ return [
...this.availableFunctions.map(fn => ({ text: fn, type: 'function' })), ...this.availableFunctions.map(fn => ({ text: fn, type: 'function' })),
...this.meta.columns.map(c => ({ text: c.cn, type: 'column', c })), ...this.meta.columns.map(c => ({ text: c._cn, type: 'column', c })),
...this.availableBinOps.map(op => ({ text: op, type: 'op' })) ...this.availableBinOps.map(op => ({ text: op, type: 'op' }))
] ]
}, },
@ -177,7 +177,7 @@ export default {
} }
pt.arguments.map(arg => this.validateAgainstMeta(arg, arr)) pt.arguments.map(arg => this.validateAgainstMeta(arg, arr))
} else if (pt.type === 'Identifier') { } else if (pt.type === 'Identifier') {
if (this.meta.columns.every(c => c.cn !== pt.name)) { if (this.meta.columns.every(c => c._cn !== pt.name)) {
arr.push(`Column with name '${pt.name}' is not available`) arr.push(`Column with name '${pt.name}' is not available`)
} }
} else if (pt.type === 'BinaryExpression') { } else if (pt.type === 'BinaryExpression') {
@ -236,24 +236,25 @@ export default {
}, },
suggestionListDown() { suggestionListDown() {
this.selected = ++this.selected % this.suggestion.length this.selected = ++this.selected % this.suggestion.length
this.scrollToSelectedOption()
}, },
suggestionListUp() { suggestionListUp() {
this.selected = --this.selected > -1 ? this.selected : this.suggestion.length - 1 this.selected = --this.selected > -1 ? this.selected : this.suggestion.length - 1
this.scrollToSelectedOption()
},
scrollToSelectedOption() {
this.$nextTick(() => {
if (this.$refs.sugOptions[this.selected]) {
try {
this.$refs.sugList.$el.scrollTo({ top: this.$refs.sugOptions[this.selected].$el.offsetTop, behavior: 'smooth' })
} catch (e) {
}
}
})
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
//.formula-wrapper {
// overflow: visible;
// position: relative;
//
// .nc-autocomplete {
// position: absolute;
// //top: 100%;
// top:0;
// left: 0
// }
//}
</style> </style>

6
packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue

@ -803,9 +803,13 @@ export default {
} }
const id = this.meta.columns.filter(c => c.pk).map(c => rowObj[c._cn]).join('___') const id = this.meta.columns.filter(c => c.pk).map(c => rowObj[c._cn]).join('___')
await this.api.update(id, {
const newData = await this.api.update(id, {
[column._cn]: rowObj[column._cn] [column._cn]: rowObj[column._cn]
}, { [column._cn]: oldRow[column._cn] }) }, { [column._cn]: oldRow[column._cn] })
this.$set(this.data[row], 'row', { ...rowObj, ...newData })
this.$set(oldRow, column._cn, rowObj[column._cn]) this.$set(oldRow, column._cn, rowObj[column._cn])
this.$toast.success(`${rowObj[this.primaryValueColumn] ? `${rowObj[this.primaryValueColumn]}'s c` : 'C'}olumn '${column.cn}' updated successfully.`, { this.$toast.success(`${rowObj[this.primaryValueColumn] ? `${rowObj[this.primaryValueColumn]}'s c` : 'C'}olumn '${column.cn}' updated successfully.`, {
position: 'bottom-center' position: 'bottom-center'

1
packages/nc-gui/layouts/default.vue

@ -675,6 +675,7 @@ export default {
toggleTreeviewWindow: 'windows/MutToggleTreeviewWindow' toggleTreeviewWindow: 'windows/MutToggleTreeviewWindow'
}), }),
async loadProjectInfo() { async loadProjectInfo() {
debugger
if (this.$route.params.project_id) { if (this.$route.params.project_id) {
try { try {
const { info } = (await this.$axios.get(`/nc/${this.$route.params.project_id}/projectApiInfo`, { const { info } = (await this.$axios.get(`/nc/${this.$route.params.project_id}/projectApiInfo`, {

10
packages/nc-gui/plugins/ncApis/gqlApi.js

@ -92,7 +92,7 @@ export default class GqlApi {
} }
// todo: query only visible columns // todo: query only visible columns
async gqlRelationReqBody(params) { async gqlRelationReqBody(params = {}) {
let str = '' let str = ''
if (params.hm) { if (params.hm) {
for (const child of params.hm.split(',')) { for (const child of params.hm.split(',')) {
@ -173,10 +173,10 @@ export default class GqlApi {
return { list, count } return { list, count }
} }
async update(id, data, oldData) { async update(id, data, oldData, params = {}) {
const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, { const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `mutation update($id:String!, $data:${this.tableCamelized}Input){ query: `mutation update($id:String!, $data:${this.tableCamelized}Input){
${this.gqlMutationUpdateName}(id: $id, data: $data) ${this.gqlMutationUpdateName}(id: $id, data: $data){${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}
}`, }`,
variables: { variables: {
id, data id, data
@ -195,10 +195,10 @@ export default class GqlApi {
return data1.data.data[this.gqlMutationUpdateName] return data1.data.data[this.gqlMutationUpdateName]
} }
async insert(data) { async insert(data, params = {}) {
const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, { const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `mutation create($data:${this.tableCamelized}Input){ query: `mutation create($data:${this.tableCamelized}Input){
${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}} ${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}
}`, }`,
variables: { variables: {
data data

8
packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts

@ -4,13 +4,11 @@ import jsep from 'jsep';
// todo: switch function based on database // todo: switch function based on database
export function formulaQueryBuilderFromString(str, alias, knex) { export function formulaQueryBuilderFromString(str, alias, knex) {
return formulaQueryBuilder(jsep(str), alias,knex) return formulaQueryBuilder(jsep(str), alias, knex)
} }
export default function formulaQueryBuilder(tree, alias, knex, aliasToColumn = {}) {
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') {
@ -61,7 +59,7 @@ export default function formulaQueryBuilder(tree, alias, knex) {
} else if (pt.type === 'Literal') { } else if (pt.type === 'Literal') {
return knex.raw(`?${colAlias}`, [pt.value]); return knex.raw(`?${colAlias}`, [pt.value]);
} else if (pt.type === 'Identifier') { } else if (pt.type === 'Identifier') {
return knex.raw(`??${colAlias}`, [pt.name]); return knex.raw(`??${colAlias}`, [aliasToColumn[pt.name] || pt.name]);
} else if (pt.type === 'BinaryExpression') { } else if (pt.type === 'BinaryExpression') {
const query = knex.raw(`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(pt.right, null, pt.operator).toQuery()}${colAlias}`) const query = knex.raw(`${fn(pt.left, null, pt.operator).toQuery()} ${pt.operator} ${fn(pt.right, null, pt.operator).toQuery()}${colAlias}`)
if (prevBinaryOp && pt.operator !== prevBinaryOp) { if (prevBinaryOp && pt.operator !== prevBinaryOp) {

10
packages/nocodb/src/lib/noco/common/BaseModel.ts

@ -76,8 +76,8 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
await this.handleHooks('after.delete', data, req) await this.handleHooks('after.delete', data, req)
} }
private async handleHooks(hookName, _data, req): Promise<void> { private async handleHooks(hookName, data, req): Promise<void> {
let data = _data; // const data = _data;
try { try {
@ -86,13 +86,13 @@ class BaseModel<T extends BaseApiBuilder<any>> extends BaseModelSql {
&& this.builder.hooks[this.tn][hookName] && this.builder.hooks[this.tn][hookName]
) { ) {
if (hookName === 'after.update') { /* if (hookName === 'after.update') {
try { try {
data = await this.nestedRead(req.params.id, this.defaultNestedQueryParams) data = await this.nestedRead(req.params.id, this.defaultNestedQueryParams)
} catch (_) { } catch (_) {
/* ignore */ /!* ignore *!/
} }
} }*/
for (const hook of this.builder.hooks[this.tn][hookName]) { for (const hook of this.builder.hooks[this.tn][hookName]) {

70
packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts

@ -1788,7 +1788,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const schemaStr = mergeTypeDefs([ const schemaStr = mergeTypeDefs([
...Object.values(this.schemas).filter(Boolean), ...Object.values(this.schemas).filter(Boolean),
` ${this.customResolver?.schema || ''} \n ${commonSchema}`, ` ${this.customResolver?.schema || ''} \n ${commonSchema}`,
...this.typesWithFormulaProps // ...this.typesWithFormulaProps
], { ], {
commentDescriptions: true, commentDescriptions: true,
forceSchemaDefinition: true, forceSchemaDefinition: true,
@ -1976,27 +1976,65 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
} }
// todo: dump it in db /* // todo: dump it in db
// extending types for formula column // extending types for formula column
private get typesWithFormulaProps(): string[] { private get typesWithFormulaProps(): string[] {
const schemas = []; const schemas = [];
for (const meta of Object.values(this.metas)) { for (const meta of Object.values(this.metas)) {
const props = []; const props = [];
for (const v of meta.v) { for (const v of meta.v) {
if (!v.formula) continue if (!v.formula) continue
props.push(`${v._cn}: JSON`) props.push(`${v._cn}: JSON`)
} }
if (props.length) { if (props.length) {
schemas.push(`type ${meta._tn} {\n${props.join('\n')}\n}`) schemas.push(`type ${meta._tn} {\n${props.join('\n')}\n}`)
}
} }
} return schemas;
return schemas; }*/
}
async onMetaUpdate(tn: string): Promise<void> { async onMetaUpdate(tn: string): Promise<void> {
await super.onMetaUpdate(tn); await super.onMetaUpdate(tn);
const meta = this.metas[tn];
const ctx = this.generateContextForTable(tn, meta.columns,
[...meta.belongsTo, meta.hasMany],
meta.hasMany,
meta.belongsTo
)
const oldSchema = this.schemas[tn];
// this.log(`onTableUpdate : Populating new schema for '%s' table`, changeObj.tn);
// meta.schema =
this.schemas[tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs({
...meta,
...ctx
})).getString();
if (oldSchema !== this.schemas[tn]) {
// this.log(`onTableUpdate : Updating and taking backup of schema - '%s' table`, tn);
const oldModel = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {
title: tn
});
// keep upto 5 schema backup on table update
let previousSchemas = [oldSchema]
if (oldModel.schema_previous) {
previousSchemas = [...JSON.parse(oldModel.schema_previous), oldSchema].slice(-5);
}
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: meta.schema,
schema_previous: JSON.stringify(previousSchemas)
}, {
title: tn
});
}
return this.reInitializeGraphqlEndpoint(); return this.reInitializeGraphqlEndpoint();
} }
} }

17
packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts

@ -76,6 +76,18 @@ abstract class BaseGqlXcTsSchema extends BaseRender {
return str; return str;
} }
protected generateFormulaTypes(args: any): string {
if (!args.v?.length) {
return '';
}
const props = [];
for (const v of args.v) {
if (!v.formula) continue
props.push(`\t\t${v._cn}: JSON`)
}
return props.length ? `\r\n${props.join('\r\n')}\r\n` : '';
}
protected _getInputType(args): string { protected _getInputType(args): string {
let str = `input ${args._tn}Input { \r\n` let str = `input ${args._tn}Input { \r\n`
for (const column of args.columns) { for (const column of args.columns) {
@ -108,7 +120,7 @@ abstract class BaseGqlXcTsSchema extends BaseRender {
protected _getMutation(args): string { protected _getMutation(args): string {
let str = `type Mutation { \r\n` let str = `type Mutation { \r\n`
str += `\t\t${args._tn}Create(data:${args._tn}Input): ${args._tn}\r\n` str += `\t\t${args._tn}Create(data:${args._tn}Input): ${args._tn}\r\n`
str += `\t\t${args._tn}Update(id:String,data:${args._tn}Input): Int\r\n` // ${args._tn}\r\n` str += `\t\t${args._tn}Update(id:String,data:${args._tn}Input): ${args._tn}\r\n` // ${args._tn}\r\n`
str += `\t\t${args._tn}Delete(id:String): Int\r\n`// ${args._tn}\r\n` str += `\t\t${args._tn}Delete(id:String): Int\r\n`// ${args._tn}\r\n`
str += `\t\t${args._tn}CreateBulk(data: [${args._tn}Input]): [Int]\r\n` str += `\t\t${args._tn}CreateBulk(data: [${args._tn}Input]): [Int]\r\n`
str += `\t\t${args._tn}UpdateBulk(data: [${args._tn}Input]): [Int]\r\n` str += `\t\t${args._tn}UpdateBulk(data: [${args._tn}Input]): [Int]\r\n`
@ -127,7 +139,7 @@ abstract class BaseGqlXcTsSchema extends BaseRender {
console.log(`Skipping ${args.tn}.${column._cn}`); console.log(`Skipping ${args.tn}.${column._cn}`);
} else { } else {
str += `\t\t${column._cn.replace(/ /g, '_')}: ${this._getGraphqlType(column)},\r\n`; str += `\t\t${column._cn.replace(/ /g, '_')}: ${this._getGraphqlType(column)},\r\n`;
strWhere += `\t\t${column._cn .replace(/ /g, '_')}: ${this._getGraphqlConditionType(column)},\r\n`; strWhere += `\t\t${column._cn.replace(/ /g, '_')}: ${this._getGraphqlConditionType(column)},\r\n`;
} }
} }
@ -150,6 +162,7 @@ abstract class BaseGqlXcTsSchema extends BaseRender {
str += this.generateManyToManyTypeProps(args); str += this.generateManyToManyTypeProps(args);
str += this.generateFormulaTypes(args);
let belongsToRelations = args.belongsTo; let belongsToRelations = args.belongsTo;
if (belongsToRelations.length > 1) { if (belongsToRelations.length > 1) {

Loading…
Cancel
Save