const { Controller } = require('libflitter') const FakeRequest = require('../../../FakeRequest') const { ObjectId } = require('mongodb') class OfflineController extends Controller { static get services() { return [...super.services, 'models', 'controllers', 'app'] } async do_prefetch(req, res, next) { const PageModel = this.models.get('api:Page') const PageNode = this.models.get('api:Node') const Codium = this.models.get('api:Codium') const Database = this.models.get('api:db:Database') const ColumnDef = this.models.get('api:db:ColumnDef') const DBEntry = this.models.get('api:db:DBEntry') const FileGroup = this.models.get('api:FileGroup') const File = this.models.get('upload::File') const pages = (await PageModel.visible_by_user(req.user)).filter(x => x.Active) const page_uuids = pages.map(x => x.UUID) const pageNodes = await PageNode.find({ PageId: { $in: page_uuids }}) const codiums = await Codium.find({ PageId: { $in: page_uuids }, Active: true }) const databases = await Database.find({ PageId: { $in: page_uuids }, Active: true }) const database_uuids = databases.map(x => x.UUID) const databaseColumns = await ColumnDef.find({ DatabaseId: { $in: database_uuids }}) const databaseEntries = await DBEntry.find({ DatabaseId: { $in: database_uuids }}) const fileGroups = (await FileGroup.find({ PageId: { $in: page_uuids }})) for ( const grp of fileGroups ) { grp.files = await File.find({_id: {$in: grp.FileIds.map(x => ObjectId(x))}}) } return res.api({ pages, pageNodes, codiums, databases, databaseColumns, databaseEntries, fileGroups, }) } async do_sync(req, res, next) { // TODO account for modify date to not overwrite more recent data!! const OfflineDataSync = this.models.get('api:OfflineDataSync') const record = await OfflineDataSync.from_request(req) const return_maps = {} // pages if ( Array.isArray(record.pages) ) { return_maps.pages = await this.do_sync_pages(req, record.pages) } // pageNodes if ( Array.isArray(record.pageNodes) ) { return_maps.pageNodes = await this.do_sync_page_nodes(req, record.pageNodes, record.pages) } // codiums if ( Array.isArray(record.codiums) ) { return_maps.codiums = await this.do_sync_codiums(req, record.codiums) } // databases if ( Array.isArray(record.databases) ) { return_maps.databases = await this.do_sync_databases(req, record.databases) } // databaseColumns if ( Array.isArray(record.databaseColumns) ) { return_maps.databaseColumns = await this.do_sync_database_columns(req, record.databases, record.databaseColumns) } // databaseEntries if ( Array.isArray(record.databaseEntries) ) { return_maps.databaseEntries = await this.do_sync_database_entries(req, record.databases, record.databaseEntries) } // fileGroups if ( Array.isArray(record.fileGroups) ) { return_maps.fileGroups = await this.do_sync_file_groups(req, record.fileGroups) } return res.api(return_maps) } async do_sync_file_groups(req, file_recs) { const FileController = this.controllers.get('api:v1:File') const FileGroup = this.models.get('api:FileGroup') const uuid_mapping = {} for ( const rec of file_recs ) { const existing_rec = await FileGroup.findOne({ UUID: rec.UUID }) const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, FilesId: rec.UUID } fake_req.body = rec if ( existing_rec && rec.deleted ) { await FileController.delete_group(fake_req, fake_req.response) uuid_mapping[rec.UUID] = false } else if ( !existing_rec ) { await FileController.create_config(fake_req, fake_req.response) uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID } // Currently, there's no reason to update a file group, since they have no internal metadata } return uuid_mapping } async do_sync_database_entries(req, database_recs, database_row_recs) { const FormDatabaseController = this.controllers.get('api:v1:FormDatabase') const uuid_mapping = {} for ( const rec of database_recs ) { const entries = database_row_recs.filter(x => !x.deleted && x.DatabaseId === rec.UUID) const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, DatabaseId: rec.UUID } fake_req.body = entries.map(x => JSON.parse(x.RowDataJSON || '{}')) await FormDatabaseController.set_data(fake_req, fake_req.response) entries.forEach((rec, i) => { uuid_mapping[rec.UUID] = fake_req.response?._api_data?.[i]?.UUID }) } return uuid_mapping } async do_sync_database_columns(req, database_recs, database_col_recs) { const FormDatabaseController = this.controllers.get('api:v1:FormDatabase') const uuid_mapping = {} for ( const rec of database_recs ) { const col_recs = database_col_recs.filter(x => rec.ColumnIds.includes(x.UUID) && !x.deleted) const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, DatabaseId: rec.UUID } fake_req.body = { columns: col_recs } await FormDatabaseController.set_columns(fake_req, fake_req.response) col_recs.forEach((rec, i) => { uuid_mapping[rec.UUID] = fake_req.response?._api_data?.[i]?.UUID }) } return uuid_mapping } async do_sync_databases(req, database_recs) { const FormDatabaseController = this.controllers.get('api:v1:FormDatabase') const Database = this.models.get('api:db:Database') const uuid_mapping = {} for ( const rec of database_recs ) { const existing_db = await Database.findOne({ UUID: rec.UUID }) const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, DatabaseId: rec.UUID } fake_req.body = rec if ( !existing_db && !rec.deleted ) { // this was created on the client side await FormDatabaseController.create_new(fake_req, fake_req.response) uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID } else { if ( rec.deleted ) { // the database was deleted await FormDatabaseController.drop_database(fake_req, fake_req.response) uuid_mapping[rec.UUID] = false } else { // the database was updated await FormDatabaseController.set_name(fake_req, fake_req.response) uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID } } } return uuid_mapping } async do_sync_codiums(req, codium_recs) { const FormCodeController = this.controllers.get('api:v1:FormCode') const Codium = this.models.get('api:Codium') const uuid_mapping = {} for ( const rec of codium_recs ) { const existing_code = await Codium.findOne({ UUID: rec.UUID }) const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.params = { PageId: rec.PageId, NodeId: rec.NodeId, CodiumId: rec.UUID } fake_req.body = rec if ( !existing_code && !rec.deleted ) { // This was created on the client side await FormCodeController.create_new(fake_req, fake_req.response) uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID } else { if ( rec.deleted ) { // The code was deleted await FormCodeController.drop_code(fake_req, fake_req.response) uuid_mapping[rec.UUID] = false } else { // The code was updated await FormCodeController.set_values(fake_req, fake_req.response) uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID } } } return uuid_mapping } async do_sync_page_nodes(req, page_node_recs, page_recs) { const PageController = this.controllers.get('api:v1:Page') const PageModel = this.models.get('api:Page') const NodeModel = this.models.get('api:Node') const page_id_x_page = {} for ( const page of page_recs ) { page_id_x_page[page.UUID] = page } const uuid_mapping = {} for ( const rec of page_node_recs ) { rec.Value = rec.ValueJSON ? JSON.parse(rec.ValueJSON) : {} const existing_node = await NodeModel.findOne({ UUID: rec.UUID }) const offline_page = page_id_x_page[rec.PageId] let online_page = await PageModel.findOne({ UUID: rec.PageId }) if ( existing_node && rec.deleted ) { // node that exists on the server was deleted if ( online_page ) { // if it existed in the online page, delete it online_page.NodeIds = online_page.NodeIds.filter(x => x !== rec.UUID) await online_page.version_save('Updated from offline sync', req.user.id) } await existing_node.delete() uuid_mapping[rec.UUID] = false } else if ( existing_node ) { // if the node exists, we assume it's already in a page structure // update the server-side record if the user can access it if ( await online_page.is_accessible_by(req.user, 'edit') ) { existing_node.Type = rec.Type existing_node.Value = rec.Value existing_node.UpdatedAt = new Date(rec.UpdatedAt) existing_node.UpdateUserId = req.user.id await existing_node.version_save('Updated from offline sync', req.user) uuid_mapping[rec.UUID] = existing_node.UUID } } else if ( !existing_node && online_page && !rec.deleted ) { // the node was created offline // first, save the node to the page const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.params = { PageId: online_page.UUID } fake_req.body = { nodeData: {...rec} } await PageController.save_node_to_page(fake_req, fake_req.response) const new_uuid = uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID online_page = await PageModel.findOne({ UUID: rec.PageId }) // to refresh the model's data // now, try to place the node in the correct spot // if first in offline page, make first in online page if ( Array.isArray(offline_page?.NodeIds) && offline_page.NodeIds[0] === rec.UUID ) { online_page.NodeIds = [new_uuid, ...online_page.NodeIds.filter(x => x !== new_uuid && x !== rec.UUID)] } else if ( Array.isArray(offline_page?.NodeIds) && offline_page.NodeIds.includes(rec.UUID) ) { if ( offline_page.NodeIds.slice(-1)[0] !== rec.UUID ) { // We're not first, and we're not last, so try to place in the correct spot const index = offline_page.NodeIds.findIndex(x => x.UUID === rec.UUID) let predecessor = undefined let index_diff = 1 while ( !predecessor && (index - index_diff) >= 0 ) { const maybe_predecessor = offline_page.NodeIds[index - index_diff] if ( online_page.NodeIds.includes(maybe_predecessor) ) { predecessor = maybe_predecessor } else { index_diff += 1 } } if ( predecessor ) { // We found the predecessor to insert the child in the existing node const newNodeIds = [] online_page.NodeIds.forEach(uuid => { newNodeIds.push(uuid) if ( uuid === predecessor ) { newNodeIds.push(new_uuid) } }) online_page.NodeIds = newNodeIds } } } await online_page.version_save('Updated page from online save', req.user.id) } // assuming the pages were created first, we should never have a case // where we have !existing_node && !online_page } return uuid_mapping } async do_sync_pages(req, page_recs) { // TODO order pages by create date to prevent child-before-parent errors const PageController = this.controllers.get('api:v1:Page') const PageModel = this.models.get('api:Page') const uuid_mapping = {} for ( const rec of page_recs ) { const UUID = rec.UUID if ( !UUID ) continue; const existing_page = await PageModel.findOne({ UUID }) const fake_req = this.app.di().make(FakeRequest) await fake_req.inflate(req) fake_req.body = {...rec} if ( !existing_page && !rec.deleted ) { // create the new page if ( parseInt(rec.ParentId) === 0 ) { // Create a new top-level page fake_req.body.omit_starter = true fake_req.body.name = rec.Name await PageController.create_top_level(fake_req, fake_req.response) } else { // Create a new child page fake_req.body.omit_starter = true fake_req.body.name = rec.Name fake_req.body.parentId = rec.ParentId await PageController.create_child(fake_req, fake_req.response) } } else if ( existing_page ) { fake_req.params = { PageId: rec.UUID } if ( rec.deleted ) { // The page was deleted on the client await PageController.delete_page(fake_req, fake_req.response) } else { // update an existing page await PageController.save_page(fake_req, fake_req.response) } } uuid_mapping[rec.UUID] = fake_req.response?._api_data?.UUID || false } return uuid_mapping } } module.exports = exports = OfflineController