From d027c7681a35265161ed807b2c8e368937ef98c6 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 18 Jul 2024 00:57:49 +0800 Subject: [PATCH 1/2] `UniverseService` can now handle all engine booting from either registry or indexer, also added new utility function to use native fetch to make authenticated request to the fleetbase api --- addon/services/universe.js | 153 +++++++++++++++++- addon/utils/fleetbase-api-fetch.js | 63 ++++++++ addon/utils/load-installed-extensions.js | 16 ++ app/utils/fleetbase-api-fetch.js | 1 + app/utils/load-installed-extensions.js | 1 + package.json | 2 +- tests/unit/utils/fleetbase-api-fetch-test.js | 10 ++ .../utils/load-installed-extensions-test.js | 10 ++ 8 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 addon/utils/fleetbase-api-fetch.js create mode 100644 addon/utils/load-installed-extensions.js create mode 100644 app/utils/fleetbase-api-fetch.js create mode 100644 app/utils/load-installed-extensions.js create mode 100644 tests/unit/utils/fleetbase-api-fetch-test.js create mode 100644 tests/unit/utils/load-installed-extensions-test.js diff --git a/addon/services/universe.js b/addon/services/universe.js index 917b4d7..6097073 100644 --- a/addon/services/universe.js +++ b/addon/services/universe.js @@ -8,8 +8,10 @@ import { A, isArray } from '@ember/array'; import { later } from '@ember/runloop'; import { dasherize, camelize } from '@ember/string'; import { getOwner } from '@ember/application'; -import { assert } from '@ember/debug'; +import { assert, debug } from '@ember/debug'; import RSVP from 'rsvp'; +import loadInstalledExtensions from '../utils/load-installed-extensions'; +import getWithDefault from '../utils/get-with-default'; export default class UniverseService extends Service.extend(Evented) { @service router; @@ -1269,6 +1271,155 @@ export default class UniverseService extends Service.extend(Evented) { return null; } + /** + * Boot all installed engines, ensuring dependencies are resolved. + * + * This method attempts to boot all installed engines by first checking if all + * their dependencies are already booted. If an engine has dependencies that + * are not yet booted, it is deferred and retried after its dependencies are + * booted. If some dependencies are never booted, an error is logged. + * + * @method bootEngines + * @param {ApplicationInstance|null} owner - The Ember ApplicationInstance that owns the engines. + * @return {void} + */ + bootEngines(owner = null) { + const booted = []; + const pending = []; + + // If no owner provided use the owner of this service + if (owner === null) { + owner = getOwner(this); + } + + const tryBootEngine = (extension) => { + this.loadEngine(extension.name).then((engineInstance) => { + if (engineInstance.base && engineInstance.base.setupExtension) { + const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); + + // Check if all dependency engines are booted + const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + + if (!allDependenciesBooted) { + pending.push({ extension, engineInstance }); + return; + } + + engineInstance.base.setupExtension(owner, engineInstance, this); + booted.push(extension.name); + debug(`Booted : ${extension.name}`); + + // Try booting pending engines again + tryBootPendingEngines(); + } + }); + }; + + const tryBootPendingEngines = () => { + const stillPending = []; + + pending.forEach(({ extension, engineInstance }) => { + const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); + const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + + if (allDependenciesBooted) { + engineInstance.base.setupExtension(owner, engineInstance, this); + booted.push(extension.name); + debug(`Booted : ${extension.name}`); + } else { + stillPending.push({ extension, engineInstance }); + } + }); + + // If no progress was made, log an error in debug/development mode + assert('Some engines have unmet dependencies and cannot be booted:', pending.length === stillPending.length); + + pending.length = 0; + pending.push(...stillPending); + }; + + loadInstalledExtensions().then((extensions) => { + extensions.forEach((extension) => { + tryBootEngine(extension); + }); + }); + } + + /** + * Boots all installed engines, ensuring dependencies are resolved. + * + * This method loads all installed extensions and then attempts to boot each engine. + * For each extension, it loads the engine and, if the engine has a `setupExtension` + * method in its base, it calls this method to complete the setup. This function ensures + * that dependencies are resolved before booting the engines. If some dependencies are + * never booted, an error is logged. + * + * @method legacyBootEngines + * @param {ApplicationInstance|null} owner - The Ember ApplicationInstance that owns the engines. + * @return {void} + */ + legacyBootEngines(owner = null) { + const booted = []; + const pending = []; + + // If no owner provided use the owner of this service + if (owner === null) { + owner = getOwner(this); + } + + const tryBootEngine = (extension) => { + this.loadEngine(extension.name).then((engineInstance) => { + if (engineInstance.base && engineInstance.base.setupExtension) { + const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); + + // Check if all dependency engines are booted + const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + + if (!allDependenciesBooted) { + pending.push({ extension, engineInstance }); + return; + } + + engineInstance.base.setupExtension(owner, engineInstance, this); + booted.push(extension.name); + debug(`Booted : ${extension.name}`); + + // Try booting pending engines again + tryBootPendingEngines(); + } + }); + }; + + const tryBootPendingEngines = () => { + const stillPending = []; + + pending.forEach(({ extension, engineInstance }) => { + const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); + const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + + if (allDependenciesBooted) { + engineInstance.base.setupExtension(owner, engineInstance, this); + booted.push(extension.name); + debug(`Booted : ${extension.name}`); + } else { + stillPending.push({ extension, engineInstance }); + } + }); + + // If no progress was made, log an error in debug/development mode + assert('Some engines have unmet dependencies and cannot be booted:', pending.length === stillPending.length); + + pending.length = 0; + pending.push(...stillPending); + }; + + loadExtensions().then((extensions) => { + extensions.forEach((extension) => { + tryBootEngine(extension); + }); + }); + } + /** * Alias for intl service `t` * diff --git a/addon/utils/fleetbase-api-fetch.js b/addon/utils/fleetbase-api-fetch.js new file mode 100644 index 0000000..9f9aaed --- /dev/null +++ b/addon/utils/fleetbase-api-fetch.js @@ -0,0 +1,63 @@ +import config from 'ember-get-config'; + +export default async function fleetbaseApiFetch(method, uri, params = {}, fetchOptions = {}) { + // Prepare base URL + const baseUrl = `${config.API.host}/${fetchOptions.namespace ?? config.API.namespace}`; + + // Initialize headers + const headers = { + 'Content-Type': 'application/json', + }; + + // Check localStorage for the session data + const localStorageSession = JSON.parse(window.localStorage.getItem('ember_simple_auth-session')); + let token; + if (localStorageSession) { + const { authenticated } = localStorageSession; + if (authenticated) { + token = authenticated.token; + } + } + + // Set Authorization header if token is available + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + // Configure request options + const options = { + method, + headers, + }; + + // Handle params based on method + if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && params) { + options.body = JSON.stringify(params); + } else if (method === 'GET' && params) { + // Add params to URL for GET requests + const urlParams = new URLSearchParams(params).toString(); + uri += `?${urlParams}`; + } + + try { + // Make the fetch request + const response = await fetch(`${baseUrl}/${uri}`, options); + + // Check if the response is OK (status in the range 200-299) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Parse and return the JSON response + return await response.json(); + } catch (error) { + // If a fallback response is provided use it instead + if (fetchOptions && fetchOptions.fallbackResponse !== undefined) { + return fetcOptions.fallbackResponse; + } + + // Handle errors (network errors, JSON parsing errors, etc.) + console.error('Error making request:', error); + throw error; + } +} diff --git a/addon/utils/load-installed-extensions.js b/addon/utils/load-installed-extensions.js new file mode 100644 index 0000000..f3d50bd --- /dev/null +++ b/addon/utils/load-installed-extensions.js @@ -0,0 +1,16 @@ +import loadExtensions from '../utils/load-extensions'; +import fleetbaseApiFetch from '../utils/fleetbase-api-fetch'; + +export default async function loadInstalledExtensions() { + const CORE_ENGINES = ['@fleetbase/fleetops-engine', '@fleetbase/storefront-engine', '@fleetbase/registry-bridge-engine', '@fleetbase/dev-engine', '@fleetbase/iam-engine']; + const INDEXED_ENGINES = await loadExtensions(); + const INSTALLED_ENGINES = await fleetbaseApiFetch('get', 'engines', {}, { namespace: '~registry/v1', fallbackResponse: [] }); + + const isInstalledEngine = (engineName) => { + return CORE_ENGINES.includes(engineName) || INSTALLED_ENGINES.find((pkg) => pkg.name === engineName); + }; + + return INDEXED_ENGINES.filter((pkg) => { + return isInstalledEngine(pkg.name); + }); +} diff --git a/app/utils/fleetbase-api-fetch.js b/app/utils/fleetbase-api-fetch.js new file mode 100644 index 0000000..6058e4f --- /dev/null +++ b/app/utils/fleetbase-api-fetch.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ember-core/utils/fleetbase-api-fetch'; diff --git a/app/utils/load-installed-extensions.js b/app/utils/load-installed-extensions.js new file mode 100644 index 0000000..34454d0 --- /dev/null +++ b/app/utils/load-installed-extensions.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ember-core/utils/load-installed-extensions'; diff --git a/package.json b/package.json index 9794b92..3a878f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/ember-core", - "version": "0.2.12", + "version": "0.2.13", "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.", "keywords": [ "fleetbase-core", diff --git a/tests/unit/utils/fleetbase-api-fetch-test.js b/tests/unit/utils/fleetbase-api-fetch-test.js new file mode 100644 index 0000000..2e615d3 --- /dev/null +++ b/tests/unit/utils/fleetbase-api-fetch-test.js @@ -0,0 +1,10 @@ +import fleetbaseApiFetch from 'dummy/utils/fleetbase-api-fetch'; +import { module, test } from 'qunit'; + +module('Unit | Utility | fleetbase-api-fetch', function () { + // TODO: Replace this with your real tests. + test('it works', function (assert) { + let result = fleetbaseApiFetch(); + assert.ok(result); + }); +}); diff --git a/tests/unit/utils/load-installed-extensions-test.js b/tests/unit/utils/load-installed-extensions-test.js new file mode 100644 index 0000000..bfb3c87 --- /dev/null +++ b/tests/unit/utils/load-installed-extensions-test.js @@ -0,0 +1,10 @@ +import loadInstalledExtensions from 'dummy/utils/load-installed-extensions'; +import { module, test } from 'qunit'; + +module('Unit | Utility | load-installed-extensions', function () { + // TODO: Replace this with your real tests. + test('it works', function (assert) { + let result = loadInstalledExtensions(); + assert.ok(result); + }); +}); From dd3a624b7e1c2a26cf863c9e1d5518d3abd2e07d Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Thu, 18 Jul 2024 01:02:47 +0800 Subject: [PATCH 2/2] fixed linter --- addon/services/universe.js | 1 + addon/utils/fleetbase-api-fetch.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addon/services/universe.js b/addon/services/universe.js index 6097073..e3b3754 100644 --- a/addon/services/universe.js +++ b/addon/services/universe.js @@ -11,6 +11,7 @@ import { getOwner } from '@ember/application'; import { assert, debug } from '@ember/debug'; import RSVP from 'rsvp'; import loadInstalledExtensions from '../utils/load-installed-extensions'; +import loadExtensions from '../utils/load-extensions'; import getWithDefault from '../utils/get-with-default'; export default class UniverseService extends Service.extend(Evented) { diff --git a/addon/utils/fleetbase-api-fetch.js b/addon/utils/fleetbase-api-fetch.js index 9f9aaed..d7c0805 100644 --- a/addon/utils/fleetbase-api-fetch.js +++ b/addon/utils/fleetbase-api-fetch.js @@ -53,7 +53,7 @@ export default async function fleetbaseApiFetch(method, uri, params = {}, fetchO } catch (error) { // If a fallback response is provided use it instead if (fetchOptions && fetchOptions.fallbackResponse !== undefined) { - return fetcOptions.fallbackResponse; + return fetchOptions.fallbackResponse; } // Handle errors (network errors, JSON parsing errors, etc.)