const Api = require('nocodb-sdk').Api; const { UITypes } = require('nocodb-sdk'); const jsonfile = require('jsonfile'); const GRID = 3, GALLERY = 2, FORM = 1; let ncMap = { /* id: name */ }; let tblSchema = []; let api = {}; let viewStore = { columns: {}, sort: {}, filter: {} }; let inputConfig = jsonfile.readFileSync(`config.json`) let ncConfig = { projectName: inputConfig.srcProject, baseURL: inputConfig.baseURL, headers: { 'xc-auth': `${inputConfig["xc-auth"]}` } }; // helper routines // remove objects containing 0/ false/ null // fixme: how to handle when cdf (default value) is configured as 0/ null/ false function removeEmpty(obj) { return Object.fromEntries( Object.entries(obj) .filter(([_, v]) => v != null && v != 0 && v != false) .map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v]) ); } function addColumnSpecificData(c) { // pick required fields to proceed further let col; if(inputConfig.excludeDt) { col = removeEmpty( (({ id, title, column_name, uidt, pk, pv, rqd, dtxp, system, ai }) => ({ id, title, column_name, uidt, pk, pv, rqd, dtxp, system, ai }))(c) ); } else { col = removeEmpty( (({ id, title, column_name, uidt, dt, pk, pv, rqd, dtxp, system, ai }) => ({ id, title, column_name, uidt, dt, pk, pv, rqd, dtxp, system, ai }))(c) ); } switch (c.uidt) { case UITypes.Formula: col.formula = c.colOptions.formula; col.formula_raw = c.colOptions.formula_raw; break; case UITypes.LinkToAnotherRecord: col[`colOptions`] = { fk_model_id: c.fk_model_id, fk_related_model_id: c.colOptions.fk_related_model_id, fk_child_column_id: c.colOptions.fk_child_column_id, fk_parent_column_id: c.colOptions.fk_parent_column_id, type: c.colOptions.type }; break; case UITypes.Lookup: col[`colOptions`] = { fk_model_id: c.fk_model_id, fk_relation_column_id: c.colOptions.fk_relation_column_id, fk_lookup_column_id: c.colOptions.fk_lookup_column_id }; break; case UITypes.Rollup: col[`colOptions`] = { fk_model_id: c.fk_model_id, fk_relation_column_id: c.colOptions.fk_relation_column_id, fk_rollup_column_id: c.colOptions.fk_rollup_column_id, rollup_function: c.colOptions.rollup_function }; break; } return col; } function addViewDetails(v) { // pick required fields to proceed further let view = (({ id, title, type, show_system_fields, lock_type, order }) => ({ id, title, type, show_system_fields, lock_type, order }))(v); // form view if (v.type === FORM) { view.property = (({ heading, subheading, success_msg, redirect_after_secs, email, submit_another_form, show_blank_form }) => ({ heading, subheading, success_msg, redirect_after_secs, email, submit_another_form, show_blank_form }))(v.view); } // gallery view else if (v.type === GALLERY) { view.property = { fk_cover_image_col_id: ncMap[v.view.fk_cover_image_col_id] }; } // gallery view doesn't share column information in api yet if (v.type !== GALLERY) { if (v.type === GRID) view.columns = viewStore.columns[v.id].map(a => (({ id, width, order, show }) => ({ id, width, order, show }))(a) ); if (v.type === FORM) view.columns = viewStore.columns[v.id].map(a => (({ id, order, show, label, help, description, required }) => ({ id, order, show, label, help, description, required }))(a) ); for (let i = 0; i < view.columns?.length; i++) view.columns[i].title = ncMap[viewStore.columns[v.id][i].id]; // skip hm & mm columns view.columns = view.columns ?.filter(a => a.title?.includes('_nc_m2m_') === false) .filter(a => a.title?.includes('nc_') === false); } // filter & sort configurations if (v.type !== FORM) { view.sort = viewStore.sort[v.id].map(a => (({ fk_column_id, direction, order }) => ({ fk_column_id, direction, order }))(a) ); view.filter = viewStore.filter[v.id].map(a => (({ fk_column_id, logical_op, comparison_op, value, order }) => ({ fk_column_id, logical_op, comparison_op, value, order }))(a) ); } return view; } // view data stored as is for quick access async function storeViewDetails(tableId) { // read view data for each table let viewList = await api.dbView.list(tableId); for (let j = 0; j < viewList.list.length; j++) { let v = viewList.list[j]; let viewDetails = []; // invoke view specific read to populate columns information if (v.type === FORM) viewDetails = (await api.dbView.formRead(v.id)).columns; else if (v.type === GALLERY) viewDetails = await api.dbView.galleryRead(v.id); else if (v.type === GRID) viewDetails = await api.dbView.gridColumnsList(v.id); viewStore.columns[v.id] = viewDetails; // populate sort information let vSort = await api.dbTableSort.list(v.id); viewStore.sort[v.id] = vSort.sorts.list; let vFilter = await api.dbTableFilter.read(v.id); viewStore.filter[v.id] = vFilter; } } // mapping table for quick information access // store maps for tableId, columnId, viewColumnId & viewId to their names async function generateMapTbl(pId) { const tblList = await api.dbTable.list(pId); for (let i = 0; i < tblList.list.length; i++) { let tblId = tblList.list[i].id; let tbl = await api.dbTable.read(tblId); // table ID <> name ncMap[tblId] = tbl.title; // column ID <> name tbl.columns.map(x => (ncMap[x.id] = x.title)); // view ID <> name tbl.views.map(x => (ncMap[x.id] = x.tn)); for (let i = 0; i < tbl.views.length; i++) { let x = tbl.views[i]; let viewColumns = []; if (x.type === FORM) viewColumns = (await api.dbView.formRead(x.id)).columns; else if (x.type === GALLERY) viewColumns = (await api.dbView.galleryRead(x.id)).columns; else if (x.type === GRID) viewColumns = await api.dbView.gridColumnsList(x.id); // view column ID <> name viewColumns?.map(a => (ncMap[a.id] = ncMap[a.fk_column_id])); } } } // main // async function exportSchema() { api = new Api(ncConfig); // fetch project details (id et.al) const x = await api.project.list(); const p = x.list.find(a => a.title === ncConfig.projectName); await generateMapTbl(p.id); // read project const tblList = await api.dbTable.list(p.id); // for each table for (let i = 0; i < tblList.list.length; i++) { let tblId = tblList.list[i].id; await storeViewDetails(tblId); let tbl = await api.dbTable.read(tblId); // prepare schema let tSchema = { id: tbl.id, title: tbl.title, table_name: tbl?.table_name, columns: [...tbl.columns.map(c => addColumnSpecificData(c))] .filter(a => a.title.includes('_nc_m2m_') === false) // mm .filter(a => a.title.includes(p.prefix) === false) // hm .filter( a => !(a?.system === 1 && a.uidt === UITypes.LinkToAnotherRecord) ), views: [...tbl.views.map(v => addViewDetails(v))] }; tblSchema.push(tSchema); } } (async () => { await exportSchema(); jsonfile.writeFileSync( `${ncConfig.projectName.replace(/ /g, '_')}.json`, tblSchema, { spaces: 2 } ); })().catch(e => { console.log(e); });