From dd0e8d99166e3b6682ed3c1eb91826372b2c08f6 Mon Sep 17 00:00:00 2001 From: Pranav C <61551451+pranavxc@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:34:42 +0530 Subject: [PATCH] feat: GraphQL M2M Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com> --- packages/nc-gui/assets/style/style.css | 2 +- .../project/spreadsheet/apis/apiFactory.js | 4 +- .../project/spreadsheet/apis/gqlApi.js | 104 ++++++++++-- .../spreadsheet/components/virtualCell.vue | 6 +- .../components/virtualCell/belogsToCell.vue | 50 ++++-- .../components/virtualCell/hasManyCell.vue | 57 +++++-- .../components/virtualCell/manyToManyCell.vue | 86 +++++++--- .../project/spreadsheet/rowsXcDataTable.vue | 2 +- .../project/spreadsheet/xcTable.vue | 2 +- .../project/viewTabs/viewSpreadsheet.vue | 2 +- packages/nc-gui/store/meta.js | 71 ++++++++ packages/nc-gui/store/project.js | 1 + packages/nocodb/package-lock.json | 5 + packages/nocodb/package.json | 1 + .../lib/dataMapper/lib/sql/BaseModelSql.ts | 27 ++- .../src/lib/noco/common/BaseApiBuilder.ts | 3 + .../nocodb/src/lib/noco/gql/GqlApiBuilder.ts | 155 +++++++++++++++++- .../src/lib/noco/gql/GqlCommonResolvers.ts | 36 ++++ .../nocodb/src/lib/noco/gql/GqlResolver.ts | 23 ++- .../nocodb/src/lib/noco/gql/common.schema.ts | 7 + .../gql-schema/xc-ts/BaseGqlXcTsSchema.ts | 32 ++++ .../gql-schema/xc-ts/GqlXcTsSchemaMysql.ts | 13 +- 22 files changed, 589 insertions(+), 100 deletions(-) create mode 100644 packages/nc-gui/store/meta.js create mode 100644 packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts create mode 100644 packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts diff --git a/packages/nc-gui/assets/style/style.css b/packages/nc-gui/assets/style/style.css index ce66c12c75..cdf119f2e5 100644 --- a/packages/nc-gui/assets/style/style.css +++ b/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{ diff --git a/packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js b/packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js index facc52a984..e476ad6e1b 100644 --- a/packages/nc-gui/components/project/spreadsheet/apis/apiFactory.js +++ b/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') { diff --git a/packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js b/packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js index e3305c3b5c..c0818e333f 100644 --- a/packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js +++ b/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 diff --git a/packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue b/packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue index 144061a1db..fcca57e43a 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue @@ -4,7 +4,7 @@ ({ 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(); } } @@ -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); } } } diff --git a/packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue b/packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue index 9c9931db53..3cd87ddb25 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue +++ b/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" > -
+
mdi-plus mdi-arrow-expand
@@ -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(); } } @@ -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); } } } diff --git a/packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue b/packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue index d9de5d164e..11c92245b2 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/virtualCell/manyToManyCell.vue @@ -6,7 +6,7 @@
-
+
mdi-plus mdi-arrow-expand
@@ -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() } } @@ -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); } } } diff --git a/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue b/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue index cbc2d44c84..ba82b50ed8 100644 --- a/packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue +++ b/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; } }, } diff --git a/packages/nc-gui/components/project/spreadsheet/xcTable.vue b/packages/nc-gui/components/project/spreadsheet/xcTable.vue index cae220456b..310b64b5dc 100644 --- a/packages/nc-gui/components/project/spreadsheet/xcTable.vue +++ b/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 diff --git a/packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue b/packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue index a32421011d..7b0db72d87 100644 --- a/packages/nc-gui/components/project/viewTabs/viewSpreadsheet.vue +++ b/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)) }, diff --git a/packages/nc-gui/store/meta.js b/packages/nc-gui/store/meta.js new file mode 100644 index 0000000000..3fa7f342fd --- /dev/null +++ b/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 + * @author Pranav C Balan + * + * @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 . + * + */ diff --git a/packages/nc-gui/store/project.js b/packages/nc-gui/store/project.js index 000d7282eb..2aa7afd435 100644 --- a/packages/nc-gui/store/project.js +++ b/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') diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index e4e3f6a6a5..9d62dbb023 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -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", diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json index acfcbafa8e..fdaf3f5231 100644 --- a/packages/nocodb/package.json +++ b/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", diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts index 7bc6b4d6d8..522c4fea22 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts +++ b/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]; }) } diff --git a/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts b/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts index f6ed35fc27..8e9d9c1cb7 100644 --- a/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts @@ -1693,8 +1693,11 @@ export default abstract class BaseApiBuilder 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}) diff --git a/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts b/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts index 754ced0f4b..89f8014572 100644 --- a/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts +++ b/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 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 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 { + 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 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 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 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 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 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); diff --git a/packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts b/packages/nocodb/src/lib/noco/gql/GqlCommonResolvers.ts new file mode 100644 index 0000000000..2f96160243 --- /dev/null +++ b/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 + * @author Pranav C Balan + * + * @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 . + * + */ diff --git a/packages/nocodb/src/lib/noco/gql/GqlResolver.ts b/packages/nocodb/src/lib/noco/gql/GqlResolver.ts index 6ac7bc4cec..638ff288da 100644 --- a/packages/nocodb/src/lib/noco/gql/GqlResolver.ts +++ b/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 { + public async list(args, {req, res}): Promise { 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 { + 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; } diff --git a/packages/nocodb/src/lib/noco/gql/common.schema.ts b/packages/nocodb/src/lib/noco/gql/common.schema.ts index c46d4a5ed4..d071036445 100644 --- a/packages/nocodb/src/lib/noco/gql/common.schema.ts +++ b/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 * diff --git a/packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts b/packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/BaseGqlXcTsSchema.ts new file mode 100644 index 0000000000..ed99f9468e --- /dev/null +++ b/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; diff --git a/packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/GqlXcTsSchemaMysql.ts b/packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/GqlXcTsSchemaMysql.ts index ad9f3550c5..da443368a3 100644 --- a/packages/nocodb/src/lib/sqlMgr/code/gql-schema/xc-ts/GqlXcTsSchemaMysql.ts +++ b/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`