@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 +1 -1
- package/package.json +1 -1
- package/src/generators/express-service/express-service.spec.ts +18 -0
- package/src/generators/express-service/files/AGENTS.md__tmpl__ +11 -0
- package/src/generators/express-service/files/src/main.ts__tmpl__ +29 -2
- package/src/generators/express-service/files-mongo/src/database.ts__tmpl__ +10 -0
- package/src/generators/express-service/files-postgres/src/database.ts__tmpl__ +8 -0
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.
|
|
268
|
+
^1.6.0 or later).
|
|
269
269
|
|
|
270
270
|
## Agent consultation
|
|
271
271
|
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|