@abgov/nx-adsp 13.11.1 → 13.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -265,7 +265,7 @@ have a tenant yet? That picker also offers a **+ Create a new tenant** choice
265
265
  `tenant-service-admin`, and (unless `tenant-service-admin`) that doesn't already own a tenant (one
266
266
  per admin email). Picking it prompts for a name and waits for the new realm to finish provisioning
267
267
  before continuing the login as that tenant. Requires `@abgov/adsp-cli` ^1.4.0+ (this plugin pins
268
- ^1.5.2 or later).
268
+ ^1.6.0 or later).
269
269
 
270
270
  ## Agent consultation
271
271
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-adsp",
3
- "version": "13.11.1",
3
+ "version": "13.12.1",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -192,6 +192,13 @@ describe('Express Service Generator', () => {
192
192
  const database = host.read('apps/test/src/database.ts').toString();
193
193
  expect(database).toContain('drizzle-orm/node-postgres');
194
194
  expect(database).toContain('closeDatabase');
195
+ expect(database).toContain('isDatabaseReady');
196
+
197
+ // Readiness (DB-checked) is wired up separately from liveness — a DB
198
+ // outage should hold traffic, not restart a pod that can't fix it.
199
+ const mainTs = host.read('apps/test/src/main.ts').toString();
200
+ expect(mainTs).toContain('isDatabaseReady');
201
+ expect(mainTs).toContain("app.get('/health/ready'");
195
202
 
196
203
  // webpack emits a second bundle (migrate.js) for the deploy init container.
197
204
  const webpackConfig = host.read('apps/test/webpack.config.js').toString();
@@ -233,6 +240,11 @@ describe('Express Service Generator', () => {
233
240
 
234
241
  const database = host.read('apps/test/src/database.ts').toString();
235
242
  expect(database).toContain('mongoose');
243
+ expect(database).toContain('isDatabaseReady');
244
+
245
+ const mainTs = host.read('apps/test/src/main.ts').toString();
246
+ expect(mainTs).toContain('isDatabaseReady');
247
+ expect(mainTs).toContain("app.get('/health/ready'");
236
248
 
237
249
  const config = readProjectConfiguration(host, 'test');
238
250
  expect(config.targets['dev-db']).toBeTruthy();
@@ -249,6 +261,12 @@ describe('Express Service Generator', () => {
249
261
  expect(host.exists('apps/test/src/database.ts')).toBeFalsy();
250
262
  expect(host.exists('apps/test/scripts/dev-db.sh')).toBeFalsy();
251
263
 
264
+ // No database to check readiness against — don't emit a route that
265
+ // would always report ready regardless of anything real.
266
+ const mainTs = host.read('apps/test/src/main.ts').toString();
267
+ expect(mainTs).not.toContain('/health/ready');
268
+ expect(mainTs).not.toContain('isDatabaseReady');
269
+
252
270
  const config = readProjectConfiguration(host, 'test');
253
271
  expect(config.targets['dev-db']).toBeFalsy();
254
272
  expect(
@@ -68,6 +68,17 @@ need project-scoped MCP enabled. The server runs via `npx` and needs no credenti
68
68
  | `src/database.ts` | Mongoose connection helpers — `connectDatabase()` and `disconnectDatabase()` |
69
69
  <% } %>
70
70
 
71
+ ## Health and readiness checks
72
+
73
+ `/health` and (<% if (database !== 'none') { %>already, since this service has a database<% } else { %>if this service later gains a database or another required dependency<% } %>) `/health/ready` are two different questions, mapped to the two different OpenShift probes for a reason — conflating them into one endpoint means an outage in a downstream dependency gets treated as "restart this pod," which does not fix the outage and just adds a restart loop on top of it:
74
+
75
+ - **`/health` — liveness: is the process itself up.** Stays dependency-free on purpose. Don't add a check here for anything this service depends on but doesn't own.
76
+ <% if (database !== 'none') { %>
77
+ - **`/health/ready` — readiness: can this pod actually serve requests right now.** Already checks the database (`isDatabaseReady()` in `src/database.ts`) — a real round-trip query, not just a pool-state flag. **Extend this handler, not `/health`, for any other resource this service can't function without** — another required downstream API, a message broker, a required ADSP capability beyond what `healthCheck()` already covers — following the same reasoning: a dependency being down should hold traffic (503, readiness), not trigger a pod restart (liveness) that can't fix a problem outside this pod.
78
+ <% } else { %>
79
+ - If this service later gains a database or another hard dependency, add a `/health/ready` route that checks it (see `@abgov/nx-adsp:express-service --database postgres|mongo` for the shipped pattern) and point the deployment's `readinessProbe` at it instead of `/health` — don't check dependencies from the liveness route.
80
+ <% } %>
81
+
71
82
  ## SDK capabilities
72
83
 
73
84
  `initializeService()` returns `capabilities`:
@@ -7,9 +7,9 @@ import helmet from 'helmet';
7
7
  import passport from 'passport';
8
8
  import { Strategy as AnonymousStrategy } from 'passport-anonymous';
9
9
  <% if (database === 'postgres') { %>
10
- import { closeDatabase } from './database';
10
+ import { closeDatabase, isDatabaseReady } from './database';
11
11
  <% } else if (database === 'mongo') { %>
12
- import { connectDatabase, disconnectDatabase } from './database';
12
+ import { connectDatabase, disconnectDatabase, isDatabaseReady } from './database';
13
13
  <% } %>
14
14
  import { environment } from './environment';
15
15
  import { exampleEventDefinition } from './events';
@@ -54,10 +54,34 @@ async function initializeApp() {
54
54
  passport.use('tenant', tenantStrategy);
55
55
  app.use(passport.initialize());
56
56
 
57
+ // Liveness — is the process itself up. Deliberately stays dependency-free
58
+ // (platform reachability, not this app's own resources) so an outage in a
59
+ // downstream dependency doesn't get treated as "restart this pod," which
60
+ // wouldn't fix the outage and would just cause a needless restart loop.
57
61
  app.get('/health', async (_req, res) => {
58
62
  const platform = await healthCheck();
59
63
  res.json({ ...platform });
60
64
  });
65
+ <% if (database !== 'none') { %>
66
+ // Readiness — can this pod actually serve requests right now. Checks the
67
+ // database because this service can't do anything useful without it; 503
68
+ // tells the platform to hold traffic rather than route it somewhere that
69
+ // will 500. Extend this handler (not /health above) for any other resource
70
+ // this service can't function without — another required downstream API,
71
+ // a message broker, etc. — following the same "hold traffic, don't
72
+ // restart" reasoning; see AGENTS.md's "Health and readiness checks".
73
+ app.get('/health/ready', async (_req, res) => {
74
+ try {
75
+ await isDatabaseReady();
76
+ res.json({ ready: true });
77
+ } catch (err) {
78
+ res.status(503).json({
79
+ ready: false,
80
+ error: err instanceof Error ? err.message : String(err),
81
+ });
82
+ }
83
+ });
84
+ <% } %>
61
85
 
62
86
  // Generated once from the routers' registry.registerPath() calls (see
63
87
  // routes/example.ts) — every router imported above has already registered
@@ -85,6 +109,9 @@ async function initializeApp() {
85
109
  _links: {
86
110
  self: { href: new URL(req.originalUrl, rootUrl).href },
87
111
  health: { href: new URL('/health', rootUrl).href },
112
+ <% if (database !== 'none') { %>
113
+ ready: { href: new URL('/health/ready', rootUrl).href },
114
+ <% } %>
88
115
  api: { href: new URL('/<%= projectName %>/v1', rootUrl).href },
89
116
  docs: { href: new URL('/swagger/docs/v1', rootUrl).href },
90
117
  },
@@ -9,3 +9,13 @@ export async function connectDatabase(): Promise<void> {
9
9
  export async function disconnectDatabase(): Promise<void> {
10
10
  await mongoose.disconnect();
11
11
  }
12
+
13
+ // A real ping, not just `readyState === 1` — the connection can report
14
+ // "connected" while actually stale. Used by /health/ready, not by `main.ts`
15
+ // directly.
16
+ export async function isDatabaseReady(): Promise<void> {
17
+ if (!mongoose.connection.db) {
18
+ throw new Error('Database connection not established.');
19
+ }
20
+ await mongoose.connection.db.admin().ping();
21
+ }
@@ -1,3 +1,4 @@
1
+ import { sql } from 'drizzle-orm';
1
2
  import { drizzle } from 'drizzle-orm/node-postgres';
2
3
  import { Pool } from 'pg';
3
4
 
@@ -12,3 +13,10 @@ const pool = new Pool({ connectionString: environment.DATABASE_URL });
12
13
  export const db = drizzle(pool, { schema });
13
14
 
14
15
  export const closeDatabase = (): Promise<void> => pool.end();
16
+
17
+ // A trivial round-trip query, not just a pool-state check — proves the
18
+ // database is actually reachable and answering, the same reason `pg_isready`
19
+ // exists. Used by /health/ready, not by `main.ts` directly.
20
+ export const isDatabaseReady = async (): Promise<void> => {
21
+ await db.execute(sql`SELECT 1`);
22
+ };