Browse Source

Merge branch 'feat/formula'

# Conflicts:
#	packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
#	packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
pull/448/head
Pranav C 3 years ago
parent
commit
3d3a0c8eb9
  1. 2
      README.md
  2. 2
      packages/nc-gui/components/project/functionTab/functionQuery.vue
  3. 625
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  4. 280
      packages/nc-gui/components/project/spreadsheet/components/editColumn/formulaOptions.vue
  5. 46
      packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue
  6. 2
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue
  7. 10
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  8. 26
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/formulaCell.vue
  9. 36
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  10. 8
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  11. 1
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  12. 2
      packages/nc-gui/components/project/spreadsheet/xcTable.vue
  13. 2
      packages/nc-gui/components/project/sqlClient.vue
  14. 2
      packages/nc-gui/components/project/tableTabs/columns.vue
  15. 2
      packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue
  16. 79
      packages/nc-gui/helpers/NcAutocompleteTree.js
  17. 50
      packages/nc-gui/helpers/formulaList.js
  18. 66
      packages/nc-gui/helpers/index.js
  19. 4
      packages/nc-gui/helpers/sqlUi/MssqlUi.js
  20. 4
      packages/nc-gui/helpers/sqlUi/MysqlUi.js
  21. 4
      packages/nc-gui/helpers/sqlUi/OracleUi.js
  22. 4
      packages/nc-gui/helpers/sqlUi/PgUi.js
  23. 0
      packages/nc-gui/helpers/sqlUi/SqlUiFactory.js
  24. 6
      packages/nc-gui/helpers/sqlUi/SqliteUi.js
  25. 6
      packages/nc-gui/helpers/sqlUi/index.js
  26. 4
      packages/nc-gui/layouts/empty.vue
  27. 5
      packages/nc-gui/package-lock.json
  28. 1
      packages/nc-gui/package.json
  29. 2
      packages/nc-gui/pages/index.vue
  30. 29
      packages/nc-gui/plugins/ncApis/gqlApi.js
  31. 2
      packages/nc-gui/plugins/ncApis/restApi.js
  32. 7
      packages/nocodb/package-lock.json
  33. 1
      packages/nocodb/package.json
  34. 77
      packages/nocodb/src/__tests__/formula.test.ts
  35. 2
      packages/nocodb/src/interface/XcDynamicChanges.ts
  36. 31
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts
  37. 84
      packages/nocodb/src/lib/dataMapper/lib/sql/formulaQueryBuilderFromString.ts
  38. 28
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/commonFns.ts
  39. 57
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mssql.ts
  40. 30
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mysql.ts
  41. 24
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/pg.ts
  42. 39
      packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/sqlite.ts
  43. 46
      packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts
  44. 2
      packages/nocodb/src/lib/noco/NcProjectBuilder.ts
  45. 26
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  46. 10
      packages/nocodb/src/lib/noco/common/BaseModel.ts
  47. 34
      packages/nocodb/src/lib/noco/common/helpers/addErrorOnColumnDeleteInFormula.ts
  48. 43
      packages/nocodb/src/lib/noco/common/helpers/jsepTreeToFormula.ts
  49. 37
      packages/nocodb/src/lib/noco/common/helpers/updateColumnNameInFormula.ts
  50. 79
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  51. 118
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts
  52. 17
      packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts
  53. 52
      packages/nocodb/src/lib/sqlMgr/code/routers/xc-ts/SwaggerXc.ts
  54. 2
      packages/nocodb/src/plugins/mino/Minio.ts

2
README.md

@ -13,7 +13,7 @@ Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadshe
[![Build Status](https://travis-ci.org/dwyl/esta.svg?branch=master)](https://travis-ci.com/github/NocoDB/NocoDB)
[![Node version](https://badgen.net/npm/node/next)](http://nodejs.org/download/)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/NocoDB.svg?style=social&label=Follow%20%40NocoDB)](https://twitter.com/NocoDB)
<img src="https://static.scarf.sh/a.png?x-pxid=c12a77cc-855e-4602-8a0f-614b2d0da56a" />
</div>
<p align="center">

2
packages/nc-gui/components/project/functionTab/functionQuery.vue

@ -75,7 +75,7 @@ import { mapGetters, mapActions } from 'vuex'
import MonacoEditor from '../../monaco/Monaco'
import dlgLabelSubmitCancel from '../../utils/dlgLabelSubmitCancel'
import { SqlUI } from '../../../helpers/SqlUiFactory'
import { SqlUI } from '../../../helpers/sqlUi/SqlUiFactory'
export default {
components: { MonacoEditor, dlgLabelSubmitCancel },

625
packages/nc-gui/components/project/spreadsheet/components/editColumn.vue

@ -36,299 +36,313 @@
@input="newColumn.altered = newColumn.altered || 8"
/>
</v-col>
<div
<v-container
fluid
:class="{
editDisabled :isEditDisabled
}"
>
<v-col v-if="relation" cols="12">
<div class="caption">
<p class="mb-1">
Foreign Key
</p>
<v-icon small class="mt-n1">
mdi-table
</v-icon>
<span class="text-capitalize font-weight-bold body-1"> {{ relation._rtn }}</span>
<v-icon
v-ge="['columns','fk-delete']"
small
class="ml-3 mt-n1"
color="error"
@click="deleteRelation('showDialog', column)"
>
mdi-delete-forever
</v-icon>
<span v-if="relation.type=== 'virtual'" class="caption">(v)</span>
</div>
</v-col>
<template v-else>
<v-col cols="12">
<v-autocomplete
v-model="newColumn.uidt"
hide-details
item-value="name"
item-text="name"
class="caption ui-type"
:class="{'primary lighten-5' : newColumn.uidt }"
label="Column type"
dense
outlined
:items="uiTypes"
@change="onUiTypeChange"
>
<template #selection="{item}">
<div>
<v-icon color="grey darken-4" small class="mr-1">
{{ item.icon }}
</v-icon>
<span class="caption grey--text text--darken-4"> {{ item.name }}</span>
</div>
</template>
<v-row>
<v-col v-if="relation" cols="12">
<div class="caption">
<p class="mb-1">
Foreign Key
</p>
<v-icon small class="mt-n1">
mdi-table
</v-icon>
<span class="text-capitalize font-weight-bold body-1"> {{ relation._rtn }}</span>
<v-icon
v-ge="['columns','fk-delete']"
small
class="ml-3 mt-n1"
color="error"
@click="deleteRelation('showDialog', column)"
>
mdi-delete-forever
</v-icon>
<span v-if="relation.type=== 'virtual'" class="caption">(v)</span>
</div>
</v-col>
<template v-else>
<v-col cols="12">
<v-autocomplete
v-model="newColumn.uidt"
hide-details
item-value="name"
item-text="name"
class="caption ui-type"
:class="{'primary lighten-5' : newColumn.uidt }"
label="Column type"
dense
outlined
:items="uiTypes"
@change="onUiTypeChange"
>
<template #selection="{item}">
<div>
<v-icon color="grey darken-4" small class="mr-1">
{{ item.icon }}
</v-icon>
<span class="caption grey--text text--darken-4"> {{ item.name }}</span>
</div>
</template>
<template #item="{item}">
<div class="caption">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.name }}
</div>
</template>
</v-autocomplete>
<template #item="{item}">
<div class="caption">
<v-icon small class="mr-1">
{{ item.icon }}
</v-icon>
{{ item.name }}
</div>
</template>
</v-autocomplete>
<!-- <v-list dense max-height="calc(100vh - 300px)" style="overflow: auto">-->
<!-- <v-list-item v-for="item in uiTypes" @click.stop :key="item">-->
<!-- <span class="caption">{{ item }}</span>-->
<!-- </v-list-item>-->
<!-- </v-list>-->
</v-col>
<template v-if="newColumn.uidt !== 'Formula'">
<v-col
v-if="isLookup"
cols="12"
>
<lookup-options
ref="lookup"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/>
</v-col>
<v-col
v-if="isLinkToAnotherRecord"
cols="12"
>
<linked-to-another-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<v-col
v-if="isRelation"
cols="12"
>
<relation-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:is-m-s-s-q-l="isMSSQL"
:is-s-q-lite="isSQLite"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<v-col v-if="isSelect" cols="12">
<custom-select-options
v-model="newColumn.dtxp"
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<template v-if="newColumn.uidt !== 'Formula'">
<v-col
v-if="isLookup"
cols="12"
>
<lookup-options
ref="lookup"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/>
</v-col>
<v-col
v-if="isLinkToAnotherRecord"
cols="12"
>
<linked-to-another-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<v-col
v-if="isRelation"
cols="12"
>
<relation-options
ref="relation"
:column="newColumn"
:nodes="nodes"
:is-m-s-s-q-l="isMSSQL"
:is-s-q-lite="isSQLite"
@onColumnSelect="onRelColumnSelect"
/>
</v-col>
<template v-if="newColumn.cn && newColumn.uidt && !isLinkToAnotherRecord && !isLookup">
<v-col cols="12">
<v-container fluid class="wrapper">
<v-row>
<v-col cols="12">
<div class="d-flex justify-space-between caption">
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.rqd"
:disabled="newColumn.pk || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="NN"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">NN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Not Null</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.pk"
:disabled="!sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="PK"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">PK</span>
</template>
</v-checkbox>
</div>
</template>
<span>Primary Key</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.ai"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="AI"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AI</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Increment</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.un"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">UN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Unsigned</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.au"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled=" sqlUi.colPropAuDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AU</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Update Timestamp</span>
</v-tooltip>
</div>
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="newColumn.dt"
hide-details
class="caption data-type"
label="Type in Database"
dense
outlined
:items="dataTypes"
@change="onDataTypeChange"
/>
</v-col>
<v-col :cols="sqlUi.showScale(newColumn) && !isSelect ? 6 : 12">
<v-text-field
v-if="!isSelect"
v-model="newColumn.dtxp"
dense
:disabled="sqlUi.getDefaultLengthIsDisabled(newColumn.dt) || !sqlUi.columnEditable(newColumn)"
class="caption"
label="Length / Values"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col v-if="sqlUi.showScale(newColumn)" :cols="isSelect ?12 : 6">
<v-text-field
v-model="newColumn.dtxs"
dense
:disabled=" !sqlUi.columnEditable(newColumn)"
class="caption"
label="Scale"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newColumn.cdf"
label="Default value"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)"
persistent-hint
rows="3"
outlined
dense
class="caption"
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
</v-row>
</v-container>
<v-col v-if="isSelect" cols="12">
<custom-select-options
v-model="newColumn.dtxp"
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<template v-if="newColumn.cn && newColumn.uidt && !isLinkToAnotherRecord && !isLookup">
<v-col cols="12">
<v-container fluid class="wrapper">
<v-row>
<v-col cols="12">
<div class="d-flex justify-space-between caption">
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.rqd"
:disabled="newColumn.pk || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="NN"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">NN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Not Null</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.pk"
:disabled="!sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="PK"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">PK</span>
</template>
</v-checkbox>
</div>
</template>
<span>Primary Key</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.ai"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
class="mr-2 mt-0"
dense
hide-details
label="AI"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AI</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Increment</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.un"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">UN</span>
</template>
</v-checkbox>
</div>
</template>
<span>Unsigned</span>
</v-tooltip>
<v-tooltip bottom z-index="99999">
<template #activator="{on}">
<div v-on="on">
<v-checkbox
v-model="newColumn.au"
class="mr-2 mt-0"
dense
hide-details
label="UN"
:disabled=" sqlUi.colPropAuDisabled(newColumn) || !sqlUi.columnEditable(newColumn)"
@input="newColumn.altered = newColumn.altered || 2"
>
<template #label>
<span class="caption font-weight-bold">AU</span>
</template>
</v-checkbox>
</div>
</template>
<span>Auto Update Timestamp</span>
</v-tooltip>
</div>
</v-col>
<v-col cols="12">
<v-autocomplete
v-model="newColumn.dt"
hide-details
class="caption data-type"
label="Type in Database"
dense
outlined
:items="dataTypes"
@change="onDataTypeChange"
/>
</v-col>
<v-col :cols="sqlUi.showScale(newColumn) && !isSelect ? 6 : 12">
<v-text-field
v-if="!isSelect"
v-model="newColumn.dtxp"
dense
:disabled="sqlUi.getDefaultLengthIsDisabled(newColumn.dt) || !sqlUi.columnEditable(newColumn)"
class="caption"
label="Length / Values"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col v-if="sqlUi.showScale(newColumn)" :cols="isSelect ?12 : 6">
<v-text-field
v-model="newColumn.dtxs"
dense
:disabled=" !sqlUi.columnEditable(newColumn)"
class="caption"
label="Scale"
outlined
hide-details
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newColumn.cdf"
label="Default value"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)"
persistent-hint
rows="3"
outlined
dense
class="caption"
@input="newColumn.altered = newColumn.altered || 2"
/>
</v-col>
</v-row>
</v-container>
</v-col>
</template>
</template>
</template>
<template v-else>
<v-col cols12>
<v-autocomplete
<template v-else>
<v-col cols="12">
<formula-options
ref="formula"
:column="newColumn"
:nodes="nodes"
:meta="meta"
:is-s-q-lite="isSQLite"
:alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL"
:sql-ui="sqlUi"
v-on="$listeners"
/>
<!-- <v-autocomplete
label="Formula"
hide-details
class="caption formula-type"
@ -337,23 +351,24 @@
:items="formulas"
>
<template #item="{item}">
<span class="green--text text--darken-2 caption font-weight-regular">{{ item }}</span>
<span class="green&#45;&#45;text text&#45;&#45;darken-2 caption font-weight-regular">{{ item }}</span>
</template>
</v-autocomplete>
</v-col>
</v-autocomplete>-->
</v-col>
</template>
</template>
</template>
<div class="disabled-info" :class="{'d-none':!isEditDisabled}">
<v-alert dense type="warning" icon="info" class="caption mx-2" outlined>
This spreadsheet is connected to an SQLite DB.<br>
For production please see <a
href="https://github.com/nocodb/nocodb#production-setup"
target="_blank"
>here</a>.
</v-alert>
</div>
</div>
<div class="disabled-info" :class="{'d-none':!isEditDisabled}">
<v-alert dense type="warning" icon="info" class="caption mx-2" outlined>
This spreadsheet is connected to an SQLite DB.<br>
For production please see <a
href="https://github.com/nocodb/nocodb#production-setup"
target="_blank"
>here</a>.
</v-alert>
</div>
</v-row>
</v-container>
</v-row>
</v-container>
</v-form>
@ -368,18 +383,25 @@
</template>
<script>
import FormulaOptions from '@/components/project/spreadsheet/components/editColumn/formulaOptions'
import LookupOptions from '@/components/project/spreadsheet/components/editColumn/lookupOptions'
import { uiTypes } from '@/components/project/spreadsheet/helpers/uiTypes'
import CustomSelectOptions from '@/components/project/spreadsheet/components/editColumn/customSelectOptions'
import RelationOptions from '@/components/project/spreadsheet/components/editColumn/relationOptions'
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel'
import LinkedToAnotherOptions from '@/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions'
import { SqliteUi } from '@/helpers/SqliteUi'
import { MssqlUi } from '@/helpers/MssqlUi'
import { SqliteUi, MssqlUi } from '@/helpers/sqlUi'
export default {
name: 'EditColumn',
components: { LookupOptions, LinkedToAnotherOptions, DlgLabelSubmitCancel, RelationOptions, CustomSelectOptions },
components: {
FormulaOptions,
LookupOptions,
LinkedToAnotherOptions,
DlgLabelSubmitCancel,
RelationOptions,
CustomSelectOptions
},
props: {
nodes: Object,
sqlUi: [Object, Function],
@ -393,9 +415,9 @@ export default {
valid: false,
relationDeleteDlg: false,
newColumn: {},
uiTypes,
uiTypes
// dataTypes: [],
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()']
}),
computed: {
isEditDisabled() {
@ -476,9 +498,9 @@ export default {
return
}
try {
if (this.newColumn.uidt === 'Formula') {
return this.$toast.info('Coming Soon...').goAway(3000)
}
// if (this.newColumn.uidt === 'Formula') {
// return this.$toast.info('Coming Soon...').goAway(3000)
// }
if (this.isLinkToAnotherRecord && this.$refs.relation) {
await this.$refs.relation.saveRelation()
@ -487,6 +509,9 @@ export default {
if (this.isLookup && this.$refs.lookup) {
return await this.$refs.lookup.save()
}
if (this.newColumn.uidt === 'Formula' && this.$refs.formula) {
return await this.$refs.formula.save()
}
this.newColumn.tn = this.nodes.tn
this.newColumn._cn = this.newColumn.cn

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

@ -0,0 +1,280 @@
<template>
<div class="formula-wrapper">
<v-menu
v-model="autocomplete"
bottom
offset-y
nudge-bottom="-25px"
allow-overflow
>
<template #activator="_args">
<!-- todo: autocomplete based on available functions and metadata -->
<!-- <v-tooltip color="info" right>-->
<!-- <template #activator="{on}">-->
<v-text-field
ref="input"
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)]"
autocomplete="off"
@input="handleInputDeb"
@keydown.down.prevent="suggestionListDown"
@keydown.up.prevent="suggestionListUp"
@keydown.enter.prevent="selectText"
/>
<!-- </template>-->
<!-- <span class="caption">Example: AVG(column1, column2, column3)</span>-->
<!-- </v-tooltip>-->
</template>
<v-list v-if="suggestion" ref="sugList" dense max-height="50vh" style="overflow: auto">
<v-list-item-group
v-model="selected"
color="primary"
>
<v-list-item
v-for="(it,i) in suggestion"
:key="i"
ref="sugOptions"
dense
selectable
@mousedown.prevent="appendText(it)"
>
<span
class="caption"
:class="{
'primary--text text--lighten-2 font-weight-bold': it.type ==='function'
}"
>{{ it.text }}<span v-if="it.type ==='function'">(...)</span></span>
</v-list-item>
</v-list-item-group>
</v-list>
</v-menu>
</div>
</template>
<script>
import NcAutocompleteTree from '@/helpers/NcAutocompleteTree'
import { getWordUntilCaret, insertAtCursor } from '@/helpers'
import debounce from 'debounce'
import jsep from 'jsep'
import formulaList, { validations } from '../../../../../helpers/formulaList'
export default {
name: 'FormulaOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias', 'value', 'sqlUi'],
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()']
availableFunctions: formulaList,
availableBinOps: ['+', '-', '*', '/', '>', '==', '<=', '>=', '!='],
autocomplete: false,
suggestion: null,
wordToComplete: '',
selected: 0,
tooltip: true
}),
computed: {
suggestionsList() {
console.log(this)
const unsupportedFnList = this.sqlUi.getUnsupportedFnList()
return [
...this.availableFunctions.filter(fn => !unsupportedFnList.includes(fn)).map(fn => ({
text: fn,
type: 'function'
})),
...this.meta.columns.map(c => ({ text: c._cn, type: 'column', c })),
...this.availableBinOps.map(op => ({ text: op, type: 'op' }))
]
},
acTree() {
const ref = new NcAutocompleteTree()
for (const sug of this.suggestionsList) {
ref.add(sug)
}
return ref
}
},
created() {
this.formula = this.value ? { ...this.value } : {}
},
methods: {
async save() {
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]))
meta.v.push({
_cn: this.alias,
formula: {
...this.formula,
tree: jsep(this.formula.value)
}
})
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 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(', ')
}
return true
} 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`)
}
const validation = validations[pt.callee.name] && validations[pt.callee.name].validation
if (validation && validation.args) {
if (validation.args.rqd !== undefined && validation.args.rqd !== pt.arguments.length) {
arr.push(`'${pt.callee.name}' required ${validation.args.rqd} arguments`)
} else if (validation.args.min !== undefined && validation.args.min > pt.arguments.length) {
arr.push(`'${pt.callee.name}' required minimum ${validation.args.min} arguments`)
} else if (validation.args.max !== undefined && validation.args.max < pt.arguments.length) {
arr.push(`'${pt.callee.name}' required maximum ${validation.args.max} arguments`)
}
}
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
},
appendText(it) {
const text = it.text
const len = this.wordToComplete.length
if (it.type === 'function') {
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text + '()', len, 1))
} else {
this.$set(this.formula, 'value', insertAtCursor(this.$refs.input.$el.querySelector('input'), text, len))
}
},
_handleInputDeb: debounce(async function(self) {
await self.handleInput()
}, 250),
handleInputDeb() {
this._handleInputDeb(this)
},
handleInput() {
this.selected = 0
// const $fakeDiv = this.$refs.fakeDiv
this.suggestion = null
const query = getWordUntilCaret(this.$refs.input.$el.querySelector('input')) // this.formula.value
// if (query !== '') {
const parts = query.split(/\W+/)
this.wordToComplete = parts.pop()
// if (this.wordToComplete !== '') {
// get best match using popularity
this.suggestion = this.acTree.complete(this.wordToComplete)
this.autocomplete = !!this.suggestion.length
// } else {
// // $span.textContent = '' // clear ghost span
// }
// } else {
// this.autocomplete = false
// // $time.textContent = ''
// // $span.textContent = '' // clear ghost span
// }
},
selectText() {
if (this.selected > -1 && this.selected < this.suggestion.length) {
this.appendText(this.suggestion[this.selected])
this.autocomplete = false
}
},
suggestionListDown() {
this.selected = ++this.selected % this.suggestion.length
this.scrollToSelectedOption()
},
suggestionListUp() {
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>
<style scoped lang="scss">
</style>

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

@ -34,6 +34,19 @@
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"
:sql-ui="sqlUi"
/>
</v-col>
</v-row>
</v-container>
</v-form>
@ -41,14 +54,17 @@
</template>
<script>
import FormulaOptions from '@/components/project/spreadsheet/components/editColumn/formulaOptions'
export default {
name: 'EditVirtualColumn',
components: {},
components: { FormulaOptions },
props: {
nodes: Object,
meta: Object,
value: Boolean,
column: Object
column: Object,
sqlUi: [Function, Object]
},
data: () => ({
valid: false,
@ -71,21 +87,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
>

10
packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue

@ -63,11 +63,17 @@
:column="column"
v-on="$listeners "
/>
<formula-cell
v-else-if="formula"
:row="row"
:column="column"
/>
</v-lazy>
</div>
</template>
<script>
import FormulaCell from '@/components/project/spreadsheet/components/virtualCell/formulaCell'
import hasManyCell from '@/components/project/spreadsheet/components/virtualCell/hasManyCell'
import LookupCell from '@/components/project/spreadsheet/components/virtualCell/lookupCell'
import manyToManyCell from '@/components/project/spreadsheet/components/virtualCell/manyToManyCell'
@ -78,6 +84,7 @@ import belongsToCell from '@/components/project/spreadsheet/components/virtualCe
export default {
name: 'VirtualCell',
components: {
FormulaCell,
LookupCell,
belongsToCell,
manyToManyCell,
@ -119,6 +126,9 @@ export default {
},
lookup() {
return this.column && this.column.lk
},
formula() {
return this.column && this.column.formula
}
},
methods: {

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

@ -0,0 +1,26 @@
<template>
<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>
export default {
name: 'FormulaCell',
props: ['column', 'row']
}
</script>
<style scoped>
</style>

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

@ -11,6 +11,9 @@
<v-icon v-else-if="column.mm" color="pink" x-small class="mr-1" v-on="on">
mdi-table-network
</v-icon>
<v-icon v-else-if="column.formula" x-small class="mr-1" v-on="on">
mdi-math-integral
</v-icon>
<template v-else-if="column.lk">
<v-icon v-if="column.lk.type === 'hm'" color="warning" x-small class="mr-1" v-on="on">
mdi-table-column-plus-before
@ -24,7 +27,6 @@
</template>
<span class="name flex-grow-1" :title="column._cn" v-on="on" v-html="alias">
<span v-if="column.rqd" class="error--text text--lighten-1" v-on="on">&nbsp;*</span>
</span>
</template>
@ -107,6 +109,7 @@
:edit-column="true"
:column="column"
:meta="meta"
:sql-ui="sqlUi"
v-on="$listeners"
/>
</v-menu>
@ -118,7 +121,7 @@ import EditVirtualColumn from '@/components/project/spreadsheet/components/editV
export default {
name: 'VirtualHeaderCell',
components: { EditVirtualColumn },
props: ['column', 'nodes', 'meta', 'isForm', 'isPublicView'],
props: ['column', 'nodes', 'meta', 'isForm', 'isPublicView', 'sqlUi'],
data: () => ({
columnDeleteDialog: false,
editColumnMenu: false
@ -199,6 +202,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 ''
}
@ -252,9 +257,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()
}

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

@ -461,7 +461,6 @@
<script>
import DebugMetas from '@/components/project/spreadsheet/components/debugMetas'
import { SqlUI } from '@/helpers/SqlUiFactory'
import { mapActions } from 'vuex'
import AdditionalFeatures from '@/components/project/spreadsheet/overlay/additinalFeatures'
@ -477,6 +476,7 @@ import spreadsheet from '@/components/project/spreadsheet/mixins/spreadsheet'
import LockMenu from '@/components/project/spreadsheet/components/lockMenu'
import ExpandedForm from '@/components/project/spreadsheet/components/expandedForm'
import Pagination from '@/components/project/spreadsheet/components/pagination'
import { SqlUI } from '~/helpers/sqlUi'
import ColumnFilter from '~/components/project/spreadsheet/components/columnFilterMenu'
export default {
@ -803,9 +803,13 @@ export default {
}
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]: oldRow[column._cn] })
this.$set(this.data[row], 'row', { ...rowObj, ...newData })
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.`, {
position: 'bottom-center'

1
packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue

@ -34,6 +34,7 @@
:column="col"
:nodes="nodes"
:meta="meta"
:sql-ui="sqlUi"
:is-public-view="isPublicView"
@saved="onNewColCreation"
/>

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

@ -172,7 +172,7 @@
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
// import EditableCell from "@/components/project/spreadsheet/editableCell";
import { SqlUI } from '@/helpers/SqlUiFactory'
import { SqlUI } from '@/helpers/sqlUi'
import FieldsMenu from '@/components/project/spreadsheet/components/fieldsMenu'
import SortListMenu from '@/components/project/spreadsheet/components/sortListMenu'
import ColumnFilterMenu from '@/components/project/spreadsheet/components/columnFilterMenu'

2
packages/nc-gui/components/project/sqlClient.vue

@ -325,7 +325,7 @@ import { VueTreeList, Tree, TreeNode } from 'vue-tree-list'
import { Splitpanes, Pane } from 'splitpanes'
import sqlRightClickOptions from '../../helpers/sqlRightClickOptions'
import dlgLabelSubmitCancel from '../utils/dlgLabelSubmitCancel.vue'
import { SqlUI } from '../../helpers/SqlUiFactory'
import { SqlUI } from '../../helpers/sqlUi/SqlUiFactory'
import Utils from '../../helpers/Utils'

2
packages/nc-gui/components/project/tableTabs/columns.vue

@ -713,7 +713,7 @@ import uiTypes from '@/components/project/spreadsheet/helpers/uiTypes'
import addRelationDlg from '../dlgs/dlgAddRelation.vue'
import dlgLabelSubmitCancel from '../../utils/dlgLabelSubmitCancel.vue'
import { SqlUI } from '../../../helpers/SqlUiFactory'
import { SqlUI } from '../../../helpers/sqlUi/SqlUiFactory'
import jsonToColumn from './columnActions/jsonToColumn'
// const {path} = require("electron").remote.require(

2
packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue

@ -222,7 +222,7 @@
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
// import Table from '@/components/project/table'
// import EditableCell from "@/components/project/spreadsheet/editableCell";
import { SqlUI } from '@/helpers/SqlUiFactory'
import { SqlUI } from '@/helpers/sqlUi/SqlUiFactory'
import FieldsMenu from '@/components/project/spreadsheet/components/fieldsMenu'
import SortListMenu from '@/components/project/spreadsheet/components/sortListMenu'
import ColumnFilterMenu from '@/components/project/spreadsheet/components/columnFilterMenu'

79
packages/nc-gui/helpers/NcAutocompleteTree.js

@ -0,0 +1,79 @@
// ref : https://medium.com/weekly-webtips/js-implementing-auto-complete-f4c5a5d5c009
class NcAutocompleteTree {
constructor() {
this.trie = null
this.suggestions = []
}
newNode() {
return {
isLeaf: false,
children: {}
}
}
add(word) {
if (!this.trie) {
this.trie = this.newNode()
}
let root = this.trie
for (const letter of word.text.toLowerCase()) {
if (!(letter in root.children)) {
root.children[letter] = this.newNode()
}
root = root.children[letter]
}
root.value = root.value || []
root.value.push(word)
}
find(word) {
let root = this.trie
for (const letter of word) {
if (letter in root.children) {
root = root.children[letter]
} else {
return null // if not found return null
}
}
return root // return the root where it ends search
}
traverse(root) {
if (root.value && root.value.length) {
this.suggestions.push(...root.value)
}
for (const letter in root.children) {
this.traverse(root.children[letter])
}
}
complete(word, CHILDREN = null) {
this.suggestions = []
const root = this.find(word.toLowerCase())
if (!root) {
return this.suggestions
} // cannot suggest anything
const children = root.children
let spread = 0
for (const letter in children) {
this.traverse(children[letter], word + letter)
spread++
if (CHILDREN && spread === CHILDREN) {
break
}
}
return this.suggestions
}
}
export default NcAutocompleteTree

50
packages/nc-gui/helpers/formulaList.js

@ -0,0 +1,50 @@
const validations = {
AVG: {
validation: {
args: { min: 1 }
}
},
ADD: {
validation: {
args: { min: 1 }
}
},
CONCAT: {
validation: { args: { min: 1 } }
},
TRIM: {
validation: { args: { min: 1 } }
},
UPPER: {
validation: { args: { rqd: 1 } }
},
LOWER: { validation: { args: { rqd: 1 } } },
LEN: { validation: { args: { rqd: 1 } } },
MIN: { validation: { args: { min: 1 } } },
MAX: { validation: { args: { min: 1 } } },
CEILING: { validation: { args: { rqd: 1 } } },
FLOOR: { validation: { args: { rqd: 1 } } },
ROUND: { validation: { args: { rqd: 1 } } },
MOD: { validation: { args: { rqd: 2 } } },
REPEAT: { validation: { args: { rqd: 2 } } },
LOG: { validation: {} },
EXP: { validation: {} },
POWER: { validation: { args: { rqd: 2 } } },
SQRT: { validation: { args: { rqd: 1 } } },
ABS: { validation: { args: { rqd: 1 } } },
NOW: { validation: { args: { rqd: 0 } } },
REPLACE: { validation: { args: { rqd: 2 } } },
SEARCH: { validation: { args: { rqd: 2 } } },
INT: { validation: { args: { rqd: 1 } } },
RIGHT: { validation: { args: { rqd: 2 } } },
LEFT: {
validation: { args: { rqd: 1 } }
},
SUBSTR: { validation: { args: { min: 2, max: 3 } } },
MID: { validation: { args: { rqd: 1 } } },
IF: { validation: { args: { min: 2, max: 3 } } },
SWITCH: { validation: { args: { min: 3 } } }
}
export default Object.keys(validations)
export { validations }

66
packages/nc-gui/helpers/index.js

@ -12,6 +12,72 @@ export const isValidURL = (str) => {
}
export const parseIfInteger = v => /^\d+$/.test(v) ? +v : v
// ref : https://stackoverflow.com/a/11077016
export function insertAtCursor(myField, myValue, len = 0, b = 0) {
// IE support
if (document.selection) {
myField.focus()
const sel = document.selection.createRange()
sel.text = myValue
} // MOZILLA and others
else if (myField.selectionStart || myField.selectionStart == '0') {
const startPos = myField.selectionStart
const endPos = myField.selectionEnd
myField.value = myField.value.substring(0, startPos - len) +
myValue +
myField.value.substring(endPos, myField.value.length)
const pos = +startPos - len + myValue.length - b
// https://stackoverflow.com/a/4302688
if (myField.setSelectionRange) {
myField.focus()
myField.setSelectionRange(pos, pos)
} else if (myField.createTextRange) {
const range = myField.createTextRange()
range.collapse(true)
range.moveEnd('character', pos)
range.moveStart('character', pos)
range.select()
}
} else {
myField.value += myValue
}
return myField.value
}
function ReturnWord(text, caretPos) {
const index = text.indexOf(caretPos)
const preText = text.substring(0, caretPos)
if (preText.indexOf(' ') > 0) {
const words = preText.split(' ')
return words[words.length - 1] // return last word
} else {
return preText
}
}
export function getWordUntilCaret(ctrl) {
const caretPos = GetCaretPosition(ctrl)
const word = ReturnWord(ctrl.value, caretPos)
return word || ''
}
function GetCaretPosition(ctrl) {
let CaretPos = 0 // IE Support
if (document.selection) {
ctrl.focus()
const Sel = document.selection.createRange()
Sel.moveStart('character', -ctrl.value.length)
CaretPos = Sel.text.length
}
// Firefox support
else if (ctrl.selectionStart || ctrl.selectionStart == '0') {
CaretPos = ctrl.selectionStart
}
return (CaretPos)
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

4
packages/nc-gui/helpers/MssqlUi.js → packages/nc-gui/helpers/sqlUi/MssqlUi.js

@ -1321,6 +1321,10 @@ export class MssqlUi {
return dbTypes
}
}
static getUnsupportedFnList() {
return []
}
}
// module.exports = PgUiHelp;

4
packages/nc-gui/helpers/MysqlUi.js → packages/nc-gui/helpers/sqlUi/MysqlUi.js

@ -1245,6 +1245,10 @@ export class MysqlUi {
return dbTypes
}
}
static getUnsupportedFnList() {
return []
}
}
// module.exports = MysqlUiHelp;

4
packages/nc-gui/helpers/OracleUi.js → packages/nc-gui/helpers/sqlUi/OracleUi.js

@ -889,6 +889,10 @@ export class OracleUi {
}
return colProp
}
static getUnsupportedFnList() {
return []
}
}
// module.exports = PgUiHelp;

4
packages/nc-gui/helpers/PgUi.js → packages/nc-gui/helpers/sqlUi/PgUi.js

@ -1928,6 +1928,10 @@ export class PgUi {
return dbTypes
}
}
static getUnsupportedFnList() {
return []
}
}
// module.exports = PgUiHelp;

0
packages/nc-gui/helpers/SqlUiFactory.js → packages/nc-gui/helpers/sqlUi/SqlUiFactory.js

6
packages/nc-gui/helpers/SqliteUi.js → packages/nc-gui/helpers/sqlUi/SqliteUi.js

@ -1087,6 +1087,12 @@ export class SqliteUi {
return dbTypes
}
}
static getUnsupportedFnList() {
return [
'LOG', 'EXP', 'POWER', 'SQRT'
]
}
}
// module.exports = PgUiHelp;

6
packages/nc-gui/helpers/sqlUi/index.js

@ -0,0 +1,6 @@
export * from './MysqlUi'
export * from './PgUi'
export * from './MssqlUi'
export * from './OracleUi'
export * from './SqliteUi'
export * from './SqlUiFactory'

4
packages/nc-gui/layouts/empty.vue

@ -1,12 +1,12 @@
<template>
<v-app>
<snackbar />
<v-content>
<v-main>
<v-app-bar v-show="false" dark />
<v-container fluid style="">
<nuxt />
</v-container>
</v-content>
</v-main>
</v-app>
</template>

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",

2
packages/nc-gui/pages/index.vue

@ -93,7 +93,7 @@
<div v-else>
<p class="display-4 text-center font-weight-bold textColor--text text--lighten-1 welcome-msg">
<vue-typer
repeat="0"
:repeat="0"
text="Every once in a while,
a revolutionary tech comes
along that changes everything."

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

@ -38,7 +38,9 @@ export default class GqlApi {
}
generateQueryParams(params) {
if (!params) { return '(where:"")' }
if (!params) {
return '(where:"")'
}
const res = []
if ('limit' in params) {
res.push(`limit: ${params.limit}`)
@ -65,7 +67,7 @@ export default class GqlApi {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}{${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}}`
}
async gqlReadQuery(id) {
async gqlReadQuery(id, params = {}) {
return `{${this.gqlQueryReadName}(id:"${id}"){${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}}`
}
@ -89,7 +91,8 @@ export default class GqlApi {
return `\n${this.columns.map(c => c._cn).join('\n')}\n`
}
async gqlRelationReqBody(params) {
// todo: query only visible columns
async gqlRelationReqBody(params = {}) {
let str = ''
if (params.hm) {
for (const child of params.hm.split(',')) {
@ -130,6 +133,14 @@ export default class GqlApi {
}
}
}
// add formula columns to query
str += this.meta.v.reduce((arr, v) => {
if (v.formula) {
arr.push(v._cn)
}
return arr
}, []).join('\n')
return str
}
@ -162,10 +173,10 @@ export default class GqlApi {
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`, {
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: {
id, data
@ -184,10 +195,10 @@ export default class GqlApi {
return data1.data.data[this.gqlMutationUpdateName]
}
async insert(data) {
async insert(data, params = {}) {
const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `mutation create($data:${this.tableCamelized}Input){
${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}}
${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}
}`,
variables: {
data
@ -207,9 +218,9 @@ export default class GqlApi {
return data1.data.data[this.gqlMutationDeleteName]
}
async read(id) {
async read(id, params = {}) {
const data = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: await this.gqlReadQuery(id),
query: await this.gqlReadQuery(id, params),
variables: null
})
return data.data.data[this.gqlQueryReadName]

2
packages/nc-gui/plugins/ncApis/restApi.js

@ -76,7 +76,7 @@ export default class RestApi {
prevValue: oldData[colName]
}])
return res
return res.data
}
async insert(data) {

7
packages/nocodb/package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.11.15",
"version": "0.11.16",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -10025,6 +10025,11 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"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/nocodb/package.json

@ -128,6 +128,7 @@
"inflection": "^1.12.0",
"is-docker": "^2.2.1",
"js-beautify": "^1.11.0",
"jsep": "^0.4.0",
"json2csv": "^5.0.6",
"jsonfile": "^6.1.0",
"jsonwebtoken": "^8.5.1",

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

@ -0,0 +1,77 @@
import {expect} from 'chai';
import 'mocha';
import knex from '../lib/dataMapper/lib/sql/CustomKnex';
import formulaQueryBuilderFromString from "../lib/dataMapper/lib/sql/formulaQueryBuilderFromString";
process.env.TEST = 'test';
describe('{Auth, CRUD, HasMany, Belongs} Tests', () => {
let knexMysqlRef;
let knexPgRef;
let knexMssqlRef;
let knexSqliteRef;
// Called once before any of the tests in this block begin.
before(function (done) {
knexMysqlRef = knex({client:'mysql2'})
knexMssqlRef = knex({client:'mssql'})
knexPgRef = knex({client:'pg'})
knexSqliteRef = knex({client:'sqlite3'})
done()
});
after((done) => {
done();
});
describe('Formulas', function () {
it('Simple formula', function (done) {
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(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(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()
});
});
});/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Naveen MR <oof1lab@gmail.com>
* @author Pranav C Balan <pranavxc@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*"concat(city, ' _ ',city_id+country_id)", 'a'
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

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>;
}

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

@ -3,6 +3,7 @@ import _ from 'lodash';
import Validator from 'validator';
import BaseModel, {XcFilter, XcFilterWithAlias} from '../BaseModel';
import formulaQueryBuilder from "./formulaQueryBuilderFromString";
/**
@ -122,7 +123,7 @@ class BaseModelSql extends BaseModel {
return this.dbDriver(this.tnPath);
}
public get tnPath(){
public get tnPath() {
const schema = (this.dbDriver as any).searchPath?.();
const table = this.isMssql() && schema ? this.dbDriver.raw('??.??', [schema, this.tn]) : this.tn;
return table;
@ -327,8 +328,10 @@ class BaseModelSql extends BaseModel {
const driver = trx ? trx : this.dbDriver
// this.validate(data);
const response = await this._run(driver(this.tnPath).update(mappedData).where(this._wherePk(id)));
await this.afterUpdate(data, trx, cookie);
await this._run(driver(this.tnPath).update(mappedData).where(this._wherePk(id)));
const response = await this.nestedRead(id, this.defaultNestedQueryParams)
await this.afterUpdate(response, trx, cookie);
return response;
} catch (e) {
console.log(e);
@ -661,6 +664,7 @@ class BaseModelSql extends BaseModel {
try {
return await this._run(
this.$db.select(this.selectQuery('*'))
.select(...this.selectFormulas)
.conditionGraph(args?.conditionGraph)
.where(this._wherePk(id)).first()
) || {};
@ -715,13 +719,10 @@ class BaseModelSql extends BaseModel {
const {fields, where, limit, offset, sort, condition, conditionGraph = null} = this._getListArgs(args);
// if (fields === '*') {
// fields = `${this.tn}.*`;
// }
const query = this.$db
// .select(...fields.split(','))
.select(this.selectQuery(fields))
.select(...this.selectFormulas)
.xwhere(where, this.selectQuery(''))
.condition(condition, this.selectQuery(''))
.conditionGraph(conditionGraph);
@ -1742,10 +1743,11 @@ class BaseModelSql extends BaseModel {
: '*';
}
// @ts-ignore
public selectQuery(fields) {
const fieldsArr = fields.split(',');
return this.columns?.reduce((selectObj, col) => {
const selectObj = this.columns?.reduce((selectObj, col) => {
if (
!fields
|| fieldsArr.includes('*')
@ -1757,6 +1759,10 @@ class BaseModelSql extends BaseModel {
}
return selectObj;
}, {}) || '*';
return selectObj;
}
// @ts-ignore
@ -1857,6 +1863,15 @@ class BaseModelSql extends BaseModel {
}
}
protected get selectFormulas() {
return (this.virtualColumns || [])?.reduce((arr, v) => {
if (v.formula?.value && !v.formula?.error?.length) {
arr.push(formulaQueryBuilder(v.formula?.tree, v._cn, this.dbDriver))
}
return arr;
}, [])
}
}

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

@ -0,0 +1,84 @@
import jsep from 'jsep';
import mapFunctionName from "./mapFunctionName";
// todo: switch function based on database
export function formulaQueryBuilderFromString(str, alias, knex) {
return formulaQueryBuilder(jsep(str), alias, knex)
}
export default function formulaQueryBuilder(tree, alias, knex, aliasToColumn = {}) {
const fn = (pt, a?, prevBinaryOp ?) => {
const colAlias = a ? ` as ${a}` : '';
if (pt.type === 'CallExpression') {
switch (pt.callee.name) {
case 'ADD':
case 'SUM':
if (pt.arguments.length > 1) {
return fn({
type: 'BinaryExpression',
operator: '+',
left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)}
}, a, prevBinaryOp)
} else {
return fn(pt.arguments[0], a, prevBinaryOp)
}
break;
case 'AVG':
if (pt.arguments.length > 1) {
return fn({
type: 'BinaryExpression',
operator: '/',
left: {...pt, callee: {name: 'SUM'}},
right: {type: 'Literal', value: pt.arguments.length}
}, a, prevBinaryOp)
} else {
return fn(pt.arguments[0], a, prevBinaryOp)
}
break;
case 'CONCAT':
if (knex.clientType() === 'sqlite3') {
if (pt.arguments.length > 1) {
return fn({
type: 'BinaryExpression',
operator: '||',
left: pt.arguments[0],
right: {...pt, arguments: pt.arguments.slice(1)}
}, a, prevBinaryOp)
} else {
return fn(pt.arguments[0], a, prevBinaryOp)
}
}
break;
default: {
const res = mapFunctionName({pt, knex, alias, aliasToCol: aliasToColumn, fn, colAlias})
if (res) return res;
}
break
}
return knex.raw(`${pt.callee.name}(${pt.arguments.map(arg => fn(arg).toQuery()).join()})${colAlias}`)
} else if (pt.type === 'Literal') {
return knex.raw(`?${colAlias}`, [pt.value]);
} else if (pt.type === 'Identifier') {
return knex.raw(`??${colAlias}`, [aliasToColumn[pt.name] || pt.name]);
} else if (pt.type === 'BinaryExpression') {
if (pt.operator === '==') {
pt.operator = '='
}
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) {
query.wrap('(', ')')
}
return query;
}
};
return fn(tree, alias)
}

28
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/commonFns.ts

@ -0,0 +1,28 @@
import {MapFnArgs} from "../mapFunctionName";
export default {
// todo: handle default case
SWITCH: (args: MapFnArgs) => {
const count = Math.floor((args.pt.arguments.length-1) / 2)
let query = '';
const switchVal = args.fn(args.pt.arguments[0]).toQuery();
for (let i = 0; i < count; i++) {
query += args.knex.raw(`\n\tWHEN ${args.fn(args.pt.arguments[i * 2 + 1]).toQuery()} THEN ${args.fn(args.pt.arguments[i * 2 + 2]).toQuery()}`).toQuery()
}
if (args.pt.arguments.length % 2 === 0) {
query += args.knex.raw(`\n\tELSE ${args.fn(args.pt.arguments[args.pt.arguments.length - 1]).toQuery()}`).toQuery()
}
return args.knex.raw(`CASE ${switchVal} ${query}\n END${args.colAlias}`)
},
IF: (args: MapFnArgs) => {
let query = args.knex.raw(`\n\tWHEN ${args.fn(args.pt.arguments[0]).toQuery()} THEN ${args.fn(args.pt.arguments[1]).toQuery()}`).toQuery();
if (args.pt.arguments[2]) {
query += args.knex.raw(`\n\tELSE ${args.fn(args.pt.arguments[2]).toQuery()}`).toQuery()
}
return args.knex.raw(`CASE ${query}\n END${args.colAlias}`)
},
TRUE:(_args) => 1,
FALSE:(_args) => 0
}

57
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mssql.ts

@ -0,0 +1,57 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
const mssql = {
...commonFns,
MIN: (args: MapFnArgs) => {
if (args.pt.arguments.length === 1) {
return args.fn(args.pt.arguments[0])
}
let query = '';
for (const [i, arg] of Object.entries(args.pt.arguments)) {
if (+i === args.pt.arguments.length - 1) {
query += args.knex.raw(`\n\tElse ${args.fn(arg).toQuery()}`).toQuery()
} else {
query += args.knex.raw(`\n\tWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} < ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery()
}
}
return args.knex.raw(`Case ${query}\n End${args.colAlias}`)
},
MAX: (args: MapFnArgs) => {
if (args.pt.arguments.length === 1) {
return args.fn(args.pt.arguments[0])
}
let query = '';
for (const [i, arg] of Object.entries(args.pt.arguments)) {
if (+i === args.pt.arguments.length - 1) {
query += args.knex.raw(`\nElse ${args.fn(arg).toQuery()}`).toQuery()
} else {
query += args.knex.raw(`\nWhen ${args.pt.arguments.filter((_, j) => +i !== j).map(arg1 => `${args.fn(arg).toQuery()} > ${args.fn(arg1).toQuery()}`).join(' And ')} Then ${args.fn(arg).toQuery()}`).toQuery()
}
}
return args.knex.raw(`Case ${query}\n End${args.colAlias}`)
},
MOD: (pt) => {
Object.assign(pt, {
type: 'BinaryExpression',
operator: '%',
left: pt.arguments[0],
right: pt.arguments[1]
})
},
REPEAT: 'REPLICATE',
NOW: 'getdate',
SEARCH: (args: MapFnArgs) => {
args.pt.callee.name = 'CHARINDEX';
const temp = args.pt.arguments[0]
args.pt.arguments[0] = args.pt.arguments[1]
args.pt.arguments[1] = temp;
},
INT: (args: MapFnArgs) => {
return args.knex.raw(`CASE WHEN ISNUMERIC(${args.fn(args.pt.arguments[0]).toQuery()}) = 1 THEN FLOOR(${args.fn(args.pt.arguments[0]).toQuery()}) ELSE 0 END${args.colAlias}`)
},
MID:'SUBSTR',
}
export default mssql;

30
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/mysql.ts

@ -0,0 +1,30 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
const mysql2 = {
...commonFns,
LEN: 'CHAR_LENGTH',
MIN: 'LEAST',
MAX: 'GREATEST',
SEARCH: (args: MapFnArgs) => {
args.pt.callee.name = 'LOCATE';
const temp = args.pt.arguments[0]
args.pt.arguments[0] = args.pt.arguments[1]
args.pt.arguments[1] = temp;
},
INT:(args: MapFnArgs) =>{
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as SIGNED)${args.colAlias}`)
},
LEFT:(args: MapFnArgs)=> {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(args.pt.arguments[1])})${args.colAlias}`)
},
RIGHT:(args: MapFnArgs)=> {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(args.pt.arguments[1])})${args.colAlias}`)
},
MID:'SUBSTR',
}
export default mysql2;

24
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/pg.ts

@ -0,0 +1,24 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
const pg = {
...commonFns,
LEN: 'length',
MIN: 'least',
MAX: 'greatest',
CEILING: 'ceil',
ROUND: 'round',
POWER: 'pow',
SQRT: 'sqrt',
SEARCH: (args: MapFnArgs) => {
return args.knex.raw(`POSITION(${args.knex.raw(args.fn(args.pt.arguments[1]).toQuery())} in ${args.knex.raw(args.fn(args.pt.arguments[0]).toQuery())})${args.colAlias}`)
},
INT(args: MapFnArgs) {
// todo: correction
return args.knex.raw(`REGEXP_REPLACE(COALESCE(${args.fn(args.pt.arguments[0])}::character varying, '0'), '[^0-9]+|\\.[0-9]+' ,'')${args.colAlias}`)
},
MID: 'SUBSTR',
}
export default pg;

39
packages/nocodb/src/lib/dataMapper/lib/sql/functionMappings/sqlite.ts

@ -0,0 +1,39 @@
import {MapFnArgs} from "../mapFunctionName";
import commonFns from "./commonFns";
const sqlite3 = {
...commonFns,
LEN: 'LENGTH',
CEILING(args) {
return args.knex.raw(`round(${args.fn(args.pt.arguments[0])} + 0.5)${args.colAlias}`)
}, FLOOR(args) {
return args.knex.raw(`round(${args.fn(args.pt.arguments[0])} - 0.5)${args.colAlias}`)
},
MOD: (args: MapFnArgs) => {
return args.fn({
type: 'BinaryExpression',
operator: '%',
left: args.pt.arguments[0],
right: args.pt.arguments[1]
})
},
REPEAT(args: MapFnArgs) {
return args.knex.raw(`replace(printf('%.' || ${args.fn(args.pt.arguments[1])} || 'c', '/'),'/',${args.fn(args.pt.arguments[0])})${args.colAlias}`)
},
NOW: 'DATE',
SEARCH: 'INSTR',
INT(args: MapFnArgs) {
return args.knex.raw(`CAST(${args.fn(args.pt.arguments[0])} as INTEGER)${args.colAlias}`)
},
LEFT: (args: MapFnArgs) => {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},1,${args.fn(args.pt.arguments[1])})${args.colAlias}`)
},
RIGHT: (args: MapFnArgs) => {
return args.knex.raw(`SUBSTR(${args.fn(args.pt.arguments[0])},-${args.fn(args.pt.arguments[1])})${args.colAlias}`)
},
MID: 'SUBSTR',
}
export default sqlite3;

46
packages/nocodb/src/lib/dataMapper/lib/sql/mapFunctionName.ts

@ -0,0 +1,46 @@
import {XKnex} from "../../index";
import mssql from "./functionMappings/mssql";
import mysql from "./functionMappings/mysql";
import pg from "./functionMappings/pg";
import sqlite from "./functionMappings/sqlite";
import {QueryBuilder} from "knex";
export interface MapFnArgs {
pt: any,
aliasToCol: { [alias: string]: string },
knex: XKnex,
alias: string,
fn: (...args: any) => QueryBuilder,
colAlias:string
}
const mapFunctionName = (args: MapFnArgs): any => {
const name = args.pt.callee.name;
let val;
switch (args.knex.clientType()) {
case 'mysql':
case 'mysql2':
val = mysql[name] || name;
break;
case 'pg':
case 'postgre':
val = pg[name] || name;
break;
case 'mssql':
val = mssql[name] || name;
break;
case 'sqlite':
case 'sqlite3':
val = sqlite[name] || name;
break;
}
if (typeof val === 'function') {
return val(args)
} else if (typeof val === 'string') {
args.pt.callee.name = val;
}
}
export default mapFunctionName;

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':

26
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));
@ -1347,7 +1365,7 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
}
protected async getManyToManyRelations({parent = null, child = null, localMetas = null} = {}) {
protected async getManyToManyRelations({parent = null, child = null, localMetas = null} = {}): Promise<Set<any>> {
const metas = new Set<any>();
const assocMetas = new Set<any>();
@ -1474,6 +1492,8 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
this.models[meta.tn] = this.getBaseModel(meta)
}
}
return metas;
}

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

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;
}

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

@ -1497,7 +1497,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
public async onTableUpdate(changeObj: any): Promise<void> {
this.log(`onTableUpdate : '%s'`, changeObj.tn);
await super.onTableUpdate(changeObj, async ({ctx, meta}) => {
await super.onTableUpdate(changeObj, async ({ctx}) => {
const tn = changeObj.tn;
@ -1515,7 +1515,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
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(enabledModelCtx)).getString();
this.schemas[tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(enabledModelCtx)).getString();
if (oldSchema !== this.schemas[tn]) {
this.log(`onTableUpdate : Updating and taking backup of schema - '%s' table`, changeObj.tn);
@ -1530,7 +1530,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: meta.schema,
schema: this.schemas[tn],
schema_previous: JSON.stringify(previousSchemas)
}, {
title: tn
@ -1785,7 +1785,11 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}, ...Object.values(this.resolvers).map(r => r.mapResolvers(this.customResolver))]);
this.log(`initGraphqlRoute : Building graphql schema`);
const schemaStr = mergeTypeDefs([...Object.values(this.schemas).filter(Boolean), ` ${this.customResolver?.schema || ''} \n ${commonSchema}`], {
const schemaStr = mergeTypeDefs([
...Object.values(this.schemas).filter(Boolean),
` ${this.customResolver?.schema || ''} \n ${commonSchema}`,
// ...this.typesWithFormulaProps
], {
commentDescriptions: true,
forceSchemaDefinition: true,
reverseDirectives: true,
@ -1852,8 +1856,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
log(`${this.dbAlias} : ${str}`, ...args);
}
public async onManyToManyRelationCreate(parent: string, child: string, args?: any) {
await super.onManyToManyRelationCreate(parent, child, args);
public async onManyToManyRelationCreate(parent: string, child: string, args?: any): Promise<Set<any>> {
const res = await super.onManyToManyRelationCreate(parent, child, args);
for (const tn of [parent, child]) {
const meta = this.metas[tn];
const {columns, hasMany, belongsTo, manyToMany} = meta;
@ -1900,7 +1904,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
});
await this.reInitializeGraphqlEndpoint();
return res;
}
public async onManyToManyRelationDelete(parent: string, child: string, args?: any) {
@ -1972,7 +1976,68 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}
/* // todo: dump it in db
// extending types for formula column
private get typesWithFormulaProps(): string[] {
const schemas = [];
for (const meta of Object.values(this.metas)) {
const props = [];
for (const v of meta.v) {
if (!v.formula) continue
props.push(`${v._cn}: JSON`)
}
if (props.length) {
schemas.push(`type ${meta._tn} {\n${props.join('\n')}\n}`)
}
}
return schemas;
}*/
async onMetaUpdate(tn: string): Promise<void> {
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: this.schemas[tn],
schema_previous: JSON.stringify(previousSchemas)
}, {
title: tn,
type:'table'
});
}
return this.reInitializeGraphqlEndpoint();
}
}
/**

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

@ -291,7 +291,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
r._rtn = args?.tableNames?.find(t => t.tn === r.rtn)?._tn || this.getTableNameAlias(r.rtn);
r._tn = args?.tableNames?.find(t => t.tn === r.tn)?._tn || this.getTableNameAlias(r.tn);
r.enabled = true;
})
});
this.relationsCount = relations.length;
@ -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);
@ -558,7 +564,6 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
await NcHelp.executeOperations(relationRoutes, this.connectionConfig.client);
await this.getManyToManyRelations();
const swaggerDoc = {
@ -594,6 +599,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
}
await this.getManyToManyRelations();
}
@ -995,7 +1002,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 +1011,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 +1019,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 +1032,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 +1053,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 +1109,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 +1131,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 +1150,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;
@ -1744,6 +1756,94 @@ 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);
}
protected async getManyToManyRelations(args = {}): Promise<Set<any>> {
const metas:Set<any> = await super.getManyToManyRelations(args);
for (const metaObj of metas) {
const ctx = this.generateContextForTable(
metaObj.tn,
metaObj.columns,
[...metaObj.belongsTo, ...metaObj.hasMany],
metaObj.hasMany,
metaObj.belongsTo
);
const swaggerDoc = await new SwaggerXc({
dir: '', ctx: {
...ctx,
v: metaObj.v
}, filename: ''
}).getObject();
const meta = await this.xcMeta.metaGet(this.projectId, this.dbAlias, 'nc_models', {
title: metaObj.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: metaObj.tn,
type: 'table'
});
}
return metas
}
}

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

@ -76,6 +76,18 @@ abstract class BaseGqlXcTsSchema extends BaseRender {
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 {
let str = `input ${args._tn}Input { \r\n`
for (const column of args.columns) {
@ -108,7 +120,7 @@ abstract class BaseGqlXcTsSchema extends BaseRender {
protected _getMutation(args): string {
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}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}CreateBulk(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}`);
} else {
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.generateFormulaTypes(args);
let belongsToRelations = args.belongsTo;
if (belongsToRelations.length > 1) {

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

@ -63,11 +63,14 @@ class SwaggerXc extends BaseRender {
[args._tn]: {
type: 'object',
properties: {}
},
[`${args._tn}Nested`]: {
type: 'object',
properties: {}
}
};
const properties = obj[args._tn].properties;
let properties = obj[args._tn].properties;
for (const column of args.columns) {
const field: any = {};
@ -84,6 +87,35 @@ class SwaggerXc extends BaseRender {
properties[column._cn] = field;
}
properties = Object.assign(obj[`${args._tn}Nested`].properties, properties)
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.hm?._tn}`
};
field.$ref = `#/definitions/${column.hm?._tn}`
_cn = `${column.hm?._tn}List`;
} else if (column.bt) {
field.$ref = `#/definitions/${column.bt?._rtn}`
_cn = `${column.bt?._rtn}Read`;
}
properties[_cn] = field;
}
return obj;
}
@ -190,7 +222,8 @@ class SwaggerXc extends BaseRender {
{
"in": "query",
"name": "sort",
"description": "Comma separated sort fields",
"description":
"Comma separated sort fields",
"type": "string"
}
@ -198,7 +231,16 @@ class SwaggerXc extends BaseRender {
"responses": {
"405": {
"description": "Invalid input"
}
},
"200": {
"description": "successful operation",
"schema": {
type: "array",
items: {
"$ref": `#/definitions/${this.ctx._tn}Nested`
}
}
},
}
}
},
@ -244,7 +286,7 @@ class SwaggerXc extends BaseRender {
"200": {
"description": "successful operation",
"schema": {
"$ref": `#/definitions/${this.ctx._tn}`
"$ref": `#/definitions/${this.ctx._tn}Nested`
}
},
"400": {

2
packages/nocodb/src/plugins/mino/Minio.ts

@ -58,8 +58,6 @@ export default class Minio implements IStorageAdapter {
}
public async init(): Promise<any> {
// todo: update in ui(checkbox and number field)
this.input.port = +this.input.port || 9000;
this.input.useSSL = this.input.useSSL ==='true';

Loading…
Cancel
Save