Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,16 @@ jobs:
echo "HIDE_CONFIG=true" >> .env
echo "MMGIS_DEPLOYMENT_MODE=${{ matrix.mode }}" >> .env

# Lean deploys no sidecars. sample.env ships them ON; left on, init-db would
# try to create the mmgis-stac catalog DB. Turn them off so the lean env
# matches a real lean deployment.
- name: Disable sidecar services for the lean leg
if: matrix.mode == 'lean'
run: |
echo "WITH_STAC=false" >> .env
echo "WITH_TIPG=false" >> .env
echo "WITH_TITILER=false" >> .env
echo "WITH_TITILER_PGSTAC=false" >> .env
echo "WITH_VELOSERVER=false" >> .env

# In lean, also confirms the mmgis-stac catalog DB is not created (full-only).
# Append the sidecar WITH_* flags implied by the deployment mode, derived
# from capabilities.js (the one definition the app reads) rather than
# hand-listed here. In lean this disables the sidecars so init-db/boot
# don't reach services the lean topology doesn't deploy; in full it appends
# nothing and sample.env stands.
- name: Generate mode-derived sidecar env
run: node scripts/mode-env.js >> .env

# In lean, this step also proves the spatial-catalog (mmgis-stac) DB is
# NOT created (it's gated on the localSidecars capability).
- name: Initialize Database
run: node scripts/init-db.js

Expand Down
21 changes: 14 additions & 7 deletions API/Backend/Config/setup.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const router = require("./routes/configs");
const triggerWebhooks = require("../Webhooks/processes/triggerwebhooks.js");
const configurePackageJson = require("../../../configure/package.json");
const { MODE, isLean } = require("../Utils/deploymentMode");
const { MODE } = require("../Utils/deploymentMode");
const { enabled } = require("../Utils/capabilities");

let setup = {
//Once the app initializes
Expand Down Expand Up @@ -36,12 +37,18 @@ let setup = {
? ""
: process.env.WEBSOCKET_ROOT_PATH || "",
IS_DOCKER: process.env.IS_DOCKER,
WITH_STAC: isLean() ? "false" : process.env.WITH_STAC,
WITH_TIPG: isLean() ? "false" : process.env.WITH_TIPG,
WITH_TITILER: isLean() ? "false" : process.env.WITH_TITILER,
WITH_TITILER_PGSTAC: isLean()
? "false"
: process.env.WITH_TITILER_PGSTAC,
WITH_STAC: enabled("localSidecars")
? process.env.WITH_STAC
: "false",
WITH_TIPG: enabled("localSidecars")
? process.env.WITH_TIPG
: "false",
WITH_TITILER: enabled("localSidecars")
? process.env.WITH_TITILER
: "false",
WITH_TITILER_PGSTAC: enabled("localSidecars")
? process.env.WITH_TITILER_PGSTAC
: "false",
DEPLOYMENT_MODE: MODE,
});
}
Expand Down
20 changes: 10 additions & 10 deletions API/Backend/Datasets/setup.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
const router = require("./routes/datasets");
const { isFull } = require("../Utils/deploymentMode");
let setup = {
// Gated off in lean. The discovery seam (API/setups.js) skips the
// feature-presence hooks when this capability is disabled.
capability: "datasets",
//Once the app initializes
onceInit: (s) => {
if (isFull()) {
s.app.use(
s.ROOT_PATH + "/api/datasets",
s.ensureAdmin(),
s.checkHeadersCodeInjection,
s.setContentType,
router
);
}
s.app.use(
s.ROOT_PATH + "/api/datasets",
s.ensureAdmin(),
s.checkHeadersCodeInjection,
s.setContentType,
router
);
},
//Once the server starts
onceStarted: (s) => {},
Expand Down
28 changes: 14 additions & 14 deletions API/Backend/Deployments/setup.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
const { isLean } = require("../Utils/deploymentMode");

// Requiring the model registers it with Sequelize so the global
// sequelize.sync() on boot creates the `deployments` table in BOTH modes —
// a later mode flip needs no migration. In full mode the table stays
// passive: the routes below are never mounted, so nothing writes to it.
// sequelize.sync() on boot creates the `deployments` table in both modes.
// The publish flow is lean-only, so in full the routes below are never mounted
// (capability gated off via the seam) and the table stays passive. Models
// aren't per-mode-gated (ADR D2: keep, env-gated); the unused table is harmless.
require("./models/deployment");

let setup = {
// The publish flow exists ONLY in lean. The discovery seam mounts the routes
// below only when this capability is enabled.
capability: "deployments",
//Once the app initializes
onceInit: (s) => {
if (isLean()) {
const routeDeployments = require("./routes/deployments");
s.app.use(
s.ROOT_PATH + "/api/deployments",
s.ensureAdmin(),
s.checkHeadersCodeInjection,
routeDeployments.router
);
}
const routeDeployments = require("./routes/deployments");
s.app.use(
s.ROOT_PATH + "/api/deployments",
s.ensureAdmin(),
s.checkHeadersCodeInjection,
routeDeployments.router
);
},
//Once the server starts
onceStarted: (s) => {},
Expand Down
56 changes: 29 additions & 27 deletions API/Backend/Draw/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,41 @@ const routerDraw = require("./routes/draw").router;
const routerAggregations = require("./routes/aggregations");
const ufiles = require("./models/userfiles");
const file_histories = require("./models/filehistories");
const { isFull } = require("../Utils/deploymentMode");

let setup = {
// Gated off in lean (route mounts only). Draw's models register at
// require-time and sync unconditionally, so its tables (user_files /
// user_features / file_histories) exist in lean too — unused there, by design
// (ADR D2: keep, env-gated).
capability: "draw",
//Once the app initializes
onceInit: (s) => {
if (isFull()) {
s.app.use(
s.ROOT_PATH + "/api/files",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
s.stopGuests,
routerFiles
);
s.app.use(
s.ROOT_PATH + "/api/files",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
s.stopGuests,
routerFiles
);

s.app.use(
s.ROOT_PATH + "/api/draw",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
s.stopGuests,
routerDraw
);
s.app.use(
s.ROOT_PATH + "/api/draw",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
s.stopGuests,
routerDraw
);

s.app.use(
s.ROOT_PATH + "/api/draw",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
s.stopGuests,
routerAggregations
);
}
s.app.use(
s.ROOT_PATH + "/api/draw",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
s.stopGuests,
routerAggregations
);
},
//Once the server starts
onceStarted: (s) => {},
Expand Down
23 changes: 12 additions & 11 deletions API/Backend/Geodatasets/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ const router = require("./routes/geodatasets");

const geodatasets = require("./models/geodatasets");

const { isFull } = require("../Utils/deploymentMode");

let setup = {
// Gated off in lean (route mounts only). The model registers at require-time
// and syncs unconditionally, so the geodatasets table exists in lean too —
// unused there, by design (ADR D2: keep, env-gated; don't per-mode-gate model
// registration).
capability: "geodatasets",
//Once the app initializes
onceInit: (s) => {
if (isFull()) {
s.app.use(
s.ROOT_PATH + "/api/geodatasets",
s.ensureAdmin(),
s.checkHeadersCodeInjection,
s.setContentType,
router
);
}
s.app.use(
s.ROOT_PATH + "/api/geodatasets",
s.ensureAdmin(),
s.checkHeadersCodeInjection,
s.setContentType,
router
);
},
//Once the server starts
onceStarted: (s) => {},
Expand Down
20 changes: 10 additions & 10 deletions API/Backend/Shortener/setup.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
const router = require("./routes/shortener");
const { isFull } = require("../Utils/deploymentMode");

let setup = {
// Gated off in lean. The discovery seam (API/setups.js) skips the
// feature-presence hooks when this capability is disabled.
capability: "shortener",
//Once the app initializes
onceInit: (s) => {
if (isFull()) {
s.app.use(
s.ROOT_PATH + "/api/shortener",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
router
);
}
s.app.use(
s.ROOT_PATH + "/api/shortener",
s.ensureUser(),
s.checkHeadersCodeInjection,
s.setContentType,
router
);
},
//Once the server starts
onceStarted: (s) => {},
Expand Down
10 changes: 5 additions & 5 deletions API/Backend/Upload/uploadRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const path = require('path');
const crypto = require('crypto');
const busboy = require('busboy');
const logger = require('../../logger');
const { isLean } = require('../Utils/deploymentMode');
const { enabled } = require('../Utils/capabilities');
const {
extensionForMime,
isValidMission,
Expand Down Expand Up @@ -122,10 +122,10 @@ function createUploadRouter(options = {}) {

const filename = `${crypto.randomUUID()}.${ext}`;

if (isLean()) {
// Lean mode: containers are ephemeral and published dashboards
// are static, so persist to the shared admin asset bucket
// instead of local disk. Buffer-then-put (the cap is 5 MB) so
if (enabled('s3AssetUploads')) {
// s3AssetUploads (lean): containers are ephemeral and published
// dashboards are static, so persist to the shared admin asset
// bucket instead of local disk. Buffer-then-put (the cap is 5 MB) so
// an oversize upload — busboy's 'limit' — aborts without ever
// starting a PutObject, guaranteeing no partial object.
const bucket = process.env.MMGIS_SHARED_ASSET_BUCKET;
Expand Down
81 changes: 81 additions & 0 deletions API/Backend/Utils/capabilities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* capabilities.js
* The single authoritative definition of what each deployment mode includes.
*
* `deploymentMode.js` stays the one env read (MMGIS_DEPLOYMENT_MODE -> MODE);
* this file is the one *meaning* of each mode. Reading the FEATURES map below
* answers "what does lean turn off (and on)" without grepping call sites.
*
* A capability's rule is a predicate over a small context ({ mode }), so a
* future composite condition (two axes) does not force a rewrite from a flat
* boolean table. Booleans and mode-lists are accepted as sugar for the common
* single-axis case; full predicates are used only where they earn their keep.
*
* Contract (backend): an unknown capability THROWS — a typo is a deploy error,
* not a silently-disabled feature. (The frontend twin in
* configure/src/core/capabilities.js deliberately warns + hides instead, so a
* render-time gate fails safe rather than white-screening the SPA.)
*
* Transitional artifact: enumerating every gated feature by name in the core is
* the kind of core-knows-every-feature coupling the plugin direction aims to
* dissolve. The durable half is that a module/tool *declares* the capability it
* needs and a seam decides; expect a future plugin manifest to subsume this map.
*/

const { MODE } = require("./deploymentMode");

// Sugar coercions:
// - string[] -> enabled only in the listed modes
// - function -> predicate over { mode }
// (A bare-boolean rule was supported but never used; drop it until a real
// always-on/off capability needs it, then re-add it with a test.)
const coerce = (rule) => {
if (typeof rule === "function") return rule;
if (Array.isArray(rule)) return ({ mode }) => rule.includes(mode);
throw new Error(`Invalid capability rule: ${JSON.stringify(rule)}`);
};

// enabling mode(s)
const FEATURES = {
// Geodata management — gated off in lean.
datasets: ["full"],
geodatasets: ["full"],
// Collaborative drawing — gated off in lean.
draw: ["full"],
// Link shortener — gated off in lean.
shortener: ["full"],
// Dashboard publish flow — exists ONLY in lean.
deployments: ["lean"],
// On-disk Missions/ filesystem (static mount) plus the server-side raster /
// SPICE utils endpoints (GDAL + Python shellouts) that read it. Not a
// sidecar — its own capability.
localMissions: ["full"],
// The bundled sidecar cluster: the adjacent-servers proxy mounts, the
// adjacent-servers spawner, the mmgis-stac database creation, and the
// derived WITH_* Configure flags. They always move together, so one row.
localSidecars: ["full"],
// Image uploads persist to the shared S3 asset bucket instead of local disk.
// Lean containers are ephemeral and published dashboards are static, so disk
// isn't durable there; full keeps writing under the on-disk Missions/ tree.
s3AssetUploads: ["lean"],
};

const RULES = Object.fromEntries(
Object.entries(FEATURES).map(([name, rule]) => [name, coerce(rule)])
);

/**
* enabled(capability) -> boolean
* Throws on an unknown capability so typos fail at boot.
*/
const enabled = (capability) => {
const rule = RULES[capability];
if (rule == null) {
throw new Error(
`Unknown capability: '${capability}'. Add it to API/Backend/Utils/capabilities.js or fix the typo.`
);
}
return rule({ mode: MODE });
};

module.exports = { enabled };
16 changes: 7 additions & 9 deletions API/Backend/Utils/deploymentMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
* - "full" (default): the complete MMGIS application as shipped today.
* - "lean": a gated-down deployment shape.
* Any other value is a configuration error and throws at startup.
*
* This module is the one env read and exposes only the resolved MODE string.
* It deliberately exports no isFull()/isLean() predicates: what each mode
* actually turns on or off is the job of capabilities.js, which reads MODE here
* and is the single place that interprets it. Ask `enabled("<capability>")`
* instead of comparing MODE.
*/

const VALID_MODES = ["full", "lean"];
Expand All @@ -18,12 +24,4 @@ if (!VALID_MODES.includes(mode)) {
);
}

function isLean() {
return mode === "lean";
}

function isFull() {
return mode === "full";
}

module.exports = { MODE: mode, isLean, isFull };
module.exports = { MODE: mode };
Loading