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.