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. 102
      packages/nc-gui/components/project/spreadsheet/apis/gqlApi.js
  4. 6
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  5. 40
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  6. 45
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  7. 74
      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. 21
      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; min-height: 40px !important;
} }
.api-treeview .vtl-node-content{ .api-treeview .vtl-node-content{
max-width: calc(100% - 60px); max-width: calc(100% - 44px);
} }
/* for expansion panel content wrapper */ /* for expansion panel content wrapper */
.expansion-wrap-0 .v-expansion-panel-content__wrap{ .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"; import GrpcApi from "@/components/project/spreadsheet/apis/grpcApi";
export default class ApiFactory { export default class ApiFactory {
static create(type, table, columns, ctx) { static create(type, table, columns, ctx, meta) {
if (type === 'graphql') { if (type === 'graphql') {
return new GqlApi(table, columns, ctx); return new GqlApi(table, columns, meta, ctx,);
} else if (type === 'grpc') { } else if (type === 'grpc') {
return new GrpcApi(table, ctx) return new GrpcApi(table, ctx)
} else if (type === 'rest') { } else if (type === 'rest') {

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

@ -3,16 +3,17 @@ import inflection from 'inflection';
export default class GqlApi { export default class GqlApi {
constructor(table, columns, $ctx) { constructor(table, columns, meta, $ctx) {
// this.table = table; // this.table = table;
this.columns = columns; this.columns = columns;
this.meta = meta;
this.$ctx = $ctx; this.$ctx = $ctx;
} }
// todo: - get version letter and use table alias // todo: - get version letter and use table alias
async list(params) { async list(params) {
const data = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, { 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 variables: null
}); });
return data.data.data[this.gqlQueryListName]; return data.data.data[this.gqlQueryListName];
@ -51,15 +52,21 @@ export default class GqlApi {
if ('sort' in params) { if ('sort' in params) {
res.push(`sort: ${JSON.stringify(params.sort)}`) 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(',')})`; return `(${res.join(',')})`;
} }
gqlQuery(params) { async gqlQuery(params) {
return `{${this.gqlQueryListName}${this.generateQueryParams(params)}${this.gqlReqBody}}` return `{${this.gqlQueryListName}${this.generateQueryParams(params)}{${this.gqlReqBody}${await this.gqlRelationReqBody(params)}}}`
} }
gqlReadQuery(id) { gqlReadQuery(id) {
return `{${this.gqlQueryReadName}(id:"${id}")${this.gqlReqBody}}` return `{${this.gqlQueryReadName}(id:"${id}"){${this.gqlReqBody}}}`
} }
gqlCountQuery(params) { gqlCountQuery(params) {
@ -79,7 +86,52 @@ export default class GqlApi {
} }
get gqlReqBody() { 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() { get gqlQueryCountName() {
@ -102,7 +154,13 @@ export default class GqlApi {
async paginatedList(params) { async paginatedList(params) {
// const list = await this.list(params); // const list = await this.list(params);
// const count = (await this.count({where: params.where || ''})); // 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}; return {list, count};
} }
@ -113,7 +171,7 @@ export default class GqlApi {
${this.gqlMutationUpdateName}(id: $id, data: $data) ${this.gqlMutationUpdateName}(id: $id, data: $data)
}`, }`,
variables: { variables: {
id, data id: id, data
} }
}); });
@ -130,10 +188,9 @@ export default class GqlApi {
} }
async insert(data) { async insert(data) {
const data1 = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, { const data1 = await this.post(`/nc/${this.$ctx.$route.params.project_id}/v1/graphql`, {
query: `mutation create($data:${this.tableCamelized}Input){ query: `mutation create($data:${this.tableCamelized}Input){
${this.gqlMutationCreateName}(data: $data)${this.gqlReqBody} ${this.gqlMutationCreateName}(data: $data){${this.gqlReqBody}}
}`, }`,
variables: { variables: {
data data
@ -170,9 +227,32 @@ export default class GqlApi {
} }
get table() { 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 * @copyright Copyright (c) 2021, Xgene Cloud Ltd

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

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

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

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

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

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

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

@ -6,7 +6,7 @@
<item-chip v-for="(v,j) in (value || localState)" <item-chip v-for="(v,j) in (value || localState)"
:active="active" :active="active"
:item="v" :item="v"
:value="Object.values(v)[2]" :value="getCellValue(v)"
:key="j" :key="j"
@edit="editChild" @edit="editChild"
@unlink="unlinkChild" @unlink="unlinkChild"
@ -14,7 +14,8 @@
</template> </template>
</div> </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 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> <x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div> </div>
@ -134,7 +135,8 @@ 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 ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip"; import ItemChip from "@/components/project/spreadsheet/components/virtualCell/components/item-chip";
import ListChildItems from "@/components/project/spreadsheet/components/virtualCell/components/listChildItems"; 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 { export default {
name: "many-to-many-cell", name: "many-to-many-cell",
@ -155,8 +157,8 @@ export default {
isNewChild: false, isNewChild: false,
newRecordModal: false, newRecordModal: false,
childListModal: false, childListModal: false,
childMeta: null, // childMeta: null,
assocMeta: null, // assocMeta: null,
childList: null, childList: null,
dialogShow: false, dialogShow: false,
confirmAction: null, confirmAction: null,
@ -222,25 +224,35 @@ export default {
async loadChildMeta() { async loadChildMeta() {
// todo: optimize // todo: optimize
if (!this.childMeta) { if (!this.childMeta) {
const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env, env: this.nodes.env,
dbAlias: this.nodes.dbAlias dbAlias: this.nodes.dbAlias,
}, 'tableXcModelGet', {
tn: this.mm.rtn 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() { async loadAssociateTableMeta() {
// todo: optimize // todo: optimize
if (!this.assocMeta) { if (!this.assocMeta) {
const assocTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env, env: this.nodes.env,
dbAlias: this.nodes.dbAlias dbAlias: this.nodes.dbAlias,
}, 'tableXcModelGet', {
tn: this.mm.vtn 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() { async showNewRecordModal() {
@ -254,7 +266,6 @@ export default {
this.newRecordModal = false; this.newRecordModal = false;
return 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 pid = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
@ -262,8 +273,8 @@ export default {
const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn; const vpidCol = this.assocMeta.columns.find(c => c.cn === this.mm.vcn)._cn;
try { try {
await this.assocApi.insert({ await this.assocApi.insert({
[vcidCol]: cid, [vcidCol]: +cid,
[vpidCol]: pid [vpidCol]: +pid
}); });
this.$emit('loadTableData') this.$emit('loadTableData')
@ -299,15 +310,30 @@ export default {
this.$refs.expandedForm && this.$refs.expandedForm.reload() this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500) }, 500)
}, },
getCellValue(cellObj) {
if (cellObj) {
if (this.parentMeta && this.childPrimaryCol) {
return cellObj[this.childPrimaryCol]
}
return Object.values(cellObj)[1]
}
}
}, },
computed: { computed: {
childMeta() {
return this.$store.state.meta.metas[this.mm.rtn]
},
assocMeta() {
return this.$store.state.meta.metas[this.mm.vtn]
},
childApi() { childApi() {
return this.childMeta && this.childMeta._tn ? return this.childMeta && this.childMeta._tn ?
ApiFactory.create( ApiFactory.create(
this.$store.getters['project/GtrProjectType'], this.$store.getters['project/GtrProjectType'],
this.childMeta._tn, this.childMeta._tn,
this.childMeta.columns, this.childMeta.columns,
this this,
this.childMeta
) : null; ) : null;
}, },
assocApi() { assocApi() {
@ -316,7 +342,8 @@ export default {
this.$store.getters['project/GtrProjectType'], this.$store.getters['project/GtrProjectType'],
this.assocMeta._tn, this.assocMeta._tn,
this.assocMeta.columns, this.assocMeta.columns,
this this,
this.assocMeta
) : null; ) : null;
}, },
childPrimaryCol() { childPrimaryCol() {
@ -375,6 +402,10 @@ export default {
this.$emit('newRecordsSaved') this.$emit('newRecordsSaved')
} }
} }
},
created() {
this.loadChildMeta();
this.loadAssociateTableMeta()
} }
} }
</script> </script>
@ -436,9 +467,10 @@ export default {
.chips { .chips {
max-width: 100%; max-width: 100%;
} }
&.active { &.active {
.chips { .chips {
max-width: calc(100% - 60px); 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); 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) : 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() { 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() { colLength() {
return (this.meta && this.meta.columns && this.meta.columns.length) || 0 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); return SqlUI.create(this.nodes.dbConnection);
}, },
api() { 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() { }, edited() {
return this.data && this.data.some(r => r.rowMeta && (r.rowMeta.new || r.rowMeta.changed)) 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 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});
} catch (e) { } catch (e) {
this.$toast.error(e).goAway(3000); this.$toast.error(e).goAway(3000);
this.$router.push('/projects') 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": { "growl": {
"version": "1.10.5", "version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",

1
packages/nocodb/package.json

@ -122,6 +122,7 @@
"glob": "^7.1.6", "glob": "^7.1.6",
"graphql": "^15.3.0", "graphql": "^15.3.0",
"graphql-depth-limit": "^1.1.0", "graphql-depth-limit": "^1.1.0",
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
"inflection": "^1.12.0", "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); let gs = _.groupBy(childs, _cn);
parent.forEach(row => { 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 * @private
*/ */
async _getManyToManyList({parent, child}, rest = {}, index) { 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); 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) || {}; const {tn, cn, vtn, vcn, vrcn, rtn, rcn} = this.manyToManyRelations.find(({rtn}) => rtn === child) || {};
// @ts-ignore // @ts-ignore
@ -1159,12 +1171,12 @@ class BaseModelSql extends BaseModel {
const childs = await this._run(this.dbDriver.union( const childs = await this._run(this.dbDriver.union(
parent.map(p => { parentIds.map(id => {
const query = const query =
this this
.dbDriver(child) .dbDriver(child)
.join(vtn, `${vtn}.${vrcn}`, `${rtn}.${rcn}`) .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('')) .xwhere(where, this.dbModels[child].selectQuery(''))
.select({[`${tn}_${vcn}`]: `${vtn}.${vcn}`, ...this.dbModels[child].selectQuery(fields)}) // ...fields.split(',')); .select({[`${tn}_${vcn}`]: `${vtn}.${vcn}`, ...this.dbModels[child].selectQuery(fields)}) // ...fields.split(','));
@ -1173,11 +1185,8 @@ class BaseModelSql extends BaseModel {
}), !this.isSqlite() }), !this.isSqlite()
)); ));
let gs = _.groupBy(childs, `${tn}_${vcn}`); const gs = _.groupBy(childs, `${tn}_${vcn}`);
parent.forEach(row => { return parentIds.map(id => gs[id] || [])
row[this.dbModels?.[child]?._tn || child] = gs[row[this.pks[0]._cn]] || [];
})
} }
/** /**
@ -1453,7 +1462,7 @@ class BaseModelSql extends BaseModel {
const gs = _.groupBy(parents, this.dbModels[parent]?.columnToAlias?.[rcn] || rcn); const gs = _.groupBy(parents, this.dbModels[parent]?.columnToAlias?.[rcn] || rcn);
childs.forEach(row => { 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); const meta = JSON.parse(metaObj.meta);
metas.push(meta); metas.push(meta);
const ctx = this.generateContextForTable(meta.tn, meta.columns, [], meta.hasMany, meta.belongsTo, meta.type, meta._tn); 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(); meta.v = ModelXcMetaFactory.create(this.connectionConfig, {dir: '', ctx, filename: ''}).getVitualColumns();
// set default primary values
ModelXcMetaFactory.create(this.connectionConfig, {}).mapDefaultPrimaryValue(meta.columns); ModelXcMetaFactory.create(this.connectionConfig, {}).mapDefaultPrimaryValue(meta.columns);
// update meta
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', { await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(meta) meta: JSON.stringify(meta)
}, {title: meta.tn}) }, {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 ModelXcMetaFactory from "../../sqlMgr/code/models/xc/ModelXcMetaFactory";
import ExpressXcTsPolicyGql from "../../sqlMgr/code/gql-policies/xc-ts/ExpressXcTsPolicyGql"; 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'); 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); 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) { 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) { private addHmCountResolverMethodToType(hm, mw, tn: string, loaderFunctionsObj, countPropName: string, colNameAlias) {
{ {
this.log(`xcTablesRead : Creating loader for '%s'`, `${tn}Hm${hm.tn}Count`) 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); this.log(`xcTablesPopulate : Generating schema of '%s' %s`, table.tn, table.type);
/**************** prepare GQL: schemas, types, resolvers ****************/ /**************** 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 // tslint:disable-next-line:max-classes-per-file
this.types[table.tn] = class extends XCType { this.types[table.tn] = class extends XCType {
@ -600,7 +665,7 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
title: table.tn, title: table.tn,
type: table.type || 'table', type: table.type || 'table',
meta: JSON.stringify(this.metas[table.tn]), meta: JSON.stringify(this.metas[table.tn]),
schema: this.schemas[table.tn], // schema: this.schemas[table.tn],
alias: this.metas[table.tn]._tn, alias: this.metas[table.tn]._tn,
}) })
} }
@ -712,6 +777,84 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
})); }));
await this.getManyToManyRelations(); 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([{ const rootValue = mergeResolvers([{
nocodb_health() { nocodb_health() {
return 'Coming soon' 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))]); }, ...Object.values(this.resolvers).map(r => r.mapResolvers(this.customResolver))]);
this.log(`initGraphqlRoute : Building graphql schema`); this.log(`initGraphqlRoute : Building graphql schema`);
@ -1568,7 +1714,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}, },
rootValue, rootValue,
schema, schema,
validationRules: [depthLimit(this.connectionConfig?.meta?.api?.graphqlDepthLimit ?? 10), validationRules: [
depthLimit(this.connectionConfig?.meta?.api?.graphqlDepthLimit ?? 10),
], ],
customExecuteFn: async (args) => { customExecuteFn: async (args) => {
const data = await execute(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/>.
*
*/

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

@ -42,7 +42,16 @@ export default class GqlResolver extends GqlBaseResolver {
public async list(args, {req, res}): Promise<any> { public async list(args, {req, res}): Promise<any> {
const startTime = process.hrtime(); 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 data = await req.model.list(args);
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime)); const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds); res.setHeader('xc-db-response', elapsedSeconds);
@ -98,6 +107,16 @@ export default class GqlResolver extends GqlBaseResolver {
} }
public async count(args, {req}): Promise<any> { 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); const data = await req.model.countByPk(args);
return data.count; return data.count;
} }

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

@ -38,10 +38,17 @@ input ConditionFloat{
ge: Float ge: Float
} }
scalar JSON
type Query{ type Query{
nocodb_health:String 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 * @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 inflection from "inflection";
import lodash from "lodash"; import lodash from "lodash";
import {AGG_DEFAULT_COLS, GROUPBY_DEFAULT_COLS} from "./schemaHelp"; 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._getMutation(args)}\r\n
${this._getType(args)}\r\n ${this._getType(args)}\r\n
` `
str += ''; str += '';
return str; return str;
@ -81,11 +80,11 @@ class GqlXcTsSchemaMysql extends BaseRender {
_getQuery(args) { _getQuery(args) {
let str = `type Query { \r\n` 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}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}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}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}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}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` 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 += `\t\t${childTable}Count: Int\r\n`;
} }
str+= this.generateManyToManyTypeProps(args);
let belongsToRelations = args.relations.filter(r => r.tn === args.tn); let belongsToRelations = args.relations.filter(r => r.tn === args.tn);
if (belongsToRelations.length > 1) if (belongsToRelations.length > 1)
belongsToRelations = lodash.uniqBy(belongsToRelations, function (e) { belongsToRelations = lodash.uniqBy(belongsToRelations, function (e) {
@ -151,6 +153,7 @@ class GqlXcTsSchemaMysql extends BaseRender {
strWhere += `\t\t${parentTable}Read: Condition${parentTable}\r\n`; strWhere += `\t\t${parentTable}Read: Condition${parentTable}\r\n`;
} }
str += `\t}\r\n` str += `\t}\r\n`

Loading…
Cancel
Save