// tbd // - formula dependency list // - nested lookup/ rollup const Api = require('nocodb-sdk').Api; const { UITypes } = require('nocodb-sdk'); const jsonfile = require('jsonfile'); let inputConfig = jsonfile.readFileSync(`config.json`) let ncConfig = { srcProject: inputConfig.srcProject, projectName: inputConfig.dstProject, baseURL: inputConfig.baseURL, headers: { 'xc-auth': `${inputConfig["xc-auth"]}` } }; let ncIn = jsonfile.readFileSync(`${ncConfig.srcProject}.json`); let api = {}; let ncProject = {}; let link = []; let lookup = []; let rollup = []; let formula = []; let rootLinks = []; // maps v1 table ID, v2 table ID & table title to table schema let ncTables = {}; async function createBaseTables() { console.log(`createBaseTables`); for (let i = 0; i < ncIn.length; i++) { let tblSchema = ncIn[i]; let reducedColumnSet = tblSchema.columns.filter( a => a.uidt !== UITypes.LinkToAnotherRecord && a.uidt !== UITypes.Lookup && a.uidt !== UITypes.Rollup && a.uidt !== UITypes.Formula ); link.push( ...tblSchema.columns.filter(a => a.uidt === UITypes.LinkToAnotherRecord) ); lookup.push(...tblSchema.columns.filter(a => a.uidt === UITypes.Lookup)); rollup.push(...tblSchema.columns.filter(a => a.uidt === UITypes.Rollup)); formula.push(...tblSchema.columns.filter(a => a.uidt === UITypes.Formula)); formula.map(a => (a['table_id'] = tblSchema.id)); let tbl = await api.dbTable.create(ncProject.id, { title: tblSchema.title, table_name: tblSchema.title, columns: reducedColumnSet.map(({ id, ...rest }) => ({ ...rest })) }); ncTables[tbl.title] = tbl; ncTables[tbl.id] = tbl; ncTables[tblSchema.id] = tbl; } } let linksCreated = []; function isLinkCreated(pId, cId) { let idx = linksCreated.findIndex(a => a.cId === pId && a.pId === cId); if (idx === -1) { linksCreated.push({ pId: pId, cId: cId }); return false; } return true; } // retrieve nc-view column ID from corresponding nc-column ID async function nc_getViewColumnId(viewId, viewType, ncColumnId) { // retrieve view Info let viewDetails; if (viewType === 'form') viewDetails = (await api.dbView.formRead(viewId)).columns; else if (viewType === 'gallery') viewDetails = (await api.dbView.galleryRead(viewId)).columns; else viewDetails = await api.dbView.gridColumnsList(viewId); return viewDetails.find(x => x.fk_column_id === ncColumnId)?.id; } async function createFormula() { for (let i = 0; i < formula.length; i++) { let tbl = await api.dbTableColumn.create(ncTables[formula[i].table_id].id, { uidt: UITypes.Formula, title: formula[i].title, formula_raw: formula[i].formula_raw }); } } async function createLinks() { console.log(`createLinks`); for (let i = 0; i < link.length; i++) { if ( (link[i].colOptions.type === 'mm' && false === isLinkCreated( link[i].colOptions.fk_parent_column_id, link[i].colOptions.fk_child_column_id )) || link[i].colOptions.type === 'hm' ) { let srcTbl = ncTables[link[i].colOptions.fk_model_id]; let dstTbl = ncTables[link[i].colOptions.fk_related_model_id]; // create link let tbl = await api.dbTableColumn.create(srcTbl.id, { uidt: UITypes.LinkToAnotherRecord, title: link[i].title, parentId: srcTbl.id, childId: dstTbl.id, type: link[i].colOptions.type }); ncTables[tbl.title] = tbl; ncTables[tbl.id] = tbl; ncTables[link[i].colOptions.fk_model_id] = tbl; // for data-link procedure later rootLinks.push({ linkColumn: link[i], linkSrcTbl: srcTbl }); // symmetry field update // let v2ColSchema = tbl.columns.find(x => x.title === link[i].title); // read related table again after link is created dstTbl = await api.dbTable.read(dstTbl.id); let v2SymmetricColumn = link[i].colOptions.type === 'mm' ? dstTbl.columns.find( x => x.uidt === UITypes.LinkToAnotherRecord && x?.colOptions.fk_parent_column_id === v2ColSchema.colOptions.fk_child_column_id && x?.colOptions.fk_child_column_id === v2ColSchema.colOptions.fk_parent_column_id ) : dstTbl.columns.find( x => x.uidt === UITypes.LinkToAnotherRecord && x?.colOptions.fk_parent_column_id === v2ColSchema.colOptions.fk_parent_column_id && x?.colOptions.fk_child_column_id === v2ColSchema.colOptions.fk_child_column_id ); let v1SymmetricColumn = link[i].colOptions.type === 'mm' ? link.find( x => x.colOptions.fk_parent_column_id === link[i].colOptions.fk_child_column_id && x.colOptions.fk_child_column_id === link[i].colOptions.fk_parent_column_id && x.colOptions.type === 'mm' ) : link.find( x => x.colOptions.fk_parent_column_id === link[i].colOptions.fk_parent_column_id && x.colOptions.fk_child_column_id === link[i].colOptions.fk_child_column_id && x.colOptions.type === 'bt' ); tbl = await api.dbTableColumn.update(v2SymmetricColumn.id, { ...v2SymmetricColumn, title: v1SymmetricColumn.title, column_name: null }); ncTables[tbl.title] = tbl; ncTables[tbl.id] = tbl; ncTables[v1SymmetricColumn.colOptions.fk_model_id] = tbl; } } } function get_v2Id(v1ColId) { for (let i = 0; i < ncIn.length; i++) { let tblSchema = ncIn[i]; let colSchema = {}; if ( undefined !== (colSchema = tblSchema.columns.find(x => x.id === v1ColId)) ) { let colName = colSchema.title; let v2Tbl = ncTables[tblSchema.id]; return v2Tbl.columns.find(y => y.title === colName)?.id; } } } async function createLookup() { console.log(`createLookup`); for (let i = 0; i < lookup.length; i++) { let srcTbl = ncTables[lookup[i].colOptions.fk_model_id]; let v2_fk_relation_column_id = get_v2Id( lookup[i].colOptions.fk_relation_column_id ); let v2_lookup_column_id = get_v2Id( lookup[i].colOptions.fk_lookup_column_id ); if (v2_lookup_column_id) { let tbl = await api.dbTableColumn.create(srcTbl.id, { uidt: UITypes.Lookup, title: lookup[i].title, fk_relation_column_id: v2_fk_relation_column_id, fk_lookup_column_id: v2_lookup_column_id }); ncTables[tbl.title] = tbl; ncTables[tbl.id] = tbl; ncTables[lookup[i].colOptions.fk_model_id] = tbl; } } } async function createRollup() { console.log(`createRollup`); for (let i = 0; i < rollup.length; i++) { let srcTbl = ncTables[rollup[i].colOptions.fk_model_id]; let v2_fk_relation_column_id = get_v2Id( rollup[i].colOptions.fk_relation_column_id ); let v2_rollup_column_id = get_v2Id( rollup[i].colOptions.fk_rollup_column_id ); if (v2_rollup_column_id) { let tbl = await api.dbTableColumn.create(srcTbl.id, { uidt: UITypes.Rollup, title: rollup[i].title, column_name: rollup[i].title, fk_relation_column_id: v2_fk_relation_column_id, fk_rollup_column_id: v2_rollup_column_id, rollup_function: rollup[i].colOptions.rollup_function }); ncTables[tbl.title] = tbl; ncTables[tbl.id] = tbl; ncTables[rollup[i].colOptions.fk_model_id] = tbl; } } } async function configureGrid() { console.log(`configureGrid`); for (let i = 0; i < ncIn.length; i++) { let tblSchema = ncIn[i]; let tblId = ncTables[tblSchema.id].id; let gridList = tblSchema.views.filter(a => a.type === 3); let srcTbl = await api.dbTable.read(tblId); const view = await api.dbView.list(tblId); // create / rename view for (let gridCnt = 0; gridCnt < gridList.length; gridCnt++) { let viewCreated = {}; // rename first view; default view already created if (gridCnt === 0) { viewCreated = await api.dbView.update(view.list[0].id, { title: gridList[gridCnt].title }); } // create new views else { viewCreated = await api.dbView.gridCreate(tblId, { title: gridList[gridCnt].title }); } // retrieve view Info let viewId = viewCreated.id; let viewDetails = await api.dbView.gridColumnsList(viewId); // column visibility for ( let colCnt = 0; colCnt < gridList[gridCnt].columns.length; colCnt++ ) { let ncColumnId = srcTbl.columns.find( a => a.title === gridList[gridCnt].columns[colCnt].title )?.id; // let ncViewColumnId = await nc_getViewColumnId( viewCreated.id, "grid", ncColumnId ) let ncViewColumnId = viewDetails.find( x => x.fk_column_id === ncColumnId )?.id; // column order & visibility await api.dbViewColumn.update(viewCreated.id, ncViewColumnId, { show: gridList[gridCnt].columns[colCnt].show, order: gridList[gridCnt].columns[colCnt].order }); await api.dbView.gridColumnUpdate(ncViewColumnId, { width: gridList[gridCnt].columns[colCnt].width }); } // sort for (let sCnt = 0; sCnt < gridList[gridCnt].sort.length; sCnt++) { let sColName = tblSchema.columns.find( a => gridList[gridCnt].sort[sCnt].fk_column_id === a.id ).title; await api.dbTableSort.create(viewId, { fk_column_id: srcTbl.columns.find(a => a.title === sColName)?.id, direction: gridList[gridCnt].sort[sCnt].direction }); } // filter for (let fCnt = 0; fCnt < gridList[gridCnt].filter.length; fCnt++) { let fColName = tblSchema.columns.find( a => gridList[gridCnt].sort[fCnt].fk_column_id === a.id ).title; await api.dbTableFilter.create(viewId, { ...gridList[gridCnt].filter[fCnt], fk_column_id: srcTbl.columns.find(a => a.title === fColName)?.id }); } } } } async function configureGallery() { console.log(`configureGallery`); for (let i = 0; i < ncIn.length; i++) { let tblSchema = ncIn[i]; let tblId = ncTables[tblSchema.id].id; let galleryList = tblSchema.views.filter(a => a.type === 2); for (let cnt = 0; cnt < galleryList.length; cnt++) { const viewCreated = await api.dbView.galleryCreate(tblId, { title: galleryList[cnt].title }); } } } async function configureForm() { console.log(`configureForm`); for (let i = 0; i < ncIn.length; i++) { let tblSchema = ncIn[i]; let tblId = ncTables[tblSchema.id].id; let formList = tblSchema.views.filter(a => a.type === 1); let srcTbl = await api.dbTable.read(tblId); for (let formCnt = 0; formCnt < formList.length; formCnt++) { const formData = { title: formList[formCnt].title, ...formList[formCnt].property }; const viewCreated = await api.dbView.formCreate(tblId, formData); // column visibility for ( let colCnt = 0; colCnt < formList[formCnt].columns.length; colCnt++ ) { let ncColumnId = srcTbl.columns.find( a => a.title === formList[formCnt].columns[colCnt].title )?.id; let ncViewColumnId = await nc_getViewColumnId( viewCreated.id, 'form', ncColumnId ); // column order & visibility await api.dbView.formColumnUpdate(ncViewColumnId, { show: formList[formCnt].columns[colCnt].show, order: formList[formCnt].columns[colCnt].order, label: formList[formCnt].columns[colCnt].label, description: formList[formCnt].columns[colCnt].description, required: formList[formCnt].columns[colCnt].required }); } } } } async function restoreBaseData() { console.log(`restoreBaseData`); for (let i = 0; i < ncIn.length; i++) { let tblSchema = ncIn[i]; let tblId = ncTables[tblSchema.id].id; let pk = tblSchema.columns.find(a => a.pk).title; let moreRecords = true; let offset = 0, limit = 25; while (moreRecords) { let recList = await api.dbTableRow.list( 'nc', ncConfig.srcProject, tblSchema.title, {}, { query: { limit: limit, offset: offset } } ); moreRecords = !recList.pageInfo.isLastPage; offset += limit; for (let recCnt = 0; recCnt < recList.list.length; recCnt++) { let record = await api.dbTableRow.read( 'nc', ncConfig.srcProject, tblSchema.title, recList.list[recCnt][pk] ); // post-processing on the record for (const [key, value] of Object.entries(record)) { let table = ncTables[tblId]; // retrieve datatype const dt = table.columns.find(x => x.title === key)?.uidt; if (dt === UITypes.LinkToAnotherRecord) delete record[key]; if (dt === UITypes.Lookup) delete record[key]; if (dt === UITypes.Rollup) delete record[key]; } await api.dbTableRow.create( 'nc', ncConfig.projectName, tblSchema.title, record ); } } } } async function restoreLinks() { console.log(`restoreLinks`); for (let i = 0; i < rootLinks.length; i++) { let pk = rootLinks[i].linkSrcTbl.columns.find(a => a.pk).title; let moreRecords = true; let offset = 0, limit = 25; while (moreRecords) { let recList = await api.dbTableRow.list( 'nc', ncConfig.srcProject, rootLinks[i].linkSrcTbl.title, {}, { query: { limit: limit, offset: offset } } ); moreRecords = !recList.pageInfo.isLastPage; offset += limit; for (let recCnt = 0; recCnt < recList.list.length; recCnt++) { let record = await api.dbTableRow.read( 'nc', ncConfig.srcProject, rootLinks[i].linkSrcTbl.title, recList.list[recCnt][pk] ); let linkField = record[rootLinks[i].linkColumn.title]; if (linkField.length) { await api.dbTableRow.nestedAdd( 'nc', ncConfig.projectName, rootLinks[i].linkSrcTbl.title, record[pk], rootLinks[i].linkColumn.colOptions.type, encodeURIComponent(rootLinks[i].linkColumn.title), linkField[0][pk] ); } } } } } async function importSchema() { api = new Api(ncConfig); const x = await api.project.list(); const p = x.list.find(a => a.title === ncConfig.projectName); if (p) await api.project.delete(p.id); ncProject = await api.project.create({ title: ncConfig.projectName }); await createBaseTables(); await createLinks(); await createLookup(); await createRollup(); await createFormula(); // configure views await configureGrid(); await configureGallery(); await configureForm(); // restore data only if source project exists const p2 = x.list.find(a => a.title === ncConfig.srcProject); if (p2 !== undefined) { await restoreBaseData(); await restoreLinks(); } } (async () => { await importSchema(); console.log('completed'); })().catch(e => console.log(e));