Browse Source

feat: Lookup - column creation, deletion, duplicate validation, optimization

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/401/head
Pranav C 3 years ago
parent
commit
c128376257
  1. 38
      packages/nc-gui/components/project/spreadsheet/components/debugMetas.vue
  2. 4
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  3. 30
      packages/nc-gui/components/project/spreadsheet/components/editColumn/lookupOptions.vue
  4. 15
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  5. 15
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  6. 30
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/lookupCell.vue
  7. 49
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  8. 2
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  9. 21
      packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js
  10. 19
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  11. 21
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  12. 1
      packages/nc-gui/nuxt.config.js
  13. 34
      packages/nc-gui/plugins/ncApis/apiFactory.js
  14. 263
      packages/nc-gui/plugins/ncApis/gqlApi.js
  15. 35
      packages/nc-gui/plugins/ncApis/index.js
  16. 128
      packages/nc-gui/plugins/ncApis/restApi.js
  17. 4
      packages/nc-gui/store/project.js
  18. 11
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  19. 13
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  20. 6
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts

38
packages/nc-gui/components/project/spreadsheet/components/debugMetas.vue

@ -0,0 +1,38 @@
<template>
<div>
<v-icon small @click="metas = true">
mdi-bug-outline
</v-icon>
<v-dialog v-model="metas" max-width="800px">
<v-card>
<v-tabs height="30">
<v-tab v-for="(meta,table) in metasObj" :key="table">
<span class="caption text-capitalize">{{ table }}</span>
</v-tab>
<v-tab-item v-for="(meta,table) in metasObj" :key="table">
<monaco-json-object-editor :value="meta" style="height:80vh" />
</v-tab-item>
</v-tabs>
</v-card>
</v-dialog>
</div>
</template>
<script>
import MonacoJsonObjectEditor from '@/components/monaco/MonacoJsonObjectEditor'
export default {
name: 'DebugMetas',
components: { MonacoJsonObjectEditor },
data: () => ({ metas: false, tab: 0 }),
computed: {
metasObj() {
return this.$store.state.meta.metas || {}
}
}
}
</script>
<style scoped>
</style>

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

@ -117,6 +117,7 @@
:is-s-q-lite="isSQLite" :is-s-q-lite="isSQLite"
:alias="newColumn.cn" :alias="newColumn.cn"
:is-m-s-s-q-l="isMSSQL" :is-m-s-s-q-l="isMSSQL"
v-on="$listeners"
/> />
</v-col> </v-col>
<v-col <v-col
@ -485,7 +486,6 @@ export default {
} }
if (this.isLookup && this.$refs.lookup) { if (this.isLookup && this.$refs.lookup) {
await this.$refs.lookup.save() await this.$refs.lookup.save()
return this.$emit('saved', this.newColumn.cn)
} }
this.newColumn.tn = this.nodes.tn this.newColumn.tn = this.nodes.tn
@ -517,7 +517,7 @@ export default {
await this.$refs.relation.saveRelation() await this.$refs.relation.saveRelation()
} }
this.$emit('saved') this.$emit('saved', this.newColumn._cn)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }

30
packages/nc-gui/components/project/spreadsheet/components/editColumn/lookupOptions.vue

@ -16,7 +16,14 @@
:item-value="v => v" :item-value="v => v"
:rules="[v => !!v || 'Required']" :rules="[v => !!v || 'Required']"
dense dense
/> >
<template #item="{item}">
<span class="caption"><span class="font-weight-bold"> {{
item._tn
}}</span> <small>({{ relationNames[item.type] }})
</small></span>
</template>
</v-autocomplete>
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<v-autocomplete <v-autocomplete
@ -32,20 +39,27 @@
dense dense
:loading="loadingColumns" :loading="loadingColumns"
:item-value="v => v" :item-value="v => v"
:rules="[v => !!v || 'Required']" :rules="[v => !!v || 'Required',checkLookupExist]"
/> />
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'LookupOptions', name: 'LookupOptions',
props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'], props: ['nodes', 'column', 'meta', 'isSQLite', 'alias'],
data: () => ({ data: () => ({
lookup: {}, lookup: {},
loadingColumns: false loadingColumns: false,
relationNames: {
mm: 'Many To Many',
hm: 'Has Many',
bt: 'Belongs To'
}
}), }),
computed: { computed: {
refTables() { refTables() {
@ -68,6 +82,14 @@ export default {
} }
}, },
methods: { methods: {
checkLookupExist(v) {
return (this.lookup.table && (this.meta.v || []).every(c => !(
c.lookup &&
c.type === this.lookup.table.type &&
c.tn === this.lookup.table.tn &&
c.cn === v.cn
))) || 'Lookup already exist'
},
async onTableChange() { async onTableChange() {
this.loadingColumns = true this.loadingColumns = true
if (this.lookup.table) { if (this.lookup.table) {
@ -108,6 +130,8 @@ export default {
tn: this.nodes.tn, tn: this.nodes.tn,
meta meta
}]) }])
return this.$emit('saved', `${this.lookup.column._cn} (from ${this.lookup.table._tn})`)
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000)
} }

15
packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue

@ -95,7 +95,7 @@
</template> </template>
<script> <script>
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory' // import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems' import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems'
import ListChildItems from '@/components/project/spreadsheet/components/virtualCell/components/listChildItems' import ListChildItems from '@/components/project/spreadsheet/components/virtualCell/components/listChildItems'
import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/itemChip' import ItemChip from '~/components/project/spreadsheet/components/virtualCell/components/itemChip'
@ -143,10 +143,15 @@ export default {
}, },
// todo : optimize // todo : optimize
parentApi() { parentApi() {
return this.parentMeta && this.parentMeta._tn return this.parentMeta && this.$ncApis.get({
? ApiFactory.create(this.$store.getters['project/GtrProjectType'], env: this.nodes.env,
this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this, this.parentMeta) dbAlias: this.nodes.dbAlias,
: null table: this.parentMeta.tn
})
// return this.parentMeta && this.parentMeta._tn
// ? ApiFactory.create(this.$store.getters['project/GtrProjectType'],
// this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this, this.parentMeta)
// : null
}, },
parentId() { parentId() {
return this.pid ?? (this.value && this.parentMeta && this.parentMeta.columns.filter(c => c.pk).map(c => this.value[c._cn]).join('___')) return this.pid ?? (this.value && this.parentMeta && this.parentMeta.columns.filter(c => c.pk).map(c => this.value[c._cn]).join('___'))

15
packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue

@ -121,7 +121,7 @@
</template> </template>
<script> <script>
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory' // import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel' import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel'
import Pagination from '@/components/project/spreadsheet/components/pagination' import Pagination from '@/components/project/spreadsheet/components/pagination'
import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems' import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems'
@ -178,10 +178,15 @@ export default {
}, },
// todo : optimize // todo : optimize
childApi() { childApi() {
return this.childMeta && this.childMeta._tn return this.childMeta && this.$ncApis.get({
? ApiFactory.create(this.$store.getters['project/GtrProjectType'], env: this.nodes.env,
this.childMeta && this.childMeta._tn, this.childMeta && this.childMeta.columns, this, this.childMeta) dbAlias: this.nodes.dbAlias,
: null table: this.childMeta.tn
})
// return this.childMeta && this.childMeta._tn
// ? ApiFactory.create(this.$store.getters['project/GtrProjectType'],
// this.childMeta && this.childMeta._tn, this.childMeta && this.childMeta.columns, this, this.childMeta)
// : null
}, },
childPrimaryCol() { childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn

30
packages/nc-gui/components/project/spreadsheet/components/virtualCell/lookupCell.vue

@ -35,7 +35,7 @@
</template> </template>
<script> <script>
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory' // import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import ItemChip from '@/components/project/spreadsheet/components/virtualCell/components/itemChip' import ItemChip from '@/components/project/spreadsheet/components/virtualCell/components/itemChip'
import ListChildItemsModal import ListChildItemsModal
from '@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal' from '@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal'
@ -60,21 +60,21 @@ export default {
computed: { computed: {
// todo : optimize // todo : optimize
lookupApi() { lookupApi() {
// return this.$ncApis({ return this.column && this.$ncApis.get({
// env: this.nodes.env, env: this.nodes.env,
// dbAlias: this.nodes.dbAlias, dbAlias: this.nodes.dbAlias,
// table: this.column.tn table: this.column.tn
// }) })
return this.lookUpMeta && this.lookUpMeta._tn // return this.lookUpMeta && this.lookUpMeta._tn
? ApiFactory.create( // ? ApiFactory.create(
this.$store.getters['project/GtrProjectType'], // this.$store.getters['project/GtrProjectType'],
this.lookUpMeta._tn, // this.lookUpMeta._tn,
this.lookUpMeta.columns, // this.lookUpMeta.columns,
this, // this,
this.lookUpMeta // this.lookUpMeta
) // )
: null // : null
}, },
lookUpMeta() { lookUpMeta() {
return this.$store.state.meta.metas[this.column.tn] return this.$store.state.meta.metas[this.column.tn]

49
packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue

@ -110,7 +110,7 @@
</template> </template>
<script> <script>
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory' // import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory'
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel' import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel'
import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems' import ListItems from '@/components/project/spreadsheet/components/virtualCell/components/listItems'
import ListChildItems from '@/components/project/spreadsheet/components/virtualCell/components/listChildItems' import ListChildItems from '@/components/project/spreadsheet/components/virtualCell/components/listChildItems'
@ -173,27 +173,38 @@ export default {
}, },
// todo : optimize // todo : optimize
childApi() { childApi() {
return this.childMeta && this.childMeta._tn return this.childMeta && this.$ncApis.get({
? ApiFactory.create( env: this.nodes.env,
this.$store.getters['project/GtrProjectType'], dbAlias: this.nodes.dbAlias,
this.childMeta._tn, table: this.childMeta.tn
this.childMeta.columns, })
this, //
this.childMeta // return this.childMeta && this.childMeta._tn
) // ? ApiFactory.create(
: null // this.$store.getters['project/GtrProjectType'],
// this.childMeta._tn,
// this.childMeta.columns,
// this,
// this.childMeta
// )
// : null
}, },
// todo : optimize // todo : optimize
assocApi() { assocApi() {
return this.assocMeta && this.assocMeta._tn return this.childMeta && this.$ncApis.get({
? ApiFactory.create( env: this.nodes.env,
this.$store.getters['project/GtrProjectType'], dbAlias: this.nodes.dbAlias,
this.assocMeta._tn, table: this.assocMeta.tn
this.assocMeta.columns, })
this, // return this.assocMeta && this.assocMeta._tn
this.assocMeta // ? ApiFactory.create(
) // this.$store.getters['project/GtrProjectType'],
: null // this.assocMeta._tn,
// this.assocMeta.columns,
// this,
// this.assocMeta
// )
// : null
}, },
childPrimaryCol() { childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn

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

@ -184,6 +184,8 @@ export default {
return `'${this.column.mm._tn}' & '${this.column.mm._rtn}' have <br>many to many relation` return `'${this.column.mm._tn}' & '${this.column.mm._rtn}' have <br>many to many relation`
} else if (this.column.bt) { } else if (this.column.bt) {
return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'` return `'${this.column.bt._tn}' belongs to '${this.column.bt._rtn}'`
} else if (this.column.lookup) {
return `'${this.column._cn}' from '${this.column._tn}' (${this.column.type}))`
} }
return '' return ''
} }

21
packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js

@ -64,7 +64,7 @@ export default {
}, },
fieldList() { fieldList() {
return this.availableColumns.map((c) => { return this.availableColumns.map((c) => {
return c._cn return c.alias
}) })
}, },
realFieldList() { realFieldList() {
@ -95,10 +95,25 @@ export default {
columns = [...columns, ...this.meta.v.map(v => ({ ...v, virtual: 1 }))] columns = [...columns, ...this.meta.v.map(v => ({ ...v, virtual: 1 }))]
} }
{
const _ref = {}
columns.forEach((c) => {
if (c.virtual && c.lookup) {
c.alias = `${c._cn} (from ${c._tn})`
} else {
c.alias = c._cn
}
if (c.alias in _ref) {
c.alias += _ref[c.alias]++
} else {
_ref[c.alias] = 1
}
})
}
if (this.fieldsOrder.length) { if (this.fieldsOrder.length) {
return [...columns].sort((c1, c2) => { return [...columns].sort((c1, c2) => {
const i1 = this.fieldsOrder.indexOf(c1._cn) const i1 = this.fieldsOrder.indexOf(c1.alias)
const i2 = this.fieldsOrder.indexOf(c2._cn) const i2 = this.fieldsOrder.indexOf(c2.alias)
return (i1 === -1 ? Infinity : i1) - (i2 === -1 ? Infinity : i2) return (i1 === -1 ? Infinity : i1) - (i2 === -1 ? Infinity : i2)
}) })
} }

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

@ -59,7 +59,9 @@
relationPrimaryValue relationPrimaryValue
}}) -> {{ relationType === 'hm' ? ' Has Many ' : ' Belongs To ' }} -> {{ table }}</span> }}) -> {{ relationType === 'hm' ? ' Has Many ' : ' Belongs To ' }} -> {{ table }}</span>
<v-spacer /> <v-spacer class="h-100" @dblclick="debug=true" />
<debug-metas v-if="debug" class="mr-3" />
<lock-menu v-if="_isUIAllowed('view-type')" v-model="viewStatus.type" /> <lock-menu v-if="_isUIAllowed('view-type')" v-model="viewStatus.type" />
<x-btn tooltip="Reload view data" outlined small text @click="reload"> <x-btn tooltip="Reload view data" outlined small text @click="reload">
@ -476,7 +478,7 @@
<script> <script>
import ApiFactory from '@/components/project/spreadsheet/apis/apiFactory' import DebugMetas from '@/components/project/spreadsheet/components/debugMetas'
import { SqlUI } from '@/helpers/SqlUiFactory' import { SqlUI } from '@/helpers/SqlUiFactory'
import { mapActions } from 'vuex' import { mapActions } from 'vuex'
@ -498,6 +500,7 @@ import ColumnFilter from '~/components/project/spreadsheet/components/columnFilt
export default { export default {
name: 'RowsXcDataTable', name: 'RowsXcDataTable',
components: { components: {
DebugMetas,
Pagination, Pagination,
ExpandedForm, ExpandedForm,
LockMenu, LockMenu,
@ -525,6 +528,7 @@ export default {
showTabs: [Boolean, Number] showTabs: [Boolean, Number]
}, },
data: () => ({ data: () => ({
debug: false,
key: 1, key: 1,
dataLoaded: false, dataLoaded: false,
searchQueryVal: '', searchQueryVal: '',
@ -803,10 +807,10 @@ export default {
} }
} }
}, },
// todo: move debounce to cell since this will skip few update api call
onCellValueChangeDebounce: debounce(async function(col, row, column, self) { onCellValueChangeDebounce: debounce(async function(col, row, column, self) {
await self.onCellValueChangeFn(col, row, column) await self.onCellValueChangeFn(col, row, column)
}, 300), }, 100),
onCellValueChange(col, row, column) { onCellValueChange(col, row, column) {
this.onCellValueChangeDebounce(col, row, column, this) this.onCellValueChangeDebounce(col, row, column, this)
}, },
@ -1002,7 +1006,12 @@ export default {
return SqlUI.create(this.nodes.dbConnection) return SqlUI.create(this.nodes.dbConnection)
}, },
api() { api() {
return this.meta && this.meta._tn ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta._tn, this.meta && this.meta.columns, this, this.meta) : null return this.meta && this.$ncApis.get({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table: this.meta.tn
})
// return this.meta && this.meta._tn ? ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.meta && this.meta._tn, this.meta && this.meta.columns, this, this.meta) : null
} }
} }
} }

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

@ -16,14 +16,14 @@
</th> </th>
<th <th
v-for="(col) in availableColumns" v-for="(col) in availableColumns"
v-show="showFields[col._cn]" v-show="showFields[col.alias]"
:key="col._cn" :key="col.alias"
v-xc-ver-resize v-xc-ver-resize
class="grey-border caption font-wight-regular" class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'" :class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
:data-col="col._cn" :data-col="col.alias"
@xcresize="onresize(col._cn,$event)" @xcresize="onresize(col.alias,$event)"
@xcresizing="onXcResizing(col._cn,$event)" @xcresizing="onXcResizing(col.alias,$event)"
@xcresized="resizingCol = null" @xcresized="resizingCol = null"
> >
<!-- :style="columnsWidth[col._cn] ? `min-width:${columnsWidth[col._cn]}; max-width:${columnsWidth[col._cn]}` : ''" <!-- :style="columnsWidth[col._cn] ? `min-width:${columnsWidth[col._cn]}; max-width:${columnsWidth[col._cn]}` : ''"
@ -130,8 +130,8 @@
</td> </td>
<td <td
v-for="(columnObj,col) in availableColumns" v-for="(columnObj,col) in availableColumns"
v-show="showFields[columnObj._cn]" v-show="showFields[columnObj.alias]"
:key="row + columnObj._cn + col" :key="row + columnObj.alias"
class="cell pointer" class="cell pointer"
:class="{ :class="{
'active' : !isPublicView && selected.col === col && selected.row === row && isEditable , 'active' : !isPublicView && selected.col === col && selected.row === row && isEditable ,
@ -139,7 +139,7 @@
'text-center': isCentrallyAligned(columnObj), 'text-center': isCentrallyAligned(columnObj),
'required': isRequired(columnObj,rowObj) 'required': isRequired(columnObj,rowObj)
}" }"
:data-col="columnObj._cn" :data-col="columnObj.alias"
@dblclick="makeEditable(col,row,columnObj.ai)" @dblclick="makeEditable(col,row,columnObj.ai)"
@click="makeSelected(col,row);" @click="makeSelected(col,row);"
> >
@ -278,9 +278,6 @@ export default {
colLength() { colLength() {
return (this.availableColumns && this.availableColumns.length) || 0 return (this.availableColumns && this.availableColumns.length) || 0
}, },
// visibleColLength() {
// return (this.availableColumns && this.availableColumns.length) || 0
// },
rowLength() { rowLength() {
return (this.data && this.data.length) || 0 return (this.data && this.data.length) || 0
}, },
@ -416,7 +413,7 @@ export default {
return return
} }
if (e.key && e.key.length === 1) { if (e.key && e.key.length === 1) {
this.$set(this.data[this.selected.row].row, this.availableColumns[this.selected.col]._cn, e.key) this.$set(this.data[this.selected.row].row, this.availableColumns[this.selected.col]._cn, '')
this.editEnabled = { ...this.selected } this.editEnabled = { ...this.selected }
} }
} }

1
packages/nc-gui/nuxt.config.js

@ -55,6 +55,7 @@ export default {
'@/plugins/globalComponentLoader', '@/plugins/globalComponentLoader',
'@/plugins/globalMixin', '@/plugins/globalMixin',
'@/plugins/globalEventBus', '@/plugins/globalEventBus',
'@/plugins/ncApis',
'~/plugins/i18n.js', '~/plugins/i18n.js',
{ src: '~plugins/projectLoader.js', ssr: false } { src: '~plugins/projectLoader.js', ssr: false }
], ],

34
packages/nc-gui/plugins/ncApis/apiFactory.js

@ -0,0 +1,34 @@
import RestApi from './restApi'
import GqlApi from './gqlApi'
export default class ApiFactory {
static create(table, type, ctx) {
if (type === 'graphql') {
return new GqlApi(table, ctx)
} else if (type === 'rest') {
return new RestApi(table, ctx)
}
}
}
/**
* @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.
*
* 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/>.
*
*/

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

@ -0,0 +1,263 @@
export default class GqlApi {
constructor(table, $ctx) {
this.$ctx = $ctx
this.table = $ctx.$store.state.meta.metas[table]._tn
}
get meta() {
return this.$ctx.$store.state.meta.metas[this.$ctx.table] || {}
}
get columns() {
return this.meta.columns || []
}
// todo: - get version letter and use table alias
async list(params) {
const data = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: await this.gqlQuery(params),
variables: null
})
return data.data.data[this.gqlQueryListName]
}
async count(params) {
const data = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: this.gqlCountQuery(params),
variables: null
})
return data.data.data[this.gqlQueryCountName]
}
post(url, params) {
return this.$axios({
url: `${this.$axios.defaults.baseURL}${url}`,
method: 'post',
data: params
})
}
generateQueryParams(params) {
if (!params) { return '(where:"")' }
const res = []
if ('limit' in params) {
res.push(`limit: ${params.limit}`)
}
if ('offset' in params) {
res.push(`offset: ${params.offset}`)
}
if ('where' in params) {
res.push(`where: ${JSON.stringify(params.where)}`)
}
if ('sort' in params) {
res.push(`sort: ${JSON.stringify(params.sort)}`)
}
if (params.condition) {
res.push(`condition: ${JSON.stringify(params.condition)}`)
}
if (params.conditionGraph) {
res.push(`conditionGraph: ${JSON.stringify(JSON.stringify(params.conditionGraph))}`)
}
return `(${res.join(',')})`
}
async gqlQuery(params) {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}{${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}}`
}
gqlReadQuery(id) {
return `{${this.gqlQueryReadName}(id:"${id}"){${this.gqlReqBody}}}`
}
gqlCountQuery(params) {
return `{${this.gqlQueryCountName}${this.generateQueryParams(params)}}`
}
get gqlQueryListName() {
return `${this.meta._tn}List`
}
get gqlQueryReadName() {
return `${this.meta._tn}Read`
}
get tableCamelized() {
return `${this.meta._tn}`
}
get gqlReqBody() {
return `\n${this.columns.map(c => c._cn).join('\n')}\n`
}
async gqlRelationReqBody(params) {
let str = ''
if (params.hm) {
for (const child of params.hm.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.dbAlias,
env: this.$ctx.env,
tn: child
})
const meta = this.$ctx.$store.state.meta.metas[child]
if (meta) {
str += `\n${meta._tn}List{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
}
}
}
if (params.bt) {
for (const parent of params.bt.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.dbAlias,
env: this.$ctx.env,
tn: parent
})
const meta = this.$ctx.$store.state.meta.metas[parent]
if (meta) {
str += `\n${meta._tn}Read{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
}
}
}
if (params.mm) {
for (const mm of params.mm.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.dbAlias,
env: this.$ctx.env,
tn: mm
})
const meta = this.$ctx.$store.state.meta.metas[mm]
if (meta) {
str += `\n${meta._tn}MMList{\n${meta.columns.map(c => c._cn).join('\n')}\n}`
}
}
}
return str
}
get gqlQueryCountName() {
return `${this.tableCamelized}Count`
}
get gqlMutationCreateName() {
return `${this.tableCamelized}Create`
}
get gqlMutationUpdateName() {
return `${this.tableCamelized}Update`
}
get gqlMutationDeleteName() {
return `${this.tableCamelized}Delete`
}
async paginatedList(params) {
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''}));
const [list, count] = await Promise.all([
this.list(params), this.count({
where: params.where || '',
conditionGraph: params.conditionGraph,
condition: params.condition
})
])
return { list, count }
}
async update(id, data, oldData) {
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)
}`,
variables: {
id, data
}
})
const colName = Object.keys(data)[0]
this.$ctx.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.$ctx.dbAlias }, 'xcAuditCreate', {
tn: this.table,
cn: colName,
pk: id,
value: data[colName],
prevValue: oldData[colName]
}])
return data1.data.data[this.gqlMutationUpdateName]
}
async insert(data) {
const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `mutation create($data:${this.tableCamelized}Input){
${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}}
}`,
variables: {
data
}
})
return data1.data.data[this.gqlMutationCreateName]
}
async delete(id) {
const data1 = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `mutation delete($id:String!){
${this.gqlMutationDeleteName}(id: $id)
}`,
variables: { id }
})
return data1.data.data[this.gqlMutationDeleteName]
}
async read(id) {
const data = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: this.gqlReadQuery(id),
variables: null
})
return data.data.data[this.gqlQueryReadName]
}
get $axios() {
return this.$ctx.$axios
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
const list = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `query m2mNotChildren($pid: String!,$assoc:String!,$parent:String!, $limit:Int, $offset:Int){
m2mNotChildren(pid: $pid,assoc:$assoc,parent:$parent,limit:$limit, offset:$offset)
}`,
variables: {
parent: this.meta.tn, assoc, pid: pid + '', ...params
}
})
const count = await this.post(`/nc/${this.$ctx.projectId}/v1/graphql`, {
query: `query m2mNotChildrenCount($pid: String!,$assoc:String!,$parent:String!){
m2mNotChildrenCount(pid: $pid,assoc:$assoc,parent:$parent)
}`,
variables: {
parent: this.meta.tn, assoc, pid: pid + ''
}
})
return { list: list.data.data.m2mNotChildren, count: count.data.data.m2mNotChildrenCount.count }
}
}
/**
* @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.
*
* 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/>.
*
*/

35
packages/nc-gui/plugins/ncApis/index.js

@ -0,0 +1,35 @@
import ApiFactory from '@/plugins/ncApis/apiFactory'
export default function({ store: $store, $axios, ...rest }, inject) {
let instanceRefs = {}
let projectId = null
inject('ncApis', {
get: ({ table, dbAlias, env }) => {
if (!$store.state.meta.metas[table]) {
return
}
if (instanceRefs[env] && instanceRefs[env][dbAlias] && instanceRefs[env][dbAlias][table]) {
return instanceRefs[env][dbAlias][table]
}
instanceRefs[env] = instanceRefs[env] || {}
instanceRefs[env][dbAlias] = instanceRefs[env][dbAlias] || {}
instanceRefs[env][dbAlias][table] = ApiFactory.create(
table,
$store.getters['project/GtrProjectType'],
{ $store, $axios, projectId, dbAlias, env, table }
)
return instanceRefs[env][dbAlias][table]
},
clear: () => {
instanceRefs = {}
},
setProjectId: (_projectId) => {
projectId = _projectId
}
})
}

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

@ -0,0 +1,128 @@
export default class RestApi {
constructor(table, $ctx) {
this.$ctx = $ctx
this.table = $ctx.$store.state.meta.metas[table]._tn
}
// todo: - get version letter and use table alias
async list(params) {
// const data = await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}`, params)
const data = await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}`, params)
return data.data
}
async read(id) {
const data = await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}/${id}`)
return data.data
}
async count(params) {
if (this.timeout) {
return this.timeout
}
try {
const data = await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}/count`, params, {
timeout: 10000
})
return data && data.data
} catch (e) {
if (e.code === 'ECONNABORTED') {
// eslint-disable-next-line no-return-assign
return this.timeout = { count: Infinity }
} else {
throw e
}
}
}
get(url, params, extras = {}) {
return this.$axios({
url,
params,
...extras
})
}
async paginatedList(params) {
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})).count;
const [list, { count }] = await Promise.all([this.list(params), this.count({
where: params.where || '',
conditionGraph: params.conditionGraph
})])
return { list, count }
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
/// api/v1/Film/m2mNotChildren/film_actor/44
// const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})).count;
const { list, info: { count } } = (await this.get(`/nc/${this.$ctx.projectId}/api/v1/${this.table}/m2mNotChildren/${assoc}/${pid}`, params)).data
return { list, count }
}
async update(id, data, oldData) {
const res = await this.$axios({
method: 'put',
url: `/nc/${this.$ctx.projectId}/api/v1/${this.table}/${id}`,
data
})
const colName = Object.keys(data)[0]
this.$ctx.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: this.$ctx.dbAlias }, 'xcAuditCreate', {
tn: this.table,
cn: colName,
pk: id,
value: data[colName],
prevValue: oldData[colName]
}])
return res
}
async insert(data) {
return (await this.$axios({
method: 'post',
url: `/nc/${this.$ctx.projectId}/api/v1/${this.table}`,
data
})).data
}
async delete(id) {
return this.$axios({
method: 'delete',
url: `/nc/${this.$ctx.projectId}/api/v1/${this.table}/${id}`
})
}
get $axios() {
return this.$ctx.$axios
}
get apiUrl() {
return `${process.env.NODE_ENV === 'production'
? `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`
: 'http://localhost:8080'}/nc/${this.$ctx.projectId}/api/v1/${this.table}`
}
}
/**
* @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.
*
* 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/>.
*
*/

4
packages/nc-gui/store/project.js

@ -267,6 +267,10 @@ export const actions = {
const data = await this.dispatch('sqlMgr/ActSqlOp', [null, 'PROJECT_READ_BY_WEB']); // unsearialized data const data = await this.dispatch('sqlMgr/ActSqlOp', [null, 'PROJECT_READ_BY_WEB']); // unsearialized data
commit("list", data.data.list); commit("list", data.data.list);
commit("meta/MutClear", null, {root: true}); commit("meta/MutClear", null, {root: true});
if(this.$ncApis){
this.$ncApis.clear();
this.$ncApis.setProjectId(this.$router.currentRoute.params.project_id);
}
} catch (e) { } catch (e) {
this.$toast.error(e).goAway(3000); this.$toast.error(e).goAway(3000);
this.$router.push('/projects') this.$router.push('/projects')

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

@ -901,8 +901,15 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
parentMeta.manyToMany = parentMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent)) parentMeta.manyToMany = parentMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
childMeta.manyToMany = childMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent)) childMeta.manyToMany = childMeta.manyToMany.filter(mm => !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
parentMeta.v = parentMeta.v.filter(({mm}) => !mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent)) // filter lookup and relation virtual columns
childMeta.v = childMeta.v.filter(({mm}) => !mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent)) parentMeta.v = parentMeta.v.filter(({mm, ...rest}) => (!mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
// check for lookup
&& !(rest.type === 'mm' && (rest.relation.tn === parent && rest.relation.rtn === child || rest.relation.tn === child && rest.relation.rtn === parent))
)
childMeta.v = childMeta.v.filter(({mm, ...rest}) => (!mm || !(mm.tn === parent && mm.rtn === child || mm.tn === child && mm.rtn === parent))
// check for lookup
&& !(rest.type === 'mm' && (rest.relation.tn === parent && rest.relation.rtn === child || rest.relation.tn === child && rest.relation.rtn === parent))
)
for (const meta of [parentMeta, childMeta]) { for (const meta of [parentMeta, childMeta]) {
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', { await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {

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

@ -24,8 +24,6 @@ import GqlResolver from "./GqlResolver";
import commonSchema from './common.schema'; import commonSchema from './common.schema';
const log = debug('nc:api:gql'); const log = debug('nc:api:gql');
@ -66,7 +64,7 @@ class XCType {
export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr { export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
public readonly type='rest'; public readonly type = 'gql';
private resolvers: { [key: string]: GqlResolver | GqlProcedureResolver, ___procedure?: GqlProcedureResolver }; private resolvers: { [key: string]: GqlResolver | GqlProcedureResolver, ___procedure?: GqlProcedureResolver };
private schemas: { [key: string]: any }; private schemas: { [key: string]: any };
private types: { [key: string]: new(o: any) => any }; private types: { [key: string]: new(o: any) => any };
@ -444,8 +442,9 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
return (await this.models[tn]._getGroupedManyToManyList({ return (await this.models[tn]._getGroupedManyToManyList({
parentIds, parentIds,
child: mm.rtn, child: mm.rtn,
// todo: optimize - query only required fields
rest: { rest: {
fields1: '*' mfields1: '*'
} }
}))?.map(child => child.map(c => new self.types[mm.rtn](c))); }))?.map(child => child.map(c => new self.types[mm.rtn](c)));
}, },
@ -1381,7 +1380,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const oldMeta = JSON.parse(existingModel.meta); const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, { Object.assign(oldMeta, {
hasMany: meta.hasMany, hasMany: meta.hasMany,
v: oldMeta.v.filter(({hm}) => !hm || hm.rtn !== tnp || hm.tn !== tnc) v: oldMeta.v.filter(({hm, lookup, relation, type}) => (!hm || hm.rtn !== tnp || hm.tn !== tnc) &&
!(lookup && relation && type === 'hm' && relation.rtn === tnp && relation.tn === tnc))
}); });
// todo: backup schema // todo: backup schema
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', { await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
@ -1430,7 +1430,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const oldMeta = JSON.parse(existingModel.meta); const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, { Object.assign(oldMeta, {
belongsTo: meta.belongsTo, belongsTo: meta.belongsTo,
v: oldMeta.v.filter(({bt}) => !bt || bt.rtn !== tnp || bt.tn !== tnc) v: oldMeta.v.filter(({bt, lookup, relation, type}) => (!bt || bt.rtn !== tnp || bt.tn !== tnc) &&
!(lookup && relation && type === 'bt' && relation.rtn === tnp && relation.tn === tnc))
}); });
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', { await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
title: tnc, title: tnc,

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

@ -1215,7 +1215,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
const oldMeta = JSON.parse(existingModel.meta); const oldMeta = JSON.parse(existingModel.meta);
Object.assign(oldMeta, { Object.assign(oldMeta, {
hasMany: meta.hasMany, hasMany: meta.hasMany,
v: oldMeta.v.filter(({hm}) => !hm || hm.rtn !== tnp || hm.tn !== tnc) v: oldMeta.v.filter(({hm, lookup, relation, type}) => (!hm || hm.rtn !== tnp || hm.tn !== tnc) &&
!(lookup && relation && type==='hm'&&relation.rtn === tnp && relation.tn === tnc ))
}); });
// todo: delete from query_params // todo: delete from query_params
@ -1251,7 +1252,8 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
Object.assign(oldMeta, { Object.assign(oldMeta, {
belongsTo: meta.belongsTo, belongsTo: meta.belongsTo,
v: oldMeta.v.filter(({bt}) => !bt || bt.rtn !== tnp || bt.tn !== tnc) v: oldMeta.v.filter(({bt, relation, lookup,type}) => (!bt || bt.rtn !== tnp || bt.tn !== tnc) &&
!(lookup && relation && type==='bt'&&relation.rtn === tnp && relation.tn === tnc ))
}); });
// todo: delete from query_params // todo: delete from query_params
await this.xcMeta.metaUpdate(this.projectId, await this.xcMeta.metaUpdate(this.projectId,

Loading…
Cancel
Save