Browse Source

feat: manyToMany support

Signed-off-by: Pranav C Balan <pranavxc@gmail.com>
pull/341/head
Pranav C Balan 3 years ago committed by Pranav C
parent
commit
3c5bfe289e
  1. 22
      README.md
  2. 5
      packages/nc-gui/components/project/spreadsheet/apis/restApi.js
  3. 5
      packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js
  4. 43
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  5. 1
      packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts
  6. 2
      packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts
  7. 60
      packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts
  8. 4
      packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts
  9. 12
      packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts
  10. 10
      packages/nocodb/src/lib/noco/rest/RestCtrl.ts
  11. 26
      packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts

22
README.md

@ -238,3 +238,25 @@ Our mission is to provide the most powerful no-code interface for databases whic
- - - - - - -
### Has Many - Schema
- Create
* Create a new relation
- Create foreign key in child table
- Update metadata
* Existing
- We need to show existing relation
- Update metadata
- Delete
* Delete virtual field
* Delete virtual field and relation
- HasMany Lookup column
* Rendering
* Adding new child
- Associate an existing child
- Create and add
* Pagination
* Remove child

5
packages/nc-gui/components/project/spreadsheet/apis/restApi.js

@ -7,9 +7,8 @@ export default class RestApi {
// todo: - get version letter and use table alias
async list(params) {
const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}`, params);
// data.headers['xc-db-response'];
// const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}`, params)
const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}/nestedList`, params)
return data.data;
}

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

@ -103,7 +103,10 @@ export default {
limit: this.size,
offset: this.size * (this.page - 1),
where: this.concatenatedXWhere,
sort: this.sort
sort: this.sort,
childs: (this.meta && this.meta.hasMany && this.meta.hasMany.map(hm => hm.tn).join())||'',
parents: (this.meta && this.meta.belongsTo && this.meta.belongsTo.map(hm => hm.rtn).join())||'',
many: (this.meta && this.meta.manyToMany && this.meta.manyToMany.map(mm => mm.rtn).join())||''
}
}, colLength() {
return (this.availableColumns && this.availableColumns.length) || 0

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

@ -40,6 +40,29 @@
<!--{{ col.cn }}-->
</th>
<th v-for="bt in meta.belongsTo"
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
>
{{ bt._rtn }} (Belongs To)
</th>
<th v-for="hm in meta.hasMany"
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
>
{{ hm._tn }} (Has Many)
</th>
<th v-for="mm in meta.manyToMany"
class="grey-border caption font-wight-regular"
:class="$store.state.windows.darkTheme ? 'grey darken-3 grey--text text--lighten-1' : 'grey lighten-4 grey--text text--darken-2'"
>
{{ mm.rtn }} (Many To Many)
</th>
<th
v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')"
@click="addNewColMenu = true"
@ -63,6 +86,7 @@
</v-menu>
</th>
</tr>
</thead>
<tbody>
@ -261,6 +285,25 @@
:sql-ui="sqlUi"
></table-cell>
</td>
<td v-for="(bt,i) in meta.belongsTo" class="caption">
<v-chip x-small v-if="rowObj[bt._rtn]" :color="colors[i%colors.length]">{{ Object.values(rowObj[bt._rtn])[1] }}</v-chip>
</td>
<td v-for="hm in meta.hasMany" class="caption">
<v-chip v-if="Array.isArray(rowObj[hm._tn])" x-small v-for="(v,i) in rowObj[hm._tn].map(v=>Object.values(v)[1])" :color="colors[i%colors.length]">
{{ v }}
</v-chip>
</td>
<td v-for="mm in meta.manyToMany" class="caption">
<v-chip v-if="rowObj[mm._rtn]" x-small v-for="(v,i) in rowObj[mm._rtn].map(v=>Object.values(v)[2])" :color="colors[i%colors.length]">{{
v
}}
</v-chip>
</td>
</tr>
<tr v-if="!isLocked && !isPublicView && isEditable && relationType !== 'bt'">
<td :colspan="visibleColLength + 1" class="text-left pointer" @click="insertNewRow(true)">

1
packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts

@ -60,6 +60,7 @@ abstract class BaseModel {
protected pks: any[];
protected hasManyRelations: any;
protected belongsToRelations: any;
protected manyToManyRelations: any;
protected config: any;
protected clientType: string;
public readonly type: string;

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

@ -39,6 +39,7 @@ class BaseModelSql extends BaseModel {
columns,
hasMany = [],
belongsTo = [],
manyToMany = [],
type,
dbModels
}: {
@ -63,6 +64,7 @@ class BaseModelSql extends BaseModel {
this.pks = columns.filter(c => c.pk === true);
this.hasManyRelations = hasMany;
this.belongsToRelations = belongsTo;
this.manyToManyRelations = manyToMany;
this.config = {
limitDefault: process.env.DB_QUERY_LIMIT_DEFAULT || 10,
limitMax: process.env.DB_QUERY_LIMIT_MAX || 500,

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

@ -737,8 +737,6 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
protected initDbDriver(): void {
if (!this.dbDriver) {
if (this.connectionConfig?.connection?.ssl && typeof this.connectionConfig?.connection?.ssl === 'object') {
if (this.connectionConfig.connection.ssl.caFilePath) {
this.connectionConfig.connection.ssl.ca = fs
@ -1497,6 +1495,64 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
public getProjectId() {
return this.projectId;
}
protected async getManyToManyRelations() {
const metas = new Set<any>();
for (const meta of Object.values(this.metas)) {
// check if table is a Bridge table(or Associative Table) by checking
// number of foreign keys and columns
if (meta.belongsTo?.length === 2 && meta.columns.length < 4) {
const tableMetaA = this.metas[meta.belongsTo[0].rtn];
const tableMetaB = this.metas[meta.belongsTo[1].rtn];
// add manytomany data under metadata of both related columns
tableMetaA.manyToMany = tableMetaA.manyToMany || [];
tableMetaA.manyToMany.push({
"tn": tableMetaA.tn,
"cn": meta.belongsTo[0].rcn,
"vtn": meta.tn,
"vcn": meta.belongsTo[0].cn,
"vrcn": meta.belongsTo[1].cn,
"rtn": meta.belongsTo[1].rtn,
"rcn": meta.belongsTo[1].rcn,
"_tn": tableMetaA._tn,
"_cn": meta.belongsTo[0]._rcn,
"_rtn": meta.belongsTo[1]._rtn,
"_rcn": meta.belongsTo[1]._rcn
})
tableMetaB.manyToMany = tableMetaB.manyToMany || [];
tableMetaB.manyToMany.push({
"tn": tableMetaB.tn,
"cn": meta.belongsTo[1].rcn,
"vtn": meta.tn,
"vcn": meta.belongsTo[1].cn,
"vrcn": meta.belongsTo[0].cn,
"rtn": meta.belongsTo[0].rtn,
"rcn": meta.belongsTo[0].rcn,
"_tn": tableMetaB._tn,
"_cn": meta.belongsTo[1]._rcn,
"_rtn": meta.belongsTo[0]._rtn,
"_rcn": meta.belongsTo[0]._rcn
})
metas.add(tableMetaA)
metas.add(tableMetaB)
}
}
// Update metadata of tables which have manytomany relation
// and recreate basemodel with new meta information
for (const meta of metas) {
await this.xcMeta.metaUpdate(this.projectId, this.dbAlias, 'nc_models', {
meta: JSON.stringify(meta)
}, {title: meta.tn})
XcCache.del([this.projectId, this.dbAlias, 'table', meta.tn].join('::'));
this.models[meta.tn] = this.getBaseModel(meta)
}
}
}
export {IGNORE_TABLES};

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

@ -620,12 +620,10 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
await NcHelp.executeOperations(insertResolvers, this.connectionConfig.client);
}
}
});
await NcHelp.executeOperations(tableResolvers, this.connectionConfig.client);
await Promise.all(Object.entries(this.metas).map(async ([tn, schema]) => {
for (const hm of schema.hasMany) {
@ -757,6 +755,8 @@ export class GqlApiBuilder extends BaseApiBuilder<Noco> implements XcMetaMgr {
}
}));
await this.getManyToManyRelations();
}
}

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

@ -53,14 +53,10 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
constructor(app: Noco, projectBuilder: NcProjectBuilder, config: NcConfig, connectionConfig: DbConfig, xcMeta?: NcMetaIO) {
super(app, projectBuilder, config, connectionConfig);
autoBind(this)
this.models = {};
this.controllers = {};
this.routers = {};
this.hooks = {};
this.xcMeta = xcMeta;
}
@ -292,7 +288,6 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
let tables;
const swaggerRefs: { [table: string]: any[] } = {};
/* Get all relations */
const relations = await this.relationsSyncAndGet();
this.relationsCount = relations.length;
@ -545,11 +540,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
this.controllers[name].mapRoutes(router, this.customRoutes);
}
});
}
});
/* handle xc_tables update in parallel */
@ -557,6 +548,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
await NcHelp.executeOperations(relationRoutes, this.connectionConfig.client);
await this.getManyToManyRelations();
const swaggerDoc = {
tags: [],
paths: {},

10
packages/nocodb/src/lib/noco/rest/RestCtrl.ts

@ -135,7 +135,15 @@ export class RestCtrl extends RestBaseCtrl {
const data = await req.model.delb(req.body)
res.json(data);
}
public async nestedList(req: Request | any, res): Promise<void> {
const startTime = process.hrtime();
const data = await req.model.nestedList({
...req.query
} as any);
const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTime));
res.setHeader('xc-db-response', elapsedSeconds);
res.xcJson(data);
}
protected async middleware(req: Request | any, res: Response, next: NextFunction): Promise<any> {

26
packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts

@ -12,7 +12,7 @@ class ExpressXcTsRoutes extends BaseRender {
* @param ctx.columns
* @param ctx.relations
*/
constructor({dir, filename, ctx}:any) {
constructor({dir, filename, ctx}: any) {
super({dir, filename, ctx});
}
@ -21,7 +21,7 @@ class ExpressXcTsRoutes extends BaseRender {
*/
prepare() {
let data:any = {};
let data: any = {};
/* example of simple variable */
data = this.ctx;
@ -31,9 +31,8 @@ class ExpressXcTsRoutes extends BaseRender {
}
getObject() {
const ejsData:any = this.prepare();
const ejsData: any = this.prepare();
const routes = [
{
path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}`,
@ -65,6 +64,23 @@ async function(req, res){
res.json(data);
}
`]
}, {
path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}/nestedList`,
type: 'get',
handler: ['nestedList'],
acl: {
admin: true,
user: true,
guest: true
},
functions: [`
async function(req, res){
const data = await req.parentModel.hasManyList({
...req.query
});
res.json(data);
}
`]
}, {
path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}/groupby/:column_name`,
type: 'get',
@ -290,7 +306,7 @@ async function(req, res){
res.json(data);
}
`]
},
}
];
if (this.ctx.type === 'view') {

Loading…
Cancel
Save