create-nexwire 4.3.0 → 4.4.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/README.md +10 -5
- package/express/src/framework/facade.ts +1 -0
- package/express/src/framework/http/logger.ts +13 -4
- package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/express/src/framework/notification/channels/mail.ts +8 -6
- package/express/src/middlewares/auth-middleware.ts +1 -2
- package/express/src/middlewares/role-middleware.ts +3 -13
- package/express/src/modules/auth/routes/index.ts +1 -2
- package/express/src/providers/notification.ts +2 -2
- package/express/src/resources/src/components/Switch.vue +60 -65
- package/express/src/resources/src/pages/auth/forgetPassword.vue +23 -30
- package/express/src/resources/src/pages/auth/login.vue +25 -28
- package/express/src/resources/src/pages/auth/register.vue +35 -39
- package/express/src/resources/src/pages/auth/resetPassword.vue +29 -37
- package/express/src/resources/src/pages/auth/verifyEmail.vue +15 -19
- package/express/src/resources/src/plugins/gum.ts +519 -203
- package/express/src/resources/src/stores/auth.ts +9 -6
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/fastify/.dockerignore +14 -0
- package/fastify/.env +44 -0
- package/fastify/.env.example +42 -0
- package/fastify/.vscode/extensions.json +6 -0
- package/fastify/.vscode/settings.json +37 -0
- package/fastify/LICENSE +21 -0
- package/fastify/README.md +232 -0
- package/fastify/biome.json +99 -0
- package/fastify/drizzle.config.ts +25 -0
- package/fastify/gitignore-stub +31 -0
- package/fastify/package.json +130 -0
- package/fastify/public/.gitkeep +0 -0
- package/fastify/src/config/app.ts +41 -0
- package/fastify/src/config/auth.ts +12 -0
- package/fastify/src/config/cache.ts +16 -0
- package/fastify/src/config/cookie.ts +15 -0
- package/fastify/src/config/cors.ts +12 -0
- package/fastify/src/config/database.ts +48 -0
- package/fastify/src/config/index.ts +83 -0
- package/fastify/src/config/jwt.ts +19 -0
- package/fastify/src/config/logging.ts +13 -0
- package/fastify/src/config/mail.ts +24 -0
- package/fastify/src/config/openapi.ts +36 -0
- package/fastify/src/config/queue.ts +30 -0
- package/fastify/src/config/rateLimit.ts +17 -0
- package/fastify/src/config/realtime.ts +15 -0
- package/fastify/src/config/redis.ts +18 -0
- package/fastify/src/config/security.ts +44 -0
- package/fastify/src/config/session.ts +17 -0
- package/fastify/src/config/storage.ts +22 -0
- package/fastify/src/config/validate.ts +50 -0
- package/fastify/src/database/migrations/sqlite/0000_init.sql +48 -0
- package/fastify/src/database/migrations/sqlite/meta/0000_snapshot.json +324 -0
- package/fastify/src/database/migrations/sqlite/meta/_journal.json +13 -0
- package/fastify/src/database/schema.ts +2 -0
- package/fastify/src/env.ts +76 -0
- package/fastify/src/framework/cache/cache.ts +114 -0
- package/fastify/src/framework/circuit-breaker/cb.ts +88 -0
- package/fastify/src/framework/database/app-schema.ts +37 -0
- package/fastify/src/framework/database/connection.ts +223 -0
- package/fastify/src/framework/database/migrate-hooks.ts +87 -0
- package/fastify/src/framework/database/paginate.ts +226 -0
- package/fastify/src/framework/database/schema.ts +20 -0
- package/fastify/src/framework/database/seed.ts +152 -0
- package/fastify/src/framework/events/dispatcher.ts +61 -0
- package/fastify/src/framework/facade.ts +96 -0
- package/fastify/src/framework/http/app.ts +176 -0
- package/fastify/src/framework/http/cors.ts +13 -0
- package/fastify/src/framework/http/fastify.d.ts +13 -0
- package/fastify/src/framework/http/logger.ts +110 -0
- package/fastify/src/framework/http/metrics.ts +22 -0
- package/fastify/src/framework/http/openapi.ts +48 -0
- package/fastify/src/framework/http/ratelimiter.ts +47 -0
- package/fastify/src/framework/http/security.ts +25 -0
- package/fastify/src/framework/http/static.ts +83 -0
- package/fastify/src/framework/http/validation.ts +49 -0
- package/fastify/src/framework/kernel.ts +43 -0
- package/fastify/src/framework/maker-cli/db/core.mjs +291 -0
- package/fastify/src/framework/maker-cli/db/index.mjs +204 -0
- package/fastify/src/framework/maker-cli/deploy/core.mjs +1046 -0
- package/fastify/src/framework/maker-cli/deploy/index.mjs +91 -0
- package/fastify/src/framework/maker-cli/index.mjs +65 -0
- package/fastify/src/framework/maker-cli/middleware/core.mjs +41 -0
- package/fastify/src/framework/maker-cli/middleware/index.mjs +17 -0
- package/fastify/src/framework/maker-cli/module/core.mjs +1066 -0
- package/fastify/src/framework/maker-cli/module/index.mjs +211 -0
- package/fastify/src/framework/maker-cli/provider/core.mjs +49 -0
- package/fastify/src/framework/maker-cli/provider/index.mjs +12 -0
- package/fastify/src/framework/maker-cli/runtime/build-ui.mjs +16 -0
- package/fastify/src/framework/maker-cli/runtime/core.mjs +560 -0
- package/fastify/src/framework/maker-cli/runtime/index.mjs +120 -0
- package/fastify/src/framework/maker-cli/runtime/redis-commander-patch.cjs +20 -0
- package/fastify/src/framework/maker-cli/stubs/controller/openapi.ts.stub +74 -0
- package/fastify/src/framework/maker-cli/stubs/controller/plain.ts.stub +77 -0
- package/fastify/src/framework/maker-cli/stubs/controller/standalone.ts.stub +51 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +52 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +55 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +51 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +55 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/README.stub +59 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +68 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +68 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +67 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +41 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/README.stub +78 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +120 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +133 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +131 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +11 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +16 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +13 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +7 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +54 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +70 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +24 -0
- package/fastify/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/fastify/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/job/name.ts.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/middleware/name.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/notification/controller.ts.stub +79 -0
- package/fastify/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -0
- package/fastify/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/fastify/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/fastify/src/framework/maker-cli/stubs/notification/route.api.ts.stub +71 -0
- package/fastify/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +26 -0
- package/fastify/src/framework/maker-cli/stubs/notification/schema.ts.stub +26 -0
- package/fastify/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
- package/fastify/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
- package/fastify/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
- package/fastify/src/framework/maker-cli/stubs/route/api.ts.stub +101 -0
- package/fastify/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -0
- package/fastify/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +120 -0
- package/fastify/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +50 -0
- package/fastify/src/framework/maker-cli/stubs/schedule/name.ts.stub +36 -0
- package/fastify/src/framework/maker-cli/stubs/schema/name.plain.ts.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/schema/name.ts.stub +33 -0
- package/fastify/src/framework/maker-cli/stubs/seeder/name.ts.stub +18 -0
- package/fastify/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/fastify/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/fastify/src/framework/maker-cli/stubs/test/unit.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/utils/env-config.mjs +44 -0
- package/fastify/src/framework/maker-cli/utils/env-db.mjs +61 -0
- package/fastify/src/framework/maker-cli/utils/file-ops.mjs +30 -0
- package/fastify/src/framework/maker-cli/utils/flags.mjs +14 -0
- package/fastify/src/framework/maker-cli/utils/help.mjs +379 -0
- package/fastify/src/framework/maker-cli/utils/naming.mjs +119 -0
- package/fastify/src/framework/maker-cli/utils/process.mjs +52 -0
- package/fastify/src/framework/maker-cli/utils/project-commands.mjs +201 -0
- package/fastify/src/framework/modules/discover.ts +57 -0
- package/fastify/src/framework/modules/routes.ts +31 -0
- package/fastify/src/framework/notification/channels/broadcast.ts +15 -0
- package/fastify/src/framework/notification/channels/mail.ts +42 -0
- package/fastify/src/framework/notification/contracts.ts +29 -0
- package/fastify/src/framework/notification/index.ts +59 -0
- package/fastify/src/framework/notification/registry.ts +15 -0
- package/fastify/src/framework/providers/adapter.ts +149 -0
- package/fastify/src/framework/providers/contracts.ts +126 -0
- package/fastify/src/framework/providers/loader.ts +105 -0
- package/fastify/src/framework/providers/registry.ts +88 -0
- package/fastify/src/framework/queue/clear.ts +22 -0
- package/fastify/src/framework/queue/queue.ts +401 -0
- package/fastify/src/framework/queue/ui.ts +194 -0
- package/fastify/src/framework/queue/worker.ts +61 -0
- package/fastify/src/framework/realtime/broadcast.ts +70 -0
- package/fastify/src/framework/realtime/index.ts +2 -0
- package/fastify/src/framework/realtime/socket-cookie.ts +72 -0
- package/fastify/src/framework/realtime/socket.ts +149 -0
- package/fastify/src/framework/realtime/types.ts +6 -0
- package/fastify/src/framework/realtime/ui.ts +16 -0
- package/fastify/src/framework/redis/client.ts +148 -0
- package/fastify/src/framework/runtime/runtime.ts +23 -0
- package/fastify/src/framework/runtime/server-adapter.ts +33 -0
- package/fastify/src/framework/scheduler/lock.ts +128 -0
- package/fastify/src/framework/scheduler/run.ts +48 -0
- package/fastify/src/framework/scheduler/scheduler.ts +155 -0
- package/fastify/src/framework/server.ts +162 -0
- package/fastify/src/framework/session/session.ts +160 -0
- package/fastify/src/framework/storage/storage.ts +744 -0
- package/fastify/src/framework/support/cookie.ts +71 -0
- package/fastify/src/framework/support/jwt.ts +33 -0
- package/fastify/src/framework/support/lifecycle.ts +35 -0
- package/fastify/src/framework/support/logger.ts +148 -0
- package/fastify/src/framework/support/mail.ts +56 -0
- package/fastify/src/framework/support/password.ts +23 -0
- package/fastify/src/framework/support/status-codes.ts +75 -0
- package/fastify/src/framework/support/upload.ts +181 -0
- package/fastify/src/framework/support/url.ts +41 -0
- package/fastify/src/middlewares/auth-middleware.ts +134 -0
- package/fastify/src/middlewares/role-middleware.ts +19 -0
- package/fastify/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
- package/fastify/src/modules/auth/__tests__/login.test.ts +171 -0
- package/fastify/src/modules/auth/__tests__/logout.test.ts +60 -0
- package/fastify/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
- package/fastify/src/modules/auth/__tests__/me.test.ts +91 -0
- package/fastify/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/fastify/src/modules/auth/__tests__/register.test.ts +291 -0
- package/fastify/src/modules/auth/__tests__/resetpass.test.ts +206 -0
- package/fastify/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
- package/fastify/src/modules/auth/controllers/auth.ts +252 -0
- package/fastify/src/modules/auth/controllers/role.ts +35 -0
- package/fastify/src/modules/auth/database/models/role.ts +14 -0
- package/fastify/src/modules/auth/database/models/user.ts +46 -0
- package/fastify/src/modules/auth/database/seeders/role.ts +18 -0
- package/fastify/src/modules/auth/database/seeders/user.ts +35 -0
- package/fastify/src/modules/auth/facade.ts +12 -0
- package/fastify/src/modules/auth/helpers/auth.ts +115 -0
- package/fastify/src/modules/auth/jobs/forgetpass.ts +20 -0
- package/fastify/src/modules/auth/jobs/registeruser.ts +23 -0
- package/fastify/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/fastify/src/modules/auth/routes/index.ts +185 -0
- package/fastify/src/modules/auth/routes/role.ts +56 -0
- package/fastify/src/modules/auth/schemas/auth.ts +116 -0
- package/fastify/src/modules/auth/services/forgetpassword.ts +66 -0
- package/fastify/src/modules/auth/services/login.ts +63 -0
- package/fastify/src/modules/auth/services/logout.ts +25 -0
- package/fastify/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/fastify/src/modules/auth/services/me.ts +31 -0
- package/fastify/src/modules/auth/services/refreshtoken.ts +110 -0
- package/fastify/src/modules/auth/services/register.ts +141 -0
- package/fastify/src/modules/auth/services/resetpassword.ts +69 -0
- package/fastify/src/modules/auth/services/verifyemail.ts +77 -0
- package/fastify/src/modules/auth/types/auth.ts +31 -0
- package/fastify/src/providers/README.md +107 -0
- package/fastify/src/providers/notification.ts +4 -0
- package/fastify/src/resources/index.html +17 -0
- package/fastify/src/resources/src/App.vue +5 -0
- package/fastify/src/resources/src/assets/css/styles.css +14761 -0
- package/fastify/src/resources/src/assets/css/styles.css.map +1 -0
- package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin-ext.woff2 +0 -0
- package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin.woff2 +0 -0
- package/fastify/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/fastify/src/resources/src/assets/images/logo.png +0 -0
- package/fastify/src/resources/src/assets/scss/custom.css +217 -0
- package/fastify/src/resources/src/assets/scss/custom.css.map +1 -0
- package/fastify/src/resources/src/assets/scss/custom.scss +1346 -0
- package/fastify/src/resources/src/components/Button.vue +32 -0
- package/fastify/src/resources/src/components/Checkbox.vue +32 -0
- package/fastify/src/resources/src/components/Datepicker.vue +106 -0
- package/fastify/src/resources/src/components/FeatureButton.vue +53 -0
- package/fastify/src/resources/src/components/FloatButton.vue +33 -0
- package/fastify/src/resources/src/components/Href.vue +32 -0
- package/fastify/src/resources/src/components/Input.vue +243 -0
- package/fastify/src/resources/src/components/InputGroup.vue +147 -0
- package/fastify/src/resources/src/components/InputPasswordToggle.vue +229 -0
- package/fastify/src/resources/src/components/Modal.vue +71 -0
- package/fastify/src/resources/src/components/Pagebar.vue +26 -0
- package/fastify/src/resources/src/components/Refresh.vue +19 -0
- package/fastify/src/resources/src/components/Select.vue +384 -0
- package/fastify/src/resources/src/components/Spinner.vue +42 -0
- package/fastify/src/resources/src/components/Switch.vue +60 -0
- package/fastify/src/resources/src/components/TextArea.vue +117 -0
- package/fastify/src/resources/src/components/Toast.vue +56 -0
- package/fastify/src/resources/src/components/datatable/DataTableSkeleton.vue +98 -0
- package/fastify/src/resources/src/components/datatable/Pagination.vue +147 -0
- package/fastify/src/resources/src/components/datatable/SelectOpption.vue +64 -0
- package/fastify/src/resources/src/components/datatable/index.vue +199 -0
- package/fastify/src/resources/src/composables/useAuth.ts +52 -0
- package/fastify/src/resources/src/helpers/assets.ts +51 -0
- package/fastify/src/resources/src/helpers/nformatter.ts +10 -0
- package/fastify/src/resources/src/helpers/utils.ts +75 -0
- package/fastify/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/fastify/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/fastify/src/resources/src/layouts/Layout/Header.vue +124 -0
- package/fastify/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/fastify/src/resources/src/layouts/Layout/index.vue +87 -0
- package/fastify/src/resources/src/main.ts +28 -0
- package/fastify/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/fastify/src/resources/src/pages/auth/login.vue +96 -0
- package/fastify/src/resources/src/pages/auth/register.vue +129 -0
- package/fastify/src/resources/src/pages/auth/resetPassword.vue +110 -0
- package/fastify/src/resources/src/pages/auth/verifyEmail.vue +64 -0
- package/fastify/src/resources/src/pages/dashboard/index.vue +66 -0
- package/fastify/src/resources/src/plugins/axios.ts +31 -0
- package/fastify/src/resources/src/plugins/browserDetect.ts +55 -0
- package/fastify/src/resources/src/plugins/dialog.ts +216 -0
- package/fastify/src/resources/src/plugins/gum.ts +683 -0
- package/fastify/src/resources/src/plugins/pulse.ts +157 -0
- package/fastify/src/resources/src/plugins/routeProgress.ts +87 -0
- package/fastify/src/resources/src/router/index.ts +85 -0
- package/fastify/src/resources/src/stores/admin-ui.ts +148 -0
- package/fastify/src/resources/src/stores/auth.ts +187 -0
- package/fastify/src/resources/tsconfig.json +19 -0
- package/fastify/src/resources/vite.config.ts +64 -0
- package/fastify/src/storage/app/private/.gitkeep +0 -0
- package/fastify/src/storage/app/public/.gitkeep +0 -0
- package/fastify/src/storage/app/tmp/generated/.gitkeep +0 -0
- package/fastify/src/storage/database/nexwire.sqlite +0 -0
- package/fastify/src/storage/logs/.gitkeep +0 -0
- package/fastify/src/storage/logs/app.log +0 -0
- package/fastify/src/storage/logs/fatal.log +0 -0
- package/fastify/src/storage/tmp/.gitkeep +0 -0
- package/fastify/src/storage/trash/modules/.gitkeep +0 -0
- package/fastify/src/types/global.d.ts +32 -0
- package/fastify/tsconfig.json +19 -0
- package/fastify/vitest.config.ts +38 -0
- package/hono/src/framework/facade.ts +1 -0
- package/hono/src/framework/http/logger.ts +86 -81
- package/hono/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/hono/src/framework/notification/channels/mail.ts +8 -6
- package/hono/src/middlewares/auth-middleware.ts +1 -2
- package/hono/src/middlewares/role-middleware.ts +3 -13
- package/hono/src/modules/auth/routes/index.ts +1 -2
- package/hono/src/providers/notification.ts +2 -2
- package/hono/src/resources/src/components/Switch.vue +60 -65
- package/hono/src/resources/src/pages/auth/forgetPassword.vue +23 -29
- package/hono/src/resources/src/pages/auth/login.vue +26 -29
- package/hono/src/resources/src/pages/auth/register.vue +36 -40
- package/hono/src/resources/src/pages/auth/resetPassword.vue +29 -37
- package/hono/src/resources/src/pages/auth/verifyEmail.vue +15 -19
- package/hono/src/resources/src/plugins/gum.ts +519 -203
- package/hono/src/resources/src/stores/auth.ts +9 -6
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +3 -2
- package/src/index.js +11 -8
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import swagger from "@fastify/swagger";
|
|
4
|
+
import scalarApiReference from "@scalar/fastify-api-reference";
|
|
5
|
+
import type { FastifyInstance } from "fastify";
|
|
6
|
+
import { jsonSchemaTransform } from "fastify-type-provider-zod";
|
|
7
|
+
import { openApiConfig } from "@/config/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Why: Registers the OpenAPI document and interactive docs UI.
|
|
11
|
+
* When: OPEN_API is enabled during app boot.
|
|
12
|
+
* Where: HTTP app setup, before module routes.
|
|
13
|
+
* How: `@fastify/swagger` collects route schemas through the Zod transform;
|
|
14
|
+
* Scalar renders them at the configured docs path.
|
|
15
|
+
*/
|
|
16
|
+
export function configureOpenApi(app: FastifyInstance) {
|
|
17
|
+
const { specUrl, docsPath, favicon, ...scalarOptions } = openApiConfig.scalar;
|
|
18
|
+
|
|
19
|
+
app.register(swagger, {
|
|
20
|
+
openapi: {
|
|
21
|
+
openapi: openApiConfig.version,
|
|
22
|
+
info: {
|
|
23
|
+
title: openApiConfig.title,
|
|
24
|
+
version: openApiConfig.apiVersion,
|
|
25
|
+
...(openApiConfig.description ? { description: openApiConfig.description } : {})
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
transform: jsonSchemaTransform
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
app.get(specUrl, { schema: { hide: true } }, async () => app.swagger());
|
|
32
|
+
|
|
33
|
+
if (favicon) {
|
|
34
|
+
app.get("/favicon.ico", { schema: { hide: true } }, async (_request, reply) => {
|
|
35
|
+
const faviconPath = path.join(process.cwd(), favicon);
|
|
36
|
+
if (!fs.existsSync(faviconPath)) return reply.code(404).send();
|
|
37
|
+
return reply.type("image/x-icon").send(fs.readFileSync(faviconPath));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
app.register(scalarApiReference, {
|
|
42
|
+
routePrefix: docsPath,
|
|
43
|
+
configuration: {
|
|
44
|
+
url: specUrl,
|
|
45
|
+
...scalarOptions
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import rateLimit from "@fastify/rate-limit";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import { openApiConfig, rateLimitConfig } from "@/config/index.js";
|
|
4
|
+
|
|
5
|
+
const skipPaths = new Set([
|
|
6
|
+
"/health",
|
|
7
|
+
"/ready",
|
|
8
|
+
"/live",
|
|
9
|
+
"/metrics",
|
|
10
|
+
openApiConfig.scalar.specUrl,
|
|
11
|
+
openApiConfig.scalar.docsPath
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function isInfrastructurePath(pathname: string) {
|
|
15
|
+
return (
|
|
16
|
+
skipPaths.has(pathname) ||
|
|
17
|
+
pathname.startsWith(`${openApiConfig.scalar.docsPath}/`) ||
|
|
18
|
+
pathname.startsWith("/queues") ||
|
|
19
|
+
pathname.startsWith("/storage") ||
|
|
20
|
+
pathname.startsWith("/socket.io")
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why: Protects the API from burst abuse and accidental flooding.
|
|
26
|
+
* When: Applied globally while the app boots.
|
|
27
|
+
* Where: App plugin stack.
|
|
28
|
+
* How: Fastify's rate-limit plugin uses an in-memory store, matching the Express
|
|
29
|
+
* engine. Infrastructure paths are excluded so probes and docs stay usable.
|
|
30
|
+
*/
|
|
31
|
+
export function registerRateLimit(app: FastifyInstance) {
|
|
32
|
+
app.register(rateLimit, {
|
|
33
|
+
global: true,
|
|
34
|
+
max: rateLimitConfig.maxRequests,
|
|
35
|
+
timeWindow: rateLimitConfig.windowMs,
|
|
36
|
+
enableDraftSpec: true,
|
|
37
|
+
allowList: (request) => isInfrastructurePath((request.url || "").split("?")[0])
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stricter limit for the login route. Passed as `config.rateLimit` on the route.
|
|
43
|
+
*/
|
|
44
|
+
export const loginLimiter = {
|
|
45
|
+
max: rateLimitConfig.loginMaxRequests,
|
|
46
|
+
timeWindow: rateLimitConfig.windowMs
|
|
47
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { securityConfig } from "@/config/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Why: Sets browser security response headers (CSP, HSTS, X-Frame).
|
|
6
|
+
* When: Every incoming HTTP request.
|
|
7
|
+
* Where: App request lifecycle, after CORS and session.
|
|
8
|
+
* How: All headers disabled when securityConfig.enabled is false.
|
|
9
|
+
* HSTS only applied when the request is HTTPS.
|
|
10
|
+
*/
|
|
11
|
+
export async function securityHook(request: FastifyRequest, reply: FastifyReply) {
|
|
12
|
+
if (!securityConfig.enabled) return;
|
|
13
|
+
|
|
14
|
+
reply.header("Content-Security-Policy", securityConfig.csp);
|
|
15
|
+
|
|
16
|
+
const forwardedProto = request.headers["x-forwarded-proto"];
|
|
17
|
+
const isHttps = forwardedProto === "https" || request.protocol === "https";
|
|
18
|
+
if (securityConfig.hsts && isHttps) {
|
|
19
|
+
reply.header("Strict-Transport-Security", securityConfig.hstsMaxAge);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
reply.header("X-Frame-Options", securityConfig.xFrame);
|
|
23
|
+
reply.header("X-Content-Type-Options", "nosniff");
|
|
24
|
+
reply.header("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fastifyStatic from "@fastify/static";
|
|
4
|
+
import type { FastifyInstance } from "fastify";
|
|
5
|
+
|
|
6
|
+
const publicDir = path.resolve(process.cwd(), "public");
|
|
7
|
+
const storageDir = path.resolve(process.cwd(), "src/storage/app/public");
|
|
8
|
+
|
|
9
|
+
export function hasUiBuild() {
|
|
10
|
+
return fs.existsSync(path.join(publicDir, "index.html"));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ensurePublicDir() {
|
|
14
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Why: Serves uploaded public files and, when present, the built SPA.
|
|
19
|
+
* When: App boot.
|
|
20
|
+
* Where: HTTP app setup.
|
|
21
|
+
* How: `@fastify/static` mounts storage under `/storage/`; the UI build is
|
|
22
|
+
* served with `wildcard: false`, so only real build files get a route. An
|
|
23
|
+
* unknown `/api/...` path therefore falls through to the JSON 404 handler
|
|
24
|
+
* instead of being swallowed by a catch-all static route.
|
|
25
|
+
*/
|
|
26
|
+
export function registerStatic(app: FastifyInstance) {
|
|
27
|
+
if (fs.existsSync(storageDir)) {
|
|
28
|
+
app.register(fastifyStatic, {
|
|
29
|
+
root: storageDir,
|
|
30
|
+
prefix: "/storage/",
|
|
31
|
+
decorateReply: false
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (hasUiBuild()) {
|
|
36
|
+
app.register(fastifyStatic, { root: publicDir, wildcard: false, redirect: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Why: Identifies paths that must never receive the SPA shell.
|
|
42
|
+
* When: A request reaches the not-found handler.
|
|
43
|
+
* Where: SPA fallback resolution.
|
|
44
|
+
* How: API, storage, queue, socket, and metrics paths all keep JSON 404s.
|
|
45
|
+
*/
|
|
46
|
+
export function isInfrastructurePath(pathname: string) {
|
|
47
|
+
return (
|
|
48
|
+
pathname.startsWith("/api") ||
|
|
49
|
+
pathname.startsWith("/storage") ||
|
|
50
|
+
pathname.startsWith("/queues") ||
|
|
51
|
+
pathname.startsWith("/socket.io") ||
|
|
52
|
+
pathname === "/metrics"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Why: Decides between the SPA shell and a JSON 404.
|
|
58
|
+
* When: A request reaches the not-found handler.
|
|
59
|
+
* Where: `registerSpaFallback`.
|
|
60
|
+
* How: Only GET requests outside infrastructure paths get the shell, and only
|
|
61
|
+
* when a build actually exists.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveSpaFallback(method: string, pathname: string, hasBuild: boolean) {
|
|
64
|
+
return method === "GET" && !isInfrastructurePath(pathname) && hasBuild ? "spa" : "json";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Why: Falls back to the SPA shell for client-side routes and returns JSON 404
|
|
69
|
+
* for API paths.
|
|
70
|
+
* When: No route or static file matched.
|
|
71
|
+
* Where: End of the HTTP setup.
|
|
72
|
+
*/
|
|
73
|
+
export function registerSpaFallback(app: FastifyInstance) {
|
|
74
|
+
app.setNotFoundHandler((request, reply) => {
|
|
75
|
+
const pathname = (request.url || "").split("?")[0];
|
|
76
|
+
|
|
77
|
+
if (resolveSpaFallback(request.method, pathname, hasUiBuild()) === "spa") {
|
|
78
|
+
return reply.sendFile("index.html");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return reply.code(404).send({ message: `Route not found: ${request.method} ${pathname}` });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Why: Documents the error contract Fastify actually returns for bad input.
|
|
5
|
+
* When: Any route declares a 422 response.
|
|
6
|
+
* Where: Route schemas, alongside the body/params schema that can fail.
|
|
7
|
+
* How: Fastify serializes error responses through the route's response schema,
|
|
8
|
+
* so a 422 must be described as the Zod error shape — not `{ message }`.
|
|
9
|
+
*/
|
|
10
|
+
export const ValidationErrorSchema = z.object({
|
|
11
|
+
success: z.literal(false),
|
|
12
|
+
error: z.object({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
issues: z.array(
|
|
15
|
+
z.object({
|
|
16
|
+
path: z.array(z.union([z.string(), z.number()])),
|
|
17
|
+
message: z.string(),
|
|
18
|
+
code: z.string()
|
|
19
|
+
})
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why: Validates data outside of Fastify's route schema pipeline.
|
|
26
|
+
* When: Controllers or services need to parse a payload by hand (e.g. a value
|
|
27
|
+
* built at runtime rather than read straight from the request).
|
|
28
|
+
* Where: Anywhere the framework exposes a manual parse boundary.
|
|
29
|
+
* How: Throws an Error with `statusCode = 422`; the global error handler turns
|
|
30
|
+
* it into the same `{ success:false, error:{ name, issues } }` contract the
|
|
31
|
+
* route-level validator produces.
|
|
32
|
+
*/
|
|
33
|
+
export async function validate<T extends z.ZodType>(schema: T, data: unknown): Promise<z.infer<T>> {
|
|
34
|
+
const result = await schema.safeParseAsync(data);
|
|
35
|
+
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
const error = new Error("Validation failed") as Error & {
|
|
38
|
+
statusCode: number;
|
|
39
|
+
success: boolean;
|
|
40
|
+
error: { name: string; issues: z.core.$ZodIssue[] };
|
|
41
|
+
};
|
|
42
|
+
error.statusCode = 422;
|
|
43
|
+
error.success = false;
|
|
44
|
+
error.error = { name: result.error.name, issues: result.error.issues };
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result.data;
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { appConfig } from "@/config/index.js";
|
|
2
|
+
import { validateConfig } from "@/config/validate.js";
|
|
3
|
+
import { initDatabase } from "@/framework/database/connection.js";
|
|
4
|
+
import { createHttpApp } from "@/framework/http/app.js";
|
|
5
|
+
import { ensurePublicDir, registerSpaFallback } from "@/framework/http/static.js";
|
|
6
|
+
import { registerModuleRoutes } from "@/framework/modules/routes.js";
|
|
7
|
+
import { loadProviders } from "@/framework/providers/loader.js";
|
|
8
|
+
import { bootQueueJobs } from "@/framework/queue/queue.js";
|
|
9
|
+
import { setupQueueDashboard } from "@/framework/queue/ui.js";
|
|
10
|
+
import { initRedis } from "@/framework/redis/client.js";
|
|
11
|
+
import { storage } from "@/framework/storage/storage.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Why: Assembles app kernel and boots all framework dependencies.
|
|
15
|
+
* When: HTTP server startup.
|
|
16
|
+
* Where: Called by server runtime entrypoint.
|
|
17
|
+
* How: Initializes storage/db/redis/queues/events/routes and optional UI.
|
|
18
|
+
*/
|
|
19
|
+
export async function createKernel() {
|
|
20
|
+
validateConfig();
|
|
21
|
+
|
|
22
|
+
await storage.init();
|
|
23
|
+
|
|
24
|
+
await initRedis();
|
|
25
|
+
|
|
26
|
+
const app = createHttpApp();
|
|
27
|
+
|
|
28
|
+
await initDatabase();
|
|
29
|
+
await loadProviders();
|
|
30
|
+
await bootQueueJobs();
|
|
31
|
+
await registerModuleRoutes(app);
|
|
32
|
+
|
|
33
|
+
const queueDashboard = await setupQueueDashboard();
|
|
34
|
+
await queueDashboard.route(app);
|
|
35
|
+
|
|
36
|
+
if (appConfig.uiEnabled) {
|
|
37
|
+
ensurePublicDir();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
registerSpaFallback(app);
|
|
41
|
+
|
|
42
|
+
return { app, bullBoard: queueDashboard };
|
|
43
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { databaseNameFromUrl, databaseUrl, detectDialect, parsedDatabaseUrl, sqlitePathFromUrl } from "../utils/env-db.mjs";
|
|
5
|
+
import { packageScript, runCommand, runNodeScript } from "../utils/process.mjs";
|
|
6
|
+
|
|
7
|
+
/** Detect SQL dialect from raw SQL content by matching against known syntax hints. */
|
|
8
|
+
export function detectMigrationDialectFromSql(sql = "") {
|
|
9
|
+
const source = sql.toLowerCase();
|
|
10
|
+
if (!source.trim()) return "unknown";
|
|
11
|
+
|
|
12
|
+
const postgresHints = ["generated always as identity", "bigserial", "serial", "timestamp with time zone", "public."];
|
|
13
|
+
const mysqlHints = ["auto_increment", "engine=", "`"];
|
|
14
|
+
const sqliteHints = ["autoincrement", "pragma"];
|
|
15
|
+
|
|
16
|
+
if (postgresHints.some((hint) => source.includes(hint))) return "postgresql";
|
|
17
|
+
// SQLite output quotes identifiers with backticks too, so the distinctive
|
|
18
|
+
// hints must be tested before the MySQL ones or every SQLite migration is
|
|
19
|
+
// misread as MySQL and the history is reset on the next generate.
|
|
20
|
+
if (sqliteHints.some((hint) => source.includes(hint))) return "sqlite";
|
|
21
|
+
if (mysqlHints.some((hint) => source.includes(hint))) return "mysql";
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** List existing migration .sql files for the current dialect, sorted. */
|
|
26
|
+
export async function migrationFiles() {
|
|
27
|
+
const files = await glob(`src/database/migrations/${detectDialect()}/*.sql`, {
|
|
28
|
+
cwd: process.cwd(),
|
|
29
|
+
nodir: true,
|
|
30
|
+
windowsPathsNoEscape: true
|
|
31
|
+
});
|
|
32
|
+
return files.sort();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Generate drizzle-kit CLI args for migration generation. Uses --name init for the first migration. */
|
|
36
|
+
export async function drizzleGenerateArgs() {
|
|
37
|
+
const files = await migrationFiles();
|
|
38
|
+
if (!files.length) return ["generate", "--name", "init"];
|
|
39
|
+
return ["generate"];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Create MySQL database if it does not exist. Uses DATABASE_URL credentials. */
|
|
43
|
+
async function ensureMysqlDatabaseExists() {
|
|
44
|
+
const targetUrl = parsedDatabaseUrl();
|
|
45
|
+
const database = databaseNameFromUrl(targetUrl);
|
|
46
|
+
if (!targetUrl || !database) return;
|
|
47
|
+
const mysql = await import("mysql2/promise");
|
|
48
|
+
const connection = await mysql.createConnection({
|
|
49
|
+
host: targetUrl.hostname,
|
|
50
|
+
port: targetUrl.port ? Number(targetUrl.port) : 3306,
|
|
51
|
+
user: decodeURIComponent(targetUrl.username || "root"),
|
|
52
|
+
password: decodeURIComponent(targetUrl.password || ""),
|
|
53
|
+
database: "mysql",
|
|
54
|
+
ssl: targetUrl.searchParams.get("ssl") === "true" ? {} : undefined
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const [rows] = await connection.query("SELECT SCHEMA_NAME as name FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", [database]);
|
|
58
|
+
const exists = Array.isArray(rows) && rows.length > 0;
|
|
59
|
+
if (!exists) {
|
|
60
|
+
const safe = database.replace(/`/g, "``");
|
|
61
|
+
await connection.query(`CREATE DATABASE \`${safe}\``);
|
|
62
|
+
console.log(`Created MySQL database: ${database}`);
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await connection.end();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Create Postgres database if it does not exist. Uses DATABASE_URL credentials. */
|
|
70
|
+
async function ensurePostgresDatabaseExists() {
|
|
71
|
+
const targetUrl = parsedDatabaseUrl();
|
|
72
|
+
const database = databaseNameFromUrl(targetUrl);
|
|
73
|
+
if (!targetUrl || !database) return;
|
|
74
|
+
const postgres = (await import("postgres")).default;
|
|
75
|
+
const admin = postgres({
|
|
76
|
+
host: targetUrl.hostname,
|
|
77
|
+
port: targetUrl.port ? Number(targetUrl.port) : 5432,
|
|
78
|
+
user: decodeURIComponent(targetUrl.username || "postgres"),
|
|
79
|
+
password: decodeURIComponent(targetUrl.password || ""),
|
|
80
|
+
database: "postgres",
|
|
81
|
+
ssl: targetUrl.searchParams.get("ssl") === "true" ? "require" : false
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const rows = await admin.unsafe("SELECT 1 FROM pg_database WHERE datname = $1", [database]);
|
|
85
|
+
if (!rows.length) {
|
|
86
|
+
const safe = database.replace(/"/g, '""');
|
|
87
|
+
await admin.unsafe(`CREATE DATABASE "${safe}"`);
|
|
88
|
+
console.log(`Created Postgres database: ${database}`);
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
await admin.end();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Ensure the database exists for the current DATABASE_URL dialect. Creates it if missing (MySQL/Postgres). */
|
|
96
|
+
export async function ensureDatabaseExists() {
|
|
97
|
+
const dialect = detectDialect();
|
|
98
|
+
if (dialect === "sqlite") {
|
|
99
|
+
await ensureDatabaseDirectory();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (dialect === "mysql") {
|
|
103
|
+
await ensureMysqlDatabaseExists();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (dialect === "postgresql") {
|
|
107
|
+
await ensurePostgresDatabaseExists();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Resolve the filesystem path for the SQLite database file. */
|
|
112
|
+
function sqliteDatabasePath() {
|
|
113
|
+
return path.resolve(process.cwd(), sqlitePathFromUrl(databaseUrl()));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Delete and recreate the SQLite database file. */
|
|
117
|
+
async function resetSqliteDatabase() {
|
|
118
|
+
const file = sqliteDatabasePath();
|
|
119
|
+
await fs.rm(file, { force: true });
|
|
120
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Drop and recreate the MySQL database. */
|
|
124
|
+
async function resetMysqlDatabase() {
|
|
125
|
+
const targetUrl = parsedDatabaseUrl();
|
|
126
|
+
const database = databaseNameFromUrl(targetUrl);
|
|
127
|
+
if (!targetUrl || !database) return;
|
|
128
|
+
const mysql = await import("mysql2/promise");
|
|
129
|
+
const connection = await mysql.createConnection({
|
|
130
|
+
host: targetUrl.hostname,
|
|
131
|
+
port: targetUrl.port ? Number(targetUrl.port) : 3306,
|
|
132
|
+
user: decodeURIComponent(targetUrl.username || "root"),
|
|
133
|
+
password: decodeURIComponent(targetUrl.password || ""),
|
|
134
|
+
database: "mysql",
|
|
135
|
+
ssl: targetUrl.searchParams.get("ssl") === "true" ? {} : undefined
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
const safe = database.replace(/`/g, "``");
|
|
139
|
+
await connection.query(`DROP DATABASE IF EXISTS \`${safe}\``);
|
|
140
|
+
await connection.query(`CREATE DATABASE \`${safe}\``);
|
|
141
|
+
} finally {
|
|
142
|
+
await connection.end();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Drop and recreate the Postgres database. Terminates active connections first. */
|
|
147
|
+
async function resetPostgresDatabase() {
|
|
148
|
+
const targetUrl = parsedDatabaseUrl();
|
|
149
|
+
const database = databaseNameFromUrl(targetUrl);
|
|
150
|
+
if (!targetUrl || !database) return;
|
|
151
|
+
const postgres = (await import("postgres")).default;
|
|
152
|
+
const admin = postgres({
|
|
153
|
+
host: targetUrl.hostname,
|
|
154
|
+
port: targetUrl.port ? Number(targetUrl.port) : 5432,
|
|
155
|
+
user: decodeURIComponent(targetUrl.username || "postgres"),
|
|
156
|
+
password: decodeURIComponent(targetUrl.password || ""),
|
|
157
|
+
database: "postgres",
|
|
158
|
+
ssl: targetUrl.searchParams.get("ssl") === "true" ? "require" : false
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
const safe = database.replace(/"/g, '""');
|
|
162
|
+
await admin.unsafe("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()", [database]);
|
|
163
|
+
await admin.unsafe(`DROP DATABASE IF EXISTS "${safe}"`);
|
|
164
|
+
await admin.unsafe(`CREATE DATABASE "${safe}"`);
|
|
165
|
+
} finally {
|
|
166
|
+
await admin.end();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Drop and recreate the database for the current dialect. */
|
|
171
|
+
export async function resetDatabase() {
|
|
172
|
+
const dialect = detectDialect();
|
|
173
|
+
if (dialect === "sqlite") return await resetSqliteDatabase();
|
|
174
|
+
if (dialect === "mysql") return await resetMysqlDatabase();
|
|
175
|
+
if (dialect === "postgresql") return await resetPostgresDatabase();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Check if the database has existing app tables (excluding drizzle meta tables). */
|
|
179
|
+
export async function hasExistingAppTables() {
|
|
180
|
+
const dialect = detectDialect();
|
|
181
|
+
if (dialect === "sqlite") {
|
|
182
|
+
const libsql = await import("@libsql/client");
|
|
183
|
+
const file = sqliteDatabasePath();
|
|
184
|
+
const client = libsql.createClient({ url: `file:${file.replace(/\\/g, "/")}` });
|
|
185
|
+
try {
|
|
186
|
+
const result = await client.execute(
|
|
187
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '__drizzle_migrations'"
|
|
188
|
+
);
|
|
189
|
+
return result.rows.length > 0;
|
|
190
|
+
} finally {
|
|
191
|
+
client.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (dialect === "mysql") {
|
|
196
|
+
const mysql = await import("mysql2/promise");
|
|
197
|
+
const connection = await mysql.createConnection(databaseUrl());
|
|
198
|
+
try {
|
|
199
|
+
const [rows] = await connection.query(
|
|
200
|
+
"SELECT table_name as name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name != '__drizzle_migrations'"
|
|
201
|
+
);
|
|
202
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
203
|
+
} finally {
|
|
204
|
+
await connection.end();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const postgres = (await import("postgres")).default;
|
|
209
|
+
const client = postgres(databaseUrl());
|
|
210
|
+
try {
|
|
211
|
+
const rows = await client.unsafe("SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != '__drizzle_migrations'");
|
|
212
|
+
return rows.length > 0;
|
|
213
|
+
} finally {
|
|
214
|
+
await client.end();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Run the application seed script via tsx or Bun. */
|
|
219
|
+
export async function runSeed(moduleName = "") {
|
|
220
|
+
const seedArgs = ["src/framework/database/seed.ts"];
|
|
221
|
+
if (moduleName) seedArgs.push(`--module=${moduleName}`);
|
|
222
|
+
if (process.versions?.bun) {
|
|
223
|
+
await runCommand("bun", seedArgs);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
await runNodeScript(packageScript("tsx", "dist/cli.mjs"), seedArgs);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Ensure the SQLite database directory exists. No-op for MySQL/Postgres. */
|
|
230
|
+
export async function ensureDatabaseDirectory() {
|
|
231
|
+
if (detectDialect() !== "sqlite") return;
|
|
232
|
+
const file = path.resolve(process.cwd(), sqlitePathFromUrl(databaseUrl()));
|
|
233
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Ensure the drizzle migration meta directory and _journal.json exist. */
|
|
237
|
+
export async function ensureMigrationMeta() {
|
|
238
|
+
const migrationRoot = path.resolve(process.cwd(), "src/database/migrations", detectDialect());
|
|
239
|
+
const metaRoot = path.join(migrationRoot, "meta");
|
|
240
|
+
const journalPath = path.join(metaRoot, "_journal.json");
|
|
241
|
+
await fs.mkdir(metaRoot, { recursive: true });
|
|
242
|
+
try {
|
|
243
|
+
await fs.access(journalPath);
|
|
244
|
+
} catch {
|
|
245
|
+
const dialect = detectDialect();
|
|
246
|
+
const journalDialect = dialect === "postgresql" ? "postgresql" : dialect;
|
|
247
|
+
await fs.writeFile(
|
|
248
|
+
journalPath,
|
|
249
|
+
`{
|
|
250
|
+
"version": "7",
|
|
251
|
+
"dialect": "${journalDialect}",
|
|
252
|
+
"entries": []
|
|
253
|
+
}
|
|
254
|
+
`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Sync the migration dialect to match the current DATABASE_URL. Resets migrations if dialect changed. */
|
|
260
|
+
export async function syncMigrationDialect() {
|
|
261
|
+
async function existingMigrationDialect() {
|
|
262
|
+
const files = await migrationFiles();
|
|
263
|
+
if (!files.length) return null;
|
|
264
|
+
const first = await fs.readFile(path.resolve(process.cwd(), files[0]), "utf8");
|
|
265
|
+
return detectMigrationDialectFromSql(first);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function resetMigrationFiles() {
|
|
269
|
+
const migrationRoot = path.resolve(process.cwd(), "src/database/migrations", detectDialect());
|
|
270
|
+
const dialect = detectDialect();
|
|
271
|
+
const journalDialect = dialect === "postgresql" ? "postgresql" : dialect;
|
|
272
|
+
await fs.rm(migrationRoot, { recursive: true, force: true });
|
|
273
|
+
await fs.mkdir(path.join(migrationRoot, "meta"), { recursive: true });
|
|
274
|
+
await fs.writeFile(
|
|
275
|
+
path.join(migrationRoot, "meta", "_journal.json"),
|
|
276
|
+
`{
|
|
277
|
+
"version": "7",
|
|
278
|
+
"dialect": "${journalDialect}",
|
|
279
|
+
"entries": []
|
|
280
|
+
}
|
|
281
|
+
`
|
|
282
|
+
);
|
|
283
|
+
console.log("Reset existing migration files due to dialect change.");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const target = detectDialect();
|
|
287
|
+
const current = await existingMigrationDialect();
|
|
288
|
+
if (!current || current === "unknown" || current === target) return;
|
|
289
|
+
console.log(`Migration dialect changed (${current} -> ${target}). Regenerating migration files for current DATABASE_URL.`);
|
|
290
|
+
await resetMigrationFiles();
|
|
291
|
+
}
|