@baasix/baasix 0.1.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/LICENSE.MD +85 -0
- package/README.md +526 -0
- package/assets/banner.jpg +0 -0
- package/assets/banner_small.jpg +0 -0
- package/assets/logo_icon.svg +20 -0
- package/assets/logo_icon_rounded.svg +20 -0
- package/dist/LICENSE.MD +85 -0
- package/dist/README.md +526 -0
- package/dist/app/404/index.html +1 -0
- package/dist/app/404.html +1 -0
- package/dist/app/_next/static/chunks/041e1f03-56ae8a902a7f2fe6.js +24 -0
- package/dist/app/_next/static/chunks/1117-05479929a8da73e3.js +1 -0
- package/dist/app/_next/static/chunks/1299.77cc7b7b76b75cba.js +1 -0
- package/dist/app/_next/static/chunks/1303-35a96e9c9cdeab9d.js +1 -0
- package/dist/app/_next/static/chunks/1509-56ac00cdaaecdf53.js +1 -0
- package/dist/app/_next/static/chunks/1668-e3eabd0f6753c780.js +1 -0
- package/dist/app/_next/static/chunks/1783-d9fb550fd324300c.js +1 -0
- package/dist/app/_next/static/chunks/2117-29b5fa47421595ad.js +2 -0
- package/dist/app/_next/static/chunks/2344.35b46d2179a765b5.js +1 -0
- package/dist/app/_next/static/chunks/257.990da16794a31292.js +1 -0
- package/dist/app/_next/static/chunks/2676-73b0ee7c80073a84.js +1 -0
- package/dist/app/_next/static/chunks/3563-b8842744384391fe.js +1 -0
- package/dist/app/_next/static/chunks/363642f4-933b579ed3c85f60.js +1 -0
- package/dist/app/_next/static/chunks/3817-e20c8f0a0810fc95.js +1 -0
- package/dist/app/_next/static/chunks/3834.84944e390d902509.js +2 -0
- package/dist/app/_next/static/chunks/4043-3a30c8a75896f241.js +1 -0
- package/dist/app/_next/static/chunks/4225-14090c7c0cd9dec6.js +1 -0
- package/dist/app/_next/static/chunks/4438-c9a12ca15b6e9160.js +1 -0
- package/dist/app/_next/static/chunks/4458-679fd0c6884f456a.js +1 -0
- package/dist/app/_next/static/chunks/4475-8bdfbd536fba8c48.js +1 -0
- package/dist/app/_next/static/chunks/4883-8a924721bb21b3b0.js +1 -0
- package/dist/app/_next/static/chunks/489-683ab07188f9df2b.js +1 -0
- package/dist/app/_next/static/chunks/4952-1b97320cf61f3f21.js +1 -0
- package/dist/app/_next/static/chunks/5094-8d53e403235d4ca6.js +1 -0
- package/dist/app/_next/static/chunks/5101-3a146e0625747ad1.js +1 -0
- package/dist/app/_next/static/chunks/54a60aa6-d9747982e0a81f58.js +79 -0
- package/dist/app/_next/static/chunks/5650-f096291df402bfc2.js +1 -0
- package/dist/app/_next/static/chunks/600-539045311240f579.js +1 -0
- package/dist/app/_next/static/chunks/6170-803b82e19d3ade6d.js +89 -0
- package/dist/app/_next/static/chunks/6241-30d7169d1010e5a4.js +1 -0
- package/dist/app/_next/static/chunks/6530-a91e10cffa4200c4.js +1 -0
- package/dist/app/_next/static/chunks/6547-4bbbdb5c399aef1e.js +1 -0
- package/dist/app/_next/static/chunks/6712-781937c53a2c49da.js +1 -0
- package/dist/app/_next/static/chunks/6fcbdc68-90be1a5480b8d353.js +1 -0
- package/dist/app/_next/static/chunks/70e0d97a-aeaf0cdc26ba1a58.js +1 -0
- package/dist/app/_next/static/chunks/7214-5154a89d08d24dde.js +1 -0
- package/dist/app/_next/static/chunks/7324-b53229c59a640880.js +10 -0
- package/dist/app/_next/static/chunks/7636-66424f0b51d350e9.js +1 -0
- package/dist/app/_next/static/chunks/7874-39a3f2541165a675.js +1 -0
- package/dist/app/_next/static/chunks/7982-9da12b83f11e3f5f.js +1 -0
- package/dist/app/_next/static/chunks/8213a2eb-da25a3b3c5521b2b.js +1 -0
- package/dist/app/_next/static/chunks/8473-6598318371eca31b.js +1 -0
- package/dist/app/_next/static/chunks/8640fa6b-72e43370f68e5587.js +1 -0
- package/dist/app/_next/static/chunks/9090-3ef676f29c95f1c7.js +1 -0
- package/dist/app/_next/static/chunks/9124-a02f9e209e6e3cce.js +1 -0
- package/dist/app/_next/static/chunks/926-156f32067d111d6b.js +1 -0
- package/dist/app/_next/static/chunks/9487-b17481605e513b83.js +1 -0
- package/dist/app/_next/static/chunks/9599-a7e572bb88c3392b.js +1 -0
- package/dist/app/_next/static/chunks/9881-419697138376e755.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/all-activity/page-8917930b4d663405.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/email-log/page-b27a6ee32782d7df.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/notifications/page-b7eda523ede2702c.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/page-1cfa62d1caedaed0.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/sessions/page-3e21e20db90aeff7.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/workflow-executions/page-27bcc26b747fb29b.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/activity-log/workflow-logs/page-9f9e9e952aef436e.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/change-password/page-8d61aa499eabb127.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/dashboard/page-1ceeac9e72997a8a.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/data-browser/page-8cda2b57759dd670.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/file-manager/page-8c6f1b1da66ad7e4.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/layout-f70d225b2759c998.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/migrations/page-aacec8f7cfb40ab2.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/permissions/page-828110cfcde429c6.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/project/page-420e794bb76bd204.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/roles/page-9001d02b28f70708.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/schema/page-899574f35091dd58.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/tasks/page-ad7ab3e27c83f44f.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/templates/edit/page-bd83414cb8c4cb04.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/templates/page-3181447f8772b1d3.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/settings/tenants/page-ef9bfbacef5a1d73.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/users/invites/page-480306b7b2bbac7e.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/users/list/page-74da51254c2606b3.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/users/page-e99c6f0b915001b2.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/users/preferences/page-1a935630ce8f2b12.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/users/user-roles/page-901dfb8ea1f39ca8.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/workflows/detail/page-9a6b839aea688ca4.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/workflows/edit/page-11774efbc2fecae2.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/workflows/execution/page-8ec1aea90412c03d.js +1 -0
- package/dist/app/_next/static/chunks/app/(authenticated)/workflows/page-88bc5b36ccb0a1f7.js +1 -0
- package/dist/app/_next/static/chunks/app/(public)/forgot-password/page-ed263fd46ef81c20.js +1 -0
- package/dist/app/_next/static/chunks/app/(public)/layout-f538977545844af8.js +1 -0
- package/dist/app/_next/static/chunks/app/(public)/login/page-c0a10b137f346096.js +1 -0
- package/dist/app/_next/static/chunks/app/(public)/register/page-4cb7644893efd9b3.js +1 -0
- package/dist/app/_next/static/chunks/app/_not-found/page-653f8815b78256cc.js +1 -0
- package/dist/app/_next/static/chunks/app/layout-591ca7a3e16528a1.js +1 -0
- package/dist/app/_next/static/chunks/app/page-dd19d124b5fa2577.js +1 -0
- package/dist/app/_next/static/chunks/c37d3baf.c2ff165f5b02c692.js +1 -0
- package/dist/app/_next/static/chunks/d0deef33.0379166a4ec23470.js +1 -0
- package/dist/app/_next/static/chunks/fd9d1056-54169f07cd680d6c.js +1 -0
- package/dist/app/_next/static/chunks/framework-8e0e0f4a6b83a956.js +1 -0
- package/dist/app/_next/static/chunks/main-324e91f5a430cddf.js +1 -0
- package/dist/app/_next/static/chunks/main-app-55bcae20c77aaf0e.js +1 -0
- package/dist/app/_next/static/chunks/pages/_app-3c9ca398d360b709.js +1 -0
- package/dist/app/_next/static/chunks/pages/_error-cf5ca766ac8f493f.js +1 -0
- package/dist/app/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/app/_next/static/chunks/webpack-2c306566f7ee1b63.js +1 -0
- package/dist/app/_next/static/css/6c4002bae4e236b2.css +3 -0
- package/dist/app/_next/static/css/a275cc2b185e04f8.css +1 -0
- package/dist/app/_next/static/eCWhKA8XHqmB1zgFcEtN2/_buildManifest.js +1 -0
- package/dist/app/_next/static/eCWhKA8XHqmB1zgFcEtN2/_ssgManifest.js +1 -0
- package/dist/app/activity-log/all-activity/index.html +1 -0
- package/dist/app/activity-log/all-activity/index.txt +14 -0
- package/dist/app/activity-log/email-log/index.html +1 -0
- package/dist/app/activity-log/email-log/index.txt +14 -0
- package/dist/app/activity-log/index.html +1 -0
- package/dist/app/activity-log/index.txt +14 -0
- package/dist/app/activity-log/notifications/index.html +1 -0
- package/dist/app/activity-log/notifications/index.txt +14 -0
- package/dist/app/activity-log/sessions/index.html +1 -0
- package/dist/app/activity-log/sessions/index.txt +14 -0
- package/dist/app/activity-log/workflow-executions/index.html +1 -0
- package/dist/app/activity-log/workflow-executions/index.txt +14 -0
- package/dist/app/activity-log/workflow-logs/index.html +1 -0
- package/dist/app/activity-log/workflow-logs/index.txt +14 -0
- package/dist/app/change-password/index.html +1 -0
- package/dist/app/change-password/index.txt +14 -0
- package/dist/app/dashboard/index.html +1 -0
- package/dist/app/dashboard/index.txt +14 -0
- package/dist/app/data-browser/index.html +1 -0
- package/dist/app/data-browser/index.txt +14 -0
- package/dist/app/file-manager/index.html +1 -0
- package/dist/app/file-manager/index.txt +14 -0
- package/dist/app/forgot-password/index.html +1 -0
- package/dist/app/forgot-password/index.txt +13 -0
- package/dist/app/index.html +1 -0
- package/dist/app/index.txt +9 -0
- package/dist/app/login/index.html +1 -0
- package/dist/app/login/index.txt +13 -0
- package/dist/app/logo-dark.png +0 -0
- package/dist/app/logo-icon.svg +81 -0
- package/dist/app/logo-light.png +0 -0
- package/dist/app/register/index.html +1 -0
- package/dist/app/register/index.txt +13 -0
- package/dist/app/settings/migrations/index.html +1 -0
- package/dist/app/settings/migrations/index.txt +14 -0
- package/dist/app/settings/permissions/index.html +1 -0
- package/dist/app/settings/permissions/index.txt +14 -0
- package/dist/app/settings/project/index.html +1 -0
- package/dist/app/settings/project/index.txt +14 -0
- package/dist/app/settings/roles/index.html +1 -0
- package/dist/app/settings/roles/index.txt +14 -0
- package/dist/app/settings/schema/index.html +1 -0
- package/dist/app/settings/schema/index.txt +14 -0
- package/dist/app/settings/tasks/index.html +1 -0
- package/dist/app/settings/tasks/index.txt +14 -0
- package/dist/app/settings/templates/edit/index.html +1 -0
- package/dist/app/settings/templates/edit/index.txt +14 -0
- package/dist/app/settings/templates/index.html +1 -0
- package/dist/app/settings/templates/index.txt +14 -0
- package/dist/app/settings/tenants/index.html +1 -0
- package/dist/app/settings/tenants/index.txt +14 -0
- package/dist/app/users/index.html +1 -0
- package/dist/app/users/index.txt +14 -0
- package/dist/app/users/invites/index.html +1 -0
- package/dist/app/users/invites/index.txt +14 -0
- package/dist/app/users/list/index.html +1 -0
- package/dist/app/users/list/index.txt +14 -0
- package/dist/app/users/preferences/index.html +1 -0
- package/dist/app/users/preferences/index.txt +14 -0
- package/dist/app/users/user-roles/index.html +1 -0
- package/dist/app/users/user-roles/index.txt +14 -0
- package/dist/app/workflows/detail/index.html +1 -0
- package/dist/app/workflows/detail/index.txt +14 -0
- package/dist/app/workflows/edit/index.html +1 -0
- package/dist/app/workflows/edit/index.txt +14 -0
- package/dist/app/workflows/execution/index.html +1 -0
- package/dist/app/workflows/execution/index.txt +14 -0
- package/dist/app/workflows/index.html +1 -0
- package/dist/app/workflows/index.txt +14 -0
- package/dist/app.d.ts +36 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +546 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/adapters/baasix-adapter.d.ts +12 -0
- package/dist/auth/adapters/baasix-adapter.d.ts.map +1 -0
- package/dist/auth/adapters/baasix-adapter.js +318 -0
- package/dist/auth/adapters/baasix-adapter.js.map +1 -0
- package/dist/auth/adapters/index.d.ts +6 -0
- package/dist/auth/adapters/index.d.ts.map +1 -0
- package/dist/auth/adapters/index.js +5 -0
- package/dist/auth/adapters/index.js.map +1 -0
- package/dist/auth/core.d.ts +73 -0
- package/dist/auth/core.d.ts.map +1 -0
- package/dist/auth/core.js +528 -0
- package/dist/auth/core.js.map +1 -0
- package/dist/auth/index.d.ts +56 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +58 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth2/index.d.ts +5 -0
- package/dist/auth/oauth2/index.d.ts.map +1 -0
- package/dist/auth/oauth2/index.js +5 -0
- package/dist/auth/oauth2/index.js.map +1 -0
- package/dist/auth/oauth2/utils.d.ts +90 -0
- package/dist/auth/oauth2/utils.d.ts.map +1 -0
- package/dist/auth/oauth2/utils.js +167 -0
- package/dist/auth/oauth2/utils.js.map +1 -0
- package/dist/auth/providers/apple.d.ts +28 -0
- package/dist/auth/providers/apple.d.ts.map +1 -0
- package/dist/auth/providers/apple.js +192 -0
- package/dist/auth/providers/apple.js.map +1 -0
- package/dist/auth/providers/credential.d.ts +87 -0
- package/dist/auth/providers/credential.d.ts.map +1 -0
- package/dist/auth/providers/credential.js +162 -0
- package/dist/auth/providers/credential.js.map +1 -0
- package/dist/auth/providers/facebook.d.ts +26 -0
- package/dist/auth/providers/facebook.d.ts.map +1 -0
- package/dist/auth/providers/facebook.js +112 -0
- package/dist/auth/providers/facebook.js.map +1 -0
- package/dist/auth/providers/github.d.ts +29 -0
- package/dist/auth/providers/github.d.ts.map +1 -0
- package/dist/auth/providers/github.js +144 -0
- package/dist/auth/providers/github.js.map +1 -0
- package/dist/auth/providers/google.d.ts +32 -0
- package/dist/auth/providers/google.d.ts.map +1 -0
- package/dist/auth/providers/google.js +145 -0
- package/dist/auth/providers/google.js.map +1 -0
- package/dist/auth/providers/index.d.ts +22 -0
- package/dist/auth/providers/index.d.ts.map +1 -0
- package/dist/auth/providers/index.js +17 -0
- package/dist/auth/providers/index.js.map +1 -0
- package/dist/auth/routes.d.ts +63 -0
- package/dist/auth/routes.d.ts.map +1 -0
- package/dist/auth/routes.js +827 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/services/index.d.ts +10 -0
- package/dist/auth/services/index.d.ts.map +1 -0
- package/dist/auth/services/index.js +7 -0
- package/dist/auth/services/index.js.map +1 -0
- package/dist/auth/services/session.d.ts +81 -0
- package/dist/auth/services/session.d.ts.map +1 -0
- package/dist/auth/services/session.js +186 -0
- package/dist/auth/services/session.js.map +1 -0
- package/dist/auth/services/token.d.ts +41 -0
- package/dist/auth/services/token.d.ts.map +1 -0
- package/dist/auth/services/token.js +44 -0
- package/dist/auth/services/token.js.map +1 -0
- package/dist/auth/services/verification.d.ts +77 -0
- package/dist/auth/services/verification.d.ts.map +1 -0
- package/dist/auth/services/verification.js +143 -0
- package/dist/auth/services/verification.js.map +1 -0
- package/dist/auth/types.d.ts +318 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +6 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/customTypes/arrays.d.ts +200 -0
- package/dist/customTypes/arrays.d.ts.map +1 -0
- package/dist/customTypes/arrays.js +309 -0
- package/dist/customTypes/arrays.js.map +1 -0
- package/dist/customTypes/index.d.ts +8 -0
- package/dist/customTypes/index.d.ts.map +1 -0
- package/dist/customTypes/index.js +11 -0
- package/dist/customTypes/index.js.map +1 -0
- package/dist/customTypes/postgis.d.ts +146 -0
- package/dist/customTypes/postgis.d.ts.map +1 -0
- package/dist/customTypes/postgis.js +315 -0
- package/dist/customTypes/postgis.js.map +1 -0
- package/dist/customTypes/ranges.d.ts +128 -0
- package/dist/customTypes/ranges.d.ts.map +1 -0
- package/dist/customTypes/ranges.js +257 -0
- package/dist/customTypes/ranges.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/0.1.0-alpha.0_initial_setup.d.ts +29 -0
- package/dist/migrations/0.1.0-alpha.0_initial_setup.d.ts.map +1 -0
- package/dist/migrations/0.1.0-alpha.0_initial_setup.js +72 -0
- package/dist/migrations/0.1.0-alpha.0_initial_setup.js.map +1 -0
- package/dist/migrations/_example_migration.d.ts +31 -0
- package/dist/migrations/_example_migration.d.ts.map +1 -0
- package/dist/migrations/_example_migration.js +75 -0
- package/dist/migrations/_example_migration.js.map +1 -0
- package/dist/plugins/definePlugin.d.ts +49 -0
- package/dist/plugins/definePlugin.d.ts.map +1 -0
- package/dist/plugins/definePlugin.js +131 -0
- package/dist/plugins/definePlugin.js.map +1 -0
- package/dist/plugins/softDelete.d.ts +179 -0
- package/dist/plugins/softDelete.d.ts.map +1 -0
- package/dist/plugins/softDelete.js +235 -0
- package/dist/plugins/softDelete.js.map +1 -0
- package/dist/routes/auth.route.d.ts +14 -0
- package/dist/routes/auth.route.d.ts.map +1 -0
- package/dist/routes/auth.route.js +421 -0
- package/dist/routes/auth.route.js.map +1 -0
- package/dist/routes/file.route.d.ts +7 -0
- package/dist/routes/file.route.d.ts.map +1 -0
- package/dist/routes/file.route.js +274 -0
- package/dist/routes/file.route.js.map +1 -0
- package/dist/routes/items.route.d.ts +7 -0
- package/dist/routes/items.route.d.ts.map +1 -0
- package/dist/routes/items.route.js +369 -0
- package/dist/routes/items.route.js.map +1 -0
- package/dist/routes/migration.route.d.ts +7 -0
- package/dist/routes/migration.route.d.ts.map +1 -0
- package/dist/routes/migration.route.js +225 -0
- package/dist/routes/migration.route.js.map +1 -0
- package/dist/routes/notification.route.d.ts +7 -0
- package/dist/routes/notification.route.d.ts.map +1 -0
- package/dist/routes/notification.route.js +124 -0
- package/dist/routes/notification.route.js.map +1 -0
- package/dist/routes/openapi.route.d.ts +7 -0
- package/dist/routes/openapi.route.d.ts.map +1 -0
- package/dist/routes/openapi.route.js +2169 -0
- package/dist/routes/openapi.route.js.map +1 -0
- package/dist/routes/permission.route.d.ts +7 -0
- package/dist/routes/permission.route.d.ts.map +1 -0
- package/dist/routes/permission.route.js +158 -0
- package/dist/routes/permission.route.js.map +1 -0
- package/dist/routes/realtime.route.d.ts +21 -0
- package/dist/routes/realtime.route.d.ts.map +1 -0
- package/dist/routes/realtime.route.js +243 -0
- package/dist/routes/realtime.route.js.map +1 -0
- package/dist/routes/reports.route.d.ts +7 -0
- package/dist/routes/reports.route.d.ts.map +1 -0
- package/dist/routes/reports.route.js +95 -0
- package/dist/routes/reports.route.js.map +1 -0
- package/dist/routes/schema.route.d.ts +7 -0
- package/dist/routes/schema.route.d.ts.map +1 -0
- package/dist/routes/schema.route.js +1780 -0
- package/dist/routes/schema.route.js.map +1 -0
- package/dist/routes/settings.route.d.ts +7 -0
- package/dist/routes/settings.route.d.ts.map +1 -0
- package/dist/routes/settings.route.js +154 -0
- package/dist/routes/settings.route.js.map +1 -0
- package/dist/routes/templates.route.d.ts +7 -0
- package/dist/routes/templates.route.d.ts.map +1 -0
- package/dist/routes/templates.route.js +91 -0
- package/dist/routes/templates.route.js.map +1 -0
- package/dist/routes/utils.route.d.ts +7 -0
- package/dist/routes/utils.route.d.ts.map +1 -0
- package/dist/routes/utils.route.js +33 -0
- package/dist/routes/utils.route.js.map +1 -0
- package/dist/routes/workflow.route.d.ts +7 -0
- package/dist/routes/workflow.route.d.ts.map +1 -0
- package/dist/routes/workflow.route.js +787 -0
- package/dist/routes/workflow.route.js.map +1 -0
- package/dist/services/AssetsService.d.ts +39 -0
- package/dist/services/AssetsService.d.ts.map +1 -0
- package/dist/services/AssetsService.js +255 -0
- package/dist/services/AssetsService.js.map +1 -0
- package/dist/services/CacheService.d.ts +169 -0
- package/dist/services/CacheService.d.ts.map +1 -0
- package/dist/services/CacheService.js +722 -0
- package/dist/services/CacheService.js.map +1 -0
- package/dist/services/FilesService.d.ts +30 -0
- package/dist/services/FilesService.d.ts.map +1 -0
- package/dist/services/FilesService.js +268 -0
- package/dist/services/FilesService.js.map +1 -0
- package/dist/services/HooksManager.d.ts +38 -0
- package/dist/services/HooksManager.d.ts.map +1 -0
- package/dist/services/HooksManager.js +165 -0
- package/dist/services/HooksManager.js.map +1 -0
- package/dist/services/ItemsService.d.ts +273 -0
- package/dist/services/ItemsService.d.ts.map +1 -0
- package/dist/services/ItemsService.js +2458 -0
- package/dist/services/ItemsService.js.map +1 -0
- package/dist/services/MailService.d.ts +76 -0
- package/dist/services/MailService.d.ts.map +1 -0
- package/dist/services/MailService.js +585 -0
- package/dist/services/MailService.js.map +1 -0
- package/dist/services/MigrationService.d.ts +243 -0
- package/dist/services/MigrationService.d.ts.map +1 -0
- package/dist/services/MigrationService.js +914 -0
- package/dist/services/MigrationService.js.map +1 -0
- package/dist/services/NotificationService.d.ts +35 -0
- package/dist/services/NotificationService.d.ts.map +1 -0
- package/dist/services/NotificationService.js +159 -0
- package/dist/services/NotificationService.js.map +1 -0
- package/dist/services/PermissionService.d.ts +128 -0
- package/dist/services/PermissionService.d.ts.map +1 -0
- package/dist/services/PermissionService.js +373 -0
- package/dist/services/PermissionService.js.map +1 -0
- package/dist/services/PluginManager.d.ts +138 -0
- package/dist/services/PluginManager.d.ts.map +1 -0
- package/dist/services/PluginManager.js +463 -0
- package/dist/services/PluginManager.js.map +1 -0
- package/dist/services/RealtimeService.d.ts +209 -0
- package/dist/services/RealtimeService.d.ts.map +1 -0
- package/dist/services/RealtimeService.js +978 -0
- package/dist/services/RealtimeService.js.map +1 -0
- package/dist/services/ReportService.d.ts +13 -0
- package/dist/services/ReportService.d.ts.map +1 -0
- package/dist/services/ReportService.js +91 -0
- package/dist/services/ReportService.js.map +1 -0
- package/dist/services/SettingsService.d.ts +60 -0
- package/dist/services/SettingsService.d.ts.map +1 -0
- package/dist/services/SettingsService.js +474 -0
- package/dist/services/SettingsService.js.map +1 -0
- package/dist/services/SocketService.d.ts +129 -0
- package/dist/services/SocketService.d.ts.map +1 -0
- package/dist/services/SocketService.js +600 -0
- package/dist/services/SocketService.js.map +1 -0
- package/dist/services/StatsService.d.ts +10 -0
- package/dist/services/StatsService.d.ts.map +1 -0
- package/dist/services/StatsService.js +40 -0
- package/dist/services/StatsService.js.map +1 -0
- package/dist/services/StorageService.d.ts +20 -0
- package/dist/services/StorageService.d.ts.map +1 -0
- package/dist/services/StorageService.js +164 -0
- package/dist/services/StorageService.js.map +1 -0
- package/dist/services/TasksService.d.ts +74 -0
- package/dist/services/TasksService.d.ts.map +1 -0
- package/dist/services/TasksService.js +404 -0
- package/dist/services/TasksService.js.map +1 -0
- package/dist/services/WorkflowService.d.ts +305 -0
- package/dist/services/WorkflowService.d.ts.map +1 -0
- package/dist/services/WorkflowService.js +1811 -0
- package/dist/services/WorkflowService.js.map +1 -0
- package/dist/templates/logo/logo.png +0 -0
- package/dist/templates/mails/default.liquid +23 -0
- package/dist/types/aggregation.d.ts +40 -0
- package/dist/types/aggregation.d.ts.map +1 -0
- package/dist/types/aggregation.js +6 -0
- package/dist/types/aggregation.js.map +1 -0
- package/dist/types/assets.d.ts +32 -0
- package/dist/types/assets.d.ts.map +1 -0
- package/dist/types/assets.js +6 -0
- package/dist/types/assets.js.map +1 -0
- package/dist/types/auth.d.ts +50 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +6 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/cache.d.ts +47 -0
- package/dist/types/cache.d.ts.map +1 -0
- package/dist/types/cache.js +6 -0
- package/dist/types/cache.js.map +1 -0
- package/dist/types/database.d.ts +16 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/database.js +6 -0
- package/dist/types/database.js.map +1 -0
- package/dist/types/fields.d.ts +71 -0
- package/dist/types/fields.d.ts.map +1 -0
- package/dist/types/fields.js +6 -0
- package/dist/types/fields.js.map +1 -0
- package/dist/types/files.d.ts +33 -0
- package/dist/types/files.d.ts.map +1 -0
- package/dist/types/files.js +6 -0
- package/dist/types/files.js.map +1 -0
- package/dist/types/hooks.d.ts +29 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/hooks.js +6 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/types/import-export.d.ts +62 -0
- package/dist/types/import-export.d.ts.map +1 -0
- package/dist/types/import-export.js +6 -0
- package/dist/types/import-export.js.map +1 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +58 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mail.d.ts +34 -0
- package/dist/types/mail.d.ts.map +1 -0
- package/dist/types/mail.js +6 -0
- package/dist/types/mail.js.map +1 -0
- package/dist/types/notifications.d.ts +16 -0
- package/dist/types/notifications.d.ts.map +1 -0
- package/dist/types/notifications.js +6 -0
- package/dist/types/notifications.js.map +1 -0
- package/dist/types/plugin.d.ts +351 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +8 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/types/query.d.ts +71 -0
- package/dist/types/query.d.ts.map +1 -0
- package/dist/types/query.js +6 -0
- package/dist/types/query.js.map +1 -0
- package/dist/types/relations.d.ts +111 -0
- package/dist/types/relations.d.ts.map +1 -0
- package/dist/types/relations.js +6 -0
- package/dist/types/relations.js.map +1 -0
- package/dist/types/reports.d.ts +17 -0
- package/dist/types/reports.d.ts.map +1 -0
- package/dist/types/reports.js +6 -0
- package/dist/types/reports.js.map +1 -0
- package/dist/types/schema.d.ts +26 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +6 -0
- package/dist/types/schema.js.map +1 -0
- package/dist/types/seed.d.ts +27 -0
- package/dist/types/seed.d.ts.map +1 -0
- package/dist/types/seed.js +6 -0
- package/dist/types/seed.js.map +1 -0
- package/dist/types/services.d.ts +68 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +6 -0
- package/dist/types/services.js.map +1 -0
- package/dist/types/settings.d.ts +36 -0
- package/dist/types/settings.d.ts.map +1 -0
- package/dist/types/settings.js +6 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/sockets.d.ts +26 -0
- package/dist/types/sockets.d.ts.map +1 -0
- package/dist/types/sockets.js +6 -0
- package/dist/types/sockets.js.map +1 -0
- package/dist/types/sort.d.ts +25 -0
- package/dist/types/sort.d.ts.map +1 -0
- package/dist/types/sort.js +6 -0
- package/dist/types/sort.js.map +1 -0
- package/dist/types/spatial.d.ts +19 -0
- package/dist/types/spatial.d.ts.map +1 -0
- package/dist/types/spatial.js +6 -0
- package/dist/types/spatial.js.map +1 -0
- package/dist/types/stats.d.ts +21 -0
- package/dist/types/stats.d.ts.map +1 -0
- package/dist/types/stats.js +6 -0
- package/dist/types/stats.js.map +1 -0
- package/dist/types/storage.d.ts +19 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +6 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/types/tasks.d.ts +14 -0
- package/dist/types/tasks.d.ts.map +1 -0
- package/dist/types/tasks.js +6 -0
- package/dist/types/tasks.js.map +1 -0
- package/dist/types/utils.d.ts +54 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/utils.js +6 -0
- package/dist/types/utils.js.map +1 -0
- package/dist/types/workflow.d.ts +17 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +6 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/utils/aggregationUtils.d.ts +192 -0
- package/dist/utils/aggregationUtils.d.ts.map +1 -0
- package/dist/utils/aggregationUtils.js +450 -0
- package/dist/utils/aggregationUtils.js.map +1 -0
- package/dist/utils/auth.d.ts +93 -0
- package/dist/utils/auth.d.ts.map +1 -0
- package/dist/utils/auth.js +557 -0
- package/dist/utils/auth.js.map +1 -0
- package/dist/utils/cache.d.ts +64 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +464 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/common.d.ts +53 -0
- package/dist/utils/common.d.ts.map +1 -0
- package/dist/utils/common.js +162 -0
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/db.d.ts +101 -0
- package/dist/utils/db.d.ts.map +1 -0
- package/dist/utils/db.js +413 -0
- package/dist/utils/db.js.map +1 -0
- package/dist/utils/dirname.d.ts +30 -0
- package/dist/utils/dirname.d.ts.map +1 -0
- package/dist/utils/dirname.js +95 -0
- package/dist/utils/dirname.js.map +1 -0
- package/dist/utils/dynamicVariableResolver.d.ts +17 -0
- package/dist/utils/dynamicVariableResolver.d.ts.map +1 -0
- package/dist/utils/dynamicVariableResolver.js +262 -0
- package/dist/utils/dynamicVariableResolver.js.map +1 -0
- package/dist/utils/env.d.ts +38 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +80 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/errorHandler.d.ts +14 -0
- package/dist/utils/errorHandler.d.ts.map +1 -0
- package/dist/utils/errorHandler.js +79 -0
- package/dist/utils/errorHandler.js.map +1 -0
- package/dist/utils/fieldExpansion.d.ts +30 -0
- package/dist/utils/fieldExpansion.d.ts.map +1 -0
- package/dist/utils/fieldExpansion.js +145 -0
- package/dist/utils/fieldExpansion.js.map +1 -0
- package/dist/utils/fieldUtils.d.ts +179 -0
- package/dist/utils/fieldUtils.d.ts.map +1 -0
- package/dist/utils/fieldUtils.js +424 -0
- package/dist/utils/fieldUtils.js.map +1 -0
- package/dist/utils/filterOperators.d.ts +472 -0
- package/dist/utils/filterOperators.d.ts.map +1 -0
- package/dist/utils/filterOperators.js +1229 -0
- package/dist/utils/filterOperators.js.map +1 -0
- package/dist/utils/importUtils.d.ts +127 -0
- package/dist/utils/importUtils.d.ts.map +1 -0
- package/dist/utils/importUtils.js +437 -0
- package/dist/utils/importUtils.js.map +1 -0
- package/dist/utils/index.d.ts +75 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +101 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +41 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +217 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/orderUtils.d.ts +117 -0
- package/dist/utils/orderUtils.d.ts.map +1 -0
- package/dist/utils/orderUtils.js +249 -0
- package/dist/utils/orderUtils.js.map +1 -0
- package/dist/utils/queryBuilder.d.ts +118 -0
- package/dist/utils/queryBuilder.d.ts.map +1 -0
- package/dist/utils/queryBuilder.js +489 -0
- package/dist/utils/queryBuilder.js.map +1 -0
- package/dist/utils/relationLoader.d.ts +65 -0
- package/dist/utils/relationLoader.d.ts.map +1 -0
- package/dist/utils/relationLoader.js +1081 -0
- package/dist/utils/relationLoader.js.map +1 -0
- package/dist/utils/relationPathResolver.d.ts +30 -0
- package/dist/utils/relationPathResolver.d.ts.map +1 -0
- package/dist/utils/relationPathResolver.js +173 -0
- package/dist/utils/relationPathResolver.js.map +1 -0
- package/dist/utils/relationUtils.d.ts +139 -0
- package/dist/utils/relationUtils.d.ts.map +1 -0
- package/dist/utils/relationUtils.js +711 -0
- package/dist/utils/relationUtils.js.map +1 -0
- package/dist/utils/router.d.ts +6 -0
- package/dist/utils/router.d.ts.map +1 -0
- package/dist/utils/router.js +95 -0
- package/dist/utils/router.js.map +1 -0
- package/dist/utils/schema.d.ts +88 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +24 -0
- package/dist/utils/schema.js.map +1 -0
- package/dist/utils/schemaManager.d.ts +238 -0
- package/dist/utils/schemaManager.d.ts.map +1 -0
- package/dist/utils/schemaManager.js +1992 -0
- package/dist/utils/schemaManager.js.map +1 -0
- package/dist/utils/schemaValidator.d.ts +83 -0
- package/dist/utils/schemaValidator.d.ts.map +1 -0
- package/dist/utils/schemaValidator.js +491 -0
- package/dist/utils/schemaValidator.js.map +1 -0
- package/dist/utils/seed.d.ts +45 -0
- package/dist/utils/seed.d.ts.map +1 -0
- package/dist/utils/seed.js +248 -0
- package/dist/utils/seed.js.map +1 -0
- package/dist/utils/sessionCleanup.d.ts +10 -0
- package/dist/utils/sessionCleanup.d.ts.map +1 -0
- package/dist/utils/sessionCleanup.js +49 -0
- package/dist/utils/sessionCleanup.js.map +1 -0
- package/dist/utils/sortUtils.d.ts +117 -0
- package/dist/utils/sortUtils.d.ts.map +1 -0
- package/dist/utils/sortUtils.js +232 -0
- package/dist/utils/sortUtils.js.map +1 -0
- package/dist/utils/spatialUtils.d.ts +244 -0
- package/dist/utils/spatialUtils.d.ts.map +1 -0
- package/dist/utils/spatialUtils.js +359 -0
- package/dist/utils/spatialUtils.js.map +1 -0
- package/dist/utils/systemschema.d.ts +11040 -0
- package/dist/utils/systemschema.d.ts.map +1 -0
- package/dist/utils/systemschema.js +1777 -0
- package/dist/utils/systemschema.js.map +1 -0
- package/dist/utils/tenantUtils.d.ts +34 -0
- package/dist/utils/tenantUtils.d.ts.map +1 -0
- package/dist/utils/tenantUtils.js +124 -0
- package/dist/utils/tenantUtils.js.map +1 -0
- package/dist/utils/typeMapper.d.ts +25 -0
- package/dist/utils/typeMapper.d.ts.map +1 -0
- package/dist/utils/typeMapper.js +282 -0
- package/dist/utils/typeMapper.js.map +1 -0
- package/dist/utils/valueValidator.d.ts +60 -0
- package/dist/utils/valueValidator.d.ts.map +1 -0
- package/dist/utils/valueValidator.js +303 -0
- package/dist/utils/valueValidator.js.map +1 -0
- package/dist/utils/workflow.d.ts +87 -0
- package/dist/utils/workflow.d.ts.map +1 -0
- package/dist/utils/workflow.js +205 -0
- package/dist/utils/workflow.js.map +1 -0
- package/package.json +115 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RealtimeService - PostgreSQL WAL-based realtime change data capture
|
|
3
|
+
*
|
|
4
|
+
* This service uses PostgreSQL logical replication to capture database changes
|
|
5
|
+
* and broadcast them via WebSockets using the existing SocketService.
|
|
6
|
+
*
|
|
7
|
+
* Similar to Supabase Realtime, it allows enabling realtime on specific collections
|
|
8
|
+
* with granular control over which actions (insert, update, delete) trigger events.
|
|
9
|
+
*
|
|
10
|
+
* Realtime configuration is stored in the schema definition:
|
|
11
|
+
* {
|
|
12
|
+
* "realtime": {
|
|
13
|
+
* "enabled": true,
|
|
14
|
+
* "actions": ["insert", "update", "delete"]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* NOTE: This service ONLY works when PostgreSQL has wal_level=logical configured.
|
|
19
|
+
* If WAL is not available, no realtime broadcasting will occur.
|
|
20
|
+
*/
|
|
21
|
+
import env from "../utils/env.js";
|
|
22
|
+
import { db } from "../utils/db.js";
|
|
23
|
+
import { sql } from "drizzle-orm";
|
|
24
|
+
// Constants
|
|
25
|
+
const PUBLICATION_NAME = 'baasix_realtime';
|
|
26
|
+
const SLOT_NAME = 'baasix_realtime_slot';
|
|
27
|
+
// Redis key for WAL consumer leader election
|
|
28
|
+
const WAL_LEADER_KEY = 'baasix:wal:leader';
|
|
29
|
+
const WAL_LEADER_TTL = 30; // seconds
|
|
30
|
+
/**
|
|
31
|
+
* Custom PgoutputPlugin that doesn't send the 'messages' option
|
|
32
|
+
* This is compatible with PostgreSQL 10+ (the default library sends 'messages' which requires PG 14+)
|
|
33
|
+
*/
|
|
34
|
+
class CompatiblePgoutputPlugin {
|
|
35
|
+
options;
|
|
36
|
+
parser;
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.options = options;
|
|
39
|
+
this.parser = null;
|
|
40
|
+
}
|
|
41
|
+
get name() {
|
|
42
|
+
return 'pgoutput';
|
|
43
|
+
}
|
|
44
|
+
async initParser() {
|
|
45
|
+
if (!this.parser) {
|
|
46
|
+
const { PgoutputParser } = await import('pg-logical-replication/dist/output-plugins/pgoutput/pgoutput-parser.js');
|
|
47
|
+
this.parser = new PgoutputParser();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
parse(buffer) {
|
|
51
|
+
return this.parser.parse(buffer);
|
|
52
|
+
}
|
|
53
|
+
start(client, slotName, lastLsn) {
|
|
54
|
+
// Only send proto_version and publication_names - no 'messages' option for PG < 14 compatibility
|
|
55
|
+
const options = [
|
|
56
|
+
`proto_version '${this.options.protoVersion}'`,
|
|
57
|
+
`publication_names '${this.options.publicationNames.join(',')}'`,
|
|
58
|
+
];
|
|
59
|
+
const sql = `START_REPLICATION SLOT "${slotName}" LOGICAL ${lastLsn} (${options.join(', ')})`;
|
|
60
|
+
return client.query(sql);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class RealtimeService {
|
|
64
|
+
initialized = false;
|
|
65
|
+
connected = false;
|
|
66
|
+
replicationService = null;
|
|
67
|
+
socketService = null;
|
|
68
|
+
// Map of collection name to realtime config
|
|
69
|
+
collectionConfigs = new Map();
|
|
70
|
+
reconnectTimeout = null;
|
|
71
|
+
shuttingDown = false;
|
|
72
|
+
walAvailable = false;
|
|
73
|
+
// Whether this instance is the WAL consumer leader
|
|
74
|
+
isWalLeader = false;
|
|
75
|
+
// Unique instance ID for Redis leader election
|
|
76
|
+
instanceId = `${process.pid}-${Date.now()}`;
|
|
77
|
+
// Redis client for leader election
|
|
78
|
+
redisClient = null;
|
|
79
|
+
// Leader lock renewal interval
|
|
80
|
+
leaderRenewalInterval = null;
|
|
81
|
+
constructor() {
|
|
82
|
+
console.info("Realtime Service instance created");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Set the socket service instance for broadcasting
|
|
86
|
+
*/
|
|
87
|
+
setSocketService(socketService) {
|
|
88
|
+
this.socketService = socketService;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if PostgreSQL logical replication is enabled
|
|
92
|
+
*/
|
|
93
|
+
async checkLogicalReplicationEnabled() {
|
|
94
|
+
try {
|
|
95
|
+
const result = await db.execute(sql `SHOW wal_level`);
|
|
96
|
+
const walLevel = result[0]?.wal_level || 'unknown';
|
|
97
|
+
return {
|
|
98
|
+
enabled: walLevel === 'logical',
|
|
99
|
+
walLevel,
|
|
100
|
+
error: walLevel !== 'logical'
|
|
101
|
+
? `PostgreSQL wal_level is '${walLevel}'. Logical replication requires wal_level = 'logical'. Please update postgresql.conf and restart PostgreSQL.`
|
|
102
|
+
: undefined
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
enabled: false,
|
|
108
|
+
walLevel: 'unknown',
|
|
109
|
+
error: `Failed to check wal_level: ${error.message}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check replication configuration
|
|
115
|
+
*/
|
|
116
|
+
async checkReplicationConfig() {
|
|
117
|
+
const issues = [];
|
|
118
|
+
try {
|
|
119
|
+
const [walLevelResult, slotsResult, sendersResult] = await Promise.all([
|
|
120
|
+
db.execute(sql `SHOW wal_level`),
|
|
121
|
+
db.execute(sql `SHOW max_replication_slots`),
|
|
122
|
+
db.execute(sql `SHOW max_wal_senders`)
|
|
123
|
+
]);
|
|
124
|
+
const walLevel = walLevelResult[0]?.wal_level || 'unknown';
|
|
125
|
+
const maxReplicationSlots = parseInt(slotsResult[0]?.max_replication_slots || '0');
|
|
126
|
+
const maxWalSenders = parseInt(sendersResult[0]?.max_wal_senders || '0');
|
|
127
|
+
if (walLevel !== 'logical') {
|
|
128
|
+
issues.push(`wal_level is '${walLevel}', should be 'logical'`);
|
|
129
|
+
}
|
|
130
|
+
if (maxReplicationSlots < 1) {
|
|
131
|
+
issues.push(`max_replication_slots is ${maxReplicationSlots}, should be at least 1`);
|
|
132
|
+
}
|
|
133
|
+
if (maxWalSenders < 1) {
|
|
134
|
+
issues.push(`max_wal_senders is ${maxWalSenders}, should be at least 1`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
walLevel,
|
|
138
|
+
maxReplicationSlots,
|
|
139
|
+
maxWalSenders,
|
|
140
|
+
isConfigured: issues.length === 0,
|
|
141
|
+
issues
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
issues.push(`Failed to check config: ${error.message}`);
|
|
146
|
+
return {
|
|
147
|
+
walLevel: 'unknown',
|
|
148
|
+
maxReplicationSlots: 0,
|
|
149
|
+
maxWalSenders: 0,
|
|
150
|
+
isConfigured: false,
|
|
151
|
+
issues
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Initialize the realtime service
|
|
157
|
+
* Creates publication and replication slot if they don't exist
|
|
158
|
+
* Only works when PostgreSQL has wal_level=logical configured
|
|
159
|
+
*/
|
|
160
|
+
async initialize() {
|
|
161
|
+
if (this.initialized) {
|
|
162
|
+
console.warn("Realtime service already initialized");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
console.info("Initializing Realtime Service...");
|
|
166
|
+
// Check if logical replication is enabled
|
|
167
|
+
const replicationCheck = await this.checkLogicalReplicationEnabled();
|
|
168
|
+
if (!replicationCheck.enabled) {
|
|
169
|
+
console.warn(`⚠️ PostgreSQL logical replication not available (wal_level=${replicationCheck.walLevel})`);
|
|
170
|
+
console.warn(" WAL-based realtime is disabled. No automatic broadcasting will occur.");
|
|
171
|
+
console.warn(" To enable, set wal_level=logical in postgresql.conf and restart PostgreSQL.");
|
|
172
|
+
this.walAvailable = false;
|
|
173
|
+
this.initialized = true;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this.walAvailable = true;
|
|
177
|
+
// Create publication if it doesn't exist
|
|
178
|
+
await this.ensurePublicationExists();
|
|
179
|
+
// Create replication slot if it doesn't exist
|
|
180
|
+
await this.ensureReplicationSlotExists();
|
|
181
|
+
// Load enabled collections from database
|
|
182
|
+
await this.loadEnabledCollections();
|
|
183
|
+
// Sync publication with enabled collections
|
|
184
|
+
await this.syncPublicationWithCollections();
|
|
185
|
+
this.initialized = true;
|
|
186
|
+
console.info("✅ Realtime Service initialized with WAL support");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if WAL-based realtime is available
|
|
190
|
+
*/
|
|
191
|
+
isWalAvailable() {
|
|
192
|
+
return this.walAvailable;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Ensure the publication exists
|
|
196
|
+
*/
|
|
197
|
+
async ensurePublicationExists() {
|
|
198
|
+
try {
|
|
199
|
+
// Check if publication exists
|
|
200
|
+
const result = await db.execute(sql `
|
|
201
|
+
SELECT pubname FROM pg_publication WHERE pubname = ${PUBLICATION_NAME}
|
|
202
|
+
`);
|
|
203
|
+
if (result.length === 0) {
|
|
204
|
+
// Create empty publication (tables will be added as they're enabled)
|
|
205
|
+
await db.execute(sql.raw(`CREATE PUBLICATION ${PUBLICATION_NAME}`));
|
|
206
|
+
console.info(`Created publication: ${PUBLICATION_NAME}`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
console.info(`Publication ${PUBLICATION_NAME} already exists`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
// Handle case where publication already exists (race condition)
|
|
214
|
+
if (!error.message.includes('already exists')) {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Ensure the replication slot exists
|
|
221
|
+
*/
|
|
222
|
+
async ensureReplicationSlotExists() {
|
|
223
|
+
try {
|
|
224
|
+
// Check if slot exists
|
|
225
|
+
const result = await db.execute(sql `
|
|
226
|
+
SELECT slot_name FROM pg_replication_slots WHERE slot_name = ${SLOT_NAME}
|
|
227
|
+
`);
|
|
228
|
+
if (result.length === 0) {
|
|
229
|
+
// Create replication slot
|
|
230
|
+
await db.execute(sql.raw(`
|
|
231
|
+
SELECT pg_create_logical_replication_slot('${SLOT_NAME}', 'pgoutput')
|
|
232
|
+
`));
|
|
233
|
+
console.info(`Created replication slot: ${SLOT_NAME}`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.info(`Replication slot ${SLOT_NAME} already exists`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// Handle case where slot already exists (race condition)
|
|
241
|
+
if (!error.message.includes('already exists')) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Load collections with realtime enabled from schema definitions
|
|
248
|
+
* Supports both old format (realtime: true) and new format (realtime: { enabled: true, actions: [...] })
|
|
249
|
+
*/
|
|
250
|
+
async loadEnabledCollections() {
|
|
251
|
+
try {
|
|
252
|
+
// Query for collections with realtime enabled (supports both old and new format)
|
|
253
|
+
const result = await db.execute(sql `
|
|
254
|
+
SELECT "collectionName", schema
|
|
255
|
+
FROM "baasix_SchemaDefinition"
|
|
256
|
+
WHERE schema->>'realtime' = 'true'
|
|
257
|
+
OR schema->'realtime'->>'enabled' = 'true'
|
|
258
|
+
`);
|
|
259
|
+
this.collectionConfigs.clear();
|
|
260
|
+
for (const row of result) {
|
|
261
|
+
const schema = typeof row.schema === 'string' ? JSON.parse(row.schema) : row.schema;
|
|
262
|
+
const realtimeConfig = schema.realtime;
|
|
263
|
+
// Handle old format (realtime: true)
|
|
264
|
+
if (realtimeConfig === true) {
|
|
265
|
+
this.collectionConfigs.set(row.collectionName, {
|
|
266
|
+
enabled: true,
|
|
267
|
+
actions: ['insert', 'update', 'delete']
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Handle new format (realtime: { enabled: true, actions: [...] })
|
|
271
|
+
else if (typeof realtimeConfig === 'object') {
|
|
272
|
+
this.collectionConfigs.set(row.collectionName, {
|
|
273
|
+
enabled: realtimeConfig.enabled !== false,
|
|
274
|
+
actions: realtimeConfig.actions || ['insert', 'update', 'delete']
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
console.info(`Loaded ${this.collectionConfigs.size} realtime-enabled collections`);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.warn(`Failed to load realtime collections: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Sync the PostgreSQL publication with enabled collections
|
|
286
|
+
*/
|
|
287
|
+
async syncPublicationWithCollections() {
|
|
288
|
+
if (!this.walAvailable)
|
|
289
|
+
return;
|
|
290
|
+
try {
|
|
291
|
+
// Get current tables in publication
|
|
292
|
+
const currentTables = await db.execute(sql `
|
|
293
|
+
SELECT schemaname, tablename
|
|
294
|
+
FROM pg_publication_tables
|
|
295
|
+
WHERE pubname = ${PUBLICATION_NAME}
|
|
296
|
+
`);
|
|
297
|
+
const currentTableNames = new Set(currentTables.map(t => t.tablename));
|
|
298
|
+
// Add missing tables
|
|
299
|
+
for (const [collectionName, config] of this.collectionConfigs) {
|
|
300
|
+
if (config.enabled && !currentTableNames.has(collectionName)) {
|
|
301
|
+
try {
|
|
302
|
+
await db.execute(sql.raw(`ALTER PUBLICATION ${PUBLICATION_NAME} ADD TABLE "${collectionName}"`));
|
|
303
|
+
console.info(`Added ${collectionName} to publication`);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (!error.message.includes('already member')) {
|
|
307
|
+
console.warn(`Failed to add ${collectionName} to publication: ${error.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Remove tables that are no longer enabled
|
|
313
|
+
for (const tableName of currentTableNames) {
|
|
314
|
+
const config = this.collectionConfigs.get(tableName);
|
|
315
|
+
if (!config || !config.enabled) {
|
|
316
|
+
try {
|
|
317
|
+
await db.execute(sql.raw(`ALTER PUBLICATION ${PUBLICATION_NAME} DROP TABLE "${tableName}"`));
|
|
318
|
+
console.info(`Removed ${tableName} from publication`);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
console.warn(`Failed to remove ${tableName} from publication: ${error.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.warn(`Failed to sync publication: ${error.message}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Start consuming WAL changes
|
|
332
|
+
*/
|
|
333
|
+
async startConsuming() {
|
|
334
|
+
if (!this.initialized) {
|
|
335
|
+
throw new Error("Realtime service not initialized. Call initialize() first.");
|
|
336
|
+
}
|
|
337
|
+
if (!this.walAvailable) {
|
|
338
|
+
console.info("WAL not available, skipping WAL consumer startup");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Check if WAL consumer is explicitly disabled via environment variable
|
|
342
|
+
if (env.get("REALTIME_WAL_CONSUMER_ENABLED") === "false") {
|
|
343
|
+
console.info("WAL consumer explicitly disabled via REALTIME_WAL_CONSUMER_ENABLED=false");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (this.connected) {
|
|
347
|
+
console.warn("Already consuming WAL changes");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// Multi-instance mode: Use Redis for leader election
|
|
351
|
+
// Only one instance should consume WAL to avoid duplicate processing
|
|
352
|
+
if (env.get("SOCKET_REDIS_ENABLED") === "true") {
|
|
353
|
+
const isLeader = await this.tryBecomeWalLeader();
|
|
354
|
+
if (!isLeader) {
|
|
355
|
+
console.info("Another instance is the WAL consumer leader. This instance will not consume WAL.");
|
|
356
|
+
console.info("Realtime broadcasts will still work via Redis adapter.");
|
|
357
|
+
this.isWalLeader = false;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.isWalLeader = true;
|
|
361
|
+
this.startLeaderRenewal();
|
|
362
|
+
console.info("This instance became the WAL consumer leader");
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// Single instance mode - no Redis, this instance is the leader
|
|
366
|
+
this.isWalLeader = true;
|
|
367
|
+
console.info("Single instance mode - this instance will consume WAL");
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
// Dynamic import to avoid issues if package is not installed
|
|
371
|
+
const { LogicalReplicationService } = await import('pg-logical-replication');
|
|
372
|
+
const connectionString = env.get("DATABASE_URL");
|
|
373
|
+
if (!connectionString) {
|
|
374
|
+
throw new Error("DATABASE_URL is required for realtime service");
|
|
375
|
+
}
|
|
376
|
+
this.replicationService = new LogicalReplicationService({ connectionString }, { acknowledge: { auto: true, timeoutSeconds: 10 } });
|
|
377
|
+
// Use our compatible plugin that doesn't send the 'messages' option (PG < 14 compatible)
|
|
378
|
+
const plugin = new CompatiblePgoutputPlugin({
|
|
379
|
+
protoVersion: 1,
|
|
380
|
+
publicationNames: [PUBLICATION_NAME]
|
|
381
|
+
});
|
|
382
|
+
await plugin.initParser();
|
|
383
|
+
// Handle incoming WAL messages
|
|
384
|
+
this.replicationService.on('data', (lsn, log) => {
|
|
385
|
+
this.handleWalMessage(log);
|
|
386
|
+
});
|
|
387
|
+
// Handle errors
|
|
388
|
+
this.replicationService.on('error', (error) => {
|
|
389
|
+
console.error("Realtime replication error:", error);
|
|
390
|
+
this.handleDisconnect();
|
|
391
|
+
});
|
|
392
|
+
// Start subscription
|
|
393
|
+
await this.replicationService.subscribe(plugin, SLOT_NAME);
|
|
394
|
+
this.connected = true;
|
|
395
|
+
console.info("✅ Started consuming WAL changes");
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
// Check if this is a "slot already active" error
|
|
399
|
+
const errorMsg = error.message?.toLowerCase() || '';
|
|
400
|
+
if (errorMsg.includes('is active for pid') || error.code === '55006') {
|
|
401
|
+
console.warn("WAL replication slot is already active on another connection");
|
|
402
|
+
this.isWalLeader = false;
|
|
403
|
+
this.stopLeaderRenewal();
|
|
404
|
+
await this.releaseWalLeaderLock();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
console.error("Failed to start WAL consumer:", error);
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Try to become the WAL consumer leader using Redis
|
|
413
|
+
* Uses SETNX with TTL - only one instance can hold the key
|
|
414
|
+
*/
|
|
415
|
+
async tryBecomeWalLeader() {
|
|
416
|
+
try {
|
|
417
|
+
const redis = await this.getRedisClient();
|
|
418
|
+
if (!redis)
|
|
419
|
+
return true; // No Redis = single instance mode
|
|
420
|
+
// Try to set the key only if it doesn't exist (NX), with TTL
|
|
421
|
+
const result = await redis.set(WAL_LEADER_KEY, this.instanceId, 'EX', WAL_LEADER_TTL, 'NX');
|
|
422
|
+
return result === 'OK';
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
console.warn(`Failed to acquire WAL leader via Redis: ${error.message}`);
|
|
426
|
+
// On Redis error, allow this instance to consume WAL (fallback to single-instance behavior)
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Start periodic renewal of leader lock
|
|
432
|
+
*/
|
|
433
|
+
startLeaderRenewal() {
|
|
434
|
+
if (this.leaderRenewalInterval)
|
|
435
|
+
return;
|
|
436
|
+
// Renew every 10 seconds (TTL is 30s, so we have buffer)
|
|
437
|
+
this.leaderRenewalInterval = setInterval(async () => {
|
|
438
|
+
try {
|
|
439
|
+
const redis = await this.getRedisClient();
|
|
440
|
+
if (!redis)
|
|
441
|
+
return;
|
|
442
|
+
// Only renew if we're still the leader
|
|
443
|
+
const currentLeader = await redis.get(WAL_LEADER_KEY);
|
|
444
|
+
if (currentLeader === this.instanceId) {
|
|
445
|
+
await redis.expire(WAL_LEADER_KEY, WAL_LEADER_TTL);
|
|
446
|
+
}
|
|
447
|
+
else if (currentLeader) {
|
|
448
|
+
// Lost leadership to another instance
|
|
449
|
+
console.warn("Lost WAL consumer leadership to another instance");
|
|
450
|
+
this.isWalLeader = false;
|
|
451
|
+
this.stopLeaderRenewal();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
console.warn(`Failed to renew WAL leader lock: ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}, 10000);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Stop leader renewal interval
|
|
461
|
+
*/
|
|
462
|
+
stopLeaderRenewal() {
|
|
463
|
+
if (this.leaderRenewalInterval) {
|
|
464
|
+
clearInterval(this.leaderRenewalInterval);
|
|
465
|
+
this.leaderRenewalInterval = null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Release WAL leader lock in Redis
|
|
470
|
+
*/
|
|
471
|
+
async releaseWalLeaderLock() {
|
|
472
|
+
try {
|
|
473
|
+
const redis = await this.getRedisClient();
|
|
474
|
+
if (!redis)
|
|
475
|
+
return;
|
|
476
|
+
// Only delete if we're the current leader (atomic check-and-delete)
|
|
477
|
+
const currentLeader = await redis.get(WAL_LEADER_KEY);
|
|
478
|
+
if (currentLeader === this.instanceId) {
|
|
479
|
+
await redis.del(WAL_LEADER_KEY);
|
|
480
|
+
console.info("Released WAL consumer leadership");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.warn(`Failed to release WAL leader lock: ${error.message}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Get or create Redis client for leader election
|
|
489
|
+
* Reuses the same Redis URL as Socket.IO
|
|
490
|
+
*/
|
|
491
|
+
async getRedisClient() {
|
|
492
|
+
if (env.get("SOCKET_REDIS_ENABLED") !== "true") {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
if (this.redisClient) {
|
|
496
|
+
return this.redisClient;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const Redis = (await import('ioredis')).default;
|
|
500
|
+
const redisUrl = env.get("SOCKET_REDIS_URL");
|
|
501
|
+
if (!redisUrl) {
|
|
502
|
+
console.warn("SOCKET_REDIS_URL not set, cannot use Redis for WAL leader election");
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
this.redisClient = new Redis(redisUrl, {
|
|
506
|
+
maxRetriesPerRequest: 3,
|
|
507
|
+
lazyConnect: true,
|
|
508
|
+
retryStrategy: (times) => {
|
|
509
|
+
if (times > 3)
|
|
510
|
+
return null; // Stop retrying
|
|
511
|
+
return Math.min(times * 100, 1000);
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
this.redisClient.on('error', (err) => {
|
|
515
|
+
console.warn(`Redis client error (WAL leader): ${err.message}`);
|
|
516
|
+
});
|
|
517
|
+
await this.redisClient.connect();
|
|
518
|
+
return this.redisClient;
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
console.warn(`Failed to create Redis client: ${error.message}`);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Handle WAL messages and broadcast changes
|
|
527
|
+
*/
|
|
528
|
+
handleWalMessage(message) {
|
|
529
|
+
// Only process insert, update, delete
|
|
530
|
+
if (!['insert', 'update', 'delete'].includes(message.tag)) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const relationName = message.relation?.name;
|
|
534
|
+
const schemaName = message.relation?.schema;
|
|
535
|
+
// Skip system schemas
|
|
536
|
+
if (schemaName && schemaName !== 'public') {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!relationName) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// Get collection config
|
|
543
|
+
const config = this.collectionConfigs.get(relationName);
|
|
544
|
+
if (!config || !config.enabled) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// Check if this action is enabled for this collection
|
|
548
|
+
if (!config.actions.includes(message.tag)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
// Map WAL tag to action
|
|
552
|
+
const actionMap = {
|
|
553
|
+
'insert': 'create',
|
|
554
|
+
'update': 'update',
|
|
555
|
+
'delete': 'delete'
|
|
556
|
+
};
|
|
557
|
+
const action = actionMap[message.tag];
|
|
558
|
+
if (!action)
|
|
559
|
+
return;
|
|
560
|
+
// Prepare data payload
|
|
561
|
+
let data;
|
|
562
|
+
if (message.tag === 'delete') {
|
|
563
|
+
// For deletes, use old values or key values
|
|
564
|
+
data = message.old || message.key || {};
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// For inserts and updates, use new values
|
|
568
|
+
data = message.new || {};
|
|
569
|
+
}
|
|
570
|
+
// Include old values for updates if available (requires REPLICA IDENTITY FULL)
|
|
571
|
+
const payload = { ...data };
|
|
572
|
+
if (message.tag === 'update' && message.old) {
|
|
573
|
+
payload._old = message.old;
|
|
574
|
+
}
|
|
575
|
+
// Broadcast via SocketService
|
|
576
|
+
if (!this.socketService) {
|
|
577
|
+
console.warn("Socket service not set, cannot broadcast change");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
this.socketService.broadcastChange(relationName, action, payload);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Handle disconnect and attempt reconnection
|
|
584
|
+
*/
|
|
585
|
+
handleDisconnect() {
|
|
586
|
+
this.connected = false;
|
|
587
|
+
if (this.shuttingDown) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Only the leader should attempt reconnection
|
|
591
|
+
if (!this.isWalLeader) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// Single reconnect attempt after 5 seconds
|
|
595
|
+
if (!this.reconnectTimeout) {
|
|
596
|
+
console.info("Realtime connection lost. Attempting reconnect in 5 seconds...");
|
|
597
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
598
|
+
this.reconnectTimeout = null;
|
|
599
|
+
try {
|
|
600
|
+
// Reset connected state and try again
|
|
601
|
+
await this.startConsuming();
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
console.error("Reconnection failed:", error);
|
|
605
|
+
// Release leadership so another instance can try
|
|
606
|
+
this.stopLeaderRenewal();
|
|
607
|
+
await this.releaseWalLeaderLock();
|
|
608
|
+
this.isWalLeader = false;
|
|
609
|
+
console.info("Released WAL leadership after reconnection failure");
|
|
610
|
+
}
|
|
611
|
+
}, 5000);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Enable realtime for a collection with specific actions
|
|
616
|
+
*/
|
|
617
|
+
async enableCollection(collectionName, actions = ['insert', 'update', 'delete']) {
|
|
618
|
+
if (!this.initialized) {
|
|
619
|
+
throw new Error("Realtime service not initialized");
|
|
620
|
+
}
|
|
621
|
+
const config = { enabled: true, actions };
|
|
622
|
+
this.collectionConfigs.set(collectionName, config);
|
|
623
|
+
// Add table to publication if WAL is available
|
|
624
|
+
if (this.walAvailable) {
|
|
625
|
+
try {
|
|
626
|
+
await db.execute(sql.raw(`ALTER PUBLICATION ${PUBLICATION_NAME} ADD TABLE "${collectionName}"`));
|
|
627
|
+
console.info(`Added ${collectionName} to publication`);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (!error.message.includes('already member')) {
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Update schema definition with new config format
|
|
636
|
+
await this.updateSchemaRealtimeConfig(collectionName, config);
|
|
637
|
+
console.info(`Enabled realtime for ${collectionName} with actions: ${actions.join(', ')}`);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Disable realtime for a collection
|
|
641
|
+
*/
|
|
642
|
+
async disableCollection(collectionName) {
|
|
643
|
+
if (!this.initialized) {
|
|
644
|
+
throw new Error("Realtime service not initialized");
|
|
645
|
+
}
|
|
646
|
+
this.collectionConfigs.delete(collectionName);
|
|
647
|
+
// Remove table from publication if WAL is available
|
|
648
|
+
if (this.walAvailable) {
|
|
649
|
+
try {
|
|
650
|
+
await db.execute(sql.raw(`ALTER PUBLICATION ${PUBLICATION_NAME} DROP TABLE "${collectionName}"`));
|
|
651
|
+
console.info(`Removed ${collectionName} from publication`);
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
if (!error.message.includes('not member') && !error.message.includes('does not exist')) {
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Update schema definition
|
|
660
|
+
await this.updateSchemaRealtimeConfig(collectionName, { enabled: false, actions: [] });
|
|
661
|
+
console.info(`Disabled realtime for ${collectionName}`);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Update realtime actions for a collection
|
|
665
|
+
*/
|
|
666
|
+
async updateCollectionActions(collectionName, actions) {
|
|
667
|
+
const config = this.collectionConfigs.get(collectionName);
|
|
668
|
+
if (!config || !config.enabled) {
|
|
669
|
+
throw new Error(`Realtime is not enabled for collection: ${collectionName}`);
|
|
670
|
+
}
|
|
671
|
+
config.actions = actions;
|
|
672
|
+
await this.updateSchemaRealtimeConfig(collectionName, config);
|
|
673
|
+
console.info(`Updated realtime actions for ${collectionName}: ${actions.join(', ')}`);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Update the realtime config in schema definition
|
|
677
|
+
*/
|
|
678
|
+
async updateSchemaRealtimeConfig(collectionName, config) {
|
|
679
|
+
try {
|
|
680
|
+
await db.execute(sql `
|
|
681
|
+
UPDATE "baasix_SchemaDefinition"
|
|
682
|
+
SET schema = jsonb_set(
|
|
683
|
+
COALESCE(schema::jsonb, '{}'::jsonb),
|
|
684
|
+
'{realtime}',
|
|
685
|
+
${JSON.stringify(config)}::jsonb
|
|
686
|
+
)
|
|
687
|
+
WHERE "collectionName" = ${collectionName}
|
|
688
|
+
`);
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
console.warn(`Failed to update schema realtime flag: ${error.message}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Set replica identity to FULL for a table (enables old values on UPDATE/DELETE)
|
|
696
|
+
*/
|
|
697
|
+
async setReplicaIdentityFull(collectionName) {
|
|
698
|
+
try {
|
|
699
|
+
await db.execute(sql.raw(`
|
|
700
|
+
ALTER TABLE "${collectionName}" REPLICA IDENTITY FULL
|
|
701
|
+
`));
|
|
702
|
+
console.info(`Set REPLICA IDENTITY FULL for: ${collectionName}`);
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
console.error(`Failed to set replica identity: ${error.message}`);
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Get realtime config for a collection
|
|
711
|
+
*/
|
|
712
|
+
getCollectionConfig(collectionName) {
|
|
713
|
+
return this.collectionConfigs.get(collectionName);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Check if a collection has realtime enabled
|
|
717
|
+
*/
|
|
718
|
+
isCollectionEnabled(collectionName) {
|
|
719
|
+
const config = this.collectionConfigs.get(collectionName);
|
|
720
|
+
return config?.enabled || false;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Get list of enabled collections with their configs
|
|
724
|
+
*/
|
|
725
|
+
getEnabledCollections() {
|
|
726
|
+
const result = [];
|
|
727
|
+
for (const [collection, config] of this.collectionConfigs) {
|
|
728
|
+
if (config.enabled) {
|
|
729
|
+
result.push({ collection, config });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Reload collection configs from database and sync publication
|
|
736
|
+
* @param collectionNames - Optional list of collection names to reload. If not provided, reloads all.
|
|
737
|
+
*/
|
|
738
|
+
async reloadCollections(collectionNames) {
|
|
739
|
+
if (collectionNames && collectionNames.length > 0) {
|
|
740
|
+
// Efficient path: reload only specific collections
|
|
741
|
+
await this.loadSpecificCollections(collectionNames);
|
|
742
|
+
if (this.walAvailable) {
|
|
743
|
+
await this.syncSpecificCollections(collectionNames);
|
|
744
|
+
// Restart the replication stream to pick up publication changes
|
|
745
|
+
await this.restartReplicationStream();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
// Full reload
|
|
750
|
+
await this.loadEnabledCollections();
|
|
751
|
+
if (this.walAvailable) {
|
|
752
|
+
await this.syncPublicationWithCollections();
|
|
753
|
+
// Restart the replication stream to pick up publication changes
|
|
754
|
+
await this.restartReplicationStream();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Restart the replication stream to pick up publication changes
|
|
760
|
+
* PostgreSQL logical replication doesn't dynamically detect publication changes,
|
|
761
|
+
* so we need to disconnect and reconnect the stream.
|
|
762
|
+
*/
|
|
763
|
+
async restartReplicationStream() {
|
|
764
|
+
if (!this.connected || !this.replicationService) {
|
|
765
|
+
console.info("Replication stream not running, no restart needed");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
console.info("Restarting replication stream to apply publication changes...");
|
|
769
|
+
try {
|
|
770
|
+
// Stop the current replication service
|
|
771
|
+
if (this.replicationService) {
|
|
772
|
+
try {
|
|
773
|
+
await this.replicationService.stop();
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
// Ignore stop errors
|
|
777
|
+
}
|
|
778
|
+
this.replicationService = null;
|
|
779
|
+
}
|
|
780
|
+
this.connected = false;
|
|
781
|
+
// Small delay to ensure clean disconnect
|
|
782
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
783
|
+
// Restart consuming (non-blocking)
|
|
784
|
+
this.startConsuming().catch((error) => {
|
|
785
|
+
console.error("Failed to restart replication stream:", error);
|
|
786
|
+
});
|
|
787
|
+
console.info("Replication stream restart initiated");
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
console.warn(`Failed to restart replication stream: ${error.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Load realtime config for specific collections only
|
|
795
|
+
*/
|
|
796
|
+
async loadSpecificCollections(collectionNames) {
|
|
797
|
+
if (collectionNames.length === 0)
|
|
798
|
+
return;
|
|
799
|
+
try {
|
|
800
|
+
// Use sql.join to properly construct the IN clause with parameters
|
|
801
|
+
const result = await db.execute(sql `
|
|
802
|
+
SELECT "collectionName", schema
|
|
803
|
+
FROM "baasix_SchemaDefinition"
|
|
804
|
+
WHERE "collectionName" IN (${sql.join(collectionNames.map(n => sql `${n}`), sql `, `)})
|
|
805
|
+
`);
|
|
806
|
+
// Update configs for the specified collections
|
|
807
|
+
const foundCollections = new Set();
|
|
808
|
+
for (const row of result) {
|
|
809
|
+
foundCollections.add(row.collectionName);
|
|
810
|
+
const schema = typeof row.schema === 'string' ? JSON.parse(row.schema) : row.schema;
|
|
811
|
+
const realtimeConfig = schema?.realtime;
|
|
812
|
+
if (realtimeConfig === true) {
|
|
813
|
+
this.collectionConfigs.set(row.collectionName, {
|
|
814
|
+
enabled: true,
|
|
815
|
+
actions: ['insert', 'update', 'delete']
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
else if (typeof realtimeConfig === 'object' && realtimeConfig?.enabled) {
|
|
819
|
+
this.collectionConfigs.set(row.collectionName, {
|
|
820
|
+
enabled: true,
|
|
821
|
+
actions: realtimeConfig.actions || ['insert', 'update', 'delete']
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// Realtime not enabled or disabled - remove from configs
|
|
826
|
+
this.collectionConfigs.delete(row.collectionName);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// Handle collections that weren't found (might be deleted)
|
|
830
|
+
for (const name of collectionNames) {
|
|
831
|
+
if (!foundCollections.has(name)) {
|
|
832
|
+
this.collectionConfigs.delete(name);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
console.info(`Loaded realtime config for ${collectionNames.length} specific collections`);
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
console.warn(`Failed to load specific collections: ${error.message}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Sync publication for specific collections only
|
|
843
|
+
*/
|
|
844
|
+
async syncSpecificCollections(collectionNames) {
|
|
845
|
+
if (!this.walAvailable || collectionNames.length === 0)
|
|
846
|
+
return;
|
|
847
|
+
try {
|
|
848
|
+
// Get current tables in publication
|
|
849
|
+
const currentTables = await db.execute(sql `
|
|
850
|
+
SELECT tablename
|
|
851
|
+
FROM pg_publication_tables
|
|
852
|
+
WHERE pubname = ${PUBLICATION_NAME}
|
|
853
|
+
`);
|
|
854
|
+
const currentTableNames = new Set(currentTables.map(t => t.tablename));
|
|
855
|
+
for (const collectionName of collectionNames) {
|
|
856
|
+
const config = this.collectionConfigs.get(collectionName);
|
|
857
|
+
const isInPublication = currentTableNames.has(collectionName);
|
|
858
|
+
const shouldBeInPublication = config?.enabled || false;
|
|
859
|
+
if (shouldBeInPublication && !isInPublication) {
|
|
860
|
+
// Add to publication
|
|
861
|
+
try {
|
|
862
|
+
await db.execute(sql.raw(`ALTER PUBLICATION ${PUBLICATION_NAME} ADD TABLE "${collectionName}"`));
|
|
863
|
+
console.info(`Added ${collectionName} to publication`);
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
if (!error.message.includes('already member')) {
|
|
867
|
+
console.warn(`Failed to add ${collectionName} to publication: ${error.message}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else if (!shouldBeInPublication && isInPublication) {
|
|
872
|
+
// Remove from publication
|
|
873
|
+
try {
|
|
874
|
+
await db.execute(sql.raw(`ALTER PUBLICATION ${PUBLICATION_NAME} DROP TABLE "${collectionName}"`));
|
|
875
|
+
console.info(`Removed ${collectionName} from publication`);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
if (!error.message.includes('not member') && !error.message.includes('does not exist')) {
|
|
879
|
+
console.warn(`Failed to remove ${collectionName} from publication: ${error.message}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// If both match (both in or both out), no action needed
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
console.warn(`Failed to sync specific collections: ${error.message}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Get service status
|
|
892
|
+
*/
|
|
893
|
+
getStatus() {
|
|
894
|
+
return {
|
|
895
|
+
initialized: this.initialized,
|
|
896
|
+
connected: this.connected,
|
|
897
|
+
walAvailable: this.walAvailable,
|
|
898
|
+
isWalLeader: this.isWalLeader,
|
|
899
|
+
enabledCollections: this.getEnabledCollections(),
|
|
900
|
+
publicationName: PUBLICATION_NAME,
|
|
901
|
+
slotName: SLOT_NAME
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Cleanup resources on shutdown
|
|
906
|
+
*/
|
|
907
|
+
async shutdown() {
|
|
908
|
+
console.info("Shutting down Realtime Service...");
|
|
909
|
+
this.shuttingDown = true;
|
|
910
|
+
if (this.reconnectTimeout) {
|
|
911
|
+
clearTimeout(this.reconnectTimeout);
|
|
912
|
+
this.reconnectTimeout = null;
|
|
913
|
+
}
|
|
914
|
+
// Stop leader renewal interval
|
|
915
|
+
this.stopLeaderRenewal();
|
|
916
|
+
// Release WAL leader lock if we held it
|
|
917
|
+
if (this.isWalLeader) {
|
|
918
|
+
await this.releaseWalLeaderLock();
|
|
919
|
+
this.isWalLeader = false;
|
|
920
|
+
}
|
|
921
|
+
if (this.replicationService) {
|
|
922
|
+
try {
|
|
923
|
+
await this.replicationService.stop();
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
console.warn("Error stopping replication service:", error);
|
|
927
|
+
}
|
|
928
|
+
this.replicationService = null;
|
|
929
|
+
}
|
|
930
|
+
// Cleanup Redis client
|
|
931
|
+
if (this.redisClient) {
|
|
932
|
+
try {
|
|
933
|
+
await this.redisClient.quit();
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
// Ignore
|
|
937
|
+
}
|
|
938
|
+
this.redisClient = null;
|
|
939
|
+
}
|
|
940
|
+
this.connected = false;
|
|
941
|
+
console.info("Realtime Service shut down");
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Drop replication slot (use with caution - only for cleanup)
|
|
945
|
+
*/
|
|
946
|
+
async dropReplicationSlot() {
|
|
947
|
+
try {
|
|
948
|
+
await db.execute(sql.raw(`
|
|
949
|
+
SELECT pg_drop_replication_slot('${SLOT_NAME}')
|
|
950
|
+
`));
|
|
951
|
+
console.info(`Dropped replication slot: ${SLOT_NAME}`);
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
if (!error.message.includes('does not exist')) {
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Drop publication (use with caution - only for cleanup)
|
|
961
|
+
*/
|
|
962
|
+
async dropPublication() {
|
|
963
|
+
try {
|
|
964
|
+
await db.execute(sql.raw(`DROP PUBLICATION IF EXISTS ${PUBLICATION_NAME}`));
|
|
965
|
+
console.info(`Dropped publication: ${PUBLICATION_NAME}`);
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
console.warn(`Failed to drop publication: ${error.message}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Create singleton instance only if it doesn't exist
|
|
973
|
+
if (!globalThis.__baasix_realtimeService) {
|
|
974
|
+
globalThis.__baasix_realtimeService = new RealtimeService();
|
|
975
|
+
}
|
|
976
|
+
const realtimeService = globalThis.__baasix_realtimeService;
|
|
977
|
+
export default realtimeService;
|
|
978
|
+
//# sourceMappingURL=RealtimeService.js.map
|