From 82bbdc1c85e5d7838355ab2731d1be180473fa8e Mon Sep 17 00:00:00 2001 From: William Wills Date: Wed, 27 Nov 2024 16:39:50 -0500 Subject: [PATCH] fix: import organization assertion loop --- docs/cadt_rpc_api.md | 20 +- src/controllers/organization.controller.js | 41 +---- src/datalayer/syncService.js | 20 +- .../organizations/organizations.model.js | 172 ++++++++++++------ src/routes/v1/resources/organization.js | 11 +- src/validations/organizations.validations.js | 6 +- 6 files changed, 152 insertions(+), 118 deletions(-) diff --git a/docs/cadt_rpc_api.md b/docs/cadt_rpc_api.md index 79887ecb..cf84c86e 100644 --- a/docs/cadt_rpc_api.md +++ b/docs/cadt_rpc_api.md @@ -35,9 +35,9 @@ If using a `CADT_API_KEY` append `--header 'x-api-key: '` to - [POST Examples](#post-examples) - [Create an organization](#create-an-organization) - [PUT Examples](#put-examples) - - [Import a home organization](#import-a-home-organization-that-datalayer-is-subscribed-to) + - [Import an organization that datalayer is subscribed to](#import-an-organization-that-datalayer-is-subscribed-to) - [DELETE Examples](#delete-examples) - - [Delete a home organization](#reset-home-organization) + - [Delete a home organization](#delete-home-organization) - [Additional organizations resources](#additional-organizations-resources) - [`projects`](#projects) - [GET Examples](#get-examples-1) @@ -210,16 +210,18 @@ Response PUT Options: -| Key | Type | Description | -|:------:|:-------:|:----------------------------------------------------:| -| orgUid | String | (Required) OrgUid of the home organization to import | +| Key | Type | Description | +|:------:|:-------:|:-----------------------------------------------:| +| orgUid | String | (Required) OrgUid of the organization to import | +| isHome | Boolean | Set to true if home organization | ### PUT Examples -#### Import a home organization that datalayer is subscribed to +#### Import an organization that datalayer is subscribed to - This is typically used when an organization currently using CADT is installing a new instance and wants to use the same home organization and the current instance(s). +- This can be used to import an organization that is not a home organization as well Request ```sh @@ -232,7 +234,8 @@ curl --location -g --request PUT 'http://localhost:31310/v1/organizations/' \ Response ```json { - "message":"Importing home organization." + "message":"Successfully imported organization. cadt will begin syncing the organization's data from datalayer", + "success": true } ``` @@ -259,8 +262,7 @@ Response - POST `/organizations/remove-mirror` - given a store ID and coin ID removes the mirror for a given store - POST `/organizations/sync` - runs the process to sync all subscribed organization metadata with datalayer - POST `/organizations/create` - create an organization without an icon -- POST `/organizations/edit` - update an organization name and/or icon -- PUT `/organizations/import` - subscribe and import an organization via OrgUid +- POST `/organizations/edit` - update an organization name and/or icon - DELETE `organizations/import` - delete an organization's record from the CADT instance DB - PUT `organizations/subscribe` - subscribe to an organization datalayer singleton - DELETE `organizations/unsubscribe` - unsubscribe from an organization datalayer singleton and keep CADT data diff --git a/src/controllers/organization.controller.js b/src/controllers/organization.controller.js index 040fec03..546de9ea 100644 --- a/src/controllers/organization.controller.js +++ b/src/controllers/organization.controller.js @@ -193,45 +193,19 @@ export const resetHomeOrg = async (req, res) => { } }; -export const importOrg = async (req, res) => { +export const importOrganization = async (req, res) => { try { await assertIfReadOnlyMode(); await assertWalletIsSynced(); - const { orgUid } = req.body; + await Organization.importOrganization(req.body.orgUid, req.body?.orgUid); - res.json({ + res.status(200).json({ message: - 'Importing and subscribing organization this can take a few mins.', + "Successfully imported organization. cadt will begin syncing the organization's data from datalayer", success: true, }); - - return Organization.importOrganization(orgUid); } catch (error) { - console.trace(error); - res.status(400).json({ - message: 'Error importing organization', - error: error.message, - success: false, - }); - } -}; - -export const importHomeOrg = async (req, res) => { - try { - await assertIfReadOnlyMode(); - await assertWalletIsSynced(); - - const { orgUid } = req.body; - - await Organization.importHomeOrg(orgUid); - - res.json({ - message: 'Importing home organization.', - success: true, - }); - } catch (error) { - console.trace(error); res.status(400).json({ message: 'Error importing organization', error: error.message, @@ -246,10 +220,13 @@ export const subscribeToOrganization = async (req, res) => { await assertWalletIsSynced(); await assertHomeOrgExists(); - await Organization.subscribeToOrganization(req.body.orgUid); + const resultMessage = await Organization.subscribeToOrganization( + req.body.orgUid, + req.body.registryId, + ); return res.json({ - message: 'Subscribed to organization', + message: resultMessage, success: true, }); } catch (error) { diff --git a/src/datalayer/syncService.js b/src/datalayer/syncService.js index 6ab03425..7bc5c5f9 100644 --- a/src/datalayer/syncService.js +++ b/src/datalayer/syncService.js @@ -26,8 +26,13 @@ const subscribeToStoreOnDataLayer = async (storeId) => { } }; +/** note that although this function will succeed in subscribing to the store, datalayer will not sync data fast enough + * for the data to be available to the calling function when this function returns + * @param storeId + * @returns {Promise<*>} + */ const getSubscribedStoreData = async (storeId) => { - const { storeIds: subscriptions } = await dataLayer.getSubscriptions(storeId); + const { storeIds: subscriptions } = await dataLayer.getSubscriptions(); const alreadySubscribed = subscriptions.includes(storeId); if (!alreadySubscribed) { @@ -142,11 +147,16 @@ const getCurrentStoreData = async (storeId) => { } const encodedData = await dataLayer.getStoreData(storeId); - if (encodedData) { - return decodeDataLayerResponse(encodedData); - } else { - return []; + + if (_.isEmpty(encodedData?.keys_values)) { + throw new Error(`No data found for store ${storeId}`); } + + const decodedData = decodeDataLayerResponse(encodedData); + return decodedData.reduce((obj, current) => { + obj[current.key] = current.value; + return obj; + }, {}); }; /** diff --git a/src/models/organizations/organizations.model.js b/src/models/organizations/organizations.model.js index b77d42b0..bc617476 100644 --- a/src/models/organizations/organizations.model.js +++ b/src/models/organizations/organizations.model.js @@ -19,6 +19,11 @@ import { getConfig } from '../../utils/config-loader'; const { USE_SIMULATOR, AUTO_SUBSCRIBE_FILESTORE } = getConfig().APP; import ModelTypes from './organizations.modeltypes.cjs'; +import { + getOwnedStores, + getSubscriptions, +} from '../../datalayer/persistance.js'; +import { subscribeToStoreOnDataLayer } from '../../datalayer/simulator.js'; class Organization extends Model { static async getHomeOrg(includeAddress = true) { @@ -225,83 +230,83 @@ class Organization extends Model { await datalayer.addMirror(storeId, url, force); } - static async importHomeOrg(orgUid) { - const orgData = await datalayer.getLocalStoreData(orgUid); - - if (!orgData) { - throw new Error('Your node does not have write access to this orgUid'); - } - - const orgDataObj = orgData.reduce((obj, curr) => { - obj[curr.key] = curr.value; - return obj; - }, {}); + static async importOrganization(orgUid, isHome) { + try { + const homeOrgExists = Organization.getHomeOrg(); + if (isHome && homeOrgExists) { + throw new Error( + 'cannot import home organization. home organization already exists on this instance', + ); + } - const registryData = await datalayer.getLocalStoreData( - orgDataObj.registryId, - ); + logger.info(`Importing ${isHome ? 'home' : ''} organization ${orgUid}`); + logger.debug( + isHome + ? `checking that datalayer owns org store ${orgUid}` + : `checking that datalayer is subscribed to org store ${orgUid}`, + ); - const registryDataObj = registryData.reduce((obj, curr) => { - obj[curr.key] = curr.value; - return obj; - }, {}); + const datalayerStoresResult = isHome + ? await getOwnedStores() + : await getSubscriptions(); - const dataModelVersion = getDataModelVersion(); + if (!datalayerStoresResult.success) { + throw new Error( + isHome + ? 'failed to retrieve owned stores from datalayer' + : 'failed to retrieve store subscriptions from datalayer', + ); + } - if (!registryDataObj[dataModelVersion]) { - registryDataObj[dataModelVersion] = await Organization.appendNewRegistry( - orgDataObj.registryId, - dataModelVersion, - ); - } + if (!datalayerStoresResult?.storeIds.includes(orgUid)) { + throw new Error( + isHome + ? `your chia instance does not own store ${orgUid}. cannot import as home organization.` + : `datalayer is not subscribed to store ${orgUid}. please subscribe to this store before importing the organization`, + ); + } - await Organization.upsert({ - orgUid, - name: orgDataObj.name, - icon: orgDataObj.icon, - registryId: registryDataObj[dataModelVersion], - subscribed: true, - isHome: true, - }); - } + logger.info(`found orgUid store. attempting to import registry store`); - static async importOrganization(orgUid) { - try { - logger.info('Importing organization ' + orgUid); - const orgData = await datalayer.getSubscribedStoreData(orgUid); + const orgData = await datalayer.getCurrentStoreData(orgUid); + const registryId = orgData?.registryId; - if (!orgData.registryId) { - logger.error( - 'Corrupted organization, no registryId on the datalayer, can not import', + if (!registryId) { + throw new Error( + `store ${orgUid} does not contain a valid registry storeId, can not import`, ); - return; } - logger.info(`IMPORTING REGISTRY: ${orgData.registryId}`); + if (!datalayerStoresResult?.storeIds.includes(registryId)) { + throw new Error( + isHome + ? `your chia instance does not own store ${registryId} belonging to organization store ${orgUid}. cannot import as home organization.` + : `datalayer is NOT subscribed to registry store ${registryId} belonging to organization store ${orgUid}. please subscribe to this registry store before importing the organization`, + ); + } - const registryData = await datalayer.getSubscribedStoreData( - orgData.registryId, - ); + logger.debug(`getting registry data from registry store ${registryId}`); + const registryData = await datalayer.getCurrentStoreData(orgUid); const dataModelVersion = getDataModelVersion(); if (!registryData[dataModelVersion]) { throw new Error( - `Organization has no registry for the ${dataModelVersion} datamodel, can not import`, + `organization ${orgUid} has no registry for the ${dataModelVersion} datamodel, can not import`, ); } - logger.info(`IMPORTING REGISTRY ${dataModelVersion}: `, registryData.v1); - - await datalayer.subscribeToStoreOnDataLayer(registryData.v1); + logger.info( + `importing registry data from store ${registryId} (datamodel version ${dataModelVersion}) for organization ${orgUid}`, + ); - logger.info({ + logger.debug('upserting the following imported organization data', { orgUid, name: orgData.name, icon: orgData.icon, registryId: registryData[dataModelVersion], subscribed: true, - isHome: false, + isHome, }); await Organization.upsert({ @@ -310,24 +315,75 @@ class Organization extends Model { icon: orgData.icon, registryId: registryData[dataModelVersion], subscribed: true, - isHome: false, + isHome, }); - if (AUTO_SUBSCRIBE_FILESTORE) { + if (AUTO_SUBSCRIBE_FILESTORE && !isHome) { await FileStore.subscribeToFileStore(orgUid); } } catch (error) { - logger.info(error.message); + // catch for logging purposes. need to re-throw to controller + logger.error(`cannot import organization. Error: ${error.message}`); + throw error; } } - static async subscribeToOrganization(orgUid) { + static async subscribeToOrganization(orgUid, registryId) { + const subscribedStores = await getSubscriptions(); + if (!subscribedStores.success) { + throw new Error('failed to contact datalayer'); + } + + const subscribedToOrgStore = subscribedStores.storeIds.includes(orgUid); + const subscribedToRegistryStore = + subscribedStores.storeIds.includes(registryId); + + if (!subscribedToOrgStore) { + logger.info( + `datalayer is not subscribed to orgUid store ${orgUid}, subscribing ...`, + ); + + const result = await subscribeToStoreOnDataLayer(orgUid, true); + if (result) { + logger.info(`subscribed to store ${orgUid}`); + } else { + const error = `failed to subscribe to store ${orgUid}`; + logger.error(error); + throw new Error(error); + } + + // wait 5 secs to give RPC a break + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + + if (!subscribedToRegistryStore) { + logger.info( + `datalayer is not subscribed to registryId store ${registryId}, subscribing ...`, + ); + + const result = await subscribeToStoreOnDataLayer(registryId, true); + if (result) { + logger.info(`subscribed to store ${registryId}`); + } else { + const error = `failed to subscribe to store ${registryId}`; + logger.error(error); + throw new Error(error); + } + + // wait 5 secs to give RPC a break + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + const exists = await Organization.findOne({ where: { orgUid } }); if (exists) { await Organization.update({ subscribed: true }, { where: { orgUid } }); + return `successfully subscribed to organization ${orgUid} and its registry store ${registryId}`; } else { - throw new Error( - 'Can not subscribe, please import this organization first', + return ( + `successfully subscribed to organization store ${orgUid} the associated registry store ${registryId}. ` + + `this organization does not exist in your cadt instance database. you will need to import the organization for ` + + `cadt to begin syncing the organization's data. please allow a few minutes for datalayer to sync the store data ` + + `before attempting to import.` ); } } diff --git a/src/routes/v1/resources/organization.js b/src/routes/v1/resources/organization.js index 483e1569..36ce170e 100644 --- a/src/routes/v1/resources/organization.js +++ b/src/routes/v1/resources/organization.js @@ -11,7 +11,6 @@ import { resyncOrganizationSchema, subscribeOrganizationSchema, unsubscribeOrganizationSchema, - importHomeOrganizationSchema, removeMirrorSchema, addMirrorSchema, getMetaDataSchema, @@ -59,17 +58,9 @@ OrganizationRouter.put('/edit', upload.single('file'), (req, res) => { OrganizationRouter.put( '/', - validator.body(importHomeOrganizationSchema), - (req, res) => { - return OrganizationController.importHomeOrg(req, res); - }, -); - -OrganizationRouter.put( - '/import', validator.body(importOrganizationSchema), (req, res) => { - return OrganizationController.importOrg(req, res); + return OrganizationController.importOrganization(req, res); }, ); diff --git a/src/validations/organizations.validations.js b/src/validations/organizations.validations.js index a7241ade..35a173c2 100644 --- a/src/validations/organizations.validations.js +++ b/src/validations/organizations.validations.js @@ -7,10 +7,7 @@ export const newOrganizationWithIconSchema = Joi.object({ export const importOrganizationSchema = Joi.object({ orgUid: Joi.string().required(), -}); - -export const importHomeOrganizationSchema = Joi.object({ - orgUid: Joi.string().required(), + isHome: Joi.bool().optional(), }); export const unsubscribeOrganizationSchema = Joi.object({ @@ -19,6 +16,7 @@ export const unsubscribeOrganizationSchema = Joi.object({ export const subscribeOrganizationSchema = Joi.object({ orgUid: Joi.string().required(), + registryId: Joi.string().required(), }); export const resyncOrganizationSchema = Joi.object({