@boopkit/cli 0.3.1 → 0.3.2
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.
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@boopkit/cap-api": "0.3.
|
|
3
|
-
"@boopkit/cap-background-jobs": "0.3.
|
|
4
|
-
"@boopkit/cap-cloud-storage": "0.3.
|
|
5
|
-
"@boopkit/cap-coding": "0.3.
|
|
6
|
-
"@boopkit/cap-database": "0.3.
|
|
7
|
-
"@boopkit/cap-email": "0.3.
|
|
8
|
-
"@boopkit/cap-hello": "0.3.
|
|
9
|
-
"@boopkit/cap-noticeboard": "0.3.
|
|
10
|
-
"@boopkit/cap-product-design": "0.3.
|
|
11
|
-
"@boopkit/cap-project-mgmt": "0.3.
|
|
12
|
-
"@boopkit/cap-services": "0.3.
|
|
13
|
-
"@boopkit/cap-skills": "0.3.
|
|
14
|
-
"@boopkit/cap-testing": "0.3.
|
|
15
|
-
"@boopkit/cap-users": "0.3.
|
|
16
|
-
"@boopkit/cap-web": "0.3.
|
|
17
|
-
"@boopkit/logger": "0.3.
|
|
2
|
+
"@boopkit/cap-api": "0.3.1",
|
|
3
|
+
"@boopkit/cap-background-jobs": "0.3.1",
|
|
4
|
+
"@boopkit/cap-cloud-storage": "0.3.1",
|
|
5
|
+
"@boopkit/cap-coding": "0.3.1",
|
|
6
|
+
"@boopkit/cap-database": "0.3.1",
|
|
7
|
+
"@boopkit/cap-email": "0.3.1",
|
|
8
|
+
"@boopkit/cap-hello": "0.3.1",
|
|
9
|
+
"@boopkit/cap-noticeboard": "0.3.1",
|
|
10
|
+
"@boopkit/cap-product-design": "0.3.1",
|
|
11
|
+
"@boopkit/cap-project-mgmt": "0.3.1",
|
|
12
|
+
"@boopkit/cap-services": "0.3.1",
|
|
13
|
+
"@boopkit/cap-skills": "0.3.1",
|
|
14
|
+
"@boopkit/cap-testing": "0.3.1",
|
|
15
|
+
"@boopkit/cap-users": "0.3.1",
|
|
16
|
+
"@boopkit/cap-web": "0.3.1",
|
|
17
|
+
"@boopkit/logger": "0.3.1"
|
|
18
18
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -181,7 +181,9 @@ async function enrichWithSetupChecks(data, envValues, projectDir, projectName) {
|
|
|
181
181
|
label: (0, interpolate_1.interpolate)(c.label, vars),
|
|
182
182
|
hint: c.hint ? (0, interpolate_1.interpolate)(c.hint, vars) : undefined,
|
|
183
183
|
}));
|
|
184
|
-
|
|
184
|
+
// Extract this capability's .prisma files for scoped prisma-migrate checks
|
|
185
|
+
const prismaFiles = capDef?.files?.filter(f => f.endsWith('.prisma'));
|
|
186
|
+
const steps = await (0, db_status_1.runSetupChecks)(interpolatedChecks, envValues, projectDir, projectName, prismaFiles);
|
|
185
187
|
const completedSteps = steps.filter(s => s.done).map(s => s.label);
|
|
186
188
|
const nextSteps = steps.filter(s => !s.done).map(s => s.hint ? `${s.label}: ${s.hint}` : s.label);
|
|
187
189
|
if (nextSteps.length === 0) {
|
package/dist/lib/db-status.d.ts
CHANGED
|
@@ -26,6 +26,16 @@ export declare function checkMigrationsTable(dbUrl: string): Promise<boolean | n
|
|
|
26
26
|
* Returns true if prisma/migrations/ contains at least one migration subdirectory.
|
|
27
27
|
*/
|
|
28
28
|
export declare function hasPrismaMigrations(projectDir: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Parse a .prisma file and extract table names from model declarations.
|
|
31
|
+
* Returns the @@map() value if present, otherwise the model name.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parsePrismaTableNames(filePath: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Check if specific tables exist in the database.
|
|
36
|
+
* Returns true if ALL tables exist, false if any are missing, null if psql unavailable.
|
|
37
|
+
*/
|
|
38
|
+
export declare function checkTablesExist(dbUrl: string, tableNames: string[]): Promise<boolean | null>;
|
|
29
39
|
/**
|
|
30
40
|
* Check if any .prisma files in the project define models.
|
|
31
41
|
* Returns true if at least one file contains a `model` declaration.
|
|
@@ -52,6 +62,8 @@ export interface CheckContext {
|
|
|
52
62
|
projectName: string;
|
|
53
63
|
/** Accumulated results from earlier checks — keyed by check type. */
|
|
54
64
|
results: Map<string, boolean>;
|
|
65
|
+
/** Capability's .prisma file paths relative to projectDir — enables scoped prisma-migrate checks. */
|
|
66
|
+
prismaFiles?: string[];
|
|
55
67
|
}
|
|
56
68
|
/**
|
|
57
69
|
* Run all setup checks declared in a capability's setupChecks list.
|
|
@@ -61,4 +73,4 @@ export declare function runSetupChecks(checks: {
|
|
|
61
73
|
type: string;
|
|
62
74
|
label: string;
|
|
63
75
|
hint?: string;
|
|
64
|
-
}[], envValues: Map<string, string>, projectDir: string, projectName: string): Promise<SetupStep[]>;
|
|
76
|
+
}[], envValues: Map<string, string>, projectDir: string, projectName: string, prismaFiles?: string[]): Promise<SetupStep[]>;
|
package/dist/lib/db-status.js
CHANGED
|
@@ -38,6 +38,8 @@ exports.checkTcpReachable = checkTcpReachable;
|
|
|
38
38
|
exports.checkDatabaseConnectable = checkDatabaseConnectable;
|
|
39
39
|
exports.checkMigrationsTable = checkMigrationsTable;
|
|
40
40
|
exports.hasPrismaMigrations = hasPrismaMigrations;
|
|
41
|
+
exports.parsePrismaTableNames = parsePrismaTableNames;
|
|
42
|
+
exports.checkTablesExist = checkTablesExist;
|
|
41
43
|
exports.hasPrismaModels = hasPrismaModels;
|
|
42
44
|
exports.isPrismaClientGenerated = isPrismaClientGenerated;
|
|
43
45
|
exports.checkPrismaMigrateStatus = checkPrismaMigrateStatus;
|
|
@@ -140,6 +142,77 @@ function hasPrismaMigrations(projectDir) {
|
|
|
140
142
|
const entries = fs.readdirSync(migrationsDir, { withFileTypes: true });
|
|
141
143
|
return entries.some(e => e.isDirectory());
|
|
142
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse a .prisma file and extract table names from model declarations.
|
|
147
|
+
* Returns the @@map() value if present, otherwise the model name.
|
|
148
|
+
*/
|
|
149
|
+
function parsePrismaTableNames(filePath) {
|
|
150
|
+
if (!fs.existsSync(filePath))
|
|
151
|
+
return [];
|
|
152
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
153
|
+
const tables = [];
|
|
154
|
+
const lines = content.split('\n');
|
|
155
|
+
let currentModel = null;
|
|
156
|
+
let braceDepth = 0;
|
|
157
|
+
let mapName = null;
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
const modelMatch = line.match(/^\s*model\s+(\w+)\s*\{/);
|
|
160
|
+
if (modelMatch && !currentModel) {
|
|
161
|
+
currentModel = modelMatch[1];
|
|
162
|
+
braceDepth = 1;
|
|
163
|
+
mapName = null;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (currentModel) {
|
|
167
|
+
for (const ch of line) {
|
|
168
|
+
if (ch === '{')
|
|
169
|
+
braceDepth++;
|
|
170
|
+
if (ch === '}')
|
|
171
|
+
braceDepth--;
|
|
172
|
+
}
|
|
173
|
+
const mapMatch = line.match(/@@map\s*\(\s*"([^"]+)"\s*\)/);
|
|
174
|
+
if (mapMatch) {
|
|
175
|
+
mapName = mapMatch[1];
|
|
176
|
+
}
|
|
177
|
+
if (braceDepth === 0) {
|
|
178
|
+
tables.push(mapName || currentModel);
|
|
179
|
+
currentModel = null;
|
|
180
|
+
mapName = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return tables;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if specific tables exist in the database.
|
|
188
|
+
* Returns true if ALL tables exist, false if any are missing, null if psql unavailable.
|
|
189
|
+
*/
|
|
190
|
+
function checkTablesExist(dbUrl, tableNames) {
|
|
191
|
+
if (tableNames.length === 0)
|
|
192
|
+
return Promise.resolve(true);
|
|
193
|
+
// Validate table names — only allow alphanumeric and underscores
|
|
194
|
+
if (!tableNames.every(t => /^[a-zA-Z_]\w*$/.test(t))) {
|
|
195
|
+
return Promise.resolve(false);
|
|
196
|
+
}
|
|
197
|
+
const quotedNames = tableNames.map(t => `'${t}'`).join(',');
|
|
198
|
+
const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN (${quotedNames})`;
|
|
199
|
+
return new Promise((resolve) => {
|
|
200
|
+
(0, child_process_1.execFile)('psql', [dbUrl, '-tAc', query], { timeout: 3000 }, (error, stdout) => {
|
|
201
|
+
if (error) {
|
|
202
|
+
if ('code' in error && error.code === 'ENOENT') {
|
|
203
|
+
resolve(null);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
resolve(false);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const count = parseInt(stdout.trim(), 10);
|
|
211
|
+
resolve(count === tableNames.length);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
143
216
|
/**
|
|
144
217
|
* Check if any .prisma files in the project define models.
|
|
145
218
|
* Returns true if at least one file contains a `model` declaration.
|
|
@@ -216,7 +289,28 @@ async function runCheck(type, ctx) {
|
|
|
216
289
|
// Skip only if a database check was explicitly run and failed
|
|
217
290
|
if (ctx.results.has('database') && !ctx.results.get('database'))
|
|
218
291
|
return false;
|
|
219
|
-
//
|
|
292
|
+
// Scoped check: when prismaFiles are provided, check this capability's tables instead of global status
|
|
293
|
+
if (ctx.prismaFiles?.length) {
|
|
294
|
+
const tableNames = [];
|
|
295
|
+
for (const file of ctx.prismaFiles) {
|
|
296
|
+
const filePath = path.join(ctx.projectDir, file);
|
|
297
|
+
tableNames.push(...parsePrismaTableNames(filePath));
|
|
298
|
+
}
|
|
299
|
+
if (tableNames.length > 0) {
|
|
300
|
+
// Capability defines models — check if its tables exist in the DB
|
|
301
|
+
const dbUrl = ctx.envValues.get('DATABASE_URL');
|
|
302
|
+
if (!dbUrl)
|
|
303
|
+
return false;
|
|
304
|
+
return (await checkTablesExist(dbUrl, tableNames)) === true;
|
|
305
|
+
}
|
|
306
|
+
// Capability has prisma files but no models (e.g., base schema) —
|
|
307
|
+
// check if _prisma_migrations table exists (initial migration was run)
|
|
308
|
+
const dbUrl = ctx.envValues.get('DATABASE_URL');
|
|
309
|
+
if (!dbUrl)
|
|
310
|
+
return false;
|
|
311
|
+
return (await checkMigrationsTable(dbUrl)) === true;
|
|
312
|
+
}
|
|
313
|
+
// Fallback: no capability context — use global check
|
|
220
314
|
if (hasPrismaModels(ctx.projectDir) && !hasPrismaMigrations(ctx.projectDir))
|
|
221
315
|
return false;
|
|
222
316
|
const result = await checkPrismaMigrateStatus(ctx.projectDir);
|
|
@@ -233,8 +327,8 @@ async function runCheck(type, ctx) {
|
|
|
233
327
|
* Run all setup checks declared in a capability's setupChecks list.
|
|
234
328
|
* Returns an ordered list of steps with live done/pending status.
|
|
235
329
|
*/
|
|
236
|
-
async function runSetupChecks(checks, envValues, projectDir, projectName) {
|
|
237
|
-
const ctx = { envValues, projectDir, projectName, results: new Map() };
|
|
330
|
+
async function runSetupChecks(checks, envValues, projectDir, projectName, prismaFiles) {
|
|
331
|
+
const ctx = { envValues, projectDir, projectName, results: new Map(), prismaFiles };
|
|
238
332
|
const steps = [];
|
|
239
333
|
for (const check of checks) {
|
|
240
334
|
const done = await runCheck(check.type, ctx);
|
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ await MessageQueueService.publish('batch-ready', [message1, message2]);
|
|
|
24
24
|
- **Reuse**: The same connection and channel are reused for all subsequent publishes
|
|
25
25
|
- **Auto-reconnect**: If the connection drops (`error` or `close` event), the cached reference is cleared and the next `publish()` call opens a fresh connection
|
|
26
26
|
- **Error recovery**: If `publish()` catches an error (e.g., channel closed), it resets connection state and returns `false` — the next call will reconnect
|
|
27
|
-
- **Shutdown**: The publish connection is tracked
|
|
27
|
+
- **Shutdown**: The publish connection is tracked and closed by `close()` alongside the listener connection
|
|
28
28
|
|
|
29
29
|
### Why persistent (not per-call)
|
|
30
30
|
|
|
@@ -36,6 +36,21 @@ The original implementation opened a new AMQP connection for every `publish()` c
|
|
|
36
36
|
|--------|-------|-----------|
|
|
37
37
|
| Scheduler jobs | various | 1-2 per 10-min cycle |
|
|
38
38
|
|
|
39
|
+
## Listening
|
|
40
|
+
|
|
41
|
+
`MessageQueueService.listen()` uses a **shared AMQP connection** across all queue consumers. Each `listen()` call creates a separate **channel** on the shared connection. This keeps connection count at 2 (1 publish + 1 listen) regardless of how many queues are consumed.
|
|
42
|
+
|
|
43
|
+
### Connection lifecycle
|
|
44
|
+
|
|
45
|
+
- **Lazy init**: No connection is opened until the first `listen()` call
|
|
46
|
+
- **Shared**: All listeners share the same connection; each gets its own channel with independent prefetch and consumer
|
|
47
|
+
- **Auto-reconnect**: If the connection drops (`error` or `close` event), the cached reference is cleared and the next `listen()` call opens a fresh connection
|
|
48
|
+
- **Shutdown**: The listener connection is closed by `close()` alongside the publish connection
|
|
49
|
+
|
|
50
|
+
### Why shared (not per-queue)
|
|
51
|
+
|
|
52
|
+
CloudAMQP's shared plan has a 20-connection limit. The previous implementation opened a new AMQP connection per `listen()` call. AMQP supports multiple channels on a single connection — each channel can have its own prefetch and consumer — so sharing one connection scales to any number of queue consumers without hitting connection limits.
|
|
53
|
+
|
|
39
54
|
## Consumer Modes
|
|
40
55
|
|
|
41
56
|
The `MessageQueueService.listen()` function supports two modes controlled by `opts.manualAck`:
|