Standard structure of UnderTree microservice
This is used as a template to create new microservices.
In an empty folder named ut-*
run:
npm init ut ms
- Continuous Integration (Jenkins)
- Storybook
- API Docs
- Static Code Analysis (SonarQube)
- UI builds (Chromatic)
Back end layers are defined in \index.js
. It references various files
from the following places:
📁 ut-microservice ├── build.js (build TS type definitions ) ├── config.js (default configurations) ├── index.js (definition of layers) ├── errors.js (error definitions loader) ├── errors.json (error definitions) ├──📁 api | ├──📁 lib (local reusable utility functions) | | └── fn.js (utility function) | ├──📁 microservice (implementation of the API) | | ├── index.js (index of all API functions) | | ├── ... | | └── microservice.object.predicate.js (API handler) | └──📁 sql MSSQL definitions | ├──📁 schema (schema objects: tables, procedures, views, ... ) | | └── *.sql | ├──📁 seed (mandatory data: item types, actions, ...) | | └── *.merge.yaml | ├──📁 standard (standard data used during automated tests) | | └── *.merge.yaml | ├── schema.js (configuration for the schema folder) | ├── seed.js (configuration for the seed folder) | └── standard.js (configuration for the standard folder) ├──📁 model (define data model and mocks) | ├── index.js (index of all models) | ├── mock.js (index of all mocks) | ├── ... | ├── foo.js (model definition) | └── foo.mock.js (mock definition) ├──📁 server (back end test / debug) | ├── common.js (common configuration) | ├── index.js (microservice dependencies) | └── unit.js (unit test configuration) ├──📁 test | ├──📁 jobs (definition of parallel jobs to run during tests) | | ├── index.js (index of all jobs) | | └── test.*.js (individual jobs) | ├──📁 steps | | └── *.js (reusable test steps) | └── index.test.js (unit tests startup script) └──📁 validations (API definition) ├── index.js (index of all API definitions) └── microservice.object.predicate.js (a single API definition)
Front end is defined in the following folder structure:
📁 ut-microservice ├── ui.test.js (UT test startup script) ├── 📁 browser | └── adminPortal.js (UI test portal entry) ├── 📁 help (user guide content) | ├── _category_.yaml (title and index configuration) | ├── ... | └── microservice.tree.open.md (help pages) ├── 📁 model (define data model and mocks here) | ├── index.js (index of all models) | ├── mock.js (index of all mocks) | ├── dropdown.js (dropdowns mock) | ├── ... | ├── bar.js (model definition) | └── bar.mock.js (mock definition) ├── 📁 test | └──📁 ui (UI tests) | ├── index.js (test runner) | ├── ... | └── microservice.bar.play.js (Playwright script) └── 📁 portal ├──📁 backend (define optional backend handlers here) | ├── index.js (index of all backend handlers) | ├── ... | └── microservice.object.predicate.js (backend handler) ├──📁 component (define UI components here) | | index.js (index of all components) | | ... | └ microservice.foo.open.js (a single component) ├──📁 handle (define reusable event handlers here) | | index.js (index of all handlers) | | ... | └ microservice.foo.click.js (a single handler) ├── config.js (configuration defaults) ├── index.js (layers) ├── index.stories.js (storybook) └── mock.js (mock loader)
Each microservice should be developed in a way, that enables debugging it
without an implementation. The files needed to enable this are in the server
folder. To connect to a database, a configuration file is needed, which should
not be put in git. To avoid creation of a new configuration file for each
microservice you work on, put a file named .ut_devrc
in a common place
(see ut-config
for the possible places, where configuration can be placed).
A second file named .ut_testrc
is needed for the launch configurations which use set the environment variable UT_ENV=test
.
The files must have the following recommended structure:
db:
auto: true # this enables automatic DB creation based on user name
logLevel: info
connection:
server: bgs-vws-db-10.softwaregroup-bg.com
user: firstName.lastName # your first and last names
password: xxx
connectionTimeout: 60000
requestTimeout: 60000
create:
user: # user, which can create databases
password: # password of the above user
utLogin:
login:
expire:
cookie: 400000000 # this makes the login not expire
access: 400000000 # this makes the login not expire
Components are created by following the pattern below:
import React from 'react';
/** @type { import("../../handlers").handlerFactory } */
export default ({
import: {
handle$microserviceFooClick,
handleTabShow,
component$subjectObjectPredicate
}
}) => ({
'microservice.foo.open': () => ({
title: 'Foo edit',
permission: 'microservice.foo.open',
component: ({id}) => function FooOpen() {
return (
<div>
page content for item {id}
</div>
);
}
})
});
Components are defined in functions named using the subject.object.predicate
pattern. These functions must return an object with the following properties:
title
- A default title to be shown in the menu or other places in the UI.permission
- The permission to be checked to allow usage of the component.component
- Function which returns a React function component. This function can be async and can call to other front-end or back-end APIs before returning the React component.
Examples of recommended patterns for naming component functions:
microservice.foo.browse
- For showing collection offoo
items.microservice.foo.new
- For creating newfoo
items.microservice.foo.open
- For showing a singlefoo
item for editing or viewing. Both editing and viewing is usually controlled through user's permissions and must be reflected in the respective UI elements. The component function (FooOpen
) must receive a propertyid
in the first argument, which is the identifier of thefoo
item. Thisid
is also usually part of the URL or passed to thehandleTabShow
handler (seeut-portal
docs for more info).
/** @type { import("../../handlers").handlerFactory } */
export default ({
import: {
subjectObjectPredicate
}
}) => ({
async 'microservice.foo.click'(params) {
return {result: await subjectObjectPredicate({})};
},
'microservice.foo.clickReduce'({state, payload}) {
return {...state, ...payload};
}
});
Event handlers are defined in functions named using the
subject.object.predicate
or subject.object.predicateReduce
patterns. The primary reason for having two handlers is the way Redux works,
as it does not allow updating the state from async functions, while async
functions are needed for interacting with the back end. If handling certain
events does not involve updating Redux state, the reduce handler is not needed.
The reduce handler's first argument is an object with the properties:
state
- Current Redux state.payload
- The result from the async handler.
This handler must return the new state, as per Redux rules.
Event handler functions and component functions are wrapped in handler factory
functions, which have access to the UT framework API interface.
The following destructuring patterns are available for use within the
import
property:
import:{subjectObjectPredicate}
- Call back end methods.import:{component$subjectObjectPredicate}
- Use components defined elsewhere in the UI (in this or other modules).import:{'component/subject.complexNaming':componentXxx}
- Use components defined elsewhere in the UI (in this or other modules), which follow an arbitrary naming.import:{handle$subjectObjectPredicate}
- Use handlers defined elsewhere in the UI (in this or other modules).import:{'handle/subject.complexNaming':handleXxx}
- Use handlers defined elsewhere in the UI (in this or other modules), which follow an arbitrary naming.
To include the module in an implementation:
-
Add it as dependency in
package.json
:{ "dependencies": { "utMicroservice": "^7.8.2" } }
-
Add it to the list of modules in
server/index.js
:module.exports = (...params) => [{ // other modules }, { main: require.resolve('ut-microservice'), pkg: require.resolve('ut-microservice/package.json') }, { // other modules }].map(item => [item, ...params]);
-
Turn it on in
server/common.js
(if needed):module.exports = { // other settings utMicroservice: true // other settings }
-
Add it to the list of modules in
browser/xxxPortal.js
module.exports = (...params) => [ // other modules require('ut-microservice')(...params), // other modules ];
-
Turn it on in
browser/common.js
(if needed):module.exports = { // other settings utMicroservice: {browser: true}, // other settings }
-
If you need to add items to a portal menu, check the ut-portal readme.
-
Test UI components in
Storybook
, with React fast refresh, using mocked back-end:npm run storybook
Edit ./portal/index.stories.js to:
- match the developed components
- add new mocked responses for the needed back end methods.
-
Use Chromatic (see the project links above) to browse the published component library versions, documentation, visual testing, component screenshots, etc.