Skip to main content

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:

Monorepo Folder Structure
.
├─.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:

  1. Triggering automations on git hooks via Husky
  2. 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 use changesets (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.

danger

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.

Folder Structure
...
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 the Dashboard 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.

Example 'modlet.config.ts' for the Dashboard
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 the Web Base.
  • postInstallation An asynchronous function that returns a Promise<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 the GIS, it modifies the vite.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:

web-base-cli/src/config.ts
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