Module Development
Though we do not have specific restrictions on languages you can use (since we're a microservice architecture), we
have samples and tooling for Java (Spring Boot)
as well as Golang
for the backend services, and NodeJS (SvelteKit)
for the web.
Repository Structure
To keep things manageable, we aim to have every module contained within one repository - a monorepo. We separate
services or applications within a module by folder. For example, with the Dashboard Module
, it has a ./web
folder,
containing a Web Base
-based project that deals with the modlet development, and an ./app
folder written in Golang
that handles the backend API.
We also have additional tools installed in each repository to handle:
- Code Cleanliness
- Code Coverage
- Conventional Commits
- Change Management
- CI/CD Configuration
These are (or will be) elaborated in the Source Control section, but their place in the monorepository is described here:
.
├─.changeset
├─.github
├─.husky
├─.commitlintrc.cts
├─.gitignore
├─package.json
├─README.md
├─<project-folder-a>
...
The above shows the typical minimal structure for a monorepo. Even if your service is not a NodeJS
module, you would
have a package.json
which has tools to mainly handle 2 things:
- Triggering automations on git hooks via Husky
- Handling change management via Changesets
Each of the above tools are npm packages
.
The following list explains what each folder is for. Their specific individual configurations are currently beyond the scope of this document as they can be large topics on their own.
-
.changeset
We can track the "entire" version of the module with
package.json
. We usechangesets
(configuration within the.changeset
folder) and the npm workspace concept to handle multiple projects. -
.husky
The folder holds the configuration for Husky - we can run custom scripts here before any code is checked in, such as whether your commit messages follow our conventions, whether your code is free of TypeScript or linting errors, and whether your code has a certain code coverage percentage, or all unit tests pass.
-
.github
This folder holds GitHub-specific files, such as GitHub Actions workflows (for our CI/CD), the
CODEOWNERS
files, etc. -
.commitlintrc.cts
This file contains the commit linting conventions - used to ensure commits follow the format we want.
-
<project-folder-a
>Create folders, give them an appropriate short name, and place your individual projects in there. One folder per project.
Containers
Local Development Environment (Docker Compose)
Each module should have it's respective docker-compose.yml
file, maintained inside our
dev-containers
repository. To add your module in, you should create
a new folder with your module's short name, and place your docker-compose.yml
files in there. We shorten the name of
the file to compose.yml
for brevity, but keeping the "compose" name to keep VSCode's Intellisense with the Docker
plugin working.
Each compose.yml
file uses the include
feature to deal with dependencies on other modules/containers.
The complication comes from having traefik
as the ingress controller in our compose files. In the infra
"module"
within the dev-containers
, we have traefik
running to help route all trafic based on the hostname. We also use
traefik
to handle CORS via middlewares. Your compose.yml
must take this into account. traefik
allows you to use
labels in Docker to provide this functionality, you can learn more about using traefik
in their
official documentation.
Modlets (Frontend / Web)
Modlets
are what we call the web portions of each module. They comprise of all the web (Svelte) components, web
libraries, routes and API endpoints that make up the web portions of a module. These components are packaged into
npm node modules in a format that allows them to be easily installed using our CLI.
When developing your custom modlet on top of the Web Base
, it's essential to follow a few standards to for it to be
easily packaged for distribution.
CLI Tool Packaging Requirements
Our Web Base
features a built-in packaging feature that streamlines the process of extracting and publishing your
modlets to our registry.
Required Folders
To ensure seamless integration with the Web Base
, you must compartmentalize the files that need to be exported,
making it easy for others to use your modlet.
In the following sections, the <project-name> is currently hard-coded to "aoh" in the Web Base CLI
, we'll have to
add proper configuration for this on the next release.
-
Components / modlet folder:
Store all your reusable components and modlets, such as Svelte components, in a single folder.
src/lib/<project-name>/<module-name>
- we refer to this as the modlet's lib folder
-
Routes (public and private) folders:
SvelteKit does not support package routes, to circumvent this we also use a standardized structure to allow us to have the CLI tool copy routes to their appropriate locations. Organize your routes into two separate folders - one set under
(public)
for routes that do not require access control, and the other set under(private)
for routes that do require access control.src/routes/(public)/<project-name>/<module-name>
src/routes/(private)/<project-name>/<module-name>
By adhering to our recommended folder structure, you'll enable the packaging manager to easily identify and extract the necessary files for packaging as well as publish your modlets to our registry without errors or complications.
Example Folder Structure
The following example is for a project named foo
and a modlet/module named bar
.
...
src
├─lib
│ └─foo
│ └─bar
│ └─ <whatever files you put under here is up to you>
├─routes
│ ├─(private)
│ │ └─foo
│ │ └─bar
│ │ └── <whatever files you put under here is up to you>
│ └─(public)
│ └─foo
│ └─bar
│ └── <whatever files you put under here is up to you>
package.json
...
The structure of the contents within these folders are entirely up to you. You are free to organize your files and
create more folders as you see fit as long as they are contained within these 3. They are namespaced by project and
module names (foo
and bar
respectively) to avoid name clashes.
We recommend keeping Svelte components to a folder called "components".
Required files
modlet.config.ts
src/routes/<project-name>/<module-name>/modlet.config.ts
This is the runtime
configuration file for the modulet, at present, each modlet supplies our headerbar
and sidebar
with configuration data to determine the names, icons, and menu items to show in when the modlet is in use.
The file must export a const config
object, containing the nav
field. This is used by the headerbar
and sidebar
.
Any other fields can be defined by you (the modlet developer) for use in your modlet if you need to a way to pass
configuration data around.
The nav
field is an object that contains:
{
code: string,
header: { name: string, url: string },
sidebar: Array<{ name: string, icon: string, url: string}>
}
Description
-
code
The
code
is a string used to identify the modlet - it is the modlet's short name. For example, for theDashboard Module
, it is "DASH". -
header
The
header
is an object containing:name: string
The of the module to display in the module selector (in the headerbar).
url: string
The path to the page to display when selecting the module from the headerbar.
-
sidebar
The
sidebar
is an object containing an array of menu items to appear in the sidebar for that module. Each menu item consists of:name: string
The name of the menu item to display
icon: ComponentIcon
A lucide-svelte icon to render
url: string
The path to the page to display when selecting the menu item.
import type { ComponentIcon } from "lucide-svelte";
import ChartLine from "lucide-svelte/icons/chart-line";
import Star from "lucide-svelte/icons/star";
//This is a placeholder type, will update with real type
export type DashboardConfiguration = unknown;
export interface DashboardModletConfiguration extends ModletConfiguration {
dash: DashboardConfiguration;
}
export const config: DashboardConfiguration = {
nav: {
code: "DASH",
header: {
name: "Dashboard",
url: "/aoh/dash",
},
sidebar: [
{
name: "Favorite Dashboards",
icon: Star as unknown as ComponentIcon,
url: "/aoh/dash/favourite",
},
{
name: "Dashboard Management",
icon: ChartLine as unknown as ComponentIcon,
url: "/aoh/dash",
},
],
},
};
modlet.setup.ts
src/routes/<project-name>/<module-name>/modlet.setup.ts
This file is to supply installation-time
configuration for the modlet. It must export a const setup
, that has 2
optional fields:
env
A map of objects - each containing the "value" and "description" of the environment variable. The key of the map is the key of the environment variable. This will be used to populate the.env.template
when the modlet is installed in theWeb Base
.postInstallation
An asynchronous function that returns aPromise<boolean>
- true or false for success or failure respectively. You can do anything in this function, and the CLI tool will run it. For example, in the case of theGIS
, it modifies thevite.config.ts
to add extra configuration values.
export const setup: ModletSetupConfig = {
env: {
GIS_URL: { value: "http://gis.127.0.0.1.nip.io", description: "The URL to the GIS backend service" },
PUBLIC_RTUS_SEH_URL: {
value: "http://rtus-seh.127.0.0.1.nip.io",
description: "The URL to the RTUS Server Sent Events service",
},
PUBLIC_GIS_RTUS_MAP_NAME: {
value: "gis",
description: "The name of the distributed map to connect to in RTUS",
},
},
postInstallation: async () => {
try {
// This function is too long to list here, but you get that point
await modifyViteConfigInternal();
return true;
} catch (err) {
console.error("\nAn unexpected error occurred attempting to modify `vite.config.ts`:", err);
return false;
}
},
};
Modlet Registration in CLI
For the CLI tool to recoginize your modlet, it needs to be exported in the
config.ts
in the CLI.
Below is a sample of the config at the time of writing:
export const config: WebBaseCliConfig = {
modlet: {
dash: { name: "Dashboard", package: "@mssfoobar/dash" },
gis: { name: "Geospatial Information System", package: "@mssfoobar/gis-web" },
iams: { name: "Identity & Access Management Service", package: "@mssfoobar/iams-web" },
ian: { name: "In-App Notifications", package: "@mssfoobar/ian" },
ims: { name: "Incident Management Service", package: "@mssfoobar/ims-web" },
unh: { name: "Unified Notifications Hub", package: "@mssfoobar/unh-web" },
wfe: { name: "Workflow Engine", package: "@mssfoobar/wfe-wfd" },
headerbar: { name: "Headerbar", package: "@mssfoobar/webbase-headerbar" },
sidebar: { name: "Sidebar", package: "@mssfoobar/webbase-sidebar" },
},
};
For example, if your module short name is foo
, with the full name Foobar Service
, and the package
@mssfoobar/foo-web
, you would be adding the following to the config
export:
{
"modlet": {
"foo": { "name": "Foobar Service", "package": "@mssfoobar/foo-web" }
}
}
Permissions
When creating modules or modlets, each one should come with its own set of permissions the protect endpoints, control how pages looks etc. Our guidelines are to define who (what role) is intended to access these features, and split them up accordingly. However, you will not be protecting resources with "roles", instead, you will use resources and scopes (referred to here as a permission) to protect them. This is because projects will define their own roles, which has its own fixed names - these should roles should then be given permissions accordingly.
Scopes
By convention, we recommend sticking to just two scopes - view
and edit
. The view
scope essentially referrs to
read-only access over a feature, or module, whereas the edit
scope will allow making mutations to resources such as
writing, updating, and deleting on top of reading.
Use the lower case view
and edit
scopes for your resources. It is unlikely that more will be needed as you should
be able to create different resources and resource types to meet your needs, however, if it makes sense you may create
more scopes.
Resources
Our modules will tend to be split into an "operator" feature, that is, some feature that will be used by people conducting actual operations in a command centre, and an "adminstrative" feature, that is, a feature that is used by administrators or managers during the setup of the system. It is expected that the administrative features might further be split between system-related configuration, and business-related configuration. For example, system configuration would be things like configuration database retention policies or even adding users to the system. Business-related configuration would be to configure pre-defined dashboards for operators to focus on.
Recommendations
To keep things simple, if you cannot envisage how the system be used, it must all check for just one permission. However, we believe most of the time, they will be segregatable into "administrative" vs "operating" resource types.
Summary (Naming Conventions)
For both:
- Use
snake_case
For scopes, stick the following 2, add more only if absolutely necessary:
view
edit
For resources - each resource has a "resource name" and "resource type", both of these should be prefixed by the module's shortname, followed by a period ("."), and then the actual name of the resource in snake_case.
<module_short_name>.<resource_name>
For example:
rnr.admin