Browse Source

feat: manyToMany support

Signed-off-by: Pranav C Balan <pranavxc@gmail.com>
pull/341/head
Pranav C Balan 4 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 // todo: - get version letter and use table alias
async list(params) { async list(params) {
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}`, params)
// data.headers['xc-db-response']; const data = await this.get(`/nc/${this.$ctx.$route.params.project_id}/api/v1/${this.table}/nestedList`, params)
return data.data; return data.data;
} }

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

@ -103,7 +103,10 @@ export default {
limit: this.size, limit: this.size,
offset: this.size * (this.page - 1), offset: this.size * (this.page - 1),
where: this.concatenatedXWhere, 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() { }, colLength() {
return (this.availableColumns && this.availableColumns.length) || 0 return (this.availableColumns && this.availableColumns.length) || 0

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

@ -40,6 +40,29 @@
<!--{{ col.cn }}--> <!--{{ col.cn }}-->
</th> </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 <th
v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')"
@click="addNewColMenu = true" @click="addNewColMenu = true"
@ -63,6 +86,7 @@
</v-menu> </v-menu>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -261,6 +285,25 @@
:sql-ui="sqlUi" :sql-ui="sqlUi"
></table-cell> ></table-cell>
</td> </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>
<tr v-if="!isLocked && !isPublicView && isEditable && relationType !== 'bt'"> <tr v-if="!isLocked && !isPublicView && isEditable && relationType !== 'bt'">
<td :colspan="visibleColLength + 1" class="text-left pointer" @click="insertNewRow(true)"> <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 pks: any[];
protected hasManyRelations: any; protected hasManyRelations: any;
protected belongsToRelations: any; protected belongsToRelations: any;
protected manyToManyRelations: any;
protected config: any; protected config: any;
protected clientType: string; protected clientType: string;
public readonly type: string; public readonly type: string;

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

@ -39,6 +39,7 @@ class BaseModelSql extends BaseModel {
columns, columns,
hasMany = [], hasMany = [],
belongsTo = [], belongsTo = [],
manyToMany = [],
type, type,
dbModels dbModels
}: { }: {
@ -63,6 +64,7 @@ class BaseModelSql extends BaseModel {
this.pks = columns.filter(c => c.pk === true); this.pks = columns.filter(c => c.pk === true);
this.hasManyRelations = hasMany; this.hasManyRelations = hasMany;
this.belongsToRelations = belongsTo; this.belongsToRelations = belongsTo;
this.manyToManyRelations = manyToMany;
this.config = { this.config = {
limitDefault: process.env.DB_QUERY_LIMIT_DEFAULT || 10, limitDefault: process.env.DB_QUERY_LIMIT_DEFAULT || 10,
limitMax: process.env.DB_QUERY_LIMIT_MAX || 500, 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 { protected initDbDriver(): void {
if (!this.dbDriver) { if (!this.dbDriver) {
if (this.connectionConfig?.connection?.ssl && typeof this.connectionConfig?.connection?.ssl === 'object') { if (this.connectionConfig?.connection?.ssl && typeof this.connectionConfig?.connection?.ssl === 'object') {
if (this.connectionConfig.connection.ssl.caFilePath) { if (this.connectionConfig.connection.ssl.caFilePath) {
this.connectionConfig.connection.ssl.ca = fs this.connectionConfig.connection.ssl.ca = fs
@ -1497,6 +1495,64 @@ export default abstract class BaseApiBuilder<T extends Noco> implements XcDynami
public getProjectId() { public getProjectId() {
return this.projectId; 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}; 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(insertResolvers, this.connectionConfig.client);
} }
} }
}); });
await NcHelp.executeOperations(tableResolvers, this.connectionConfig.client); await NcHelp.executeOperations(tableResolvers, this.connectionConfig.client);
await Promise.all(Object.entries(this.metas).map(async ([tn, schema]) => { await Promise.all(Object.entries(this.metas).map(async ([tn, schema]) => {
for (const hm of schema.hasMany) { 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) { constructor(app: Noco, projectBuilder: NcProjectBuilder, config: NcConfig, connectionConfig: DbConfig, xcMeta?: NcMetaIO) {
super(app, projectBuilder, config, connectionConfig); super(app, projectBuilder, config, connectionConfig);
autoBind(this) autoBind(this)
this.models = {};
this.controllers = {}; this.controllers = {};
this.routers = {}; this.routers = {};
this.hooks = {}; this.hooks = {};
this.xcMeta = xcMeta; this.xcMeta = xcMeta;
} }
@ -292,7 +288,6 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
let tables; let tables;
const swaggerRefs: { [table: string]: any[] } = {}; const swaggerRefs: { [table: string]: any[] } = {};
/* Get all relations */ /* Get all relations */
const relations = await this.relationsSyncAndGet(); const relations = await this.relationsSyncAndGet();
this.relationsCount = relations.length; this.relationsCount = relations.length;
@ -545,11 +540,7 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
this.controllers[name].mapRoutes(router, this.customRoutes); this.controllers[name].mapRoutes(router, this.customRoutes);
} }
}); });
} }
}); });
/* handle xc_tables update in parallel */ /* handle xc_tables update in parallel */
@ -557,6 +548,9 @@ export class RestApiBuilder extends BaseApiBuilder<Noco> {
await NcHelp.executeOperations(relationRoutes, this.connectionConfig.client); await NcHelp.executeOperations(relationRoutes, this.connectionConfig.client);
await this.getManyToManyRelations();
const swaggerDoc = { const swaggerDoc = {
tags: [], tags: [],
paths: {}, 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) const data = await req.model.delb(req.body)
res.json(data); 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> { 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.columns
* @param ctx.relations * @param ctx.relations
*/ */
constructor({dir, filename, ctx}:any) { constructor({dir, filename, ctx}: any) {
super({dir, filename, ctx}); super({dir, filename, ctx});
} }
@ -21,7 +21,7 @@ class ExpressXcTsRoutes extends BaseRender {
*/ */
prepare() { prepare() {
let data:any = {}; let data: any = {};
/* example of simple variable */ /* example of simple variable */
data = this.ctx; data = this.ctx;
@ -31,9 +31,8 @@ class ExpressXcTsRoutes extends BaseRender {
} }
getObject() { getObject() {
const ejsData:any = this.prepare(); const ejsData: any = this.prepare();
const routes = [ const routes = [
{ {
path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}`, path: `/api/${this.ctx.routeVersionLetter}/${ejsData._tn}`,
@ -63,6 +62,23 @@ async function(req, res){
async function(req, res){ async function(req, res){
const data = await req.model.findOne(req.query); const data = await req.model.findOne(req.query);
res.json(data); 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);
} }
`] `]
}, { }, {
@ -290,7 +306,7 @@ async function(req, res){
res.json(data); res.json(data);
} }
`] `]
}, }
]; ];
if (this.ctx.type === 'view') { if (this.ctx.type === 'view') {

Loading…
Cancel
Save