From 62782fed6f59a439ab2ef7f00c7e7d88110a78bf Mon Sep 17 00:00:00 2001 From: Kai Vandivier <49666798+KaiVandivier@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:22:49 +0200 Subject: [PATCH] feat: parse additional namespaces from `d2.config.js` and add to `manifest.webapp` [LIBS-638] (#860) * feat: add `additionalNamespaces` config to manifest.webapp * docs: additional namespaces * docs: fix typo --- cli/src/lib/generateManifests.js | 5 ++ cli/src/lib/parseAdditionalNamespaces.js | 62 +++++++++++++++ cli/src/lib/parseAdditionalNamespaces.test.js | 79 +++++++++++++++++++ docs/config/d2-config-js-reference.md | 77 +++++++++++++----- examples/simple-app/d2.config.js | 11 +++ 5 files changed, 213 insertions(+), 21 deletions(-) create mode 100644 cli/src/lib/parseAdditionalNamespaces.js create mode 100644 cli/src/lib/parseAdditionalNamespaces.test.js diff --git a/cli/src/lib/generateManifests.js b/cli/src/lib/generateManifests.js index 5685049e..5ed866c3 100644 --- a/cli/src/lib/generateManifests.js +++ b/cli/src/lib/generateManifests.js @@ -1,6 +1,7 @@ const { reporter, chalk } = require('@dhis2/cli-helpers-engine') const fs = require('fs-extra') const { getOriginalEntrypoints } = require('./getOriginalEntrypoints') +const { parseAdditionalNamespaces } = require('./parseAdditionalNamespaces') const parseCustomAuthorities = (authorities) => { if (!authorities) { @@ -90,6 +91,7 @@ module.exports = (paths, config, publicUrl) => { }, { src: 'safari-pinned-tab.svg', + sizes: '16x16', type: 'image/svg+xml', }, ], @@ -126,6 +128,9 @@ module.exports = (paths, config, publicUrl) => { dhis: { href: '*', namespace: parseDataStoreNamespace(config.dataStoreNamespace), + additionalNamespaces: parseAdditionalNamespaces( + config.additionalNamespaces + ), }, }, authorities: parseCustomAuthorities(config.customAuthorities), diff --git a/cli/src/lib/parseAdditionalNamespaces.js b/cli/src/lib/parseAdditionalNamespaces.js new file mode 100644 index 00000000..b834057f --- /dev/null +++ b/cli/src/lib/parseAdditionalNamespaces.js @@ -0,0 +1,62 @@ +const { reporter, chalk } = require('@dhis2/cli-helpers-engine') + +const parseAdditionalNamespaces = (additionalNamespaces) => { + if (!additionalNamespaces) { + return undefined + } + if (!Array.isArray(additionalNamespaces)) { + reporter.warn( + `Invalid value ${chalk.bold( + JSON.stringify(additionalNamespaces) + )} specified for ${chalk.bold( + 'additionalNamespaces' + )} -- must be an array of objects, skipping.` + ) + return undefined + } + + const filteredNamespaces = additionalNamespaces.filter( + (additionalNamespace, index) => { + const msg = `Invalid namespace ${chalk.bold( + JSON.stringify(additionalNamespace) + )} specified at ${chalk.bold(`index ${index}`)} of ${chalk.bold( + 'additionalNamespaces' + )} -- see d2.config.js documentation for the correct form. Skipping.` + const { + namespace, + authorities, + readAuthorities, + writeAuthorities, + } = additionalNamespace + + const namespacePropIsString = typeof namespace === 'string' + const definedAuthsProps = [ + authorities, + readAuthorities, + writeAuthorities, + ].filter((auths) => auths !== undefined) + const definedAuthsPropsAreValid = + Array.isArray(definedAuthsProps) && + definedAuthsProps.every( + (auths) => + Array.isArray(auths) && + auths.every((auth) => typeof auth === 'string') + ) + + const additionalNamespaceIsValid = + namespacePropIsString && + definedAuthsProps.length > 0 && + definedAuthsPropsAreValid + if (!additionalNamespaceIsValid) { + reporter.warn(msg) + return false // skip this additional namespace + } + + return true + } + ) + + return filteredNamespaces +} + +exports.parseAdditionalNamespaces = parseAdditionalNamespaces diff --git a/cli/src/lib/parseAdditionalNamespaces.test.js b/cli/src/lib/parseAdditionalNamespaces.test.js new file mode 100644 index 00000000..b9ed1439 --- /dev/null +++ b/cli/src/lib/parseAdditionalNamespaces.test.js @@ -0,0 +1,79 @@ +const { reporter } = require('@dhis2/cli-helpers-engine') +const { parseAdditionalNamespaces } = require('./parseAdditionalNamespaces') + +jest.mock('@dhis2/cli-helpers-engine', () => ({ + reporter: { warn: jest.fn() }, + chalk: { bold: jest.fn().mockImplementation((input) => input) }, +})) + +test('undefined', () => { + const output = parseAdditionalNamespaces(undefined) + expect(output).toBe(undefined) + expect(reporter.warn).toHaveBeenCalledTimes(0) +}) + +test('the happy path', () => { + const additionalNamespaces = [ + { namespace: 'extra1', authorities: ['M_extra1'] }, + { namespace: 'extra2', readAuthorities: ['M_extra2read'] }, + { namespace: 'extra3', writeAuthorities: ['M_extra3write'] }, + { + namespace: 'extra4', + authorities: ['M_extra4readwrite'], + writeAuthotities: ['M_extra4write'], + }, + ] + + const output = parseAdditionalNamespaces(additionalNamespaces) + expect(output).toEqual(additionalNamespaces) + expect(reporter.warn).toHaveBeenCalledTimes(0) +}) + +describe('handling faults', () => { + test('additionalNamespaces is not an array', () => { + const additionalNamespaces = { + namespace: 'extra1', + authorities: ['M_extra1'], + } + + const output = parseAdditionalNamespaces(additionalNamespaces) + + expect(output).toBe(undefined) + expect(reporter.warn).toHaveBeenCalledTimes(1) + }) + + test('invalid namespace options get filtered out', () => { + const testNamespaces = [ + { namespace: 'no-authorities' }, + { authorities: ['F_MISSING-NAMESPACE-STRING'] }, + { + namespace: 'valid-namespace-1', + readAuthorities: ['M_extra-read'], + writeAuthorities: ['M_extra-write'], + }, + { namespace: ['not-a-string'], readAuthorities: ['M_extra2read'] }, + { + namespace: 'invalid-value-type', + readAuthorities: 'should-be-an-array', + }, + { + namespace: 'one-correct-auths-prop-one-error', + readAuthorities: ['M_extra-read'], + writeAuthorities: 'should-be-an-array', + }, + { + namespace: 'valid-namespace-2', + writeAuthorities: ['M_extra-write'], + }, + { + namespace: 'valid-namespace-3', + authorities: ['M_extra-readwrite'], + writeAuthotities: ['M_extra-write'], + }, + ] + + const output = parseAdditionalNamespaces(testNamespaces) + expect(output).toEqual([testNamespaces[2], ...testNamespaces.slice(-2)]) + expect(reporter.warn).toHaveBeenCalledTimes(6) + }) +}) diff --git a/docs/config/d2-config-js-reference.md b/docs/config/d2-config-js-reference.md index 78752ede..fb202cc9 100644 --- a/docs/config/d2-config-js-reference.md +++ b/docs/config/d2-config-js-reference.md @@ -11,27 +11,28 @@ All properties are technically optional, but it is recommended to set them expli The following configuration properties are supported: -| Property | Type | Default | Description | -| :--------------------: | :---------------------------: | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **type** | _string_ | **app** | Either **app**, **login_app** or **lib** | -| **name** | _string_ | `pkg.name` | A short, machine-readable unique name for this app | -| **title** | _string_ | `config.name` | The human-readable application title, which will appear in the HeaderBar | -| **direction** | `'ltr'`, `'rtl'`, or `'auto'` | `'ltr'` | Sets the `dir` HTML attribute on the `document` of the app. If set to `'auto'`, the direction will be inferred from the current user's UI locale setting. The header bar will always be considered 'auto' and is unaffected by this setting. | -| **id** | _string_ | | The ID of the app on the [App Hub](https://apps.dhis2.org/). Used when publishing the app to the App Hub with [d2 app scripts publish](../scripts/publish). See [this guide](https://developers.dhis2.org/docs/guides/publish-apphub/) to learn how to set up continuous delivery. | -| **description** | _string_ | `pkg.description` | A full-length description of the application | -| **author** | _string_ or _object_ | `pkg.author` | The name of the developer to include in the DHIS2 manifest, following [package.json author field syntax](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#people-fields-author-contributors). | -| **entryPoints.app** | _string_ | **./src/App** | The path to the application entrypoint (not used for libraries) | -| **entryPoints.plugin** | _string_ | | The path to the application's plugin entrypoint (not used for libraries) | -| **entryPoints.lib** | _string_ or _object_ | **./src/index** | The path to the library entrypoint(s) (not used for applications). Supports [conditional exports](https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#packages_conditional_exports) | -| **skipPluginLogic** | _boolean_ | **false** | By default, plugin entry points will be wrapped with logic to allow the passing of properties and resizing between the parent app and the child plugin. This logic will allow users to use the plugin inside an app when wrapped in `` component from app-runtime. If set to true, this logic will not be loaded. | -| **pluginType** | _string_ | | Gets added to the `plugin_type` field for this app in the `/api/apps` response -- an example is `pluginType: 'DASHBOARD'` for a plugin meant to be consumed by the Dashboard app. Must be contain only characters from the set A-Z (uppercase), 0-9, `-` and `_`; i.e., it's tested against the regex `/^[A-Z0-9-_]+$/`. | -| **dataStoreNamespace** | _string_ | | The DataStore and UserDataStore namespace to reserve for this application. The reserved namespace **must** be suitably unique, as other apps will fail to install if they attempt to reserve the same namespace - see the [webapp manifest docs](https://docs.dhis2.org/en/develop/loading-apps.html) | -| **customAuthorities** | _Array(string)_ | | An array of custom authorities to create when installing the app, these do not provide security protections in the DHIS2 REST API but can be assigned to user roles and used to modify the interface displayed to a user - see the [webapp manifest docs](https://docs.dhis2.org/en/develop/loading-apps.html) | -| **minDHIS2Version** | _string_ | | The minimum DHIS2 version the App supports (eg. '2.35'). Required when uploading an app to the App Hub. The app's major version in the app's package.json needs to be increased when changing this property. | -| **maxDHIS2Version** | _string_ | | The maximum DHIS2 version the App supports. | -| **coreApp** | _boolean_ | **false** | **ADVANCED** If true, build an app artifact to be included as a root-level core application | -| **standalone** | _boolean_ | **false** | **ADVANCED** If true, do NOT include a static BaseURL in the production app artifact. This includes the `Server` field in the login dialog, which is usually hidden and pre-configured in production. | -| **pwa** | _object_ | | **ADVANCED** Opts into and configures PWA settings for this app. Read more about the options in [the PWA docs](../pwa). | +| Property | Type | Default | Description | +| :----------------------: | :---------------------------: | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **type** | _string_ | **app** | Either **app**, **login_app** or **lib** | +| **name** | _string_ | `pkg.name` | A short, machine-readable unique name for this app | +| **title** | _string_ | `config.name` | The human-readable application title, which will appear in the HeaderBar | +| **direction** | `'ltr'`, `'rtl'`, or `'auto'` | `'ltr'` | Sets the `dir` HTML attribute on the `document` of the app. If set to `'auto'`, the direction will be inferred from the current user's UI locale setting. The header bar will always be considered 'auto' and is unaffected by this setting. | +| **id** | _string_ | | The ID of the app on the [App Hub](https://apps.dhis2.org/). Used when publishing the app to the App Hub with [d2 app scripts publish](../scripts/publish). See [this guide](https://developers.dhis2.org/docs/guides/publish-apphub/) to learn how to set up continuous delivery. | +| **description** | _string_ | `pkg.description` | A full-length description of the application | +| **author** | _string_ or _object_ | `pkg.author` | The name of the developer to include in the DHIS2 manifest, following [package.json author field syntax](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#people-fields-author-contributors). | +| **entryPoints.app** | _string_ | **./src/App** | The path to the application entrypoint (not used for libraries) | +| **entryPoints.plugin** | _string_ | | The path to the application's plugin entrypoint (not used for libraries) | +| **entryPoints.lib** | _string_ or _object_ | **./src/index** | The path to the library entrypoint(s) (not used for applications). Supports [conditional exports](https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#packages_conditional_exports) | +| **skipPluginLogic** | _boolean_ | **false** | By default, plugin entry points will be wrapped with logic to allow the passing of properties and resizing between the parent app and the child plugin. This logic will allow users to use the plugin inside an app when wrapped in `` component from app-runtime. If set to true, this logic will not be loaded. | +| **pluginType** | _string_ | | Gets added to the `plugin_type` field for this app in the `/api/apps` response -- an example is `pluginType: 'DASHBOARD'` for a plugin meant to be consumed by the Dashboard app. Must be contain only characters from the set A-Z (uppercase), 0-9, `-` and `_`; i.e., it's tested against the regex `/^[A-Z0-9-_]+$/`. | +| **dataStoreNamespace** | _string_ | | The DataStore and UserDataStore namespace to reserve for this application. The reserved namespace **must** be suitably unique, as other apps will fail to install if they attempt to reserve the same namespace - see the [webapp manifest docs](https://docs.dhis2.org/en/develop/loading-apps.html) | +| **additionalNamespaces** | _Array(object)_ | | An array of additional datastore namespaces that should be associated with the app. For each, the user can specify the authorities required to read/write. See more in the [Additional datastore namespaces section](#additional-datastore-namespaces) below. | +| **customAuthorities** | _Array(string)_ | | An array of custom authorities to create when installing the app, these do not provide security protections in the DHIS2 REST API but can be assigned to user roles and used to modify the interface displayed to a user - see the [webapp manifest docs](https://docs.dhis2.org/en/develop/loading-apps.html) | +| **minDHIS2Version** | _string_ | | The minimum DHIS2 version the App supports (eg. '2.35'). Required when uploading an app to the App Hub. The app's major version in the app's package.json needs to be increased when changing this property. | +| **maxDHIS2Version** | _string_ | | The maximum DHIS2 version the App supports. | +| **coreApp** | _boolean_ | **false** | **ADVANCED** If true, build an app artifact to be included as a root-level core application | +| **standalone** | _boolean_ | **false** | **ADVANCED** If true, do NOT include a static BaseURL in the production app artifact. This includes the `Server` field in the login dialog, which is usually hidden and pre-configured in production. | +| **pwa** | _object_ | | **ADVANCED** Opts into and configures PWA settings for this app. Read more about the options in [the PWA docs](../pwa). | > _Note_: Dynamic defaults above may reference `pkg` (a property of the local `package.json` file) or `config` (another property within `d2.config.js`). @@ -65,3 +66,37 @@ For example, let's say you want to adjust your app to a breaking change in the D 1. Set the `maxDHIS2Version` in `d2.config.js` to the last DHIS2 version that's still compatible with the code on `main`. 1. Branch `main` to a maintenance branch. The recommended naming pattern is `N.x`, where `N` should be replaced with the app's current major version (i.e. `100.x`, `101.x`, etc.). See also [semantic-release's documentation on branches](https://semantic-release.gitbook.io/semantic-release/usage/configuration#branches). The app's version can be found in `package.json`, look for the `version` field. 1. On `main` update the `minDHIS2Version` to the first DHIS2 version that contains the breaking change. It's important to publish the `minDHIS2Version` change as a breaking change. An easy way to do that is to include the `BREAKING CHANGE` label in the commit that changes the `minDHIS2Version` (see the [semantic-release documentation](https://semantic-release.gitbook.io/semantic-release/#commit-message-format) on their commit conventions). + +## Additional datastore namespaces + +Adds the possibility for apps to declare additional datastore namespaces that should be associated with the app. For each, the user can specify the authorities required to read/write. + +Each of the additional namespace objects must declare a namespace name, and authorities by one or more of the following properties: + +- `authorities`: a user needs any of these to read or write +- `readAuthorities`: a user needs any of these to read +- `writeAuthorities`: a user needs any of these to write + +If `authorities` is combined with read or write limited lists, the entries in authorities are added to the read/write (union). + +If only `readAuthorities` are defined, these automatically apply for write. + +If only `writeAuthorities` are defined, read is not restricted. + +Examples of valid additional namespace objects: + +```js +const config = { + // ... + additionalNamespaces: [ + { namespace: 'extra1', authorities: ['M_extra1'] }, + { namespace: 'extra2', readAuthorities: ['M_extra2read'] }, + { namespace: 'extra3', writeAuthorities: ['M_extra3write'] }, + { + namespace: 'extra4', + authorities: ['M_extra4readwrite'], + writeAuthorities: ['M_extra4write'], + }, + ], +} +``` diff --git a/examples/simple-app/d2.config.js b/examples/simple-app/d2.config.js index 715669fa..b62fa613 100644 --- a/examples/simple-app/d2.config.js +++ b/examples/simple-app/d2.config.js @@ -13,6 +13,17 @@ const config = { dataStoreNamespace: 'testapp-namespace', customAuthorities: ['testapp-authority'], + additionalNamespaces: [ + { + namespace: 'testapp-additional-namespace1', + authorities: ['testapp-additional-auth'], + }, + { + namespace: 'testapp-additional-namespace2', + writeAuthorities: ['testapp-additional-write'], + readAuthorities: ['testapp-additional-read'], + }, + ], minDHIS2Version: '2.35', }