Browse Source

feat: Has many & belongs to - new record creation

Signed-off-by: Pranav C <61551451+pranavxc@users.noreply.github.com>
pull/341/head
Pranav C 3 years ago
parent
commit
92f2a59b88
  1. 18
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  2. 80
      packages/nc-gui/components/project/spreadsheet/components/virtualCell.vue
  3. 240
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/belogsToCell.vue
  4. 112
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue
  5. 80
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/hasManyCell.vue
  6. 142
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts

18
packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue

@ -76,7 +76,7 @@
<div
style="height:100%; width:100%"
class="caption xc-input"
v-if="col.ai || (col.pk && !selectedRowMeta.new) || disabledColumns[col._cn]"
v-else-if="col.ai || (col.pk && !isNew) || disabledColumns[col._cn]"
@click="col.ai && $toast.info('Auto Increment field is not editable').goAway(3000)"
>
<input
@ -204,8 +204,8 @@ export default {
table: String,
primaryValueColumn: String,
api: [Object],
hasMany: Object,
belongsTo: Object,
hasMany: [Object, Array],
belongsTo: [Object, Array],
isNew: Boolean,
oldRow: Object,
iconColor: {
@ -215,9 +215,9 @@ export default {
availableColumns: [Object, Array],
nodes: [Object],
queryParams: Object,
disabledColumns:{
type:Object,
default(){
disabledColumns: {
type: Object,
default() {
return {}
}
}
@ -386,7 +386,7 @@ export default {
.row-col {
& > div > input,
& > div div >input,
& > div div > input,
& > div > .xc-input > input,
& > div > select,
& > div > .xc-input > select,
@ -417,7 +417,7 @@ export default {
background: #363636;
.row-col {
& > div div > input,
& > div div > input,
& > div > input,
& > div > .xc-input > input,
& > div > select,
@ -433,7 +433,7 @@ export default {
.row-col {
& > div > input,
& > div div >input,
& > div div > input,
& > div > .xc-input > input,
& > div > select,
& > div > .xc-input > select,

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

@ -1,44 +1,44 @@
<template>
<div>
<v-lazy>
<has-many-cell
v-if="hm"
:row="row"
:value="row[hm._tn]"
:meta="meta"
:hm="hm"
:nodes="nodes"
:active="active"
:sql-ui="sqlUi"
:is-new="isNew"
v-on="$listeners"
/>
<many-to-many-cell
v-else-if="mm"
:row="row"
:value="row[mm._rtn]"
:meta="meta"
:mm="mm"
:nodes="nodes"
:sql-ui="sqlUi"
:active="active"
:is-new="isNew"
v-on="$listeners"
/>
<belongs-to-cell
:disabled-columns="disabledColumns"
v-else-if="bt"
:active="active"
:row="row"
:value="row[bt._rtn]"
:meta="meta"
:bt="bt"
:nodes="nodes"
:api="api"
:sql-ui="sqlUi"
:is-new="isNew"
v-on="$listeners"
/>
<has-many-cell
v-if="hm"
:row="row"
:value="row[hm._tn]"
:meta="meta"
:hm="hm"
:nodes="nodes"
:active="active"
:sql-ui="sqlUi"
:is-new="isNew"
v-on="$listeners"
/>
<many-to-many-cell
v-else-if="mm"
:row="row"
:value="row[mm._rtn]"
:meta="meta"
:mm="mm"
:nodes="nodes"
:sql-ui="sqlUi"
:active="active"
:is-new="isNew"
v-on="$listeners"
/>
<belongs-to-cell
:disabled-columns="disabledColumns"
v-else-if="bt"
:active="active"
:row="row"
:value="row[bt._rtn]"
:meta="meta"
:bt="bt"
:nodes="nodes"
:api="api"
:sql-ui="sqlUi"
:is-new="isNew"
v-on="$listeners"
/>
</v-lazy>
</div>
</template>
@ -48,6 +48,8 @@ import hasManyCell from "@/components/project/spreadsheet/components/virtualCell
import manyToManyCell from "@/components/project/spreadsheet/components/virtualCell/manyToManyCell";
import belongsToCell from "@/components/project/spreadsheet/components/virtualCell/belogsToCell";
// todo: optimize parent/child meta extraction
export default {
name: "virtual-cell",
components: {
@ -67,7 +69,7 @@ export default {
type: Boolean,
default: false
},
disabledColumns:Object
disabledColumns: Object
},
computed: {
hm() {

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

@ -2,57 +2,115 @@
<div class="d-flex">
<div class="d-flex align-center img-container flex-grow-1 hm-items">
<template v-if="value">
<v-chip small :color="colors[0]">{{
<v-chip small :color="colors[0]"
@click="active && editParent(value)"
>{{
Object.values(value)[1]
}}
<div v-show="active" class="mr-n1 ml-2 mt-n1">
<x-icon
:color="['text' , 'textLight']"
x-small
icon.class="unlink-icon"
@click.stop="unlink(ch)"
>mdi-close-thick
</x-icon>
</div>
</v-chip>
</template>
</div>
<div v-if="!isNew" class=" align-center justify-center px-1 flex-shrink-1" :class="{'d-none': !active, 'd-flex':active }">
<div v-if="!isNew" class=" align-center justify-center px-1 flex-shrink-1"
:class="{'d-none': !active, 'd-flex':active }">
<x-icon small :color="['primary','grey']" @click="showNewRecordModal">{{
value ? 'mdi-pencil' : 'mdi-plus'
value ? 'mdi-arrow-expand' : 'mdi-plus'
}}
</x-icon>
</div>
<list-items
v-if="newRecordModal"
:size="10"
:meta="parentMeta"
:primary-col="parentPrimaryCol"
:primary-key="parentPrimaryKey"
v-model="newRecordModal"
:api="parentApi"
@add-new-record="insertAndMapNewParentRecord"
@add="addParentToChild"
:query-params="parentQueryParams"
/>
<!-- <v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600">-->
<!-- <v-card width="600" color="backgroundColor">-->
<!-- <v-card-title class="textColor&#45;&#45;text mx-2">Add Record</v-card-title>-->
<!-- <v-card-text>-->
<!-- <v-text-field-->
<!-- hide-details-->
<!-- dense-->
<!-- outlined-->
<!-- placeholder="Search record"-->
<!-- class="mb-2 mx-2 caption"-->
<!-- />-->
<!-- <div class="items-container">-->
<!-- <template v-if="list">-->
<!-- <v-card-->
<!-- v-for="(p,i) in list.list"-->
<!-- class="ma-2 child-card"-->
<!-- outlined-->
<!-- v-ripple-->
<!-- @click="addParentToChild(p)"-->
<!-- :key="i"-->
<!-- >-->
<!-- <v-card-title class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2">{{ p[parentPrimaryCol] }}-->
<!-- <span class="grey&#45;&#45;text caption primary-key"-->
<!-- v-if="parentPrimaryKey">(Primary Key : {{ p[parentPrimaryKey] }})</span>-->
<!-- </v-card-title>-->
<!-- </v-card>-->
<!-- </template>-->
<!-- </div>-->
<!-- </v-card-text>-->
<!-- <v-card-actions class="justify-center pb-6 ">-->
<!-- <v-btn small outlined class="caption" color="primary">-->
<!-- <v-icon>mdi-plus</v-icon>-->
<!-- Add New Record-->
<!-- </v-btn>-->
<!-- </v-card-actions>-->
<!-- </v-card>-->
<!-- </v-dialog>-->
<v-dialog
:overlay-opacity="0.8"
v-if="selectedParent"
width="1000px"
max-width="100%"
class=" mx-auto"
v-model="expandFormModal">
<component
v-if="selectedParent"
:is="form"
:db-alias="nodes.dbAlias"
:has-many="parentMeta.hasMany"
:belongs-to="parentMeta.belongsTo"
:table="parentMeta.tn"
:old-row="{...selectedParent}"
:meta="parentMeta"
:sql-ui="sqlUi"
:primary-value-column="parentPrimaryCol"
:api="parentApi"
:available-columns="parentAvailableColumns"
:nodes="nodes"
:query-params="parentQueryParams"
:is-new="isNewParent"
icon-color="warning"
ref="expandedForm"
v-model="selectedParent"
@cancel="selectedParent = null"
@input="onParentSave"
></component>
<v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">Add Record</v-card-title>
<v-card-text>
<v-text-field
hide-details
dense
outlined
placeholder="Search record"
class="mb-2 mx-2 caption"
/>
<div class="items-container">
<template v-if="list">
<v-card
v-for="(p,i) in list.list"
class="ma-2 child-card"
outlined
v-ripple
@click="addParentToChild(p)"
:key="i"
>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ p[parentPrimaryCol] }}
<span class="grey--text caption primary-key"
v-if="parentPrimaryKey">(Primary Key : {{ p[parentPrimaryKey] }})</span>
</v-card-title>
</v-card>
</template>
</div>
</v-card-text>
<v-card-actions class="justify-center pb-6 ">
<v-btn small outlined class="caption" color="primary">
<v-icon>mdi-plus</v-icon>
Add New Record
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@ -62,10 +120,11 @@
<script>
import colors from "@/mixins/colors";
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
export default {
name: "belongs-to-cell",
components: {ListItems},
mixins: [colors],
props: {
value: [Object, Array],
@ -76,8 +135,8 @@ export default {
api: [Object, Function],
sqlUi: [Object, Function],
active: Boolean,
isNew:Boolean,
disabledColumns:Object
isNew: Boolean,
disabledColumns: Object
},
data: () => ({
newRecordModal: false,
@ -87,13 +146,42 @@ export default {
childList: null,
dialogShow: false,
confirmAction: null,
confirmMessage: ''
confirmMessage: '',
selectedParent: null,
isNewParent: false,
expandFormModal: false,
}),
methods: {
async onParentSave(parent) {
if (this.isNewParent) {
await this.addParentToChild(parent)
} else {
this.$emit('loadTableData')
}
},
async insertAndMapNewParentRecord() {
await this.loadParentMeta();
this.newRecordModal = false;
this.isNewParent = true;
this.selectedParent = {};
this.expandFormModal = true;
},
async unlink() {
const column = this.meta.columns.find(c => c.cn === this.bt.cn);
if (column.rqd) {
this.$toast.info('Unlink is not possible, instead map to another parent.').goAway(3000)
return
}
const _cn = column._cn;
const id = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
await this.api.update(id, {[_cn]: null}, this.row)
this.$emit('loadTableData')
},
async showParentListModal() {
this.parentListModal = true;
await this.getParentMeta();
await this.loadParentMeta();
const pid = this.meta.columns.filter((c) => c.pk).map(c => this.row[c._cn]).join('___');
const _cn = this.parentMeta.columns.find(c => c.cn === this.hm.cn)._cn;
this.childList = await this.parentApi.paginatedList({
@ -116,7 +204,7 @@ export default {
}
}
},
async getParentMeta() {
async loadParentMeta() {
// todo: optimize
if (!this.parentMeta) {
const parentTableData = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
@ -129,9 +217,9 @@ export default {
}
},
async showNewRecordModal() {
await this.loadParentMeta();
this.newRecordModal = true;
await this.getParentMeta();
this.list = await this.parentApi.paginatedList({})
// this.list = await this.parentApi.paginatedList({})
},
async addParentToChild(parent) {
const pid = this.parentMeta.columns.filter((c) => c.pk).map(c => parent[c._cn]).join('___');
@ -146,7 +234,16 @@ export default {
this.newRecordModal = false;
this.$emit('loadTableData')
}
},
async editParent(parent) {
await this.loadParentMeta();
this.isNewParent = false;
this.selectedParent = parent;
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
},
},
computed: {
parentApi() {
@ -159,7 +256,32 @@ export default {
},
parentPrimaryKey() {
return this.parentMeta && (this.parentMeta.columns.find(c => c.pk) || {})._cn
}
},
parentQueryParams() {
if (!this.parentMeta) return {}
return {
childs: (this.parentMeta && this.parentMeta.hasMany && this.parentMeta.hasMany.map(hm => hm.tn).join()) || '',
parents: (this.parentMeta && this.parentMeta.belongsTo && this.parentMeta.belongsTo.map(hm => hm.rtn).join()) || '',
many: (this.parentMeta && this.parentMeta.manyToMany && this.parentMeta.manyToMany.map(mm => mm.rtn).join()) || ''
}
},
parentAvailableColumns() {
const hideCols = ['created_at', 'updated_at'];
if (!this.parentMeta) return [];
const columns = [];
if (this.parentMeta.columns) {
columns.push(...this.parentMeta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.cn)))
}
if (this.parentMeta.v) {
columns.push(...this.parentMeta.v.map(v => ({...v, virtual: 1})));
}
return columns;
},
// todo:
form() {
return this.selectedParent ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
},
}
}
</script>
@ -182,24 +304,6 @@ export default {
}
}
.child-list-modal {
position: relative;
.remove-child-icon {
position: absolute;
right: 10px;
top: 10px;
bottom: 10px;
opacity: 0;
}
&:hover .remove-child-icon {
opacity: 1;
}
}
.child-card {
cursor: pointer;

112
packages/nc-gui/components/project/spreadsheet/components/virtualCell/components/listItems.vue

@ -1,27 +1,29 @@
<template>
<v-dialog v-if="value" v-model="show" width="600">
<v-dialog v-model="show" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2">Add Record
<v-spacer>
</v-spacer>
<v-card-title class="textColor--text mx-2 justify-center">{{ title }}
<v-btn small class="caption" color="primary">
<v-icon small>mdi-plus</v-icon>&nbsp;
Add New Record
</v-btn>
</v-card-title>
<v-card-text>
<v-card-title>
<v-text-field
hide-details
dense
outlined
placeholder="Search record"
class="mb-2 mx-2 caption"
placeholder="Search records"
class=" caption search-field ml-2"
/>
<v-spacer></v-spacer>
<v-btn small class="caption mr-2" color="primary" @click="$emit('add-new-record')">
<v-icon small>mdi-plus</v-icon>&nbsp;
New Record
</v-btn>
</v-card-title>
<v-card-text>
<div class="items-container">
<template v-if="data">
<template v-if="data && data.list && data.list.length">
<v-card
v-for="(ch,i) in data.list"
class="ma-2 child-card"
@ -30,40 +32,58 @@
@click="$emit('add',ch)"
:key="i"
>
<v-card-title class="primary-value textColor--text text--lighten-2">{{ ch[primaryCol] }}
<span class="grey--text caption primary-key"
<v-card-text class="primary-value textColor--text text--lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[primaryCol] }}&nbsp;</span>
<span class="grey--text caption primary-key "
v-if="primaryKey">(Primary Key : {{ ch[primaryKey] }})</span>
</v-card-title>
<v-spacer/>
<v-chip v-if="hm && ch[meta._tn]" x-small>
{{ ch[primaryCol] }}
</v-chip>
</v-card-text>
</v-card>
</template>
<div v-else class="text-center py-15 textLight--text">
No items found
</div>
</div>
</v-card-text>
<v-card-actions class="justify-center py-2 flex-column">
<pagination
v-if="list"
:size="size"
:count="count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
<pagination
v-if="data && data.list && data.list.length"
:size="size"
:count="data.count"
v-model="page"
@input="loadData"
class="mb-3"
></pagination>
</v-card-actions>
</v-card>
</v-dialog>
<span v-else></span>
</template>
<script>
import Pagination from "@/components/project/spreadsheet/components/pagination";
export default {
name: "listItems",
components: {Pagination},
props: {
value: Boolean,
title: String,
hm:Boolean,
title: {
type: String,
default: 'Link Record'
},
queryParams: {
type: Object,
default() {
return {};
}
},
primaryKey: String,
primaryCol: String,
meta: Object,
@ -75,13 +95,17 @@ export default {
page: 1
}),
mounted() {
this.loadData();
},
methods: {
async loadData() {
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
})
if (this.api) {
this.data = await this.api.paginatedList({
limit: this.size,
offset: this.size * (this.page - 1),
...this.queryParams
})
}
}
},
computed: {
@ -92,13 +116,6 @@ export default {
return this.value;
}
}
},
watch: {
value(v) {
if (v) {
this.loadData();
}
}
}
}
</script>
@ -129,4 +146,21 @@ export default {
}
}
.primary-value {
.primary-key {
display: none;
margin-left: .5em;
}
&:hover .primary-key {
display: inline;
}
}
.items-container {
overflow-x: visible;
max-height: min(500px, 60vh);
overflow-y: auto;
}
</style>

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

@ -7,7 +7,7 @@
v-for="(ch,i) in value"
:key="i"
:color="colors[i%colors.length]"
@click="editChild(ch)"
@click="active && editChild(ch)"
>
{{ Object.values(ch)[1] }}
<div v-show="active" class="mr-n1 ml-2 mt-n1">
@ -28,17 +28,26 @@
<x-icon x-small :color="['primary','grey']" @click="showChildListModal" class="ml-2">mdi-arrow-expand</x-icon>
</div>
<!-- <list-items
:count="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
v-model="newRecordModal"
:api="childApi"
></list-items>-->
<v-dialog v-if="newRecordModal" v-model="newRecordModal" width="600">
<list-items
v-if="newRecordModal"
:hm="true"
:size="10"
:meta="childMeta"
:primary-col="childPrimaryCol"
:primary-key="childPrimaryKey"
v-model="newRecordModal"
:api="childApi"
@add-new-record="insertAndAddNewChildRecord"
@add="addChildToParent"
:query-params="{
...childQueryParams,
where: `~not(${childForeignKey},eq,${parentId})~or(${childForeignKey},is,null)`,
}"/>
<!-- <v-dialog v-if="newRecordModal && false" v-model="newRecordModal" width="600">
<v-card width="600" color="backgroundColor">
<v-card-title class="textColor--text mx-2 justify-center">Link Record
<v-card-title class="textColor&#45;&#45;text mx-2 justify-center">Link Record
</v-card-title>
@ -70,9 +79,9 @@
@click="addChildToParent(ch)"
:key="i"
>
<v-card-text class="primary-value textColor--text text--lighten-2 d-flex">
<v-card-text class="primary-value textColor&#45;&#45;text text&#45;&#45;lighten-2 d-flex">
<span class="font-weight-bold"> {{ ch[childPrimaryCol] }}</span>
<span class="grey--text caption primary-key "
<span class="grey&#45;&#45;text caption primary-key "
v-if="childPrimaryKey">(Primary Key : {{ ch[childPrimaryKey] }})</span>
<v-spacer/>
<v-chip v-if="ch[meta._tn]" x-small>
@ -84,7 +93,7 @@
</template>
<div v-else class="text-center py-15 textLight--text">
<div v-else class="text-center py-15 textLight&#45;&#45;text">
No items found
</div>
</div>
@ -102,7 +111,7 @@
</v-card-actions>
</v-card>
</v-dialog>
-->
<v-dialog v-if="childListModal" v-model="childListModal" width="600">
<v-card width="600" color="backgroundColor">
@ -182,7 +191,7 @@
width="1000px"
max-width="100%"
class=" mx-auto"
v-model="showExpandModal">
v-model="expandFormModal">
<component
v-if="selectedChild"
:is="form"
@ -206,7 +215,6 @@
:is-new="isNewChild"
:disabled-columns="disabledChildColumns"
></component>
{{ childQueryParams }}
</v-dialog>
@ -218,10 +226,12 @@ import colors from "@/mixins/colors";
import ApiFactory from "@/components/project/spreadsheet/apis/apiFactory";
import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import Pagination from "@/components/project/spreadsheet/components/pagination";
import ListItems from "@/components/project/spreadsheet/components/virtualCell/components/listItems";
export default {
name: "has-many-cell",
components: {
ListItems,
Pagination,
DlgLabelSubmitCancel
},
@ -240,17 +250,17 @@ export default {
newRecordModal: false,
childListModal: false,
childMeta: null,
list: null,
// list: null,
childList: null,
dialogShow: false,
confirmAction: null,
confirmMessage: '',
selectedChild: null,
showExpandModal: false,
listPagination: {
page: 1,
size: 10
},
expandFormModal: false,
// listPagination: {
// page: 1,
// size: 10
// },
childListPagination: {
page: 1,
size: 10
@ -302,7 +312,7 @@ export default {
await this.loadChildMeta();
const column = this.childMeta.columns.find(c => c.cn === this.hm.cn);
if (column.rqd) {
this.$toast.info('Unlink is not possible, add to another record.').goAway(3000)
this.$toast.info('Unlink is not possible, instead add to another record.').goAway(3000)
return
}
const _cn = column._cn;
@ -329,15 +339,15 @@ export default {
}
},
async showNewRecordModal() {
this.newRecordModal = true;
await this.loadChildMeta();
const _cn = this.childForeignKey;
this.list = await this.childApi.paginatedList({
...this.childQueryParams,
limit: this.listPagination.size,
offset: this.listPagination.size * (this.listPagination.page - 1),
where: `~not(${_cn},eq,${this.parentId})~or(${_cn},is,null)`
})
this.newRecordModal = true;
// const _cn = this.childForeignKey;
// this.list = await this.childApi.paginatedList({
// ...this.childQueryParams,
// limit: this.listPagination.size,
// offset: this.listPagination.size * (this.listPagination.page - 1),
// where: `~not(${_cn},eq,${this.parentId})~or(${_cn},is,null)`
// })
},
async addChildToParent(child) {
const id = this.childMeta.columns.filter((c) => c.pk).map(c => child[c._cn]).join('___');
@ -359,7 +369,7 @@ export default {
await this.loadChildMeta();
this.isNewChild = false;
this.selectedChild = child;
this.showExpandModal = true;
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.reload()
}, 500)
@ -371,7 +381,7 @@ export default {
this.selectedChild = {
[this.childForeignKey]: this.parentId
};
this.showExpandModal = true;
this.expandFormModal = true;
setTimeout(() => {
this.$refs.expandedForm && this.$refs.expandedForm.$set(this.$refs.expandedForm.changedColumns, this.childForeignKey, true)
}, 500)
@ -400,7 +410,7 @@ export default {
},
// todo:
form() {
return () => import("@/components/project/spreadsheet/components/expandedForm")
return this.selectedChild ? () => import("@/components/project/spreadsheet/components/expandedForm") : 'span';
},
childAvailableColumns() {
const hideCols = ['created_at', 'updated_at'];

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

@ -1107,7 +1107,7 @@ class BaseModelSql extends BaseModel {
* @private
*/
async _getChildListInParent({parent, child}, rest = {}, index) {
let {fields, where, limit, offset, sort} = this._getChildListArgs(rest, index);
let {fields, where, limit, offset, sort} = this._getChildListArgs(rest, index, child);
const {cn} = this.hasManyRelations.find(({tn}) => tn === child) || {};
const _cn = this.dbModels[child].columnToAlias?.[cn];
@ -1125,7 +1125,7 @@ class BaseModelSql extends BaseModel {
.xwhere(where, this.dbModels[child].selectQuery(''))
.select(this.dbModels[child].selectQuery(fields)) // ...fields.split(','));
this._paginateAndSort(query, {sort, limit, offset}, null,true);
this._paginateAndSort(query, {sort, limit, offset}, null, true);
return this.isSqlite() ? this.dbDriver.select().from(query) : query;
}), !this.isSqlite()
));
@ -1136,6 +1136,49 @@ class BaseModelSql extends BaseModel {
})
}
/**
* Get child list and map to input parent
*
* @param {Object[]} parent - parent list array
* @param {String} child - child table name
* @param {Object} rest - index suffixed fields, limit, offset, where and sort
* @param index - child table index
* @returns {Promise<void>}
* @private
*/
async _getManyToManyList({parent, child}, rest = {}, index) {
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 _cn = this.dbModels[tn].columnToAlias?.[cn];
if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
fields += ',' + cn;
}
const childs = await this._run(this.dbDriver.union(
parent.map(p => {
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])
.xwhere(where, this.dbModels[child].selectQuery(''))
.select({[_cn]: `${vtn}.${cn}`, ...this.dbModels[child].selectQuery(fields)}) // ...fields.split(','));
this._paginateAndSort(query, {sort, limit, offset}, null, true);
return this.isSqlite() ? this.dbDriver.select().from(query) : query;
}), !this.isSqlite()
));
let gs = _.groupBy(childs, _cn);
parent.forEach(row => {
row[this.dbModels?.[child]?._tn || child] = gs[row[this.pks[0]._cn]] || [];
})
}
/**
* Gets child rows for a parent row in this table
*
@ -1192,7 +1235,7 @@ class BaseModelSql extends BaseModel {
*/
// todo : add conditionGraph
async hasManyList({childs, where, fields, f, ...rest}) {
async hasManyList({childs = '', where, fields, f, ...rest}) {
fields = fields || f || '*';
try {
@ -1202,7 +1245,7 @@ class BaseModelSql extends BaseModel {
const parent = await this.list({childs, where, fields, ...rest});
if (parent && parent.length) {
await Promise.all([...new Set(childs.split('.'))].map((child, index) => this._getChildListInParent({
await Promise.all([...new Set(childs.split('.'))].map((child, index) => child && this._getChildListInParent({
parent,
child
}, rest, index)));
@ -1214,6 +1257,75 @@ class BaseModelSql extends BaseModel {
}
}
/**
* Gets parent list along with children list and parent
*
* @param {Object} args
* @param {String} args.childs - comma separated child table names
* @param {String} [args.fields=*] - commas separated column names of this table
* @param {String} [args.fields*=*] - commas separated column names of child table(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.where] - where clause with conditions within ()
* @param {String} [args.where*] - where clause with conditions within ()(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.limit] - number of rows to be limited (has default,min,max values in config)
* @param {String} [args.limit*] - number of rows to be limited of child table(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.offset] - offset from which to get the number of rows
* @param {String} [args.offset*] - offset from which to get the number of rows of child table(* is a natural number 'i' where i is index of child table in comma separated list)
* @param {String} [args.sort] - comma separated column names where each column name is cn ascending and -cn is cn descending
* @param {String} [args.sort*] - comma separated column names where each column name is cn ascending and -cn is cn descending(* is a natural number 'i' where i is index of child table in comma separated list)
* @returns {Promise<Object[]>}
*/
// todo : add conditionGraph
async nestedList({childs = '', parents = '', many = '', where, fields: fields1, f, ...rest}) {
let fields = fields1 || f || '*';
try {
if (fields !== '*' && fields.split(',').indexOf(this.pks[0].cn) === -1) {
fields += ',' + this.pks[0].cn;
}
for (const parent of parents.split(',')) {
const {cn} = this.belongsToRelations.find(({rtn}) => rtn === parent) || {};
if (fields !== '*' && fields.split(',').indexOf(cn) === -1) {
fields += ',' + cn;
}
}
const items = await this.list({childs, where, fields, ...rest});
if (items && items.length) {
await Promise.all([...new Set(childs.split(','))].map((child, index) => child && this._getChildListInParent({
parent: items,
child
}, rest, index)));
}
await Promise.all(parents.split(',').map((parent, index): any => {
if (!parent) {
return;
}
const {cn, rcn} = this.belongsToRelations.find(({rtn}) => rtn === parent) || {};
const parentIds = [...new Set(items.map(c => c[cn] || c[this.columnToAlias[cn]]))];
return this._belongsTo({parent, rcn, parentIds, childs: items, cn, ...rest}, index);
}))
if (items && items.length) {
await Promise.all([...new Set(many.split(','))].map((child, index) => child && this._getManyToManyList({
parent: items,
child
}, rest, index)));
}
return items;
} catch (e) {
console.log(e);
throw e;
}
}
/**
* Gets child list along with its parent
*
@ -1269,7 +1381,7 @@ class BaseModelSql extends BaseModel {
* @private
*/
async _belongsTo({parent, rcn, parentIds, childs, cn, ...rest}, index) {
let {fields} = this._getChildListArgs(rest, index);
let {fields} = this._getChildListArgs(rest, index, parent);
if (fields !== '*' && fields.split(',').indexOf(rcn) === -1) {
fields += ',' + rcn;
}
@ -1284,7 +1396,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.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0];
row[this.dbModels?.[parent]?._tn || parent] = gs[row[this.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0] || gs[row[cn || gs[row[this.dbModels[parent]?.columnToAlias?.[cn] || cn]]?.[0]]]?.[0];
})
}
@ -1392,7 +1504,7 @@ class BaseModelSql extends BaseModel {
* @returns {Object} query appended with paginate and sort params
* @private
*/
_paginateAndSort(query, {limit = 20, offset = 0, sort = ''}: XcFilter, table?: string, isUnion?:boolean) {
_paginateAndSort(query, {limit = 20, offset = 0, sort = ''}: XcFilter, table?: string, isUnion?: boolean) {
query.offset(offset)
.limit(limit);
@ -1465,18 +1577,24 @@ class BaseModelSql extends BaseModel {
* @returns {Object} consisting of fields*,where*,limit*,offset*,sort*
* @private
*/
_getChildListArgs(args: any, index?: number) {
_getChildListArgs(args: any, index?: number, child?: string) {
index++;
let obj: XcFilter = {};
obj.where = args[`where${index}`] || args[`w${index}`] || '';
obj.limit = Math.max(Math.min(args[`limit${index}`] || args[`l${index}`] || this.config.limitDefault, this.config.limitMax), this.config.limitMin);
obj.offset = args[`offset${index}`] || args[`o${index}`] || 0;
obj.fields = args[`fields${index}`] || args[`f${index}`] || '*';
obj.fields = args[`fields${index}`] || args[`f${index}`] || this.getPKandPV(child);
obj.sort = args[`sort${index}`] || args[`s${index}`];
return obj;
}
// @ts-ignore
private getPKandPV(child: string) {
return child ?
(this.dbModels[child]?.columns?.filter(col => col.pk || col.pv).map(col => col.cn) || ['*']).join(',')
: '*';
}
// @ts-ignore
public selectQuery(fields) {
const fieldsArr = fields.split(',');
return this.columns?.reduce((selectObj, col) => {
@ -1484,8 +1602,8 @@ class BaseModelSql extends BaseModel {
!fields
|| fieldsArr.includes('*')
|| fieldsArr.includes(`${this.tn}.*`)
|| fields.includes(col._cn)
|| fields.includes(col.cn)
|| fieldsArr.includes(col._cn)
|| fieldsArr.includes(col.cn)
) {
selectObj[col._cn] = `${this.tn}.${col.cn}`;
}

Loading…
Cancel
Save