Browse Source

feat: GraphQL M2M

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
dd0e8d9916
  1. 2
      packages/nc-gui/assets/style/style.css
  2. 4
      packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js
  3. 104
      packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js
  4. 6
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  5. 50
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  6. 57
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  7. 86
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue
  8. 2
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  9. 2
      packages/nc-gui/components/project/spreadsheet/xcTable.vue
  10. 2
      packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue
  11. 71
      packages/nc-gui/store/meta.js
  12. 1
      packages/nc-gui/store/project.js
  13. 5
      packages/nocodb/package-lock.json
  14. 1
      packages/nocodb/package.json
  15. 27
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts
  16. 3
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  17. 155
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  18. 36
      packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts
  19. 23
      packages/nocodb/src/lib/noco/gql/GqlResolver.ts
  20. 7
      packages/nocodb/src/lib/noco/gql/common.schema.ts
  21. 32
      packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts
  22. 13
      packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/GqlXcTsSchemaMysql.ts

2
packages/nc-gui/assets/style/style.css

@ -336,7 +336,7 @@ html {
min-height: 40px !important;
}
.api-treeview .vtl-node-content{
max-width: calc(100% - 60px);
max-width: calc(100% - 44px);
}
/* for expansion panel content wrapper */
.expansion-wrap-0 .v-expansion-panel-content__wrap{

4
packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js

@ -3,9 +3,9 @@ import GqlApi from "@/components/project/spreadsheet/apis/gqlApi";
import GrpcApi from "@/components/project/spreadsheet/apis/grpcApi";
export default class ApiFactory {
static create(type, table, columns, ctx) {
static create(type, table, columns, ctx, meta) {
if (type === 'graphql') {
return new GqlApi(table, columns, ctx);
return new GqlApi(table, columns, meta, ctx,);
} else if (type === 'grpc') {
return new GrpcApi(table, ctx)
} else if (type === 'rest') {

104
packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js

@ -3,16 +3,17 @@ import inflection from 'inflection';
export default class GqlApi {
constructor(table, columns, $ctx) {
constructor(table, columns, meta, $ctx) {
// this.table = table;
this.columns = columns;
this.meta = meta;
this.$ctx = $ctx;
}
// todo: - get version letter and use table alias
async list(params) {
const data = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: this.gqlQuery(params),
query: await this.gqlQuery(params),
variables: null
});
return data.data.data[this.gqlQueryListName];
@ -51,15 +52,21 @@ export default class GqlApi {
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(',')})`;
}
gqlQuery(params) {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}${this.gqlReqBody}}`
async gqlQuery(params) {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}{${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}}`
}
gqlReadQuery(id) {
return `{${this.gqlQueryReadName}(id:"${id}")${this.gqlReqBody}}`
return `{${this.gqlQueryReadName}(id:"${id}"){${this.gqlReqBody}}}`
}
gqlCountQuery(params) {
@ -79,7 +86,52 @@ export default class GqlApi {
}
get gqlReqBody() {
return `{\n${this.columns.map(c => c._cn).join('\n')}\n}`
return `\n${this.columns.map(c => c._cn).join('\n')}\n`
}
async gqlRelationReqBody(params) {
let str = '';
if (params.childs) {
for (const child of params.childs.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.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.parents) {
for (const parent of params.parents.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.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.many) {
for (const mm of params.many.split(',')) {
await this.$ctx.$store.dispatch('meta/ActLoadMeta', {
dbAlias: this.$ctx.nodes.dbAlias,
env: this.$ctx.nodes.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() {
@ -102,18 +154,24 @@ export default class GqlApi {
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 || ''})]);
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) {
async update(id, data, oldData) {
const data1 = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `mutation update($id:String!, $data:${this.tableCamelized}Input){
${this.gqlMutationUpdateName}(id: $id, data: $data)
}`,
variables: {
id, data
id: id, data
}
});
@ -130,10 +188,9 @@ export default class GqlApi {
}
async insert(data) {
const data1 = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `mutation create($data:${this.tableCamelized}Input){
${this.gqlMutationCreateName}(data: $data)${this.gqlReqBody}
${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}}
}`,
variables: {
data
@ -170,9 +227,32 @@ export default class GqlApi {
}
get table() {
return this.$ctx && this.$ctx.meta && this.$ctx.meta._tn && inflection.camelize(this.$ctx.meta._tn);
return this.meta && this.meta._tn && inflection.camelize(this.meta._tn);
}
async paginatedM2mNotChildrenList(params, assoc, pid) {
const list = await this.post(`/nc/${this.$ctx.$route.params.project_id}/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.$route.params.project_id}/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

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

@ -4,7 +4,7 @@
<has-many-cell
v-if="hm"
:row="row"
:value="row[hm._tn]"
:value="row[`${hm._tn}List`]"
:meta="meta"
:hm="hm"
:nodes="nodes"
@ -17,7 +17,7 @@
<many-to-many-cell
v-else-if="mm"
:row="row"
:value="row[mm._rtn]"
:value="row[`${mm._rtn}MMList`]"
:meta="meta"
:mm="mm"
:nodes="nodes"
@ -33,7 +33,7 @@
v-else-if="bt"
:active="active"
:row="row"
:value="row[bt._rtn]"
:value="row[`${bt._rtn}Read`]"
:meta="meta"
:bt="bt"
:nodes="nodes"

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

@ -6,7 +6,7 @@
<item-chip
:active="active"
:item="value"
:value="Object.values(value || localState)[1]"
:value="cellValue"
:key="i"
@edit="editParent"
@unlink="unlink"
@ -117,7 +117,7 @@ export default {
data: () => ({
newRecordModal: false,
parentListModal: false,
parentMeta: null,
// parentMeta: null,
list: null,
childList: null,
dialogShow: false,
@ -198,20 +198,25 @@ export default {
async loadParentMeta() {
// todo: optimize
if (!this.parentMeta) {
const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
dbAlias: this.nodes.dbAlias,
tn: this.bt.rtn
}]);
this.parentMeta = JSON.parse(parentTableData.meta)
})
// const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.bt.rtn
// }]);
// this.parentMeta = JSON.parse(parentTableData.meta)
}
},
async showNewRecordModal() {
await this.loadParentMeta();
this.newRecordModal = true;
},
async addParentToChild(parent) {
async addParentToChild(parent) {
const pid = this.parentMeta.columns.filter((c) => c.pk).map(c => parent[c._cn]).join('___');
const id = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
@ -224,7 +229,7 @@ export default {
}
await this.api.update(id, {
[_cn]: pid
[_cn]: +pid
}, {
[_cn]: parent[this.parentPrimaryKey]
});
@ -247,10 +252,13 @@ export default {
},
},
computed: {
parentMeta() {
return this.$store.state.meta.metas[this.bt.rtn];
},
parentApi() {
return this.parentMeta && this.parentMeta._tn ?
ApiFactory.create(this.$store.getters['project/GtrProjectType'],
this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this) : null;
this.parentMeta && this.parentMeta._tn, this.parentMeta && this.parentMeta.columns, this, this.parentMeta) : null;
},
parentId() {
return this.value && this.parentMeta && this.parentMeta.columns.filter((c) => c.pk).map(c => this.value[c._cn]).join('___')
@ -286,6 +294,14 @@ export default {
form() {
return this.selectedParent ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
},
cellValue() {
if (this.value || this.localState) {
if (this.parentMeta && this.parentPrimaryCol) {
return (this.value || this.localState)[this.parentPrimaryCol]
}
return Object.values(this.value || this.localState)[1]
}
}
},
watch: {
isNew(n, o) {
@ -293,6 +309,9 @@ export default {
this.localState = null
}
}
},
created() {
this.loadParentMeta();
}
}
</script>
@ -330,13 +349,14 @@ export default {
margin: 3px auto;
}
.chips-wrapper{
.chips{
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active{
.chips{
max-width: calc(100% - 30px);
&.active {
.chips {
max-width: calc(100% - 22px);
}
}
}

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

@ -7,14 +7,15 @@
v-for="(ch,i) in (value|| localState)"
:active="active"
:item="ch"
:value="Object.values(ch)[1]"
:value="getCellValue(ch)"
:key="i"
@edit="editChild"
@unlink="unlinkChild"
></item-chip>
</template>
</div>
<div class="actions align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<div class="actions align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
@ -139,7 +140,7 @@ export default {
data: () => ({
newRecordModal: false,
childListModal: false,
childMeta: null,
// childMeta: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
@ -205,13 +206,19 @@ export default {
async loadChildMeta() {
// todo: optimize
if (!this.childMeta) {
const childTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
dbAlias: this.nodes.dbAlias,
tn: this.hm.tn
}]);
this.childMeta = JSON.parse(childTableData.meta);
})
// const childTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.hm.tn
// }]);
// this.childMeta = JSON.parse(childTableData.meta);
// this.childQueryParams = JSON.parse(childTableData.query_params);
}
},
@ -231,7 +238,7 @@ export default {
this.newRecordModal = false;
await this.childApi.update(id, {
[_cn]: this.parentId
[_cn]: +this.parentId
}, {
[_cn]: child[this.childForeignKey]
});
@ -261,13 +268,24 @@ export default {
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true)
}, 500)
},
getCellValue(cellObj) {
if (cellObj) {
if (this.parentMeta && this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
}
},
computed: {
childMeta() {
return this.$store.state.meta.metas[this.hm.tn]
},
childApi() {
return this.childMeta && this.childMeta._tn ?
ApiFactory.create(this.$store.getters['project/GtrProjectType'],
this.childMeta && this.childMeta._tn, this.childMeta && this.childMeta.columns, this) : null;
this.childMeta && this.childMeta._tn, this.childMeta && this.childMeta.columns, this,this.childMeta) : null;
},
childPrimaryCol() {
return this.childMeta && (this.childMeta.columns.find(c => c.pv) || {})._cn
@ -314,15 +332,18 @@ export default {
}
},
watch: {
isNew(n, o) {
isNew(n, o) {
if (!n && o) {
let child;
while (child = this.localState.pop()) {
this.addChildToParent(child)
this.addChildToParent(child)
}
this.$emit('newRecordsSaved')
}
}
},
created() {
this.loadChildMeta();
}
}
</script>
@ -381,13 +402,15 @@ export default {
}
}
}
.chips-wrapper{
.chips{
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active{
.chips{
max-width: calc(100% - 60px);
&.active {
.chips {
max-width: calc(100% - 44px);
}
}
}

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

@ -6,7 +6,7 @@
<item-chip v-for="(v,j) in (value || localState)"
:active="active"
:item="v"
:value="Object.values(v)[2]"
:value="getCellValue(v)"
:key="j"
@edit="editChild"
@unlink="unlinkChild"
@ -14,7 +14,8 @@
</template>
</div>
<div class="actions align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<div class="actions align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">mdi-plus</x-icon>
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
@ -134,11 +135,12 @@ import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems";
import listChildItemsModal from "@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal";
import listChildItemsModal
from "@/components/project/spreadsheet/components/virtualCell/components/listChildItemsModal";
export default {
name: "many-to-many-cell",
components: {ListChildItems, ItemChip, ListItems, DlgLabelSubmitCancel, listChildItemsModal },
components: {ListChildItems, ItemChip, ListItems, DlgLabelSubmitCancel, listChildItemsModal},
props: {
value: [Object, Array],
meta: [Object],
@ -155,8 +157,8 @@ export default {
isNewChild: false,
newRecordModal: false,
childListModal: false,
childMeta: null,
assocMeta: null,
// childMeta: null,
// assocMeta: null,
childList: null,
dialogShow: false,
confirmAction: null,
@ -222,25 +224,35 @@ export default {
async loadChildMeta() {
// todo: optimize
if (!this.childMeta) {
const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
dbAlias: this.nodes.dbAlias,
tn: this.mm.rtn
}]);
this.childMeta = JSON.parse(parentTableData.meta)
})
// const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.mm.rtn
// }]);
// this.childMeta = JSON.parse(parentTableData.meta)
}
},
async loadAssociateTableMeta() {
// todo: optimize
if (!this.assocMeta) {
const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias
}, 'tableXcModelGet', {
dbAlias: this.nodes.dbAlias,
tn: this.mm.vtn
}]);
this.assocMeta = JSON.parse(assocTableData.meta)
})
// const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.mm.vtn
// }]);
// this.assocMeta = JSON.parse(assocTableData.meta)
}
},
async showNewRecordModal() {
@ -254,16 +266,15 @@ export default {
this.newRecordModal = false;
return
}
const cid = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
const cid = this.childMeta.columns. filter((c) => c.pk).map(c => child[c._cn]).join('___');
const pid = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
const vcidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vrcn)._cn;
const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn;
try {
await this.assocApi.insert({
[vcidCol]: cid,
[vpidCol]: pid
[vcidCol]: +cid,
[vpidCol]: +pid
});
this.$emit('loadTableData')
@ -299,15 +310,30 @@ export default {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
},
getCellValue(cellObj) {
if (cellObj) {
if (this.parentMeta && this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
}
},
computed: {
childMeta() {
return this.$store.state.meta.metas[this.mm.rtn]
},
assocMeta() {
return this.$store.state.meta.metas[this.mm.vtn]
},
childApi() {
return this.childMeta && this.childMeta._tn ?
ApiFactory.create(
this.$store.getters['project/GtrProjectType'],
this.childMeta._tn,
this.childMeta.columns,
this
this,
this.childMeta
) : null;
},
assocApi() {
@ -316,7 +342,8 @@ export default {
this.$store.getters['project/GtrProjectType'],
this.assocMeta._tn,
this.assocMeta.columns,
this
this,
this.assocMeta
) : null;
},
childPrimaryCol() {
@ -375,6 +402,10 @@ export default {
this.$emit('newRecordsSaved')
}
}
},
created() {
this.loadChildMeta();
this.loadAssociateTableMeta()
}
}
</script>
@ -432,13 +463,14 @@ export default {
margin: 3px auto;
}
.chips-wrapper{
.chips{
.chips-wrapper {
.chips {
max-width: 100%;
}
&.active{
.chips{
max-width: calc(100% - 60px);
&.active {
.chips {
max-width: calc(100% - 44px);
}
}
}

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

@ -917,7 +917,7 @@ export default {
return SqlUI.create(this.nodes.dbConnection);
},
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) : null;
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;
}
},
}

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

@ -468,7 +468,7 @@ export default {
}
},
api() {
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.table, this.meta && this.meta.columns, this);
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], this.table, this.meta && this.meta.columns, this, this.meta);
},
colLength() {
return (this.meta && this.meta.columns && this.meta.columns.length) || 0

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

@ -476,7 +476,7 @@ export default {
return SqlUI.create(this.nodes.dbConnection);
},
api() {
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], (this.meta && this.meta._tn) || this.table, this.meta && this.meta.columns, this);
return ApiFactory.create(this.$store.getters['project/GtrProjectType'], (this.meta && this.meta._tn) || this.table, this.meta && this.meta.columns, this, this.meta);
}, edited() {
return this.data && this.data.some(r => r.rowMeta && (r.rowMeta.new || r.rowMeta.changed))
},

71
packages/nc-gui/store/meta.js

@ -0,0 +1,71 @@
export const state = () => ({
metas: {},
loading: {}
});
export const mutations = {
MutMeta(state, {key, value}) {
state.metas = {...state.metas, [key]: value};
},
MutLoading(state, {key, value}) {
state.loading = {...state.loading, [key]: value};
},
MutClear(state) {
state.metas = {};
}
};
export const actions = {
async ActLoadMeta({state, commit, dispatch}, {tn, env, dbAlias}) {
if (state.loading[tn]) {
return await new Promise(resolve => {
const unsubscribe = this.app.store.subscribe(s => {
if (s.type === 'meta/MutLoading' && s.payload.key === tn && !s.payload.value) {
unsubscribe();
resolve(state.metas[tn])
}
})
})
}
if (state.metas[tn]) {
return state.metas[tn];
}
commit('MutLoading', {
key: tn,
value: true
})
const model = await dispatch('sqlMgr/ActSqlOp', [{env, dbAlias}, 'tableXcModelGet', {tn}], {root: true});
commit('MutMeta', {
key: tn,
value: JSON.parse(model.meta)
})
commit('MutLoading', {
key: tn,
value: undefined
})
}
}
/**
* @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/>.
*
*/

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

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

5
packages/nocodb/package-lock.json generated

@ -8013,6 +8013,11 @@
}
}
},
"graphql-type-json": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz",
"integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg=="
},
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",

1
packages/nocodb/package.json

@ -122,6 +122,7 @@
"glob": "^7.1.6",
"graphql": "^15.3.0",
"graphql-depth-limit": "^1.1.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.6",
"import-fresh": "^3.2.1",
"inflection": "^1.12.0",

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

@ -1132,7 +1132,7 @@ class BaseModelSql extends BaseModel {
let gs = _.groupBy(childs, _cn);
parent.forEach(row => {
row[this.dbModels?.[child]?._tn || child] = gs[row[this.pks[0]._cn]] || [];
row[`${this.dbModels?.[child]?._tn || child}List`] = gs[row[this.pks[0]._cn]] || [];
})
}
@ -1147,7 +1147,19 @@ class BaseModelSql extends BaseModel {
* @private
*/
async _getManyToManyList({parent, child}, rest = {}, index) {
const gs = await this._getGroupedManyToManyList({
rest,
index,
child,
parentIds: parent.map(p => p[this.columnToAlias?.[this.pks[0].cn] || this.pks[0].cn])
});
parent.forEach((row, i) => {
row[`${this.dbModels?.[child]?._tn || child}MMList`] = gs[i] || [];
})
}
public async _getGroupedManyToManyList({rest = {}, index = 0, child, parentIds}) {
let {fields, where, limit, offset, sort} = this._getChildListArgs(rest, index, child);
const {tn, cn, vtn, vcn, vrcn, rtn, rcn} = this.manyToManyRelations.find(({rtn}) => rtn === child) || {};
// @ts-ignore
@ -1159,12 +1171,12 @@ class BaseModelSql extends BaseModel {
const childs = await this._run(this.dbDriver.union(
parent.map(p => {
parentIds.map(id => {
const query =
this
.dbDriver(child)
.join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`)
.where(`${vtn}.${vcn}`, p[this.columnToAlias?.[this.pks[0].cn] || this.pks[0].cn])
.where(`${vtn}.${vcn}`, id)//p[this.columnToAlias?.[this.pks[0].cn] || this.pks[0].cn])
.xwhere(where, this.dbModels[child].selectQuery(''))
.select({[`${tn}_${vcn}`]: `${vtn}.${vcn}`, ...this.dbModels[child].selectQuery(fields)}) // ...fields.split(','));
@ -1173,11 +1185,8 @@ class BaseModelSql extends BaseModel {
}), !this.isSqlite()
));
let gs = _.groupBy(childs, `${tn}_${vcn}`);
parent.forEach(row => {
row[this.dbModels?.[child]?._tn || child] = gs[row[this.pks[0]._cn]] || [];
})
const gs = _.groupBy(childs, `${tn}_${vcn}`);
return parentIds.map(id => gs[id] || [])
}
/**
@ -1453,7 +1462,7 @@ class BaseModelSql extends BaseModel {
const gs = _.groupBy(parents, this.dbModels[parent]?.columnToAlias?.[rcn] || rcn);
childs.forEach(row => {
row[this.dbModels?.[parent]?._tn || parent] = gs[row[this?.columnToAlias?.[cn] || cn]]?.[0];
row[`${this.dbModels?.[parent]?._tn || parent}Read`] = gs[row[this?.columnToAlias?.[cn] || cn]]?.[0];
})
}

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

@ -1693,8 +1693,11 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
const meta = JSON.parse(metaObj.meta);
metas.push(meta);
const ctx = this.generateContextForTable(meta.tn, meta.columns, [], meta.hasMany, meta.belongsTo, meta.type, meta._tn);
// generate virtual columns
meta.v = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getVitualColumns();
// set default primary values
ModelXcMetaFactory.create(this.connectionConfig, {}).mapDefaultPrimaryValue(meta.columns);
// update meta
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(meta)
}, {title: meta.tn})

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

@ -31,6 +31,9 @@ import GqlXcSchemaFactory from "../../sqlMgr/code/gql-schema/xc-ts/GqlXcSchemaFa
import ModelXcMetaFactory from "../../sqlMgr/code/models/xc/ModelXcMetaFactory";
import ExpressXcTsPolicyGql from "../../sqlMgr/code/gql-policies/xc-ts/ExpressXcTsPolicyGql";
import {GraphQLJSON} from 'graphql-type-json';
import {m2mNotChildren, m2mNotChildrenCount} from "./GqlCommonResolvers";
const log = debug('nc:api:gql');
@ -341,6 +344,39 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
this.addHmCountResolverMethodToType(hm, mw, tn, loaderFunctionsObj, countPropName, colNameAlias);
}
}
for (const mm of schema.manyToMany || []) {
if (!enabledModels.includes(mm.rtn)) {
continue;
}
// todo: handle enable/disable
// if (!mm.enabled) {
// continue;
// }
const middlewareBody = middlewaresArr.find(({title}) => title === mm.rtn)?.functions?.[0];
// const countPropName = `${mm._rtn}Count`;
const listPropName = `${mm._rtn}MMList`;
if (listPropName in this.types[tn].prototype) {
continue;
}
const mw = new GqlMiddleware(this.acls, mm.tn, middlewareBody, this.models);
/* has many relation list loader with middleware */
this.addMMListResolverMethodToType(tn, mm, mw, {}, listPropName, this.metas[mm.tn].columns.find(c => c.pk)._cn);
// todo: count
// if (countPropName in this.types[tn].prototype) {
// continue;
// }
// {
// const mw = new GqlMiddleware(this.acls, hm.tn, middlewareBody, this.models);
//
// // create count loader with middleware
// this.addHmCountResolverMethodToType(hm, mw, tn, loaderFunctionsObj, countPropName, colNameAlias);
// }
}
for (const bt of schema.belongsTo) {
@ -401,6 +437,35 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}
}
private addMMListResolverMethodToType(tn: string, mm, mw: GqlMiddleware, _loaderFunctionsObj, listPropName: string, colNameAlias) {
{
const self = this;
this.log(`xcTablesRead : Creating loader for '%s'`, `${tn}Mm${mm.rtn}List`)
const listLoader = new DataLoader(
BaseType.applyMiddlewareForLoader(
[mw.middleware],
async parentIds => {
return (await this.models[tn]._getGroupedManyToManyList({
parentIds,
child: mm.rtn,
rest: {
fields1: '*'
}
}))?.map(child => child.map(c => new self.types[mm.rtn](c)));
},
[mw.postLoaderMiddleware]
));
/* defining HasMany list method within GQL Type class */
Object.defineProperty(this.types[tn].prototype, listPropName, {
async value(args: any, context: any, info: any): Promise<any> {
return listLoader.load([this[colNameAlias], args, context, info]);
},
configurable: true
})
}
}
private addHmCountResolverMethodToType(hm, mw, tn: string, loaderFunctionsObj, countPropName: string, colNameAlias) {
{
this.log(`xcTablesRead : Creating loader for '%s'`, `${tn}Hm${hm.tn}Count`)
@ -577,7 +642,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
this.log(`xcTablesPopulate : Generating schema of '%s' %s`, table.tn, table.type);
/**************** prepare GQL: schemas, types, resolvers ****************/
this.schemas[table.tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
// this.schemas[table.tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(ctx)).getString();
// tslint:disable-next-line:max-classes-per-file
this.types[table.tn] = class extends XCType {
@ -600,7 +665,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
title: table.tn,
type: table.type || 'table',
meta: JSON.stringify(this.metas[table.tn]),
schema: this.schemas[table.tn],
// schema: this.schemas[table.tn],
alias: this.metas[table.tn]._tn,
})
}
@ -712,6 +777,84 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}));
await this.getManyToManyRelations();
// generate schema of models
for (const meta of Object.values(this.metas)) {
/**************** prepare GQL: schemas, types, resolvers ****************/
this.schemas[meta.tn] = GqlXcSchemaFactory.create(this.connectionConfig, this.generateRendererArgs(
{
...this.generateContextForTable(
meta.tn,
meta.columns,
relations,
meta.hasMany,
meta.belongsTo,
meta.type,
meta._tn,
),
manyToMany: meta.manyToMany
})).getString();
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
schema: this.schemas[meta.tn],
}, {
title: meta.tn
})
}
// add property in type class for many to many relations
await Promise.all(Object.entries(this.metas).map(async ([tn, meta]) => {
if (!meta.manyToMany) {
return;
}
for (const mm of meta.manyToMany) {
const countPropName = `${mm._rtn}Count`;
const listPropName = `${mm._rtn}MMList`;
this.log(`xcTablesPopulate : Populating '%s' and '%s' many to many loaders`, listPropName, countPropName);
if (listPropName in this.types[tn].prototype) {
continue;
}
/* has many relation list loader with middleware */
const mw = new GqlMiddleware(this.acls, mm.rtn, '', this.models);
/* has many relation list loader with middleware */
this.addMMListResolverMethodToType(tn, mm, mw, {}, listPropName, meta.columns.find(c => c.pk)._cn);
// if (countPropName in this.types[tn].prototype) {
// continue;
// }
// {
// const mw = new GqlMiddleware(this.acls, hm.tn, null, this.models);
//
// // create count loader with middleware
// this.addHmCountResolverMethodToType(hm, mw, tn, {}, countPropName, colNameAlias);
// }
//
// this.log(`xcTablesPopulate : Inserting loader metadata of '%s' and '%s' loaders`, listPropName, countPropName);
//
await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
title: `${tn}Mm${mm.rtn}List`,
parent: tn,
child: mm.rtn,
relation: 'mm',
resolver: 'mmlist',
});
// await this.xcMeta.metaInsert(this.projectId, this.dbAlias, 'nc_loaders', {
// title: `${tn}Mm${hm.tn}Count`,
// parent: mm.tn,
// child: mm.rtn,
// relation: 'hm',
// resolver: 'list',
// });
}
}));
}
}
@ -1545,7 +1688,10 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
const rootValue = mergeResolvers([{
nocodb_health() {
return 'Coming soon'
}
},
m2mNotChildren: m2mNotChildren({models: this.models}),
m2mNotChildrenCount: m2mNotChildrenCount({models: this.models}),
JSON: GraphQLJSON,
}, ...Object.values(this.resolvers).map(r => r.mapResolvers(this.customResolver))]);
this.log(`initGraphqlRoute : Building graphql schema`);
@ -1568,7 +1714,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
},
rootValue,
schema,
validationRules: [depthLimit(this.connectionConfig?.meta?.api?.graphqlDepthLimit ?? 10),
validationRules: [
depthLimit(this.connectionConfig?.meta?.api?.graphqlDepthLimit ?? 10),
],
customExecuteFn: async (args) => {
const data = await execute(args);

36
packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts

@ -0,0 +1,36 @@
import {BaseModelSql} from "../../dataMapper";
export const m2mNotChildren = ({models = {}}: { models: { [key: string]: BaseModelSql } }) => {
return async (args) => {
return models[args?.parent]?.m2mNotChildren(args);
}
}
export const m2mNotChildrenCount = ({models = {}}: { models: { [key: string]: BaseModelSql } }) => {
return async (args) => {
return models[args?.parent]?.m2mNotChildrenCount(args);
}
}
/**
* @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/>.
*
*/

23
packages/nocodb/src/lib/noco/gql/GqlResolver.ts

@ -40,9 +40,18 @@ export default class GqlResolver extends GqlBaseResolver {
return this.models?.[this.table];
}
public async list(args, {req,res}): Promise<any> {
public async list(args, {req, res}): Promise<any> {
const startTime = process.hrtime();
try {
if (args.conditionGraph && typeof args.conditionGraph === 'string') {
args.conditionGraph = {models: this.models, condition: JSON.parse(args.conditionGraph)}
}
if (args.condition && typeof args.condition === 'string') {
args.condition = JSON.parse(args.condition)
}
} catch (e) {
/* ignore parse error */
}
const data = await req.model.list(args);
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
@ -98,6 +107,16 @@ export default class GqlResolver extends GqlBaseResolver {
}
public async count(args, {req}): Promise<any> {
try {
if (args.conditionGraph && typeof args.conditionGraph === 'string') {
args.conditionGraph = {models: this.models, condition: JSON.parse(args.conditionGraph)}
}
if (args.condition && typeof args.condition === 'string') {
args.condition = JSON.parse(args.condition)
}
} catch (e) {
/* ignore parse error */
}
const data = await req.model.countByPk(args);
return data.count;
}

7
packages/nocodb/src/lib/noco/gql/common.schema.ts

@ -38,10 +38,17 @@ input ConditionFloat{
ge: Float
}
scalar JSON
type Query{
nocodb_health:String
m2mNotChildren(pid:String!, assoc:String!, parent:String!, limit:Int, offset:Int):[JSON]
m2mNotChildrenCount(pid:String!, assoc:String!, parent:String!):JSON
}
`/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*

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

@ -0,0 +1,32 @@
import BaseRender from "../../BaseRender";
class BaseGqlXcTsSchema extends BaseRender {
/**
*
* @param dir
* @param filename
* @param ct
* @param ctx.tn
* @param ctx.columns
* @param ctx.relations
*/
constructor({dir, filename, ctx}) {
super({dir, filename, ctx});
}
protected generateManyToManyTypeProps(args: any): string {
if (!args.manyToMany?.length) {
return '';
}
let str = '\r\n';
for (const mm of args.manyToMany) {
str += `\t\t${mm._rtn}MMList: [${mm._rtn}]\r\n`;
}
return str;
}
}
export default BaseGqlXcTsSchema;

13
packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/GqlXcTsSchemaMysql.ts

@ -1,10 +1,10 @@
import BaseRender from "../../BaseRender";
import inflection from "inflection";
import lodash from "lodash";
import {AGG_DEFAULT_COLS, GROUPBY_DEFAULT_COLS} from "./schemaHelp";
import BaseGqlXcTsSchema from "./BaseGqlXcTsSchema";
class GqlXcTsSchemaMysql extends BaseRender {
class GqlXcTsSchemaMysql extends BaseGqlXcTsSchema {
/**
*
@ -58,7 +58,6 @@ class GqlXcTsSchemaMysql extends BaseRender {
${this._getMutation(args)}\r\n
${this._getType(args)}\r\n
`
str += '';
return str;
@ -81,11 +80,11 @@ class GqlXcTsSchemaMysql extends BaseRender {
_getQuery(args) {
let str = `type Query { \r\n`
str += `\t\t${args.tn_camelize}List(where: String,condition:Condition${args.tn_camelize}, limit: Int, offset: Int, sort: String): [${args.tn_camelize}]\r\n`
str += `\t\t${args.tn_camelize}List(where: String,condition:Condition${args.tn_camelize}, limit: Int, offset: Int, sort: String, conditionGraph: String): [${args.tn_camelize}]\r\n`
str += `\t\t${args.tn_camelize}Read(id:String!): ${args.tn_camelize}\r\n`
str += `\t\t${args.tn_camelize}Exists(id: String!): Boolean\r\n`
str += `\t\t${args.tn_camelize}FindOne(where: String!,condition:Condition${args.tn_camelize}): ${args.tn_camelize}\r\n`
str += `\t\t${args.tn_camelize}Count(where: String!,condition:Condition${args.tn_camelize}): Int\r\n`
str += `\t\t${args.tn_camelize}Count(where: String!,condition:Condition${args.tn_camelize},conditionGraph: String): Int\r\n`
str += `\t\t${args.tn_camelize}Distinct(column_name: String, where: String,condition:Condition${args.tn_camelize}, limit: Int, offset: Int, sort: String): [${args.tn_camelize}]\r\n`
str += `\t\t${args.tn_camelize}GroupBy(fields: String, having: String, limit: Int, offset: Int, sort: String): [${args.tn_camelize}GroupBy]\r\n`
str += `\t\t${args.tn_camelize}Aggregate(column_name: String!, having: String, limit: Int, offset: Int, sort: String, func: String!): [${args.tn_camelize}Aggregate]\r\n`
@ -137,6 +136,9 @@ class GqlXcTsSchemaMysql extends BaseRender {
str += `\t\t${childTable}Count: Int\r\n`;
}
str+= this.generateManyToManyTypeProps(args);
let belongsToRelations = args.relations.filter(r => r.tn === args.tn);
if (belongsToRelations.length > 1)
belongsToRelations = lodash.uniqBy(belongsToRelations, function (e) {
@ -151,6 +153,7 @@ class GqlXcTsSchemaMysql extends BaseRender {
strWhere += `\t\t${parentTable}Read: Condition${parentTable}\r\n`;
}
str += `\t}\r\n`

Loading…
Cancel
Save