@podosoft/podokit 1.1.6 → 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/deploy-render.js +5 -0
- 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/package.json +2 -2
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +81 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +53 -5
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +30 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +19 -3
- 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/database.spec.ts +149 -4
- package/dist/templates/fullstack/apps/api/src/database/database.ts +66 -13
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +86 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +4 -9
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.spec.ts +141 -1
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.ts +86 -12
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +188 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +30 -0
- package/dist/templates/fullstack/apps/web/src/routes/api/[...path]/+server.ts +3 -3
- package/dist/templates/fullstack/apps/web/src/routes/api/auth/[...all]/+server.ts +3 -3
- package/dist/templates/fullstack/compose.dev.yaml +21 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
- package/dist/templates/fullstack/dot-env.docker +3 -0
- package/dist/templates/fullstack/dot-env.example +14 -0
- package/dist/templates/fullstack/infra/k3s/api-deployment.yaml +2 -0
- 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-sessions/admin-sessions.revoker.spec.ts +25 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.spec.ts +122 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +187 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +160 -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 +22 -28
- 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/api/src/profile-image/profile-image.service.spec.ts +3 -5
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +6 -17
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.spec.ts +167 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.spec.ts +158 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +78 -28
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +19 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +6 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.test.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +102 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +141 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.test.ts +68 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.ts +108 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +74 -49
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.test.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +23 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.test.ts +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.test.ts +98 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.ts +54 -0
- 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/protected-layout.ts +7 -50
- 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/audit/+page.svelte +88 -45
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +76 -75
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.server.ts +15 -14
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +79 -37
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +10 -17
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +4 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-sessions.api.spec.ts +124 -0
- 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/audit.api.spec.ts +75 -12
- 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 +35 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +72 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +36 -2
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -13
- 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 +16 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +45 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +64 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +32 -1
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +46 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +19 -7
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +126 -3
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.spec.ts +115 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.ts +44 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +421 -43
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +382 -35
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +57 -0
- package/dist/templates/modules/audit-log/module.manifest.json +7 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +20 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +24 -29
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +28 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.spec.ts +56 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +81 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -9
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.spec.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +24 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +31 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.spec.ts +58 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +46 -57
- 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 +21 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.spec.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.ts +10 -0
- 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/apps/api/src/settings/flag-defaults.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.spec.ts +81 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +37 -34
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/auth/module.manifest.json +2 -2
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +34 -13
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.spec.ts +60 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.ts +51 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +70 -9
- package/dist/templates/modules/file-upload/module.manifest.json +9 -3
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +26 -32
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +16 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +31 -4
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +145 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.ts +37 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +94 -4
- package/dist/templates/modules/job-progress/module.manifest.json +3 -3
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.spec.ts +54 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +47 -25
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.spec.ts +86 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +159 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +172 -4
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +110 -33
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +88 -0
- package/dist/templates/modules/object-storage-local/module.manifest.json +6 -1
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +80 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +13 -7
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +156 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +39 -21
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +101 -2
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +84 -9
- package/dist/templates/modules/object-storage-s3/module.manifest.json +8 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +9 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +7 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +139 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +80 -11
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +12 -6
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +15 -2
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +46 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +4 -3
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +125 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +27 -13
- package/dist/templates/modules/sse/files/apps/api/src/events/event-envelope.ts +123 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +191 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-topics.ts +64 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +250 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +121 -15
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +407 -18
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +153 -40
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.test.ts +216 -0
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.ts +207 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +227 -70
- package/dist/templates/modules/sse/module.manifest.json +20 -5
- package/dist/update.d.ts +17 -0
- package/dist/update.js +33 -8
- package/package.json +2 -2
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/deploy-render.js
CHANGED
|
@@ -385,6 +385,8 @@ metadata:
|
|
|
385
385
|
${entries.length ? `data:\n${entries.map(([key, value]) => ` ${key}: ${quote(value)}`).join("\n")}` : "data: {}"}
|
|
386
386
|
`;
|
|
387
387
|
}
|
|
388
|
+
// Above the API's 800 ms per-check readiness bound; the Kubernetes default is 1 s.
|
|
389
|
+
const API_PROBE_TIMEOUT_SECONDS = 2;
|
|
388
390
|
function deployment(profile, kind, workload, image, rolloutStateDigest, runtimeConfigDigest) {
|
|
389
391
|
const name = `${profile.target.release}-${kind}`;
|
|
390
392
|
const workloadSecret = kind === "api" ? profile.secrets.api : profile.secrets.web;
|
|
@@ -396,14 +398,17 @@ function deployment(profile, kind, workload, image, rolloutStateDigest, runtimeC
|
|
|
396
398
|
? ` readinessProbe:
|
|
397
399
|
httpGet: { path: /health/ready, port: 3000 }
|
|
398
400
|
periodSeconds: 5
|
|
401
|
+
timeoutSeconds: ${API_PROBE_TIMEOUT_SECONDS}
|
|
399
402
|
livenessProbe:
|
|
400
403
|
httpGet: { path: /health, port: 3000 }
|
|
401
404
|
initialDelaySeconds: 20
|
|
402
405
|
periodSeconds: 10
|
|
406
|
+
timeoutSeconds: ${API_PROBE_TIMEOUT_SECONDS}
|
|
403
407
|
startupProbe:
|
|
404
408
|
httpGet: { path: /health, port: 3000 }
|
|
405
409
|
failureThreshold: 30
|
|
406
410
|
periodSeconds: 2
|
|
411
|
+
timeoutSeconds: ${API_PROBE_TIMEOUT_SECONDS}
|
|
407
412
|
`
|
|
408
413
|
: ` readinessProbe:
|
|
409
414
|
httpGet: { path: /, port: 3000 }
|
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);
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"migration:generate": "bun run typeorm -- migration:generate"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@podosoft/podokit-contracts": "^0.
|
|
20
|
-
"@podosoft/podokit-runtime": "^0.
|
|
19
|
+
"@podosoft/podokit-contracts": "^0.5.0",
|
|
20
|
+
"@podosoft/podokit-runtime": "^0.3.0",
|
|
21
21
|
"@elysia/cors": "^1.4.2",
|
|
22
22
|
"@elysia/openapi": "^1.4.15",
|
|
23
23
|
"dotenv": "^16.4.5",
|
|
@@ -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) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it, mock, type Mock } from "bun:test";
|
|
2
|
+
import { cachedOpenApiDocument, type OpenApiDocumentLoader } from "./app";
|
|
3
|
+
import type { OpenApiContribution } from "./core/services";
|
|
4
|
+
|
|
5
|
+
function baseSpec(): Response {
|
|
6
|
+
return Response.json({ openapi: "3.1.0", paths: { "/health": { get: {} } } });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function source(options: {
|
|
10
|
+
contributions: () => OpenApiContribution[];
|
|
11
|
+
routeCount?: () => number;
|
|
12
|
+
base?: () => Response;
|
|
13
|
+
}): { base: Mock<() => Promise<Response>>; load: OpenApiDocumentLoader } {
|
|
14
|
+
const base = mock(async () => (options.base ?? baseSpec)());
|
|
15
|
+
const load = cachedOpenApiDocument({
|
|
16
|
+
routeCount: options.routeCount ?? (() => 1),
|
|
17
|
+
base,
|
|
18
|
+
contributions: async () => options.contributions(),
|
|
19
|
+
});
|
|
20
|
+
return { base, load };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("cachedOpenApiDocument", () => {
|
|
24
|
+
it("reuses the merged document while the inputs are unchanged", async () => {
|
|
25
|
+
const auth = { document: { paths: { "/sign-in": { post: {} } } }, pathPrefix: "/api/auth" };
|
|
26
|
+
const { base, load } = source({ contributions: () => [{ ...auth }] });
|
|
27
|
+
|
|
28
|
+
const first = await load();
|
|
29
|
+
const second = await load();
|
|
30
|
+
|
|
31
|
+
expect(second).toBe(first);
|
|
32
|
+
expect(first).toMatchObject({
|
|
33
|
+
paths: { "/health": { get: {} }, "/api/auth/sign-in": { post: {} } },
|
|
34
|
+
});
|
|
35
|
+
expect(base).toHaveBeenCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rebuilds when a contributor returns a new document", async () => {
|
|
39
|
+
let auth: OpenApiContribution = {
|
|
40
|
+
document: { paths: { "/sign-in": { post: {} } } },
|
|
41
|
+
pathPrefix: "/api/auth",
|
|
42
|
+
};
|
|
43
|
+
const { base, load } = source({ contributions: () => [auth] });
|
|
44
|
+
|
|
45
|
+
await load();
|
|
46
|
+
auth = { document: { paths: { "/passkey": { post: {} } } }, pathPrefix: "/api/auth" };
|
|
47
|
+
const rebuilt = await load();
|
|
48
|
+
|
|
49
|
+
expect(rebuilt).toMatchObject({ paths: { "/api/auth/passkey": { post: {} } } });
|
|
50
|
+
expect(rebuilt).not.toHaveProperty(["paths", "/api/auth/sign-in"]);
|
|
51
|
+
expect(base).toHaveBeenCalledTimes(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rebuilds when routes are added", async () => {
|
|
55
|
+
let routes = 1;
|
|
56
|
+
const { base, load } = source({ contributions: () => [], routeCount: () => routes });
|
|
57
|
+
|
|
58
|
+
await load();
|
|
59
|
+
routes = 2;
|
|
60
|
+
await load();
|
|
61
|
+
|
|
62
|
+
expect(base).toHaveBeenCalledTimes(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns a failed base response without caching it", async () => {
|
|
66
|
+
let fail = true;
|
|
67
|
+
const { base, load } = source({
|
|
68
|
+
contributions: () => [],
|
|
69
|
+
base: () => fail ? Response.json({ success: false }, { status: 503 }) : baseSpec(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const failed = await load();
|
|
73
|
+
fail = false;
|
|
74
|
+
const recovered = await load();
|
|
75
|
+
|
|
76
|
+
if (!(failed instanceof Response)) throw new Error("Expected the failed base response");
|
|
77
|
+
expect(failed.status).toBe(503);
|
|
78
|
+
expect(recovered).toMatchObject({ openapi: "3.1.0" });
|
|
79
|
+
expect(base).toHaveBeenCalledTimes(2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
appContext,
|
|
9
9
|
createCoreServices,
|
|
10
10
|
type AppPlugin,
|
|
11
|
+
type OpenApiContribution,
|
|
11
12
|
type PodokitModule,
|
|
12
13
|
type ServiceRegistry,
|
|
13
14
|
} from "./core/services";
|
|
15
|
+
import { isDatabaseUnavailable } from "./database/database";
|
|
14
16
|
import { healthPlugin } from "./health/health.plugin";
|
|
15
17
|
// podokit:begin:imports
|
|
16
18
|
// podokit:end:imports
|
|
@@ -52,6 +54,47 @@ function mergeOpenApi(
|
|
|
52
54
|
return { ...base, paths, components, tags };
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
export interface OpenApiDocumentSource {
|
|
58
|
+
routeCount: () => number;
|
|
59
|
+
base: () => Promise<Response>;
|
|
60
|
+
contributions: () => Promise<OpenApiContribution[]>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type OpenApiDocumentLoader = () => Promise<Record<string, unknown> | Response>;
|
|
64
|
+
|
|
65
|
+
function sameContributions(left: OpenApiContribution[], right: OpenApiContribution[]): boolean {
|
|
66
|
+
return left.length === right.length && left.every((contribution, index) =>
|
|
67
|
+
contribution.document === right[index]?.document &&
|
|
68
|
+
contribution.pathPrefix === right[index]?.pathPrefix
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Contributors return the same document object while their inputs are unchanged,
|
|
73
|
+
* so identity plus the route count (the base spec's own cache key) identifies the
|
|
74
|
+
* merged result without a staleness window. */
|
|
75
|
+
export function cachedOpenApiDocument(source: OpenApiDocumentSource): OpenApiDocumentLoader {
|
|
76
|
+
let cached: {
|
|
77
|
+
routeCount: number;
|
|
78
|
+
contributions: OpenApiContribution[];
|
|
79
|
+
document: Record<string, unknown>;
|
|
80
|
+
} | undefined;
|
|
81
|
+
return async () => {
|
|
82
|
+
const contributions = await source.contributions();
|
|
83
|
+
const routeCount = source.routeCount();
|
|
84
|
+
if (
|
|
85
|
+
cached?.routeCount === routeCount &&
|
|
86
|
+
sameContributions(cached.contributions, contributions)
|
|
87
|
+
) {
|
|
88
|
+
return cached.document;
|
|
89
|
+
}
|
|
90
|
+
const response = await source.base();
|
|
91
|
+
if (!response.ok) return response;
|
|
92
|
+
const document = mergeOpenApi(record(await response.json()), contributions);
|
|
93
|
+
cached = { routeCount, contributions, document };
|
|
94
|
+
return document;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
55
98
|
function errorEnvelope(
|
|
56
99
|
code: string,
|
|
57
100
|
message: string,
|
|
@@ -124,6 +167,10 @@ const buildApp = (options: CreateAppOptions = {}) => {
|
|
|
124
167
|
set.status = error.statusCode;
|
|
125
168
|
return errorEnvelope(error.code, error.message, error.statusCode, path);
|
|
126
169
|
}
|
|
170
|
+
if (isDatabaseUnavailable(error)) {
|
|
171
|
+
set.status = 503;
|
|
172
|
+
return errorEnvelope("DATABASE_UNAVAILABLE", "The database is temporarily unavailable", 503, path);
|
|
173
|
+
}
|
|
127
174
|
const statusCode = code === "NOT_FOUND" ? 404 : code === "VALIDATION" ? 400 : 500;
|
|
128
175
|
set.status = statusCode;
|
|
129
176
|
const publicCode = code === "NOT_FOUND"
|
|
@@ -144,11 +191,12 @@ const buildApp = (options: CreateAppOptions = {}) => {
|
|
|
144
191
|
...modules.flatMap((module) => module.plugin ? [module.plugin] : []),
|
|
145
192
|
];
|
|
146
193
|
for (const plugin of plugins) app.use(plugin(context));
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
194
|
+
const openApiDocument = cachedOpenApiDocument({
|
|
195
|
+
routeCount: () => app.routes.length,
|
|
196
|
+
base: () => app.handle(new Request("http://localhost/api-docs-elysia-json")),
|
|
197
|
+
contributions: () => context.openapi.contributions(),
|
|
198
|
+
});
|
|
199
|
+
app.get("/api-docs-json", () => openApiDocument(), {
|
|
152
200
|
detail: { hide: true },
|
|
153
201
|
});
|
|
154
202
|
app.onStop(() => { void services.close(); });
|
|
@@ -11,6 +11,12 @@ const schema = z.object({
|
|
|
11
11
|
POSTGRES_USER: z.string().default("podokit"),
|
|
12
12
|
POSTGRES_PASSWORD: z.string().default("podokit"),
|
|
13
13
|
POSTGRES_DB: z.string().default("podokit"),
|
|
14
|
+
DB_POOL_MAX: z.coerce.number().int().positive().default(20),
|
|
15
|
+
DB_CONNECT_TIMEOUT_MS: z.coerce.number().int().positive().default(10000),
|
|
16
|
+
DB_IDLE_TIMEOUT_MS: z.coerce.number().int().nonnegative().default(30000),
|
|
17
|
+
DB_AUTH_POOL_MAX: z.coerce.number().int().positive().default(10),
|
|
18
|
+
READINESS_CHECK_TIMEOUT_MS: z.coerce.number().int().positive().default(800),
|
|
19
|
+
READINESS_CACHE_MS: z.coerce.number().int().nonnegative().default(2000),
|
|
14
20
|
});
|
|
15
21
|
|
|
16
22
|
export type AppEnv = z.infer<typeof schema>;
|
|
@@ -35,9 +35,12 @@ const MODULE_ROUTES: Record<string, string[]> = {
|
|
|
35
35
|
"POST /account/profile-image",
|
|
36
36
|
"DELETE /account/profile-image",
|
|
37
37
|
"GET /profile-images/{fileName}",
|
|
38
|
+
"GET /admin/sessions",
|
|
39
|
+
"DELETE /admin/sessions/{id}",
|
|
40
|
+
"GET /admin/users",
|
|
38
41
|
],
|
|
39
42
|
"api-key-auth": ["GET /machine/ping"],
|
|
40
|
-
"audit-log": ["GET /audit-logs"],
|
|
43
|
+
"audit-log": ["GET /audit-logs", "GET /audit-logs/actions"],
|
|
41
44
|
analytics: [
|
|
42
45
|
"GET /analytics/config",
|
|
43
46
|
"GET /admin/analytics/config",
|
|
@@ -70,6 +73,12 @@ const MODULE_ROUTES: Record<string, string[]> = {
|
|
|
70
73
|
bullmq: ["POST /jobs", "GET /jobs/{id}"],
|
|
71
74
|
"file-upload": ["POST /files", "GET /files/{key}/url"],
|
|
72
75
|
"job-progress": ["POST /progress"],
|
|
76
|
+
"object-storage-local": [
|
|
77
|
+
"GET /files/content/*",
|
|
78
|
+
"PUT /storage/{key}",
|
|
79
|
+
"GET /storage/{key}",
|
|
80
|
+
"GET /storage/{key}/presigned",
|
|
81
|
+
],
|
|
73
82
|
"object-storage-s3": [
|
|
74
83
|
"PUT /storage/{key}",
|
|
75
84
|
"GET /storage/{key}",
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { validateEnv } from "../config/env.validation";
|
|
4
|
+
import { PROVIDERS } from "../config/providers";
|
|
2
5
|
import {
|
|
3
6
|
AccessPolicy,
|
|
7
|
+
createCoreServices,
|
|
4
8
|
OpenApiRegistry,
|
|
9
|
+
REQUEST_IDENTITY,
|
|
5
10
|
RequestGuardRegistry,
|
|
6
11
|
ServiceRegistry,
|
|
7
12
|
} from "./services";
|
|
8
13
|
|
|
14
|
+
function databaseProvider(): DatabaseProviderName {
|
|
15
|
+
return PROVIDERS.database;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
describe("ServiceRegistry", () => {
|
|
10
19
|
test("awaits startup in registration order and starts only once", async () => {
|
|
11
20
|
const registry = new ServiceRegistry();
|
|
@@ -88,3 +97,24 @@ describe("OpenApiRegistry", () => {
|
|
|
88
97
|
expect(() => registry.register("auth", () => ({ document: {} }))).toThrow("already exists");
|
|
89
98
|
});
|
|
90
99
|
});
|
|
100
|
+
|
|
101
|
+
describe("RequestIdentity", () => {
|
|
102
|
+
test("treats every request as anonymous until an auth module overrides it", async () => {
|
|
103
|
+
const services = createCoreServices(validateEnv({
|
|
104
|
+
NODE_ENV: "test",
|
|
105
|
+
DATABASE_URL: databaseProvider() === "sqlite"
|
|
106
|
+
? "sqlite://:memory:"
|
|
107
|
+
: "postgres://podokit:podokit@localhost:5432/podokit",
|
|
108
|
+
}));
|
|
109
|
+
const identity = services.resolve(REQUEST_IDENTITY);
|
|
110
|
+
const request = new Request("http://localhost/todos");
|
|
111
|
+
try {
|
|
112
|
+
expect(await identity.userId(request)).toBeUndefined();
|
|
113
|
+
expect(await identity.isAdmin(request)).toBe(false);
|
|
114
|
+
expect(await identity.roles(request)).toEqual([]);
|
|
115
|
+
expect(await identity.activeOrganizationId(request)).toBeUndefined();
|
|
116
|
+
} finally {
|
|
117
|
+
await services.close();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|