-
Notifications
You must be signed in to change notification settings - Fork 54
feat(#792): add upload-ui-extensions action #798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Vedd-Patel
wants to merge
8
commits into
medic:main
Choose a base branch
from
Vedd-Patel:upload-ui-extensions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b9bc96b
feat: add upload-ui-extensions action
Vedd-Patel 190f2d1
test: add unit tests for upload-ui-extensions
Vedd-Patel b7bddef
fix: address SonarCloud code quality warnings
Vedd-Patel 873ca8a
refactor: reduce cognitive complexity and name async function
Vedd-Patel 0e02508
test: use node:fs instead of fs to resolve SonarCloud warning
Vedd-Patel 70e3e95
refactor: address review feedback for upload-ui-extensions
Vedd-Patel 5b9501e
fix: address SonarCloud warnings
Vedd-Patel 515fe03
Merge branch 'main' into fork/Vedd-Patel/upload-ui-extensions
jkuester File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| const projectPaths = require('../lib/project-paths'); | ||
| const { uploadUiExtensions } = require('../lib/upload-ui-extensions'); | ||
|
|
||
| const executeUploadUiExtensions = async (environment) => { | ||
| const uiExtensionsDir = `${environment.pathToProject}/${projectPaths.UI_EXTENSIONS_PATH}`; | ||
|
|
||
| let specificExtensions = []; | ||
| if (environment.extraArgs?.length) { | ||
| specificExtensions = environment.extraArgs.filter(arg => !arg.startsWith('--')); | ||
| } | ||
|
|
||
| await uploadUiExtensions(uiExtensionsDir, specificExtensions); | ||
| }; | ||
|
|
||
| module.exports = executeUploadUiExtensions; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| const fs = require('node:fs'); | ||
| const path = require('node:path'); | ||
| const Joi = require('joi'); | ||
| const environment = require('./environment'); | ||
| const log = require('./log'); | ||
| const insertOrReplace = require('./insert-or-replace'); | ||
| const warnUploadOverwrite = require('./warn-upload-overwrite'); | ||
| const pouch = require('./db'); | ||
| const attachmentFromFile = require('./attachment-from-file'); | ||
|
|
||
| const schema = Joi.object({ | ||
| type: Joi.string().valid('app_main_tab', 'app_drawer_tab').required(), | ||
| title: Joi.string().required(), | ||
| icon: Joi.string().required(), | ||
| roles: Joi.array().items(Joi.string()), | ||
| config: Joi.object().unknown(true) | ||
| }); | ||
|
|
||
| //validates the name against custom web component standards | ||
| const validateExtensionName = (name) => { | ||
| const startsWithLowercase = /^[a-z]/.test(name); | ||
| const hasHyphen = name.includes('-'); | ||
| const hasValidChars = /^[a-z0-9_.-]+$/.test(name); | ||
|
|
||
| return startsWithLowercase && hasHyphen && hasValidChars; | ||
| }; | ||
|
|
||
| const getNamesToUpload = (uiExtensionsDir) => { | ||
| const allFiles = fs.readdirSync(uiExtensionsDir); | ||
| const extensionNames = new Set( | ||
| allFiles | ||
| .filter(f => f.endsWith('.js') || f.endsWith('.properties.json')) | ||
| .map(f => f.replace(/(\.properties\.json|\.js)$/, '')) | ||
| ); | ||
|
|
||
| return Array.from(extensionNames); | ||
| }; | ||
|
|
||
| const getExtensionDoc = (uiExtensionsDir, name) => { | ||
| if (!validateExtensionName(name)) { | ||
| throw new Error( | ||
| `UI Extension name "${name}" is invalid. It must start with a lowercase letter, ` + | ||
| 'contain at least one hyphen, and use only lowercase letters, digits, hyphens, ' + | ||
| 'periods, or underscores.' | ||
| ); | ||
| } | ||
|
|
||
| const jsPath = path.join(uiExtensionsDir, `${name}.js`); | ||
| const propsPath = path.join(uiExtensionsDir, `${name}.properties.json`); | ||
|
|
||
| if (!fs.existsSync(jsPath) || !fs.existsSync(propsPath)) { | ||
| throw new Error(`UI Extension "${name}" is missing either its .js or .properties.json file.`); | ||
| } | ||
|
|
||
| let propsContent; | ||
| try { | ||
| const rawProps = fs.readFileSync(propsPath, 'utf-8'); | ||
| propsContent = JSON.parse(rawProps); | ||
| } catch (err) { | ||
| throw new Error(`Failed to parse ${name}.properties.json - Invalid JSON format: ${err.message}`); | ||
| } | ||
|
|
||
| const validation = schema.validate(propsContent); | ||
| if (validation.error) { | ||
| throw new Error(`Validation error for UI extension "${name}": ${validation.error.message}`); | ||
| } | ||
|
|
||
| return { | ||
| _id: `ui-extension:${name}`, | ||
| type: 'ui-extension', | ||
| ...propsContent, | ||
| _attachments: { | ||
| 'extension.js': attachmentFromFile(jsPath) | ||
| } | ||
|
Vedd-Patel marked this conversation as resolved.
|
||
| }; | ||
| }; | ||
|
|
||
| const uploadDocToDb = async (db, doc, name) => { | ||
| const changes = await warnUploadOverwrite.preUploadDoc(db, doc); | ||
| if (!changes) { | ||
| log.info(`UI Extension "${name}" not uploaded as already up to date`); | ||
| return; | ||
| } | ||
|
|
||
| await insertOrReplace(db, doc); | ||
| log.info(`UI Extension "${name}" upload complete`); | ||
| await warnUploadOverwrite.postUploadDoc(db, doc); | ||
| }; | ||
|
|
||
| const uploadUiExtensions = async (uiExtensionsDir, specificExtensions = []) => { | ||
| if (!fs.existsSync(uiExtensionsDir)) { | ||
| log.info(`No directory found at "${uiExtensionsDir}" - not uploading ui-extensions`); | ||
| return; | ||
| } | ||
|
|
||
| // if specific extensions are provided, bypass directory reading | ||
| // missing files will be caught by getExtensionDoc | ||
| const namesToUpload = specificExtensions.length ? specificExtensions : getNamesToUpload(uiExtensionsDir); | ||
|
|
||
| if (!namesToUpload.length) { | ||
| log.info('No UI extensions to upload.'); | ||
| return; | ||
| } | ||
|
|
||
| log.info(`Found UI extensions: ${namesToUpload.join(', ')}`); | ||
|
Vedd-Patel marked this conversation as resolved.
|
||
|
|
||
| // process all docs before uploading any to ensure validation passes for everything | ||
| const namesWithDocs = namesToUpload.map(name => [name, getExtensionDoc(uiExtensionsDir, name)]); | ||
|
|
||
| const db = pouch(environment.apiUrl); | ||
|
|
||
| for (const [name, doc] of namesWithDocs) { | ||
| await uploadDocToDb(db, doc, name); | ||
| } | ||
| }; | ||
|
|
||
| module.exports = { uploadUiExtensions }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| const chai = require('chai'); | ||
| const sinon = require('sinon'); | ||
| const fs = require('node:fs'); | ||
|
|
||
| const { uploadUiExtensions } = require('../../src/lib/upload-ui-extensions'); | ||
| const log = require('../../src/lib/log'); | ||
|
|
||
| const expect = chai.expect; | ||
| chai.use(require('chai-as-promised')); | ||
|
|
||
| describe('Upload UI Extensions Library', () => { | ||
| let sandbox; | ||
|
|
||
| beforeEach(() => { | ||
| sandbox = sinon.createSandbox(); | ||
| sandbox.stub(log, 'info'); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| sandbox.restore(); | ||
| }); | ||
|
|
||
| it('should log and return if the ui-extensions directory does not exist', async () => { | ||
| sandbox.stub(fs, 'existsSync').returns(false); | ||
|
|
||
| await uploadUiExtensions('/fake/path'); | ||
|
|
||
| expect(log.info.callCount).to.equal(1); | ||
| expect(log.info.args[0][0]).to.include('No directory found at "/fake/path"'); | ||
| }); | ||
|
|
||
| it('should log and return if no valid extension files are found', async () => { | ||
| sandbox.stub(fs, 'existsSync').returns(true); | ||
| sandbox.stub(fs, 'readdirSync').returns(['random-file.txt', 'image.png']); | ||
|
|
||
| await uploadUiExtensions('/fake/path'); | ||
|
|
||
| expect(log.info.callCount).to.equal(1); | ||
| expect(log.info.args[0][0]).to.equal('No UI extensions to upload.'); | ||
| }); | ||
|
|
||
| it('should throw an error if an extension name does not match custom element standards', async () => { | ||
| sandbox.stub(fs, 'existsSync').returns(true); | ||
| // "myExtension" is invalid (has uppercase, no hyphen) | ||
| sandbox.stub(fs, 'readdirSync').returns(['myExtension.js', 'myExtension.properties.json']); | ||
|
|
||
| await expect(uploadUiExtensions('/fake/path')) | ||
| .to.be.rejectedWith('UI Extension name "myExtension" is invalid.'); | ||
| }); | ||
|
|
||
| it('should throw an error if missing either the .js or .properties.json file', async () => { | ||
| sandbox.stub(fs, 'readdirSync').returns(['valid-name.js']); | ||
|
|
||
| sandbox.stub(fs, 'existsSync').callsFake((path) => !path.includes('.properties.json')); | ||
|
|
||
| await expect(uploadUiExtensions('/fake/path')) | ||
| .to.be.rejectedWith('UI Extension "valid-name" is missing either its .js or .properties.json file.'); | ||
| }); | ||
|
|
||
| it('should throw an error if properties.json is invalid JSON', async () => { | ||
| sandbox.stub(fs, 'existsSync').returns(true); | ||
| sandbox.stub(fs, 'readdirSync').returns(['valid-name.js', 'valid-name.properties.json']); | ||
|
|
||
| // simulates a broken JSON file | ||
| sandbox.stub(fs, 'readFileSync').returns('invalid json { oops'); | ||
|
|
||
| await expect(uploadUiExtensions('/fake/path')) | ||
| .to.be.rejectedWith('Failed to parse valid-name.properties.json - Invalid JSON format:'); | ||
| }); | ||
|
|
||
| it('should throw an error if properties.json fails Joi schema validation', async () => { | ||
| sandbox.stub(fs, 'existsSync').returns(true); | ||
| sandbox.stub(fs, 'readdirSync').returns(['valid-name.js', 'valid-name.properties.json']); | ||
|
|
||
| // missing required fields like 'icon' and 'config' | ||
| const invalidProps = { type: 'app_main_tab', title: 'My Extension' }; | ||
| sandbox.stub(fs, 'readFileSync').returns(JSON.stringify(invalidProps)); | ||
|
|
||
| await expect(uploadUiExtensions('/fake/path')) | ||
| .to.be.rejectedWith('Validation error for UI extension "valid-name":'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.