diff --git a/README.md b/README.md index ab6acc5821..ea7f6cee6d 100644 --- a/README.md +++ b/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 + + diff --git a/packages/nc-gui/components/project/spreadsheet/apis/restApi.js b/packages/nc-gui/components/project/spreadsheet/apis/restApi.js index 02cb421db0..a150417fb5 100644 --- a/packages/nc-gui/components/project/spreadsheet/apis/restApi.js +++ b/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; } diff --git a/packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js b/packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js index cf6f3c653f..3c7dd714b2 100644 --- a/packages/nc-gui/components/project/spreadsheet/mixins/spreadsheet.js +++ b/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 diff --git a/packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue b/packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue index a358fab740..e17c949c7a 100644 --- a/packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue +++ b/packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue @@ -40,6 +40,29 @@ + + + {{ bt._rtn }} (Belongs To) + + + + {{ hm._tn }} (Has Many) + + + + {{ mm.rtn }} (Many To Many) + + + + @@ -261,6 +285,25 @@ :sql-ui="sqlUi" > + + + + {{ Object.values(rowObj[bt._rtn])[1] }} + + + + + {{ v }} + + + + {{ + v + }} + + + + diff --git a/packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts b/packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts index 4505d6bd3a..b5a887cc94 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/BaseModel.ts +++ b/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; diff --git a/packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts b/packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts index 1b51618234..8277df2407 100644 --- a/packages/nocodb/src/lib/dataMapper/lib/sql/BaseModelSql.ts +++ b/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, diff --git a/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts b/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts index b67dd085b1..ee779d9fb4 100644 --- a/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/common/BaseApiBuilder.ts @@ -737,8 +737,6 @@ export default abstract class BaseApiBuilder 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 implements XcDynami public getProjectId() { return this.projectId; } + + + protected async getManyToManyRelations() { + const metas = new Set(); + + 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}; diff --git a/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts b/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts index a455e45bcb..ba27738b29 100644 --- a/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/gql/GqlApiBuilder.ts @@ -620,12 +620,10 @@ export class GqlApiBuilder extends BaseApiBuilder 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 implements XcMetaMgr { } })); + + await this.getManyToManyRelations(); } } diff --git a/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts b/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts index 49081350eb..3784456d25 100644 --- a/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts +++ b/packages/nocodb/src/lib/noco/rest/RestApiBuilder.ts @@ -53,14 +53,10 @@ export class RestApiBuilder extends BaseApiBuilder { 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 { 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 { this.controllers[name].mapRoutes(router, this.customRoutes); } }); - - } - - }); /* handle xc_tables update in parallel */ @@ -557,6 +548,9 @@ export class RestApiBuilder extends BaseApiBuilder { await NcHelp.executeOperations(relationRoutes, this.connectionConfig.client); + await this.getManyToManyRelations(); + + const swaggerDoc = { tags: [], paths: {}, diff --git a/packages/nocodb/src/lib/noco/rest/RestCtrl.ts b/packages/nocodb/src/lib/noco/rest/RestCtrl.ts index a01fd6a983..1bb169b531 100644 --- a/packages/nocodb/src/lib/noco/rest/RestCtrl.ts +++ b/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 { + 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 { diff --git a/packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts b/packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts index 2050514f47..15a8717aee 100644 --- a/packages/nocodb/src/lib/sqlMgr/code/routes/xc-ts/ExpressXcTsRoutes.ts +++ b/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') {