@podosoft/podokit 1.2.0 → 1.3.0
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/dist/add.d.ts +3 -1
- package/dist/add.js +116 -14
- package/dist/index.js +8 -0
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +3 -2
- package/dist/lockfile.d.ts +3 -1
- package/dist/lockfile.js +17 -6
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
- package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +59 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +81 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +13 -19
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +3 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +14 -15
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +13 -8
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +14 -3
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +53 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +16 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +2 -2
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +48 -1
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +15 -2
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +35 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +18 -1
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/modules/sse/module.manifest.json +3 -1
- package/dist/update.d.ts +17 -0
- package/dist/update.js +33 -8
- package/package.json +1 -1
package/dist/add.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TemplateVars } from "@podosoft/podokit-template-engine";
|
|
2
|
-
import { type ProviderCapability } from "@podosoft/podokit-runtime";
|
|
2
|
+
import { type ProviderCapability, type ProviderSelections } from "@podosoft/podokit-runtime";
|
|
3
3
|
interface Injection {
|
|
4
4
|
file: string;
|
|
5
5
|
marker: string;
|
|
@@ -127,6 +127,8 @@ export declare function resolveModuleDir(name: string, modulesDir: string, proje
|
|
|
127
127
|
/** Resolve a module and capture the package identity needed to replay external
|
|
128
128
|
* modules during `podo update`. */
|
|
129
129
|
export declare function resolveModule(name: string, modulesDir: string, projectRoot: string): ResolvedModule | null;
|
|
130
|
+
/** Name of the module that implements the selected provider for a capability. */
|
|
131
|
+
export declare function selectedProviderModule(modulesDir: string, projectRoot: string, providers: ProviderSelections, capability: ProviderCapability): string;
|
|
130
132
|
/** List modules available to a project: the bundled ones under `modulesDir` plus,
|
|
131
133
|
* when `projectRoot` is given, any `@podosoft/podokit-module-*` packages installed
|
|
132
134
|
* in it. Bundled names take precedence on a clash. */
|
package/dist/add.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.SUPPORTED_MANIFEST_VERSION = void 0;
|
|
|
4
4
|
exports.modulePackageOverlays = modulePackageOverlays;
|
|
5
5
|
exports.resolveModuleDir = resolveModuleDir;
|
|
6
6
|
exports.resolveModule = resolveModule;
|
|
7
|
+
exports.selectedProviderModule = selectedProviderModule;
|
|
7
8
|
exports.listModules = listModules;
|
|
8
9
|
exports.readModuleManifest = readModuleManifest;
|
|
9
10
|
exports.addModule = addModule;
|
|
@@ -106,11 +107,9 @@ function assertManifestVersion(name, manifest) {
|
|
|
106
107
|
throw new Error(`Module "${name}" declares an invalid maxApiReplicas constraint.`);
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new Error("Provider capabilities require a PodoKit project manifest.");
|
|
113
|
-
const selected = project.providers[capability];
|
|
110
|
+
/** Name of the module that implements the selected provider for a capability. */
|
|
111
|
+
function selectedProviderModule(modulesDir, projectRoot, providers, capability) {
|
|
112
|
+
const selected = providers[capability];
|
|
114
113
|
for (const available of listModules(modulesDir, projectRoot)) {
|
|
115
114
|
const resolved = resolveModule(available.name, modulesDir, projectRoot);
|
|
116
115
|
if (!resolved)
|
|
@@ -122,6 +121,12 @@ function providerModuleName(projectRoot, modulesDir, capability) {
|
|
|
122
121
|
throw new Error(`No module provides the selected ${capability} provider "${selected}". ` +
|
|
123
122
|
`Choose another provider with podo provider set ${capability} <provider> --apply.`);
|
|
124
123
|
}
|
|
124
|
+
function providerModuleName(projectRoot, modulesDir, capability) {
|
|
125
|
+
const project = (0, lockfile_1.readManifest)(projectRoot);
|
|
126
|
+
if (!project)
|
|
127
|
+
throw new Error("Provider capabilities require a PodoKit project manifest.");
|
|
128
|
+
return selectedProviderModule(modulesDir, projectRoot, project.providers, capability);
|
|
129
|
+
}
|
|
125
130
|
function requiredModules(projectRoot, modulesDir, manifest) {
|
|
126
131
|
const capabilities = (manifest.requiresCapabilities ?? []).map((capability) => providerModuleName(projectRoot, modulesDir, capability));
|
|
127
132
|
return [...new Set([...(manifest.requires ?? []), ...capabilities])];
|
|
@@ -217,14 +222,24 @@ function projectName(projectRoot) {
|
|
|
217
222
|
const pkg = readJson((0, node_path_1.join)(projectRoot, "package.json"));
|
|
218
223
|
return typeof pkg.name === "string" ? pkg.name : "app";
|
|
219
224
|
}
|
|
220
|
-
function appendEnv(projectRoot, lines) {
|
|
225
|
+
function appendEnv(projectRoot, lines, anchors = []) {
|
|
221
226
|
const file = (0, node_path_1.join)(projectRoot, ".env.example");
|
|
222
227
|
if (!(0, node_fs_1.existsSync)(file))
|
|
223
228
|
return;
|
|
224
229
|
const current = (0, node_fs_1.readFileSync)(file, "utf8");
|
|
225
|
-
const
|
|
230
|
+
const existing = current.split("\n");
|
|
231
|
+
const missing = lines.filter((line) => !existing.includes(line));
|
|
226
232
|
if (missing.length === 0)
|
|
227
233
|
return;
|
|
234
|
+
// Each module appends its own blank-line separated block. Keep that shape
|
|
235
|
+
// when the block belongs in front of a displaced module's block.
|
|
236
|
+
const wanted = new Set(anchors);
|
|
237
|
+
const index = existing.findIndex((line, position) => position > 0 && existing[position - 1] === "" && wanted.has(line));
|
|
238
|
+
if (index !== -1) {
|
|
239
|
+
existing.splice(index, 0, ...missing, "");
|
|
240
|
+
(0, node_fs_1.writeFileSync)(file, existing.join("\n"));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
228
243
|
const separator = current.endsWith("\n") ? "" : "\n";
|
|
229
244
|
(0, node_fs_1.writeFileSync)(file, `${current}${separator}\n${missing.join("\n")}\n`);
|
|
230
245
|
}
|
|
@@ -246,6 +261,91 @@ function isApplied(projectRoot, modulesDir, module) {
|
|
|
246
261
|
}
|
|
247
262
|
return false;
|
|
248
263
|
}
|
|
264
|
+
function injectionKey(file, marker) {
|
|
265
|
+
return `${file}\n${marker}`;
|
|
266
|
+
}
|
|
267
|
+
/** Modules `applyModule` would newly apply, in application order. */
|
|
268
|
+
function plannedModules(projectRoot, module, modulesDir, planned = new Set(), order = []) {
|
|
269
|
+
planned.add(module);
|
|
270
|
+
const resolved = resolveModule(module, modulesDir, projectRoot);
|
|
271
|
+
if (!resolved)
|
|
272
|
+
return order;
|
|
273
|
+
for (const required of requiredModules(projectRoot, modulesDir, readModuleManifest(resolved.dir))) {
|
|
274
|
+
if (planned.has(required) || isApplied(projectRoot, modulesDir, required))
|
|
275
|
+
continue;
|
|
276
|
+
plannedModules(projectRoot, required, modulesDir, planned, order);
|
|
277
|
+
}
|
|
278
|
+
order.push(module);
|
|
279
|
+
return order;
|
|
280
|
+
}
|
|
281
|
+
function modulePlacement(projectRoot, module, modulesDir) {
|
|
282
|
+
const project = (0, lockfile_1.readManifest)(projectRoot);
|
|
283
|
+
if (!project)
|
|
284
|
+
return undefined;
|
|
285
|
+
const installed = [...project.modules].sort((a, b) => a.order - b.order);
|
|
286
|
+
const installedNames = new Set(installed.map((entry) => entry.name));
|
|
287
|
+
const fresh = new Set(plannedModules(projectRoot, module, modulesDir).filter((name) => !installedNames.has(name)));
|
|
288
|
+
if (!fresh.size)
|
|
289
|
+
return undefined;
|
|
290
|
+
const provided = new Set();
|
|
291
|
+
for (const name of fresh) {
|
|
292
|
+
const resolved = resolveModule(name, modulesDir, projectRoot);
|
|
293
|
+
if (!resolved)
|
|
294
|
+
continue;
|
|
295
|
+
for (const capability of Object.keys(readModuleManifest(resolved.dir).provides ?? {})) {
|
|
296
|
+
provided.add(capability);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const vars = {
|
|
300
|
+
projectName: projectName(projectRoot),
|
|
301
|
+
...(0, lockfile_1.manifestTemplateVars)(project),
|
|
302
|
+
};
|
|
303
|
+
const manifests = installed.map((entry) => {
|
|
304
|
+
const resolved = resolveModule(entry.packageName ?? entry.name, modulesDir, projectRoot);
|
|
305
|
+
return resolved ? readModuleManifest(resolved.dir, vars) : undefined;
|
|
306
|
+
});
|
|
307
|
+
const first = manifests.findIndex((manifest) => manifest !== undefined && ((manifest.requires ?? []).some((name) => fresh.has(name))
|
|
308
|
+
|| (manifest.requiresCapabilities ?? []).some((capability) => provided.has(capability))));
|
|
309
|
+
if (first === -1)
|
|
310
|
+
return undefined;
|
|
311
|
+
const injections = new Map();
|
|
312
|
+
const env = [];
|
|
313
|
+
for (const manifest of manifests.slice(first)) {
|
|
314
|
+
if (!manifest)
|
|
315
|
+
continue;
|
|
316
|
+
for (const injection of manifest.inject ?? []) {
|
|
317
|
+
const key = injectionKey(injection.file, injection.marker);
|
|
318
|
+
injections.set(key, [...(injections.get(key) ?? []), injection.text]);
|
|
319
|
+
}
|
|
320
|
+
env.push(...(manifest.env ?? []));
|
|
321
|
+
}
|
|
322
|
+
return { before: installed[first].name, injections, env };
|
|
323
|
+
}
|
|
324
|
+
/** Insert wiring at its marker, or in front of the first displaced module's wiring. */
|
|
325
|
+
function injectAt(content, injection, anchors = []) {
|
|
326
|
+
if (!anchors.length || content.includes(injection.text)) {
|
|
327
|
+
return (0, podokit_template_engine_1.insertAtMarker)(content, injection.marker, injection.text);
|
|
328
|
+
}
|
|
329
|
+
const lines = content.split("\n");
|
|
330
|
+
const end = lines.findIndex((line) => line.includes(injection.marker));
|
|
331
|
+
if (end === -1)
|
|
332
|
+
throw new Error(`Marker not found: ${injection.marker}`);
|
|
333
|
+
const beginMarker = injection.marker.replace("podokit:end:", "podokit:begin:");
|
|
334
|
+
const begin = beginMarker === injection.marker
|
|
335
|
+
? -1
|
|
336
|
+
: lines.slice(0, end).findIndex((line) => line.includes(beginMarker));
|
|
337
|
+
const wanted = new Set(anchors.map((text) => text.split("\n")[0].trim()));
|
|
338
|
+
let index = end;
|
|
339
|
+
for (let line = begin + 1; line < end; line += 1) {
|
|
340
|
+
if (wanted.has(lines[line].trim())) {
|
|
341
|
+
index = line;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const indent = lines[end].match(/^\s*/)?.[0] ?? "";
|
|
346
|
+
lines.splice(index, 0, `${indent}${injection.text}`);
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
249
349
|
/**
|
|
250
350
|
* Apply a module to an existing generated project: overlay files, merge the
|
|
251
351
|
* target app's package.json dependencies and scripts, append env example lines,
|
|
@@ -262,7 +362,8 @@ function addModule(options) {
|
|
|
262
362
|
})
|
|
263
363
|
.map(([path]) => path)
|
|
264
364
|
: [];
|
|
265
|
-
const
|
|
365
|
+
const placement = modulePlacement(options.projectRoot, options.module, options.modulesDir);
|
|
366
|
+
const result = applyModule(options.projectRoot, options.module, options.modulesDir, new Set(), options.adopt ?? false, placement);
|
|
266
367
|
if (options.adopt && result.adopted.length) {
|
|
267
368
|
releaseOwnedPaths(options.projectRoot, result.adopted);
|
|
268
369
|
}
|
|
@@ -279,10 +380,10 @@ function addModule(options) {
|
|
|
279
380
|
modulePaths: [...new Set(result.touched)],
|
|
280
381
|
adoptedPaths: result.adopted,
|
|
281
382
|
}
|
|
282
|
-
: undefined, result.managedOverrides);
|
|
383
|
+
: undefined, result.managedOverrides, placement?.before);
|
|
283
384
|
return result;
|
|
284
385
|
}
|
|
285
|
-
function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
386
|
+
function applyModule(projectRoot, module, modulesDir, applied, adopt, placement) {
|
|
286
387
|
const resolved = resolveModule(module, modulesDir, projectRoot);
|
|
287
388
|
if (!resolved) {
|
|
288
389
|
const available = listModules(modulesDir, projectRoot).map((m) => m.name);
|
|
@@ -318,8 +419,8 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
318
419
|
for (const required of requiredModules(projectRoot, modulesDir, manifest)) {
|
|
319
420
|
if (applied.has(required) || isApplied(projectRoot, modulesDir, required))
|
|
320
421
|
continue;
|
|
321
|
-
const result = applyModule(projectRoot, required, modulesDir, applied, adopt);
|
|
322
|
-
added.push(
|
|
422
|
+
const result = applyModule(projectRoot, required, modulesDir, applied, adopt, placement);
|
|
423
|
+
added.push(...result.added, required);
|
|
323
424
|
ownedGlobs.push(...result.ownedGlobs);
|
|
324
425
|
managedOverrides.push(...result.managedOverrides);
|
|
325
426
|
preserved.push(...result.preserved);
|
|
@@ -356,7 +457,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
356
457
|
}
|
|
357
458
|
// 3) append env example lines
|
|
358
459
|
if (manifest.env?.length) {
|
|
359
|
-
appendEnv(projectRoot, manifest.env);
|
|
460
|
+
appendEnv(projectRoot, manifest.env, placement?.env);
|
|
360
461
|
touched.push(".env.example");
|
|
361
462
|
}
|
|
362
463
|
// 4) inject wiring at markers
|
|
@@ -370,7 +471,8 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
370
471
|
const content = (0, node_fs_1.readFileSync)(target, "utf8");
|
|
371
472
|
if (injection.optional && !content.includes(injection.marker))
|
|
372
473
|
continue;
|
|
373
|
-
|
|
474
|
+
const anchors = placement?.injections.get(injectionKey(injection.file, injection.marker));
|
|
475
|
+
(0, node_fs_1.writeFileSync)(target, injectAt(content, injection, anchors));
|
|
374
476
|
touched.push(injection.file);
|
|
375
477
|
}
|
|
376
478
|
const instructions = (manifest.instructions ?? []).map((line) => line.replace(/<app>/g, appName));
|
package/dist/index.js
CHANGED
|
@@ -652,6 +652,9 @@ async function main(argv) {
|
|
|
652
652
|
try {
|
|
653
653
|
if (args.apply) {
|
|
654
654
|
const result = (0, update_1.applyUpdate)(process.cwd(), templatesDir, { oldTemplatesDir: args.from });
|
|
655
|
+
if (result.addedModules.length) {
|
|
656
|
+
process.stdout.write(`Added required module(s): ${result.addedModules.join(", ")}\n`);
|
|
657
|
+
}
|
|
655
658
|
process.stdout.write(`Applied: ${result.written.length} written, ${result.moved.length} moved, ` +
|
|
656
659
|
`${result.removed.length} removed, ` +
|
|
657
660
|
`${result.merged.length} merged, ${result.conflicts.length} conflict.\n`);
|
|
@@ -666,6 +669,11 @@ async function main(argv) {
|
|
|
666
669
|
const counts = (0, update_1.summarize)(plan);
|
|
667
670
|
const shown = plan.changes.filter((c) => c.action !== "up-to-date" && c.action !== "skip");
|
|
668
671
|
process.stdout.write(`podo update ${plan.fromVersion} -> ${plan.toVersion} (template: ${plan.template}; modules: ${plan.modules.join(", ") || "none"})\n\n`);
|
|
672
|
+
if (plan.addedModules.length) {
|
|
673
|
+
process.stdout.write("Modules to add:\n" +
|
|
674
|
+
plan.addedModules.map((module) => ` ${module.name} (${(0, update_1.addedModuleReason)(module)})`).join("\n") +
|
|
675
|
+
"\n\n");
|
|
676
|
+
}
|
|
669
677
|
if (!shown.length) {
|
|
670
678
|
process.stdout.write("Everything is up to date.\n");
|
|
671
679
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export { listModules, addModule } from "./add";
|
|
|
8
8
|
export type { ModuleManifest, ModuleMigration, ModulePathMove, ModuleTextReplacement, AddOptions, AddResult, } from "./add";
|
|
9
9
|
export { status, diff, doctor, SUPPORTED_FRAMEWORKS } from "./inspect";
|
|
10
10
|
export type { StatusReport, DoctorFinding } from "./inspect";
|
|
11
|
-
export { planUpdate, summarize } from "./update";
|
|
12
|
-
export type { UpdatePlan, FileChange } from "./update";
|
|
11
|
+
export { addedModuleReason, planUpdate, summarize } from "./update";
|
|
12
|
+
export type { AddedModule, UpdatePlan, FileChange } from "./update";
|
|
13
13
|
export { readDevConfig, renderRoute, renderRuntimeCompose, resolveDevRuntime, runDevCommand } from "./dev";
|
|
14
14
|
export type { CommandRunner, DevConfig, DevRuntime } from "./dev";
|
|
15
15
|
export { applyDeployment, doctorDeployment, getDeploymentStatus, inspectClusterFingerprint, planDeployment, planRollback, rollbackDeployment, verifyDeployment, } from "./deploy";
|
package/dist/lib.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.verifyAnyDeployment = exports.planAnyDeployment = exports.getAnyDeploymentStatus = exports.doctorAnyDeployment = exports.describeDeploymentTarget = exports.verifyComposeDeployment = exports.rollbackComposeDeployment = exports.planComposeRollback = exports.planComposeDeployment = exports.inspectComposeEndpointFingerprint = void 0;
|
|
3
|
+
exports.doctorComposeDeployment = exports.applyComposeDeployment = exports.renderComposeDeployment = exports.parseComposeProfile = exports.initializeComposeProfile = exports.readDeploymentDriver = exports.loadKubernetesProfile = exports.loadComposeProfile = exports.loadAnyDeploymentProfile = exports.listAnyDeploymentProfiles = exports.assertAnyReleaseTag = exports.DEPLOY_DRIVERS = exports.renderDeployment = exports.loadDeploymentProfile = exports.listDeploymentProfiles = exports.initializeDeploymentProfile = exports.deploymentProfileDigest = exports.assertReleaseTag = exports.verifyDeployment = exports.rollbackDeployment = exports.planRollback = exports.planDeployment = exports.inspectClusterFingerprint = exports.getDeploymentStatus = exports.doctorDeployment = exports.applyDeployment = exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.addedModuleReason = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.PROVIDERS_SOURCE = exports.providerSource = exports.planProviderChange = exports.listProviders = exports.applyProviderChange = exports.create = void 0;
|
|
4
|
+
exports.verifyAnyDeployment = exports.planAnyDeployment = exports.getAnyDeploymentStatus = exports.doctorAnyDeployment = exports.describeDeploymentTarget = exports.verifyComposeDeployment = exports.rollbackComposeDeployment = exports.planComposeRollback = exports.planComposeDeployment = exports.inspectComposeEndpointFingerprint = exports.getComposeStatus = void 0;
|
|
5
5
|
exports.builtinTemplatesDir = builtinTemplatesDir;
|
|
6
6
|
exports.builtinModulesDir = builtinModulesDir;
|
|
7
7
|
// Library entry point: the reusable functions behind the `podo` CLI, for other
|
|
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "diff", { enumerable: true, get: function () { re
|
|
|
29
29
|
Object.defineProperty(exports, "doctor", { enumerable: true, get: function () { return inspect_1.doctor; } });
|
|
30
30
|
Object.defineProperty(exports, "SUPPORTED_FRAMEWORKS", { enumerable: true, get: function () { return inspect_1.SUPPORTED_FRAMEWORKS; } });
|
|
31
31
|
var update_1 = require("./update");
|
|
32
|
+
Object.defineProperty(exports, "addedModuleReason", { enumerable: true, get: function () { return update_1.addedModuleReason; } });
|
|
32
33
|
Object.defineProperty(exports, "planUpdate", { enumerable: true, get: function () { return update_1.planUpdate; } });
|
|
33
34
|
Object.defineProperty(exports, "summarize", { enumerable: true, get: function () { return update_1.summarize; } });
|
|
34
35
|
var dev_1 = require("./dev");
|
package/dist/lockfile.d.ts
CHANGED
|
@@ -116,4 +116,6 @@ export declare function mergeOwnedGlobs(existing: string[], add: string[]): stri
|
|
|
116
116
|
* `ownedGlobs` a module declares are merged into the manifest so its owned paths
|
|
117
117
|
* survive every future recompute. No-op on projects without a manifest.
|
|
118
118
|
*/
|
|
119
|
-
export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot, managedOverrides?: string[]
|
|
119
|
+
export declare function recordModules(projectRoot: string, moduleInputs: (string | ManifestModuleInput)[], version?: string, ownedGlobs?: string[], lockSnapshot?: RecordModulesLockSnapshot, managedOverrides?: string[],
|
|
120
|
+
/** Installed module that newly recorded modules must configure before. */
|
|
121
|
+
insertBefore?: string): void;
|
package/dist/lockfile.js
CHANGED
|
@@ -257,29 +257,40 @@ function mergeOwnedGlobs(existing, add) {
|
|
|
257
257
|
* `ownedGlobs` a module declares are merged into the manifest so its owned paths
|
|
258
258
|
* survive every future recompute. No-op on projects without a manifest.
|
|
259
259
|
*/
|
|
260
|
-
function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot, managedOverrides
|
|
260
|
+
function recordModules(projectRoot, moduleInputs, version, ownedGlobs, lockSnapshot, managedOverrides,
|
|
261
|
+
/** Installed module that newly recorded modules must configure before. */
|
|
262
|
+
insertBefore) {
|
|
261
263
|
const manifest = readManifest(projectRoot);
|
|
262
264
|
if (!manifest)
|
|
263
265
|
return;
|
|
264
266
|
const stampedWith = version ?? podokitVersion();
|
|
265
|
-
const
|
|
267
|
+
const fresh = [];
|
|
266
268
|
for (const input of moduleInputs) {
|
|
267
269
|
const module = typeof input === "string" ? { name: input } : input;
|
|
268
|
-
const existing = manifest.modules.find((entry) => entry.name === module.name)
|
|
270
|
+
const existing = manifest.modules.find((entry) => entry.name === module.name)
|
|
271
|
+
?? fresh.find((entry) => entry.name === module.name);
|
|
269
272
|
if (existing) {
|
|
270
273
|
existing.packageName = module.packageName ?? existing.packageName;
|
|
271
274
|
existing.moduleVersion = module.moduleVersion ?? existing.moduleVersion;
|
|
272
275
|
continue;
|
|
273
276
|
}
|
|
274
|
-
|
|
277
|
+
fresh.push({
|
|
275
278
|
name: module.name,
|
|
276
|
-
order: manifest.modules.length,
|
|
279
|
+
order: manifest.modules.length + fresh.length,
|
|
277
280
|
addedWith: stampedWith,
|
|
278
281
|
packageName: module.packageName,
|
|
279
282
|
moduleVersion: module.moduleVersion,
|
|
280
283
|
appliedMigrations: module.appliedMigrations,
|
|
281
284
|
});
|
|
282
|
-
|
|
285
|
+
}
|
|
286
|
+
const ordered = [...manifest.modules].sort((a, b) => a.order - b.order);
|
|
287
|
+
const position = insertBefore ? ordered.findIndex((entry) => entry.name === insertBefore) : -1;
|
|
288
|
+
if (position === -1) {
|
|
289
|
+
manifest.modules.push(...fresh);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
ordered.splice(position, 0, ...fresh);
|
|
293
|
+
manifest.modules = ordered.map((entry, order) => ({ ...entry, order }));
|
|
283
294
|
}
|
|
284
295
|
if (ownedGlobs?.length)
|
|
285
296
|
manifest.ownedGlobs = mergeOwnedGlobs(manifest.ownedGlobs, ownedGlobs);
|
|
@@ -5,6 +5,7 @@ const targets = [
|
|
|
5
5
|
{ entrypoint: "src/database/migrate.ts", outdir: "dist/database" },
|
|
6
6
|
{ entrypoint: "src/migrate.ts", outdir: "dist" },
|
|
7
7
|
{ entrypoint: "src/main-worker.ts", outdir: "dist" },
|
|
8
|
+
{ entrypoint: "src/bootstrap-admin-runtime.ts", outdir: "dist" },
|
|
8
9
|
].filter((target) => existsSync(target.entrypoint));
|
|
9
10
|
|
|
10
11
|
for (const target of targets) {
|
|
@@ -37,6 +37,7 @@ const MODULE_ROUTES: Record<string, string[]> = {
|
|
|
37
37
|
"GET /profile-images/{fileName}",
|
|
38
38
|
"GET /admin/sessions",
|
|
39
39
|
"DELETE /admin/sessions/{id}",
|
|
40
|
+
"GET /admin/users",
|
|
40
41
|
],
|
|
41
42
|
"api-key-auth": ["GET /machine/ping"],
|
|
42
43
|
"audit-log": ["GET /audit-logs", "GET /audit-logs/actions"],
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { PROVIDERS } from "../config/providers";
|
|
4
|
+
|
|
5
|
+
function databaseProvider(): DatabaseProviderName {
|
|
6
|
+
return PROVIDERS.database;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// SQLite applications migrate with the Bun SQLite migrator, not this TypeORM source.
|
|
10
|
+
describe.skipIf(databaseProvider() !== "postgres")("migration data source", () => {
|
|
11
|
+
it("runs each migration in its own transaction so one can build indexes concurrently", async () => {
|
|
12
|
+
const { dataSourceOptions } = await import("./data-source.js");
|
|
13
|
+
expect(dataSourceOptions.migrationsTransactionMode).toBe("each");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -15,6 +15,9 @@ const migrations = existsSync(compiledMigrations)
|
|
|
15
15
|
const commonOptions = {
|
|
16
16
|
entities: [],
|
|
17
17
|
migrations,
|
|
18
|
+
// One transaction per migration, so a migration can set `transaction = false`
|
|
19
|
+
// for statements such as CREATE INDEX CONCURRENTLY.
|
|
20
|
+
migrationsTransactionMode: "each" as const,
|
|
18
21
|
synchronize: false,
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -25,7 +25,7 @@ async function migrate(): Promise<void> {
|
|
|
25
25
|
await dataSource.initialize();
|
|
26
26
|
try {
|
|
27
27
|
if (process.argv.includes("--revert")) await dataSource.undoLastMigration({ transaction: "all" });
|
|
28
|
-
else await dataSource.runMigrations(
|
|
28
|
+
else await dataSource.runMigrations();
|
|
29
29
|
} finally {
|
|
30
30
|
await dataSource.destroy();
|
|
31
31
|
}
|
|
@@ -19,5 +19,12 @@ description: Use when adding or changing a SvelteKit page/route in apps/web —
|
|
|
19
19
|
skill, added with the admin-dashboard module).
|
|
20
20
|
5. **i18n**: no hardcoded user-facing strings — add keys to
|
|
21
21
|
`apps/web/src/lib/i18n/messages.ts` for **every** locale.
|
|
22
|
-
6. **
|
|
22
|
+
6. **Public pages** (with the admin-dashboard module): every page outside the
|
|
23
|
+
sign-in flow, `/`, and module pages requires a session and answers 503 while
|
|
24
|
+
the API is unavailable. Add the prefix of a page anonymous visitors should see
|
|
25
|
+
(marketing, pricing, legal, `robots.txt`) to `APP_PUBLIC_PATHS` in the
|
|
26
|
+
app-owned `apps/web/src/lib/server/public-paths.ts`. A prefix covers its
|
|
27
|
+
subpaths. Give such a page's loader a fallback for when the API is down
|
|
28
|
+
instead of throwing. Never edit `PUBLIC_PATHS` in the managed `guards.ts`.
|
|
29
|
+
7. **Verify**: `{{webRun}} build` (includes `svelte-check`)
|
|
23
30
|
and add/adjust a Playwright test in `tests/`.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import type {
|
|
4
|
+
import type { PlaywrightTestConfig } from "@playwright/test";
|
|
5
5
|
|
|
6
|
-
type Projects = NonNullable<
|
|
6
|
+
type Projects = NonNullable<PlaywrightTestConfig["projects"]>;
|
|
7
7
|
|
|
8
8
|
const requireFromTests = createRequire(__filename);
|
|
9
9
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
|
|
6
|
+
const compiledRuntime = new URL("../dist/bootstrap-admin-runtime.js", import.meta.url);
|
|
7
|
+
const sourceRuntime = new URL("../src/bootstrap-admin-runtime.ts", import.meta.url);
|
|
8
|
+
|
|
5
9
|
const args = process.argv.slice(2);
|
|
6
10
|
|
|
7
11
|
function has(flag) {
|
|
@@ -130,34 +134,15 @@ async function main() {
|
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
const input = resolveBootstrapInput();
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const { auth } =
|
|
137
|
-
const { pool } = await import("../dist/auth/db.js");
|
|
138
|
-
const { verifyPassword } = await import("better-auth/crypto");
|
|
137
|
+
// A production image runs the bundled entry; development runs the source.
|
|
138
|
+
const useCompiled = process.env.NODE_ENV === "production" && existsSync(compiledRuntime);
|
|
139
|
+
const runtime = await import((useCompiled ? compiledRuntime : sourceRuntime).href);
|
|
140
|
+
const { auth, findBootstrapUser, verifyPassword, closeAuthDatabase } = runtime;
|
|
139
141
|
|
|
140
142
|
try {
|
|
141
143
|
const result = await bootstrapAdmin(
|
|
142
144
|
{
|
|
143
|
-
findUser:
|
|
144
|
-
const query = await pool.query(
|
|
145
|
-
`SELECT u.id,
|
|
146
|
-
u.email,
|
|
147
|
-
u.role,
|
|
148
|
-
u."emailVerified" AS "emailVerified",
|
|
149
|
-
u."signupApproved" AS "signupApproved",
|
|
150
|
-
a.password AS "passwordHash"
|
|
151
|
-
FROM "user" AS u
|
|
152
|
-
LEFT JOIN "account" AS a
|
|
153
|
-
ON a."userId" = u.id
|
|
154
|
-
AND a."providerId" = 'credential'
|
|
155
|
-
WHERE lower(u.email) = lower($1)
|
|
156
|
-
LIMIT 1`,
|
|
157
|
-
[email],
|
|
158
|
-
);
|
|
159
|
-
return query.rows[0] ?? null;
|
|
160
|
-
},
|
|
145
|
+
findUser: findBootstrapUser,
|
|
161
146
|
createUser: async (user) => auth.api.createUser({ body: user }),
|
|
162
147
|
verifyPassword: async (hash, password) => verifyPassword({ hash, password }),
|
|
163
148
|
},
|
|
@@ -174,14 +159,24 @@ async function main() {
|
|
|
174
159
|
console.log(`Verified existing administrator account ${result.email}.`);
|
|
175
160
|
}
|
|
176
161
|
} finally {
|
|
177
|
-
await
|
|
162
|
+
await closeAuthDatabase();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A refused PostgreSQL connection is an AggregateError with an empty message.
|
|
167
|
+
export function errorMessage(error) {
|
|
168
|
+
if (!(error instanceof Error)) return String(error);
|
|
169
|
+
if (error.message) return error.message;
|
|
170
|
+
if (error instanceof AggregateError && error.errors.length > 0) {
|
|
171
|
+
return error.errors.map(errorMessage).join("; ");
|
|
178
172
|
}
|
|
173
|
+
return error.name;
|
|
179
174
|
}
|
|
180
175
|
|
|
181
176
|
const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
182
177
|
if (invokedPath === import.meta.url) {
|
|
183
178
|
main().catch((error) => {
|
|
184
|
-
console.error(
|
|
179
|
+
console.error(errorMessage(error));
|
|
185
180
|
process.exitCode = 1;
|
|
186
181
|
});
|
|
187
182
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import type { QueryRunner } from "typeorm";
|
|
4
|
+
import { revertLatestSqliteMigration, runSqliteMigrations } from "../database/sqlite-migrator";
|
|
5
|
+
import { IndexSessionCreatedAt1792060000000 } from "../migrations/1792060000000-IndexSessionCreatedAt";
|
|
6
|
+
|
|
7
|
+
function runner(type: "postgres" | "better-sqlite3", queries: string[], invalid = false): QueryRunner {
|
|
8
|
+
return {
|
|
9
|
+
connection: { options: { type } },
|
|
10
|
+
query: (sql: string): Promise<unknown[]> => {
|
|
11
|
+
queries.push(sql);
|
|
12
|
+
return Promise.resolve(invalid && sql.includes("indisvalid") ? [{ invalid: 1 }] : []);
|
|
13
|
+
},
|
|
14
|
+
} as unknown as QueryRunner;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Finalized at once: an open statement keeps SQLite from dropping an index.
|
|
18
|
+
function rows<Row>(database: Database, sql: string): Row[] {
|
|
19
|
+
const statement = database.prepare<Row, []>(sql);
|
|
20
|
+
try {
|
|
21
|
+
return statement.all();
|
|
22
|
+
} finally {
|
|
23
|
+
statement.finalize();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CREATE = 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "IDX_session_createdAt_id" ON "session" ("createdAt", "id")';
|
|
28
|
+
|
|
29
|
+
describe("admin session list index migration", () => {
|
|
30
|
+
it("builds the PostgreSQL index concurrently outside a transaction", async () => {
|
|
31
|
+
const migration = new IndexSessionCreatedAt1792060000000();
|
|
32
|
+
expect(migration.transaction).toBe(false);
|
|
33
|
+
const up: string[] = [];
|
|
34
|
+
await migration.up(runner("postgres", up));
|
|
35
|
+
expect(up).toEqual([
|
|
36
|
+
"SELECT 1 AS \"invalid\" FROM pg_index WHERE indexrelid = to_regclass($1) AND NOT indisvalid",
|
|
37
|
+
CREATE,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const down: string[] = [];
|
|
41
|
+
await migration.down(runner("postgres", down));
|
|
42
|
+
expect(down).toEqual(['DROP INDEX IF EXISTS "IDX_session_createdAt_id"']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rebuilds an invalid index left by an interrupted build", async () => {
|
|
46
|
+
const up: string[] = [];
|
|
47
|
+
await new IndexSessionCreatedAt1792060000000().up(runner("postgres", up, true));
|
|
48
|
+
expect(up.slice(1)).toEqual(['DROP INDEX CONCURRENTLY IF EXISTS "IDX_session_createdAt_id"', CREATE]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("serves the newest-first session page on SQLite and reverts", async () => {
|
|
52
|
+
const database = new Database(":memory:");
|
|
53
|
+
try {
|
|
54
|
+
database.run(`CREATE TABLE "session" (
|
|
55
|
+
"id" text PRIMARY KEY, "userId" text NOT NULL, "token" text NOT NULL,
|
|
56
|
+
"expiresAt" date NOT NULL, "createdAt" date NOT NULL, "updatedAt" date NOT NULL
|
|
57
|
+
)`);
|
|
58
|
+
const migrations = [{ timestamp: 1792060000000, migration: new IndexSessionCreatedAt1792060000000() }];
|
|
59
|
+
await runSqliteMigrations(database, migrations);
|
|
60
|
+
const plan = rows<{ detail: string }>(
|
|
61
|
+
database,
|
|
62
|
+
`EXPLAIN QUERY PLAN SELECT "id" FROM "session" WHERE "expiresAt" > '2026' ORDER BY "createdAt" DESC, "id" DESC LIMIT 16`,
|
|
63
|
+
).map((row) => row.detail).join("\n");
|
|
64
|
+
expect(plan).toContain("IDX_session_createdAt_id");
|
|
65
|
+
expect(plan).not.toContain("TEMP B-TREE");
|
|
66
|
+
|
|
67
|
+
expect(await revertLatestSqliteMigration(database, migrations)).toBe(true);
|
|
68
|
+
expect(rows(database, `SELECT "name" FROM "sqlite_master" WHERE "type" = 'index' AND "name" LIKE 'IDX_%'`))
|
|
69
|
+
.toEqual([]);
|
|
70
|
+
} finally {
|
|
71
|
+
database.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|