@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,1992 @@
|
|
|
1
|
+
import { pgTable, text, jsonb, timestamp } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { eq, inArray } from 'drizzle-orm';
|
|
3
|
+
import argon2 from 'argon2';
|
|
4
|
+
import { getDatabase, getSqlClient, isPgVersionAtLeast } from './db.js';
|
|
5
|
+
import { mapJsonTypeToDrizzle, isRelationField } from './typeMapper.js';
|
|
6
|
+
import { relationBuilder, createForeignKeySQL } from './relationUtils.js';
|
|
7
|
+
import systemSchemaModule from './systemschema.js';
|
|
8
|
+
import env from './env.js';
|
|
9
|
+
const systemSchemas = systemSchemaModule.schemas;
|
|
10
|
+
/**
|
|
11
|
+
* baasix_SchemaDefinition table schema
|
|
12
|
+
* Note: This is duplicated from schema.ts to avoid circular dependency
|
|
13
|
+
*/
|
|
14
|
+
const baasixSchemaDefinition = pgTable('baasix_SchemaDefinition', {
|
|
15
|
+
collectionName: text('collectionName').primaryKey().notNull(),
|
|
16
|
+
schema: jsonb('schema').notNull(),
|
|
17
|
+
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow(),
|
|
18
|
+
updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow(),
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Manages dynamic schema generation from JSON definitions
|
|
22
|
+
*/
|
|
23
|
+
export class SchemaManager {
|
|
24
|
+
static instance;
|
|
25
|
+
schemas = new Map(); // Stores Drizzle table schemas
|
|
26
|
+
schemaDefinitions = new Map(); // Stores JSON schema definitions
|
|
27
|
+
relations = new Map();
|
|
28
|
+
initialized = false;
|
|
29
|
+
pluginSchemas = []; // Stores plugin schemas
|
|
30
|
+
constructor() { }
|
|
31
|
+
/**
|
|
32
|
+
* Register plugin schemas before initialization
|
|
33
|
+
* This should be called before initialize() to ensure plugin schemas are created with system schemas
|
|
34
|
+
*/
|
|
35
|
+
registerPluginSchemas(schemas) {
|
|
36
|
+
if (this.initialized) {
|
|
37
|
+
console.warn('SchemaManager already initialized. Plugin schemas should be registered before initialize().');
|
|
38
|
+
}
|
|
39
|
+
this.pluginSchemas = schemas;
|
|
40
|
+
console.log(`Registered ${schemas.length} plugin schema(s)`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get all registered plugin schemas
|
|
44
|
+
*/
|
|
45
|
+
getPluginSchemas() {
|
|
46
|
+
return this.pluginSchemas;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get singleton instance
|
|
50
|
+
*/
|
|
51
|
+
static getInstance() {
|
|
52
|
+
// Use globalThis to ensure singleton across different module loading paths
|
|
53
|
+
if (!globalThis.__baasix_schemaManager) {
|
|
54
|
+
globalThis.__baasix_schemaManager = new SchemaManager();
|
|
55
|
+
}
|
|
56
|
+
return globalThis.__baasix_schemaManager;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Initialize schema manager by loading all schemas from database
|
|
60
|
+
* Flow matches Sequelize implementation:
|
|
61
|
+
* 1. Ensure SchemaDefinition table exists
|
|
62
|
+
* 2. Ensure system schemas are in SchemaDefinition table
|
|
63
|
+
* 3. Create tables for schemas that need syncing
|
|
64
|
+
* 4. Load all schemas
|
|
65
|
+
* 5. Seed database if empty
|
|
66
|
+
*/
|
|
67
|
+
async initialize() {
|
|
68
|
+
if (this.initialized) {
|
|
69
|
+
console.log('SchemaManager already initialized.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log('Initializing Schema Manager...');
|
|
73
|
+
try {
|
|
74
|
+
// Step 0: Enable required PostgreSQL extensions
|
|
75
|
+
await this.enablePostgresExtensions();
|
|
76
|
+
// Step 1: Ensure baasix_SchemaDefinition table exists
|
|
77
|
+
await this.ensureSchemaDefinitionTable();
|
|
78
|
+
// Step 2: Ensure system schemas are in the table
|
|
79
|
+
const needSyncing = await this.ensureSystemSchemas();
|
|
80
|
+
// Step 3: Create/sync tables for schemas that need it
|
|
81
|
+
if (needSyncing.length > 0) {
|
|
82
|
+
console.info('Need to sync the following schemas:', needSyncing);
|
|
83
|
+
await this.loadAndCreateAllSchemas(needSyncing);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.info('No system schemas need syncing.');
|
|
87
|
+
}
|
|
88
|
+
// Step 4: Load all schemas into memory (pass needSyncing to skip unnecessary sync for unchanged schemas)
|
|
89
|
+
await this.loadAllSchemas(needSyncing);
|
|
90
|
+
this.initialized = true;
|
|
91
|
+
console.log('Schema Manager initialized successfully');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error('Failed to initialize Schema Manager:', error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Enable required PostgreSQL extensions
|
|
100
|
+
*/
|
|
101
|
+
async enablePostgresExtensions() {
|
|
102
|
+
const sql = getSqlClient();
|
|
103
|
+
try {
|
|
104
|
+
// Enable pgcrypto for gen_random_uuid()
|
|
105
|
+
await sql.unsafe('CREATE EXTENSION IF NOT EXISTS pgcrypto');
|
|
106
|
+
console.log('PostgreSQL extension pgcrypto enabled');
|
|
107
|
+
// Enable PostGIS if configured
|
|
108
|
+
if (env.get('DATABASE_POSTGIS') === 'true') {
|
|
109
|
+
await sql.unsafe('CREATE EXTENSION IF NOT EXISTS postgis');
|
|
110
|
+
console.log('PostgreSQL extension postgis enabled');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error('Failed to enable PostgreSQL extensions:', error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Ensure baasix_SchemaDefinition table exists
|
|
120
|
+
*/
|
|
121
|
+
async ensureSchemaDefinitionTable() {
|
|
122
|
+
const sql = getSqlClient();
|
|
123
|
+
// Check if table exists
|
|
124
|
+
const result = await sql `
|
|
125
|
+
SELECT EXISTS (
|
|
126
|
+
SELECT FROM information_schema.tables
|
|
127
|
+
WHERE table_name = 'baasix_SchemaDefinition'
|
|
128
|
+
)
|
|
129
|
+
`;
|
|
130
|
+
if (!result[0].exists) {
|
|
131
|
+
console.log('Creating baasix_SchemaDefinition table...');
|
|
132
|
+
await sql `
|
|
133
|
+
CREATE TABLE "baasix_SchemaDefinition" (
|
|
134
|
+
"collectionName" TEXT PRIMARY KEY NOT NULL,
|
|
135
|
+
schema JSONB NOT NULL,
|
|
136
|
+
"createdAt" TIMESTAMPTZ DEFAULT NOW(),
|
|
137
|
+
"updatedAt" TIMESTAMPTZ DEFAULT NOW()
|
|
138
|
+
)
|
|
139
|
+
`;
|
|
140
|
+
console.log('SchemaDefinition table created.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Ensure system schemas and plugin schemas are in the baasix_SchemaDefinition table
|
|
145
|
+
* Returns list of schemas that need syncing
|
|
146
|
+
*/
|
|
147
|
+
async ensureSystemSchemas() {
|
|
148
|
+
const db = getDatabase();
|
|
149
|
+
const needUpdate = [];
|
|
150
|
+
// Combine system schemas with plugin schemas
|
|
151
|
+
const allSchemas = [
|
|
152
|
+
...systemSchemas,
|
|
153
|
+
...this.pluginSchemas.map(ps => ({
|
|
154
|
+
collectionName: ps.collectionName,
|
|
155
|
+
schema: ps.schema
|
|
156
|
+
}))
|
|
157
|
+
];
|
|
158
|
+
for (const schemaData of allSchemas) {
|
|
159
|
+
// Prepare schema with timestamp fields added if timestamps: true
|
|
160
|
+
const schemaToStore = JSON.parse(JSON.stringify(schemaData.schema));
|
|
161
|
+
if (schemaToStore.timestamps !== false) {
|
|
162
|
+
// Add createdAt and updatedAt fields to schema definition if not already present
|
|
163
|
+
if (!schemaToStore.fields.createdAt) {
|
|
164
|
+
schemaToStore.fields.createdAt = {
|
|
165
|
+
type: "DateTime",
|
|
166
|
+
allowNull: true,
|
|
167
|
+
SystemGenerated: "true",
|
|
168
|
+
defaultValue: { type: "NOW" }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (!schemaToStore.fields.updatedAt) {
|
|
172
|
+
schemaToStore.fields.updatedAt = {
|
|
173
|
+
type: "DateTime",
|
|
174
|
+
allowNull: true,
|
|
175
|
+
SystemGenerated: "true",
|
|
176
|
+
defaultValue: { type: "NOW" }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Add deletedAt if paranoid mode
|
|
181
|
+
if (schemaToStore.paranoid && !schemaToStore.fields.deletedAt) {
|
|
182
|
+
schemaToStore.fields.deletedAt = {
|
|
183
|
+
type: "DateTime",
|
|
184
|
+
allowNull: true,
|
|
185
|
+
SystemGenerated: "true"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// Check if schema already exists
|
|
189
|
+
const existing = await db
|
|
190
|
+
.select()
|
|
191
|
+
.from(baasixSchemaDefinition)
|
|
192
|
+
.where(eq(baasixSchemaDefinition.collectionName, schemaData.collectionName))
|
|
193
|
+
.limit(1);
|
|
194
|
+
if (existing.length === 0) {
|
|
195
|
+
// Insert new schema
|
|
196
|
+
await db.insert(baasixSchemaDefinition).values({
|
|
197
|
+
collectionName: schemaData.collectionName,
|
|
198
|
+
schema: schemaToStore,
|
|
199
|
+
});
|
|
200
|
+
console.log(`Added system schema: ${schemaData.collectionName}`);
|
|
201
|
+
needUpdate.push(schemaData.collectionName);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Compare and update if needed (add new fields, check modified fields, and sync schema-level properties)
|
|
205
|
+
const existingSchema = existing[0].schema;
|
|
206
|
+
let hasChanges = false;
|
|
207
|
+
// Ensure existingSchema.fields exists
|
|
208
|
+
if (!existingSchema.fields) {
|
|
209
|
+
existingSchema.fields = {};
|
|
210
|
+
}
|
|
211
|
+
// Helper function to normalize schema for comparison
|
|
212
|
+
// Removes SystemGenerated field which can differ between code ("true") and db (true)
|
|
213
|
+
const normalizeForComparison = (obj) => {
|
|
214
|
+
if (obj === null || typeof obj !== 'object')
|
|
215
|
+
return obj;
|
|
216
|
+
if (Array.isArray(obj)) {
|
|
217
|
+
return obj.map(normalizeForComparison);
|
|
218
|
+
}
|
|
219
|
+
const result = {};
|
|
220
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
221
|
+
if (key === 'SystemGenerated')
|
|
222
|
+
continue; // Skip this field for comparison
|
|
223
|
+
result[key] = normalizeForComparison(value);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
227
|
+
// Helper function for deep equality comparison (ignores property order)
|
|
228
|
+
const deepEqual = (a, b) => {
|
|
229
|
+
if (a === b)
|
|
230
|
+
return true;
|
|
231
|
+
if (a == null || b == null)
|
|
232
|
+
return a === b;
|
|
233
|
+
if (typeof a !== typeof b)
|
|
234
|
+
return false;
|
|
235
|
+
if (typeof a !== 'object')
|
|
236
|
+
return a === b;
|
|
237
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
238
|
+
return false;
|
|
239
|
+
if (Array.isArray(a)) {
|
|
240
|
+
if (a.length !== b.length)
|
|
241
|
+
return false;
|
|
242
|
+
// For arrays, compare each element
|
|
243
|
+
// Sort arrays of objects by 'name' field for stable comparison
|
|
244
|
+
const sortKey = (item) => item?.name || JSON.stringify(item);
|
|
245
|
+
const sortedA = [...a].sort((x, y) => String(sortKey(x)).localeCompare(String(sortKey(y))));
|
|
246
|
+
const sortedB = [...b].sort((x, y) => String(sortKey(x)).localeCompare(String(sortKey(y))));
|
|
247
|
+
return sortedA.every((item, i) => deepEqual(item, sortedB[i]));
|
|
248
|
+
}
|
|
249
|
+
// For objects, compare all keys regardless of order
|
|
250
|
+
const keysA = Object.keys(a);
|
|
251
|
+
const keysB = Object.keys(b);
|
|
252
|
+
if (keysA.length !== keysB.length)
|
|
253
|
+
return false;
|
|
254
|
+
return keysA.every(key => deepEqual(a[key], b[key]));
|
|
255
|
+
};
|
|
256
|
+
// Check for new fields (fields in system schema but not in DB)
|
|
257
|
+
const newFields = Object.keys(schemaToStore.fields || {}).filter((field) => !existingSchema.fields[field]);
|
|
258
|
+
if (newFields.length > 0) {
|
|
259
|
+
for (const field of newFields) {
|
|
260
|
+
existingSchema.fields[field] = schemaToStore.fields[field];
|
|
261
|
+
}
|
|
262
|
+
hasChanges = true;
|
|
263
|
+
console.log(`[SCHEMA DIFF] ${schemaData.collectionName} new fields:`, newFields);
|
|
264
|
+
}
|
|
265
|
+
// Check for modified fields (existing fields with changed definitions)
|
|
266
|
+
const modifiedFields = [];
|
|
267
|
+
for (const fieldName of Object.keys(schemaToStore.fields || {})) {
|
|
268
|
+
if (existingSchema.fields[fieldName]) {
|
|
269
|
+
// Field exists in both - check if definition changed (normalize to ignore SystemGenerated differences)
|
|
270
|
+
const normalizedExisting = normalizeForComparison(existingSchema.fields[fieldName]);
|
|
271
|
+
const normalizedNew = normalizeForComparison(schemaToStore.fields[fieldName]);
|
|
272
|
+
if (!deepEqual(normalizedExisting, normalizedNew)) {
|
|
273
|
+
existingSchema.fields[fieldName] = schemaToStore.fields[fieldName];
|
|
274
|
+
modifiedFields.push(fieldName);
|
|
275
|
+
hasChanges = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (modifiedFields.length > 0) {
|
|
280
|
+
console.log(`[SCHEMA DIFF] ${schemaData.collectionName} modified fields:`, modifiedFields);
|
|
281
|
+
}
|
|
282
|
+
// Note: We intentionally do NOT remove fields that exist in DB but not in system schema
|
|
283
|
+
// This preserves user data and allows for gradual migrations
|
|
284
|
+
// Sync structural schema-level properties (indexes, name) from system schema
|
|
285
|
+
// These are enforced from code and should not be user-modified
|
|
286
|
+
const structuralProps = ['indexes', 'name'];
|
|
287
|
+
for (const prop of structuralProps) {
|
|
288
|
+
// Normalize both values before comparison to ignore SystemGenerated differences
|
|
289
|
+
const normalizedExisting = normalizeForComparison(existingSchema[prop]);
|
|
290
|
+
const normalizedNew = normalizeForComparison(schemaToStore[prop]);
|
|
291
|
+
if (schemaToStore[prop] !== undefined && !deepEqual(normalizedExisting, normalizedNew)) {
|
|
292
|
+
console.log(`[SCHEMA DIFF] ${schemaData.collectionName} property '${prop}' differs:`, {
|
|
293
|
+
existing: existingSchema[prop],
|
|
294
|
+
new: schemaToStore[prop]
|
|
295
|
+
});
|
|
296
|
+
existingSchema[prop] = schemaToStore[prop];
|
|
297
|
+
hasChanges = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// User-configurable properties (timestamps, paranoid, usertrack, sortEnabled)
|
|
301
|
+
// Only set these if they don't exist in the DB yet - preserve user changes
|
|
302
|
+
const userConfigurableProps = ['timestamps', 'paranoid', 'usertrack', 'sortEnabled'];
|
|
303
|
+
for (const prop of userConfigurableProps) {
|
|
304
|
+
// Only set if the property doesn't exist in the existing schema (initial setup)
|
|
305
|
+
if (existingSchema[prop] === undefined && schemaToStore[prop] !== undefined) {
|
|
306
|
+
existingSchema[prop] = schemaToStore[prop];
|
|
307
|
+
hasChanges = true;
|
|
308
|
+
console.log(`Set initial ${prop} for ${schemaData.collectionName}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (hasChanges) {
|
|
312
|
+
await db
|
|
313
|
+
.update(baasixSchemaDefinition)
|
|
314
|
+
.set({
|
|
315
|
+
schema: existingSchema,
|
|
316
|
+
updatedAt: new Date()
|
|
317
|
+
})
|
|
318
|
+
.where(eq(baasixSchemaDefinition.collectionName, schemaData.collectionName));
|
|
319
|
+
console.log(`Updated system schema: ${schemaData.collectionName}`);
|
|
320
|
+
needUpdate.push(schemaData.collectionName);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
console.log('System schemas ensured in SchemaDefinition table.');
|
|
325
|
+
return needUpdate;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Sort schemas by dependency order (topological sort)
|
|
329
|
+
*/
|
|
330
|
+
sortSchemasByDependencies(schemas) {
|
|
331
|
+
// Build dependency graph
|
|
332
|
+
const dependencies = new Map();
|
|
333
|
+
const schemaMap = new Map();
|
|
334
|
+
for (const schemaDef of schemas) {
|
|
335
|
+
const collectionName = schemaDef.collectionName;
|
|
336
|
+
schemaMap.set(collectionName, schemaDef);
|
|
337
|
+
dependencies.set(collectionName, new Set());
|
|
338
|
+
// Find all BelongsTo relations (foreign key dependencies)
|
|
339
|
+
const schema = schemaDef.schema;
|
|
340
|
+
if (schema.fields) {
|
|
341
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
342
|
+
const fs = fieldSchema;
|
|
343
|
+
if (fs.relType === 'BelongsTo' && fs.target && fs.target !== collectionName) {
|
|
344
|
+
dependencies.get(collectionName).add(fs.target);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Topological sort using Kahn's algorithm
|
|
350
|
+
const sorted = [];
|
|
351
|
+
const inDegree = new Map();
|
|
352
|
+
const queue = [];
|
|
353
|
+
// Calculate in-degrees (number of dependencies for each table)
|
|
354
|
+
for (const [node, deps] of dependencies) {
|
|
355
|
+
inDegree.set(node, deps.size);
|
|
356
|
+
}
|
|
357
|
+
// Debug: Log dependencies
|
|
358
|
+
console.log('Dependencies map:');
|
|
359
|
+
for (const [node, deps] of dependencies) {
|
|
360
|
+
if (deps.size > 0) {
|
|
361
|
+
console.log(` ${node} depends on: [${Array.from(deps).join(', ')}]`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Find nodes with no dependencies (in-degree = 0)
|
|
365
|
+
for (const [node, degree] of inDegree) {
|
|
366
|
+
if (degree === 0) {
|
|
367
|
+
queue.push(node);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
console.log('Starting with tables that have no dependencies:', queue.join(', '));
|
|
371
|
+
// Process queue
|
|
372
|
+
while (queue.length > 0) {
|
|
373
|
+
const node = queue.shift();
|
|
374
|
+
const schemaDef = schemaMap.get(node);
|
|
375
|
+
if (schemaDef) {
|
|
376
|
+
sorted.push(schemaDef);
|
|
377
|
+
}
|
|
378
|
+
// Reduce in-degree for nodes that depend on this one
|
|
379
|
+
for (const [otherNode, deps] of dependencies) {
|
|
380
|
+
if (deps.has(node)) {
|
|
381
|
+
const newDegree = (inDegree.get(otherNode) || 0) - 1;
|
|
382
|
+
inDegree.set(otherNode, newDegree);
|
|
383
|
+
if (newDegree === 0) {
|
|
384
|
+
queue.push(otherNode);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// If sorted length != schemas length, there's a circular dependency
|
|
390
|
+
// In that case, just return original order and let FK constraints be added later
|
|
391
|
+
if (sorted.length !== schemas.length) {
|
|
392
|
+
console.info('Circular dependency detected in schemas, using original order');
|
|
393
|
+
return schemas;
|
|
394
|
+
}
|
|
395
|
+
return sorted;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Load and create tables for specific schemas
|
|
399
|
+
*/
|
|
400
|
+
async loadAndCreateAllSchemas(needSyncing) {
|
|
401
|
+
const db = getDatabase();
|
|
402
|
+
const schemas = await db
|
|
403
|
+
.select()
|
|
404
|
+
.from(baasixSchemaDefinition)
|
|
405
|
+
.where(inArray(baasixSchemaDefinition.collectionName, needSyncing));
|
|
406
|
+
// Sort schemas by dependency order to ensure referenced tables exist first
|
|
407
|
+
const sortedSchemas = this.sortSchemasByDependencies(schemas);
|
|
408
|
+
console.log('Schema creation order:', sortedSchemas.map(s => s.collectionName).join(', '));
|
|
409
|
+
// First pass: Create all tables and models without FK constraints
|
|
410
|
+
for (const schemaDef of sortedSchemas) {
|
|
411
|
+
await this.createOrUpdateModel(schemaDef.collectionName, schemaDef.schema);
|
|
412
|
+
// Create table using raw SQL (FK constraints will be added in second pass)
|
|
413
|
+
await this.createTableFromSchema(schemaDef.collectionName, schemaDef.schema, true);
|
|
414
|
+
}
|
|
415
|
+
// Second pass: Add foreign key constraints
|
|
416
|
+
console.log('Adding foreign key constraints...');
|
|
417
|
+
for (const schemaDef of sortedSchemas) {
|
|
418
|
+
await this.ensureForeignKeyConstraints(schemaDef.collectionName, schemaDef.schema);
|
|
419
|
+
}
|
|
420
|
+
console.log('All schemas loaded, models created/updated.');
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Normalize legacy Sequelize schemas - add default values for timestamp fields
|
|
424
|
+
* and update DB column defaults if missing
|
|
425
|
+
*/
|
|
426
|
+
async normalizeLegacySchema(collectionName, schema) {
|
|
427
|
+
const sql = getSqlClient();
|
|
428
|
+
const db = getDatabase();
|
|
429
|
+
let schemaUpdated = false;
|
|
430
|
+
const normalizedSchema = { ...schema, fields: { ...schema.fields } };
|
|
431
|
+
// Check timestamp fields if timestamps are enabled (default: true)
|
|
432
|
+
if (schema.timestamps !== false) {
|
|
433
|
+
// Normalize createdAt field
|
|
434
|
+
if (normalizedSchema.fields.createdAt && !normalizedSchema.fields.createdAt.defaultValue) {
|
|
435
|
+
normalizedSchema.fields.createdAt = {
|
|
436
|
+
...normalizedSchema.fields.createdAt,
|
|
437
|
+
defaultValue: { type: "NOW" }
|
|
438
|
+
};
|
|
439
|
+
schemaUpdated = true;
|
|
440
|
+
// Also update DB column default
|
|
441
|
+
try {
|
|
442
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ALTER COLUMN "createdAt" SET DEFAULT NOW()`);
|
|
443
|
+
console.log(`Updated createdAt default value for ${collectionName}`);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
// Column might not exist yet or already have default, ignore
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Normalize updatedAt field
|
|
450
|
+
if (normalizedSchema.fields.updatedAt && !normalizedSchema.fields.updatedAt.defaultValue) {
|
|
451
|
+
normalizedSchema.fields.updatedAt = {
|
|
452
|
+
...normalizedSchema.fields.updatedAt,
|
|
453
|
+
defaultValue: { type: "NOW" }
|
|
454
|
+
};
|
|
455
|
+
schemaUpdated = true;
|
|
456
|
+
// Also update DB column default
|
|
457
|
+
try {
|
|
458
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ALTER COLUMN "updatedAt" SET DEFAULT NOW()`);
|
|
459
|
+
console.log(`Updated updatedAt default value for ${collectionName}`);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
// Column might not exist yet or already have default, ignore
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Update schema definition in database if changed
|
|
467
|
+
if (schemaUpdated) {
|
|
468
|
+
await db
|
|
469
|
+
.update(baasixSchemaDefinition)
|
|
470
|
+
.set({ schema: normalizedSchema })
|
|
471
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName));
|
|
472
|
+
console.log(`Normalized legacy schema definition for ${collectionName}`);
|
|
473
|
+
}
|
|
474
|
+
return normalizedSchema;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Load all schemas from database into memory
|
|
478
|
+
* @param schemasNeedingSync - List of schema names that need table sync (from ensureSystemSchemas)
|
|
479
|
+
* If empty, skip expensive sync operations for faster startup
|
|
480
|
+
*/
|
|
481
|
+
async loadAllSchemas(schemasNeedingSync = []) {
|
|
482
|
+
const db = getDatabase();
|
|
483
|
+
const sql = getSqlClient();
|
|
484
|
+
const needsSyncSet = new Set(schemasNeedingSync);
|
|
485
|
+
const skipSync = schemasNeedingSync.length === 0;
|
|
486
|
+
const schemaDefinitions = await db
|
|
487
|
+
.select()
|
|
488
|
+
.from(baasixSchemaDefinition);
|
|
489
|
+
console.log(`Found ${schemaDefinitions.length} schema definitions`);
|
|
490
|
+
// Get list of existing tables in one query (for fast lookup)
|
|
491
|
+
const existingTables = await sql `
|
|
492
|
+
SELECT table_name FROM information_schema.tables
|
|
493
|
+
WHERE table_schema = 'public'
|
|
494
|
+
`;
|
|
495
|
+
const existingTableSet = new Set(existingTables.map((t) => t.table_name));
|
|
496
|
+
if (skipSync) {
|
|
497
|
+
console.log('No schema changes detected, using fast startup path');
|
|
498
|
+
}
|
|
499
|
+
// Sort schemas by dependency order
|
|
500
|
+
const sortedSchemas = this.sortSchemasByDependencies(schemaDefinitions);
|
|
501
|
+
// Schemas that need FK constraint sync (only those that were synced)
|
|
502
|
+
const schemasForFKSync = [];
|
|
503
|
+
// First pass: Create all tables and models without FK constraints
|
|
504
|
+
for (const schemaDef of sortedSchemas) {
|
|
505
|
+
const tableExists = existingTableSet.has(schemaDef.collectionName);
|
|
506
|
+
// Determine if this schema needs table sync:
|
|
507
|
+
// 1. If it was explicitly marked as needing sync
|
|
508
|
+
// 2. If the table doesn't exist yet (new schema)
|
|
509
|
+
// 3. If skipSync is false (there were schema changes, so sync all)
|
|
510
|
+
const needsSync = needsSyncSet.has(schemaDef.collectionName) ||
|
|
511
|
+
!tableExists ||
|
|
512
|
+
!skipSync;
|
|
513
|
+
let normalizedSchema = schemaDef.schema;
|
|
514
|
+
// Only run legacy normalization if sync is needed (skips DB queries on fast path)
|
|
515
|
+
if (needsSync) {
|
|
516
|
+
normalizedSchema = await this.normalizeLegacySchema(schemaDef.collectionName, schemaDef.schema);
|
|
517
|
+
// Update the schemaDef with normalized schema for subsequent operations
|
|
518
|
+
schemaDef.schema = normalizedSchema;
|
|
519
|
+
}
|
|
520
|
+
// Store JSON schema definition for later use (e.g., in getPrimaryKey)
|
|
521
|
+
this.schemaDefinitions.set(schemaDef.collectionName, schemaDef);
|
|
522
|
+
await this.createOrUpdateModel(schemaDef.collectionName, normalizedSchema);
|
|
523
|
+
if (needsSync) {
|
|
524
|
+
// Create table if it doesn't exist (FK constraints will be added in second pass)
|
|
525
|
+
await this.createTableFromSchema(schemaDef.collectionName, normalizedSchema, true);
|
|
526
|
+
schemasForFKSync.push(schemaDef);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Second pass: Add foreign key constraints only for schemas that were synced
|
|
530
|
+
if (schemasForFKSync.length > 0) {
|
|
531
|
+
console.log(`Adding foreign key constraints for ${schemasForFKSync.length} schemas...`);
|
|
532
|
+
for (const schemaDef of schemasForFKSync) {
|
|
533
|
+
await this.ensureForeignKeyConstraints(schemaDef.collectionName, schemaDef.schema);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log('Skipping foreign key constraint check (no schemas need sync)');
|
|
538
|
+
}
|
|
539
|
+
// Check if we need to seed the database
|
|
540
|
+
await this.checkAndSeedDatabase();
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Sync table columns with schema definition (add missing columns)
|
|
544
|
+
*/
|
|
545
|
+
async syncTableColumns(collectionName, schema) {
|
|
546
|
+
const sql = getSqlClient();
|
|
547
|
+
// Get existing columns in the table
|
|
548
|
+
const existingColumns = await sql `
|
|
549
|
+
SELECT column_name, data_type
|
|
550
|
+
FROM information_schema.columns
|
|
551
|
+
WHERE table_name = ${collectionName}
|
|
552
|
+
`;
|
|
553
|
+
const existingColumnNames = existingColumns.map((col) => col.column_name);
|
|
554
|
+
// Check each field in schema
|
|
555
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
556
|
+
const fs = fieldSchema;
|
|
557
|
+
// Skip relation fields that don't have an explicit type
|
|
558
|
+
if (fs.relType && !fs.type) {
|
|
559
|
+
// For BelongsTo relations, check if the foreign key column needs to be added
|
|
560
|
+
if (fs.relType === 'BelongsTo') {
|
|
561
|
+
const foreignKey = fs.foreignKey || `${fieldName}_Id`;
|
|
562
|
+
if (!existingColumnNames.includes(foreignKey)) {
|
|
563
|
+
// Column doesn't exist, add it via ensureForeignKeyConstraints
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
// Check if column exists
|
|
570
|
+
if (!existingColumnNames.includes(fieldName)) {
|
|
571
|
+
// Column is missing, add it
|
|
572
|
+
const columnDef = this.buildColumnDefinition(fieldName, fs);
|
|
573
|
+
if (columnDef) {
|
|
574
|
+
// Extract just the type and constraints from columnDef (remove field name)
|
|
575
|
+
const columnDefParts = columnDef.split(' ').slice(1).join(' '); // Remove first part which is field name
|
|
576
|
+
try {
|
|
577
|
+
// Use IF NOT EXISTS for safety in case of race conditions or schema query issues
|
|
578
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS ${columnDef}`);
|
|
579
|
+
console.log(`Added missing column ${fieldName} to ${collectionName}`);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
console.error(`Failed to add column ${fieldName} to ${collectionName}:`, error);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Add timestamp columns if needed
|
|
588
|
+
if (schema.timestamps !== false) {
|
|
589
|
+
if (!existingColumnNames.includes('createdAt')) {
|
|
590
|
+
try {
|
|
591
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "createdAt" TIMESTAMPTZ DEFAULT NOW()`);
|
|
592
|
+
console.log(`Added createdAt column to ${collectionName}`);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
// Column might already exist due to race condition, ignore
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (!existingColumnNames.includes('updatedAt')) {
|
|
599
|
+
try {
|
|
600
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMPTZ DEFAULT NOW()`);
|
|
601
|
+
console.log(`Added updatedAt column to ${collectionName}`);
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
// Column might already exist due to race condition, ignore
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Add deletedAt column if paranoid mode is enabled
|
|
609
|
+
if (schema.paranoid && !existingColumnNames.includes('deletedAt')) {
|
|
610
|
+
try {
|
|
611
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMPTZ`);
|
|
612
|
+
console.log(`Added deletedAt column to ${collectionName}`);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
// Column might already exist due to race condition, ignore
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Create table from schema definition using raw SQL
|
|
621
|
+
*/
|
|
622
|
+
async createTableFromSchema(collectionName, schema, skipFKConstraints = false) {
|
|
623
|
+
const sql = getSqlClient();
|
|
624
|
+
// Check if table already exists
|
|
625
|
+
const exists = await sql `
|
|
626
|
+
SELECT EXISTS (
|
|
627
|
+
SELECT FROM information_schema.tables
|
|
628
|
+
WHERE table_name = ${collectionName}
|
|
629
|
+
)
|
|
630
|
+
`;
|
|
631
|
+
if (exists[0].exists) {
|
|
632
|
+
console.log(`Table ${collectionName} already exists, syncing schema changes`);
|
|
633
|
+
// Sync missing columns with existing table
|
|
634
|
+
await this.syncTableColumns(collectionName, schema);
|
|
635
|
+
// Check/add foreign key constraints
|
|
636
|
+
if (!skipFKConstraints) {
|
|
637
|
+
await this.ensureForeignKeyConstraints(collectionName, schema);
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// Build CREATE TABLE statement
|
|
642
|
+
const columns = [];
|
|
643
|
+
const foreignKeyAssociations = [];
|
|
644
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
645
|
+
const fs = fieldSchema;
|
|
646
|
+
// Handle BelongsTo relations - they need a foreign key column
|
|
647
|
+
if (fs.relType === 'BelongsTo') {
|
|
648
|
+
const foreignKey = fs.foreignKey || `${fieldName}_Id`;
|
|
649
|
+
// Check if foreign key column already exists as a separate field
|
|
650
|
+
// If foreignKey === fieldName AND field has explicit type, we'll create it below
|
|
651
|
+
const foreignKeyExists = foreignKey !== fieldName && Object.keys(schema.fields).includes(foreignKey);
|
|
652
|
+
if (!foreignKeyExists && foreignKey !== fieldName) {
|
|
653
|
+
// Only create foreign key column if it doesn't already exist as a separate field
|
|
654
|
+
const columnDef = this.buildColumnDefinition(foreignKey, {
|
|
655
|
+
type: fs.type || 'UUID',
|
|
656
|
+
allowNull: fs.allowNull,
|
|
657
|
+
unique: fs.unique
|
|
658
|
+
});
|
|
659
|
+
if (columnDef) {
|
|
660
|
+
columns.push(columnDef);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Store association for later foreign key constraint creation
|
|
664
|
+
foreignKeyAssociations.push({ fieldName, assoc: fs });
|
|
665
|
+
// If foreignKey === fieldName AND field has explicit type, don't skip - create column below
|
|
666
|
+
if (foreignKey === fieldName && fs.type) {
|
|
667
|
+
// Fall through to create the column
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Skip other relation types that don't have explicit type
|
|
674
|
+
if (fs.relType && !fs.type)
|
|
675
|
+
continue;
|
|
676
|
+
const columnDef = this.buildColumnDefinition(fieldName, fs);
|
|
677
|
+
if (columnDef) {
|
|
678
|
+
columns.push(columnDef);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Add timestamps if enabled (default: true unless explicitly set to false)
|
|
682
|
+
if (schema.timestamps !== false) {
|
|
683
|
+
if (!schema.fields.createdAt) {
|
|
684
|
+
columns.push('"createdAt" TIMESTAMPTZ DEFAULT NOW()');
|
|
685
|
+
}
|
|
686
|
+
if (!schema.fields.updatedAt) {
|
|
687
|
+
columns.push('"updatedAt" TIMESTAMPTZ DEFAULT NOW()');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Add deletedAt for paranoid mode
|
|
691
|
+
if (schema.paranoid) {
|
|
692
|
+
if (!schema.fields.deletedAt) {
|
|
693
|
+
columns.push('"deletedAt" TIMESTAMPTZ');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (columns.length === 0) {
|
|
697
|
+
console.warn(`No columns to create for table ${collectionName}`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const createTableSQL = `CREATE TABLE "${collectionName}" (${columns.join(', ')})`;
|
|
701
|
+
try {
|
|
702
|
+
await sql.unsafe(createTableSQL);
|
|
703
|
+
console.log(`Created table: ${collectionName}`);
|
|
704
|
+
// Create foreign key constraints for BelongsTo relations (unless skipped)
|
|
705
|
+
if (!skipFKConstraints && foreignKeyAssociations.length > 0) {
|
|
706
|
+
await this.ensureForeignKeyConstraints(collectionName, schema);
|
|
707
|
+
}
|
|
708
|
+
// Create indexes if defined in schema
|
|
709
|
+
if (schema.indexes && Array.isArray(schema.indexes)) {
|
|
710
|
+
for (const index of schema.indexes) {
|
|
711
|
+
await this.createIndex(collectionName, index);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
console.error(`Failed to create table ${collectionName}:`, error);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Ensure foreign key constraints exist for BelongsTo relations
|
|
721
|
+
*/
|
|
722
|
+
async ensureForeignKeyConstraints(collectionName, schema) {
|
|
723
|
+
const sql = getSqlClient();
|
|
724
|
+
// Extract BelongsTo relations from schema fields
|
|
725
|
+
const belongsToRelations = [];
|
|
726
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
|
|
727
|
+
const fs = fieldSchema;
|
|
728
|
+
if (fs.relType === 'BelongsTo') {
|
|
729
|
+
belongsToRelations.push({ fieldName, assoc: fs });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (belongsToRelations.length === 0) {
|
|
733
|
+
return; // No BelongsTo relations
|
|
734
|
+
}
|
|
735
|
+
// Track if any columns were added
|
|
736
|
+
let columnsAdded = false;
|
|
737
|
+
// Process each BelongsTo relation
|
|
738
|
+
for (const { fieldName, assoc } of belongsToRelations) {
|
|
739
|
+
// Skip if constraints are explicitly disabled (for polymorphic relations)
|
|
740
|
+
if (assoc.constraints === false) {
|
|
741
|
+
console.log(`Skipping FK constraint for ${fieldName} (constraints: false)`);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const foreignKey = assoc.foreignKey || `${fieldName}_Id`;
|
|
745
|
+
const targetTable = assoc.target;
|
|
746
|
+
const targetKey = assoc.targetKey || 'id';
|
|
747
|
+
const onDelete = (assoc.onDelete || 'CASCADE').toUpperCase();
|
|
748
|
+
const onUpdate = (assoc.onUpdate || 'CASCADE').toUpperCase();
|
|
749
|
+
const constraintName = `fk_${collectionName}_${foreignKey}`;
|
|
750
|
+
try {
|
|
751
|
+
// First, check if the foreign key column exists
|
|
752
|
+
const columnExists = await sql `
|
|
753
|
+
SELECT column_name
|
|
754
|
+
FROM information_schema.columns
|
|
755
|
+
WHERE table_name = ${collectionName}
|
|
756
|
+
AND column_name = ${foreignKey}
|
|
757
|
+
`;
|
|
758
|
+
if (columnExists.length === 0) {
|
|
759
|
+
// Column doesn't exist, add it
|
|
760
|
+
console.log(`Adding foreign key column ${foreignKey} to ${collectionName}`);
|
|
761
|
+
// Get the type from the foreign key field definition, not the relation definition
|
|
762
|
+
const columnType = schema.fields[foreignKey]?.type || assoc.type || 'UUID';
|
|
763
|
+
const pgType = columnType === 'UUID' ? 'UUID' :
|
|
764
|
+
columnType === 'Integer' ? 'INTEGER' :
|
|
765
|
+
columnType === 'String' ? 'TEXT' : 'UUID';
|
|
766
|
+
// Use IF NOT EXISTS for safety
|
|
767
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" ADD COLUMN IF NOT EXISTS "${foreignKey}" ${pgType}`);
|
|
768
|
+
console.log(`Added column ${foreignKey} to ${collectionName}`);
|
|
769
|
+
columnsAdded = true;
|
|
770
|
+
// Update schema definition to include the new field
|
|
771
|
+
if (!schema.fields[foreignKey]) {
|
|
772
|
+
schema.fields[foreignKey] = {
|
|
773
|
+
type: columnType,
|
|
774
|
+
allowNull: true,
|
|
775
|
+
SystemGenerated: true
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Check if constraint already exists
|
|
780
|
+
const existingConstraint = await sql `
|
|
781
|
+
SELECT
|
|
782
|
+
tc.constraint_name,
|
|
783
|
+
rc.delete_rule,
|
|
784
|
+
rc.update_rule
|
|
785
|
+
FROM information_schema.table_constraints tc
|
|
786
|
+
JOIN information_schema.referential_constraints rc
|
|
787
|
+
ON tc.constraint_name = rc.constraint_name
|
|
788
|
+
WHERE tc.table_name = ${collectionName}
|
|
789
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
790
|
+
AND tc.constraint_name = ${constraintName}
|
|
791
|
+
`;
|
|
792
|
+
// If constraint exists, check if onDelete/onUpdate actions match
|
|
793
|
+
if (existingConstraint.length > 0) {
|
|
794
|
+
const existing = existingConstraint[0];
|
|
795
|
+
const existingOnDelete = existing.delete_rule.replace(' ', '_').toUpperCase();
|
|
796
|
+
const existingOnUpdate = existing.update_rule.replace(' ', '_').toUpperCase();
|
|
797
|
+
if (existingOnDelete === onDelete && existingOnUpdate === onUpdate) {
|
|
798
|
+
console.log(`Foreign key constraint ${constraintName} already exists with correct actions`);
|
|
799
|
+
continue; // Constraint is correct, skip
|
|
800
|
+
}
|
|
801
|
+
// Drop the old constraint if actions don't match
|
|
802
|
+
console.log(`Dropping foreign key constraint ${constraintName} to update actions`);
|
|
803
|
+
await sql.unsafe(`ALTER TABLE "${collectionName}" DROP CONSTRAINT "${constraintName}"`);
|
|
804
|
+
}
|
|
805
|
+
// Create the foreign key constraint
|
|
806
|
+
const fkSQL = createForeignKeySQL(collectionName, foreignKey, targetTable, targetKey, onDelete, onUpdate);
|
|
807
|
+
await sql.unsafe(fkSQL);
|
|
808
|
+
console.log(`Created foreign key constraint: ${constraintName}`);
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
console.error(`Failed to create foreign key constraint ${constraintName}:`, error);
|
|
812
|
+
// Don't throw - allow table creation to continue even if FK constraint fails
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// If columns were added, regenerate the Drizzle schema to include them
|
|
816
|
+
if (columnsAdded) {
|
|
817
|
+
console.log(`Regenerating Drizzle schema for ${collectionName} to include new foreign key columns`);
|
|
818
|
+
console.log(`Fields in schema:`, Object.keys(schema.fields));
|
|
819
|
+
// Update schemaDefinitions Map with the modified schema
|
|
820
|
+
const schemaDef = this.schemaDefinitions.get(collectionName);
|
|
821
|
+
if (schemaDef) {
|
|
822
|
+
schemaDef.schema = schema;
|
|
823
|
+
this.schemaDefinitions.set(collectionName, schemaDef);
|
|
824
|
+
}
|
|
825
|
+
// Update the schema in the database table
|
|
826
|
+
const db = getDatabase();
|
|
827
|
+
await db
|
|
828
|
+
.update(baasixSchemaDefinition)
|
|
829
|
+
.set({ schema: schema })
|
|
830
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName));
|
|
831
|
+
console.log(`Updated schema definition in database for ${collectionName}`);
|
|
832
|
+
// Regenerate the Drizzle table schema
|
|
833
|
+
await this.createOrUpdateModel(collectionName, schema);
|
|
834
|
+
const updatedSchema = this.schemas.get(collectionName);
|
|
835
|
+
console.log(`After regeneration, ${collectionName} table has columns:`, Object.keys(updatedSchema || {}).filter(k => !k.startsWith('_')));
|
|
836
|
+
console.log(`Drizzle schema for ${collectionName} regenerated successfully`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Create an index on a table
|
|
841
|
+
*/
|
|
842
|
+
async createIndex(tableName, indexDef) {
|
|
843
|
+
const sql = getSqlClient();
|
|
844
|
+
try {
|
|
845
|
+
const fields = indexDef.fields.map((f) => `"${f}"`).join(', ');
|
|
846
|
+
const indexName = indexDef.name || `${tableName}_${indexDef.fields.join('_')}_idx`;
|
|
847
|
+
const unique = indexDef.unique ? 'UNIQUE' : '';
|
|
848
|
+
// Support NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
|
|
849
|
+
let nullsNotDistinct = '';
|
|
850
|
+
if (indexDef.unique && indexDef.nullsNotDistinct) {
|
|
851
|
+
const supportsNullsNotDistinct = await isPgVersionAtLeast(15);
|
|
852
|
+
if (supportsNullsNotDistinct) {
|
|
853
|
+
nullsNotDistinct = ' NULLS NOT DISTINCT';
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
console.warn(`Index ${indexName}: NULLS NOT DISTINCT requires PostgreSQL 15+, ignoring option`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const createIndexSQL = `CREATE ${unique} INDEX IF NOT EXISTS "${indexName}" ON "${tableName}" (${fields})${nullsNotDistinct}`;
|
|
860
|
+
await sql.unsafe(createIndexSQL);
|
|
861
|
+
console.log(`Created index: ${indexName} on ${tableName}`);
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
console.error(`Failed to create index on ${tableName}:`, error);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Build column definition for CREATE TABLE
|
|
869
|
+
*/
|
|
870
|
+
buildColumnDefinition(fieldName, fieldSchema) {
|
|
871
|
+
const parts = [`"${fieldName}"`];
|
|
872
|
+
// Handle VIRTUAL (computed) fields - these are GENERATED columns
|
|
873
|
+
if (fieldSchema.type === 'VIRTUAL') {
|
|
874
|
+
if (fieldSchema.calculated) {
|
|
875
|
+
// VIRTUAL fields are GENERATED ALWAYS AS ... STORED
|
|
876
|
+
parts.push('TEXT'); // Default to TEXT for computed fields
|
|
877
|
+
parts.push(`GENERATED ALWAYS AS (${fieldSchema.calculated}) STORED`);
|
|
878
|
+
return parts.join(' ');
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
console.warn(`VIRTUAL field "${fieldName}" has no calculated expression. Skipping.`);
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Check for AUTOINCREMENT first
|
|
886
|
+
const hasAutoIncrement = fieldSchema.defaultValue?.type === 'AUTOINCREMENT';
|
|
887
|
+
// Map type
|
|
888
|
+
let pgType = 'TEXT';
|
|
889
|
+
switch (fieldSchema.type) {
|
|
890
|
+
case 'UUID':
|
|
891
|
+
pgType = 'UUID';
|
|
892
|
+
break;
|
|
893
|
+
case 'String':
|
|
894
|
+
pgType = fieldSchema.values?.stringLength ? `VARCHAR(${fieldSchema.values.stringLength})` : 'TEXT';
|
|
895
|
+
break;
|
|
896
|
+
case 'Text':
|
|
897
|
+
pgType = 'TEXT';
|
|
898
|
+
break;
|
|
899
|
+
case 'HTML':
|
|
900
|
+
// HTML content - stored as TEXT in database
|
|
901
|
+
pgType = 'TEXT';
|
|
902
|
+
break;
|
|
903
|
+
case 'Integer':
|
|
904
|
+
// Use SERIAL for auto-increment integers
|
|
905
|
+
pgType = hasAutoIncrement ? 'SERIAL' : 'INTEGER';
|
|
906
|
+
break;
|
|
907
|
+
case 'BigInt':
|
|
908
|
+
// Use BIGSERIAL for auto-increment bigints
|
|
909
|
+
pgType = hasAutoIncrement ? 'BIGSERIAL' : 'BIGINT';
|
|
910
|
+
break;
|
|
911
|
+
case 'Boolean':
|
|
912
|
+
pgType = 'BOOLEAN';
|
|
913
|
+
break;
|
|
914
|
+
case 'DateTime':
|
|
915
|
+
pgType = 'TIMESTAMPTZ';
|
|
916
|
+
break;
|
|
917
|
+
case 'DateTime_NO_TZ':
|
|
918
|
+
pgType = 'TIMESTAMP';
|
|
919
|
+
break;
|
|
920
|
+
case 'Date':
|
|
921
|
+
pgType = 'DATE';
|
|
922
|
+
break;
|
|
923
|
+
case 'Time':
|
|
924
|
+
pgType = 'TIMETZ';
|
|
925
|
+
break;
|
|
926
|
+
case 'Time_NO_TZ':
|
|
927
|
+
pgType = 'TIME';
|
|
928
|
+
break;
|
|
929
|
+
case 'JSON':
|
|
930
|
+
case 'JSONB':
|
|
931
|
+
pgType = 'JSONB';
|
|
932
|
+
break;
|
|
933
|
+
case 'Decimal':
|
|
934
|
+
case 'Real':
|
|
935
|
+
case 'Double':
|
|
936
|
+
pgType = 'NUMERIC';
|
|
937
|
+
break;
|
|
938
|
+
// PostGIS Geometry types
|
|
939
|
+
case 'Point':
|
|
940
|
+
pgType = `geometry(Point, ${fieldSchema.values?.srid || 4326})`;
|
|
941
|
+
break;
|
|
942
|
+
case 'LineString':
|
|
943
|
+
pgType = `geometry(LineString, ${fieldSchema.values?.srid || 4326})`;
|
|
944
|
+
break;
|
|
945
|
+
case 'Polygon':
|
|
946
|
+
pgType = `geometry(Polygon, ${fieldSchema.values?.srid || 4326})`;
|
|
947
|
+
break;
|
|
948
|
+
case 'MultiPoint':
|
|
949
|
+
pgType = `geometry(MultiPoint, ${fieldSchema.values?.srid || 4326})`;
|
|
950
|
+
break;
|
|
951
|
+
case 'MultiLineString':
|
|
952
|
+
pgType = `geometry(MultiLineString, ${fieldSchema.values?.srid || 4326})`;
|
|
953
|
+
break;
|
|
954
|
+
case 'MultiPolygon':
|
|
955
|
+
pgType = `geometry(MultiPolygon, ${fieldSchema.values?.srid || 4326})`;
|
|
956
|
+
break;
|
|
957
|
+
case 'GeometryCollection':
|
|
958
|
+
pgType = `geometry(GeometryCollection, ${fieldSchema.values?.srid || 4326})`;
|
|
959
|
+
break;
|
|
960
|
+
case 'Geography':
|
|
961
|
+
pgType = `geography(Point, ${fieldSchema.values?.srid || 4326})`;
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
parts.push(pgType);
|
|
965
|
+
// Primary key
|
|
966
|
+
if (fieldSchema.primaryKey) {
|
|
967
|
+
parts.push('PRIMARY KEY');
|
|
968
|
+
}
|
|
969
|
+
// Not null
|
|
970
|
+
if (fieldSchema.allowNull === false) {
|
|
971
|
+
parts.push('NOT NULL');
|
|
972
|
+
}
|
|
973
|
+
// Unique
|
|
974
|
+
if (fieldSchema.unique) {
|
|
975
|
+
parts.push('UNIQUE');
|
|
976
|
+
}
|
|
977
|
+
// Default value (skip if AUTOINCREMENT as SERIAL handles it)
|
|
978
|
+
if (fieldSchema.defaultValue !== undefined && !hasAutoIncrement) {
|
|
979
|
+
if (typeof fieldSchema.defaultValue === 'object' && fieldSchema.defaultValue.type) {
|
|
980
|
+
switch (fieldSchema.defaultValue.type) {
|
|
981
|
+
case 'UUIDV4':
|
|
982
|
+
parts.push('DEFAULT gen_random_uuid()');
|
|
983
|
+
break;
|
|
984
|
+
case 'SUID':
|
|
985
|
+
// Short unique ID - uses gen_random_uuid() for now
|
|
986
|
+
parts.push('DEFAULT gen_random_uuid()');
|
|
987
|
+
break;
|
|
988
|
+
case 'NOW':
|
|
989
|
+
parts.push('DEFAULT NOW()');
|
|
990
|
+
break;
|
|
991
|
+
case 'SQL':
|
|
992
|
+
// Raw SQL default expression
|
|
993
|
+
if (fieldSchema.defaultValue.value) {
|
|
994
|
+
parts.push(`DEFAULT ${fieldSchema.defaultValue.value}`);
|
|
995
|
+
}
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else if (typeof fieldSchema.defaultValue === 'string') {
|
|
1000
|
+
parts.push(`DEFAULT '${fieldSchema.defaultValue}'`);
|
|
1001
|
+
}
|
|
1002
|
+
else if (typeof fieldSchema.defaultValue === 'number') {
|
|
1003
|
+
parts.push(`DEFAULT ${fieldSchema.defaultValue}`);
|
|
1004
|
+
}
|
|
1005
|
+
else if (typeof fieldSchema.defaultValue === 'boolean') {
|
|
1006
|
+
parts.push(`DEFAULT ${fieldSchema.defaultValue}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return parts.join(' ');
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if database is empty and seed if needed
|
|
1013
|
+
*/
|
|
1014
|
+
async checkAndSeedDatabase() {
|
|
1015
|
+
// Check if we have the necessary tables
|
|
1016
|
+
const userSchema = this.schemas.get('baasix_User');
|
|
1017
|
+
const roleSchema = this.schemas.get('baasix_Role');
|
|
1018
|
+
if (!userSchema || !roleSchema) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const sql = getSqlClient();
|
|
1022
|
+
// Count users and roles
|
|
1023
|
+
const userCount = await sql `SELECT COUNT(*) FROM "baasix_User"`;
|
|
1024
|
+
const roleCount = await sql `SELECT COUNT(*) FROM "baasix_Role"`;
|
|
1025
|
+
if (parseInt(userCount[0].count) === 0 && parseInt(roleCount[0].count) === 0) {
|
|
1026
|
+
console.log('Database is empty, seeding...');
|
|
1027
|
+
await this.seedDatabase();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Seed the database with initial data
|
|
1032
|
+
*/
|
|
1033
|
+
async seedDatabase() {
|
|
1034
|
+
const sql = getSqlClient();
|
|
1035
|
+
try {
|
|
1036
|
+
console.log('Starting seeding...');
|
|
1037
|
+
// Create default roles
|
|
1038
|
+
await sql `
|
|
1039
|
+
INSERT INTO "baasix_Role" (id, name, description, "isTenantSpecific")
|
|
1040
|
+
VALUES
|
|
1041
|
+
(gen_random_uuid(), 'administrator', 'Full system access', false),
|
|
1042
|
+
(gen_random_uuid(), 'user', 'Standard user access', true),
|
|
1043
|
+
(gen_random_uuid(), 'public', 'Public access (unauthenticated)', true)
|
|
1044
|
+
ON CONFLICT (name) DO NOTHING
|
|
1045
|
+
`;
|
|
1046
|
+
console.log('Default roles created');
|
|
1047
|
+
// Get admin role ID
|
|
1048
|
+
const adminRole = await sql `
|
|
1049
|
+
SELECT id FROM "baasix_Role" WHERE name = 'administrator' LIMIT 1
|
|
1050
|
+
`;
|
|
1051
|
+
if (adminRole.length > 0) {
|
|
1052
|
+
const adminRoleId = adminRole[0].id;
|
|
1053
|
+
// Hash the admin password
|
|
1054
|
+
const hashedPassword = await argon2.hash('admin@123');
|
|
1055
|
+
// Create default admin user
|
|
1056
|
+
const adminUserId = await sql `
|
|
1057
|
+
INSERT INTO "baasix_User" (id, email, "firstName", "lastName", password)
|
|
1058
|
+
VALUES (gen_random_uuid(), 'admin@baasix.com', 'Baasix', 'Admin', ${hashedPassword})
|
|
1059
|
+
ON CONFLICT (email) DO NOTHING
|
|
1060
|
+
RETURNING id
|
|
1061
|
+
`;
|
|
1062
|
+
if (adminUserId.length > 0) {
|
|
1063
|
+
// Assign admin role to admin user
|
|
1064
|
+
await sql `
|
|
1065
|
+
INSERT INTO "baasix_UserRole" (id, "user_Id", "role_Id", "tenant_Id")
|
|
1066
|
+
VALUES (gen_random_uuid(), ${adminUserId[0].id}, ${adminRoleId}, NULL)
|
|
1067
|
+
ON CONFLICT DO NOTHING
|
|
1068
|
+
`;
|
|
1069
|
+
}
|
|
1070
|
+
console.log('Default admin user created');
|
|
1071
|
+
}
|
|
1072
|
+
// Seed default email templates
|
|
1073
|
+
await this.seedDefaultTemplates();
|
|
1074
|
+
console.log('Seeding complete');
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
console.error('Error seeding database:', error);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Seed default email templates into baasix_Template table
|
|
1082
|
+
*/
|
|
1083
|
+
async seedDefaultTemplates() {
|
|
1084
|
+
const sql = getSqlClient();
|
|
1085
|
+
// Check if templates table exists
|
|
1086
|
+
const templateTableExists = await sql `
|
|
1087
|
+
SELECT EXISTS (
|
|
1088
|
+
SELECT FROM information_schema.tables
|
|
1089
|
+
WHERE table_name = 'baasix_Template'
|
|
1090
|
+
)
|
|
1091
|
+
`;
|
|
1092
|
+
if (!templateTableExists[0].exists) {
|
|
1093
|
+
console.log('baasix_Template table does not exist yet, skipping template seeding');
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const defaultTemplates = [
|
|
1097
|
+
{
|
|
1098
|
+
type: 'inviteNewUser',
|
|
1099
|
+
subject: "You've been invited to join {{ tenant }}",
|
|
1100
|
+
body: `<h2>Welcome!</h2>
|
|
1101
|
+
<p>Hi,</p>
|
|
1102
|
+
<p>You've been invited by <strong>{{ inviterName }}</strong> to join <strong>{{ tenant }}</strong>.</p>
|
|
1103
|
+
<p>Click the button below to accept your invitation and create your account:</p>
|
|
1104
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1105
|
+
<a href="{{ inviteLink }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Accept Invitation</a>
|
|
1106
|
+
</p>
|
|
1107
|
+
<p><strong>Note:</strong> This invitation will expire on {{ expirationDate }}.</p>
|
|
1108
|
+
<p>If you didn't expect this invitation, you can safely ignore this email.</p>`,
|
|
1109
|
+
description: 'Template for inviting new users who do not have an account yet'
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
type: 'inviteExistingUser',
|
|
1113
|
+
subject: "You've been invited to join {{ tenant }}",
|
|
1114
|
+
body: `<h2>New Invitation</h2>
|
|
1115
|
+
<p>Hi,</p>
|
|
1116
|
+
<p>You've been invited by <strong>{{ inviterName }}</strong> to join <strong>{{ tenant }}</strong>.</p>
|
|
1117
|
+
<p>Since you already have an account, click the button below to accept the invitation:</p>
|
|
1118
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1119
|
+
<a href="{{ inviteLink }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Accept Invitation</a>
|
|
1120
|
+
</p>
|
|
1121
|
+
<p><strong>Note:</strong> This invitation will expire on {{ expirationDate }}.</p>
|
|
1122
|
+
<p>If you didn't expect this invitation, you can safely ignore this email.</p>`,
|
|
1123
|
+
description: 'Template for inviting existing users to a new tenant'
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
type: 'magicLinkUrl',
|
|
1127
|
+
subject: 'Sign in to {{ project_name }}',
|
|
1128
|
+
body: `<h2>Sign In Request</h2>
|
|
1129
|
+
<p>Hi {{ name }},</p>
|
|
1130
|
+
<p>Click the button below to sign in to your account:</p>
|
|
1131
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1132
|
+
<a href="{{ magicLinkUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Sign In</a>
|
|
1133
|
+
</p>
|
|
1134
|
+
<p>This link will expire in 15 minutes for security purposes.</p>
|
|
1135
|
+
<p>If you didn't request this sign-in link, you can safely ignore this email.</p>`,
|
|
1136
|
+
description: 'Template for magic link URL authentication'
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
type: 'magicLinkCode',
|
|
1140
|
+
subject: 'Your sign in code for {{ project_name }}',
|
|
1141
|
+
body: `<h2>Sign In Code</h2>
|
|
1142
|
+
<p>Hi {{ name }},</p>
|
|
1143
|
+
<p>Use the following code to sign in to your account:</p>
|
|
1144
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1145
|
+
<span style="background-color: #f5f5f5; padding: 16px 32px; font-size: 24px; font-family: monospace; letter-spacing: 4px; border-radius: 4px; display: inline-block;">{{ code }}</span>
|
|
1146
|
+
</p>
|
|
1147
|
+
<p>This code will expire in 15 minutes for security purposes.</p>
|
|
1148
|
+
<p>If you didn't request this code, you can safely ignore this email.</p>`,
|
|
1149
|
+
description: 'Template for magic link code authentication'
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
type: 'passwordReset',
|
|
1153
|
+
subject: 'Reset your password for {{ project_name }}',
|
|
1154
|
+
body: `<h2>Password Reset</h2>
|
|
1155
|
+
<p>Hi {{ name }},</p>
|
|
1156
|
+
<p>We received a request to reset your password. Click the button below to choose a new password:</p>
|
|
1157
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1158
|
+
<a href="{{ resetUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Reset Password</a>
|
|
1159
|
+
</p>
|
|
1160
|
+
<p>This link will expire in 1 hour for security purposes.</p>
|
|
1161
|
+
<p>If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.</p>`,
|
|
1162
|
+
description: 'Template for password reset emails'
|
|
1163
|
+
},
|
|
1164
|
+
{
|
|
1165
|
+
type: 'emailVerification',
|
|
1166
|
+
subject: 'Verify your email for {{ project_name }}',
|
|
1167
|
+
body: `<h2>Email Verification</h2>
|
|
1168
|
+
<p>Hi {{ name }},</p>
|
|
1169
|
+
<p>Please verify your email address by clicking the button below:</p>
|
|
1170
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1171
|
+
<a href="{{ verifyUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Verify Email</a>
|
|
1172
|
+
</p>
|
|
1173
|
+
<p>This link will expire in 24 hours.</p>
|
|
1174
|
+
<p>If you didn't create an account, you can safely ignore this email.</p>`,
|
|
1175
|
+
description: 'Template for email verification'
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
type: 'welcome',
|
|
1179
|
+
subject: 'Welcome to {{ project_name }}!',
|
|
1180
|
+
body: `<h2>Welcome!</h2>
|
|
1181
|
+
<p>Hi {{ name }},</p>
|
|
1182
|
+
<p>Thank you for joining {{ project_name }}! We're excited to have you on board.</p>
|
|
1183
|
+
<p>Your account has been successfully created and you're ready to get started.</p>
|
|
1184
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1185
|
+
<a href="{{ loginUrl }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Get Started</a>
|
|
1186
|
+
</p>
|
|
1187
|
+
<p>If you have any questions, feel free to reach out to our support team.</p>`,
|
|
1188
|
+
description: 'Template for welcome emails to new users'
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
type: 'notification',
|
|
1192
|
+
subject: '{{ notification_title }}',
|
|
1193
|
+
body: `<h2>{{ notification_title }}</h2>
|
|
1194
|
+
<p>Hi {{ name }},</p>
|
|
1195
|
+
<div>{{ notification_message }}</div>
|
|
1196
|
+
{% if action_url %}
|
|
1197
|
+
<p style="text-align: center; margin: 30px 0;">
|
|
1198
|
+
<a href="{{ action_url }}" style="background-color: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">{{ action_text | default: 'View Details' }}</a>
|
|
1199
|
+
</p>
|
|
1200
|
+
{% endif %}`,
|
|
1201
|
+
description: 'Generic notification template'
|
|
1202
|
+
}
|
|
1203
|
+
];
|
|
1204
|
+
try {
|
|
1205
|
+
for (const template of defaultTemplates) {
|
|
1206
|
+
await sql `
|
|
1207
|
+
INSERT INTO "baasix_Template" (id, type, subject, body, "tenant_Id", "isActive", description)
|
|
1208
|
+
VALUES (gen_random_uuid(), ${template.type}, ${template.subject}, ${template.body}, NULL, true, ${template.description})
|
|
1209
|
+
ON CONFLICT ("tenant_Id", type) DO NOTHING
|
|
1210
|
+
`;
|
|
1211
|
+
}
|
|
1212
|
+
console.log('Default email templates created');
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
console.error('Error seeding default templates:', error);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Ensure baasix_SchemaDefinition table exists
|
|
1220
|
+
*/
|
|
1221
|
+
async ensureSchemaDefinitionTableOLD() {
|
|
1222
|
+
const sql = getSqlClient();
|
|
1223
|
+
// Check if table exists
|
|
1224
|
+
const result = await sql `
|
|
1225
|
+
SELECT EXISTS (
|
|
1226
|
+
SELECT FROM information_schema.tables
|
|
1227
|
+
WHERE table_name = 'baasix_SchemaDefinition'
|
|
1228
|
+
)
|
|
1229
|
+
`;
|
|
1230
|
+
if (!result[0].exists) {
|
|
1231
|
+
console.log('Creating baasix_SchemaDefinition table...');
|
|
1232
|
+
await sql `
|
|
1233
|
+
CREATE TABLE "baasix_SchemaDefinition" (
|
|
1234
|
+
id SERIAL PRIMARY KEY,
|
|
1235
|
+
"collectionName" TEXT NOT NULL UNIQUE,
|
|
1236
|
+
schema JSONB NOT NULL,
|
|
1237
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
1238
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
1239
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
1240
|
+
"deletedAt" TIMESTAMPTZ
|
|
1241
|
+
)
|
|
1242
|
+
`;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Create or update a model from JSON schema definition
|
|
1247
|
+
*/
|
|
1248
|
+
async createOrUpdateModel(collectionName, jsonSchema) {
|
|
1249
|
+
try {
|
|
1250
|
+
console.log(`Creating/updating model: ${collectionName}`);
|
|
1251
|
+
let { fields, options, associations } = jsonSchema;
|
|
1252
|
+
// Add tenant fields for non-system schemas in multi-tenant mode
|
|
1253
|
+
const isSystemSchema = collectionName.startsWith('baasix_');
|
|
1254
|
+
const envValue = env.get('MULTI_TENANT');
|
|
1255
|
+
const isMultiTenant = envValue === 'true';
|
|
1256
|
+
console.log(`[createOrUpdateModel] ${collectionName}:`, {
|
|
1257
|
+
isSystemSchema,
|
|
1258
|
+
envValue: `"${envValue}"`,
|
|
1259
|
+
isMultiTenant,
|
|
1260
|
+
willAddTenantFields: isMultiTenant && !isSystemSchema
|
|
1261
|
+
});
|
|
1262
|
+
if (isMultiTenant && !isSystemSchema) {
|
|
1263
|
+
console.log(`[createOrUpdateModel] Adding tenant fields to ${collectionName}`);
|
|
1264
|
+
// Add tenant_Id field and tenant relation for multi-tenant isolation
|
|
1265
|
+
// IMPORTANT: Modify jsonSchema.fields directly so changes are reflected in createTableFromSchema
|
|
1266
|
+
jsonSchema.fields = {
|
|
1267
|
+
...fields,
|
|
1268
|
+
tenant_Id: {
|
|
1269
|
+
type: 'UUID',
|
|
1270
|
+
allowNull: true,
|
|
1271
|
+
SystemGenerated: 'true',
|
|
1272
|
+
description: 'Tenant identifier for multi-tenant isolation'
|
|
1273
|
+
},
|
|
1274
|
+
tenant: {
|
|
1275
|
+
relType: 'BelongsTo',
|
|
1276
|
+
target: 'baasix_Tenant',
|
|
1277
|
+
foreignKey: 'tenant_Id',
|
|
1278
|
+
as: 'tenant',
|
|
1279
|
+
SystemGenerated: 'true',
|
|
1280
|
+
description: 'M2O relationship to tenant'
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
// Update local fields variable to match
|
|
1284
|
+
fields = jsonSchema.fields;
|
|
1285
|
+
// Add tenant_Id to unique indexes for proper multi-tenant isolation
|
|
1286
|
+
if (!options) {
|
|
1287
|
+
options = {};
|
|
1288
|
+
jsonSchema.options = options;
|
|
1289
|
+
}
|
|
1290
|
+
if (!options.indexes) {
|
|
1291
|
+
options.indexes = [];
|
|
1292
|
+
}
|
|
1293
|
+
options.indexes = options.indexes.map((index) => {
|
|
1294
|
+
if (index.unique && !index.fields.includes('tenant_Id')) {
|
|
1295
|
+
return {
|
|
1296
|
+
...index,
|
|
1297
|
+
fields: [...index.fields, 'tenant_Id']
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return index;
|
|
1301
|
+
});
|
|
1302
|
+
// Update jsonSchema.options to reflect index changes
|
|
1303
|
+
jsonSchema.options = options;
|
|
1304
|
+
}
|
|
1305
|
+
// Extract associations from fields if not provided separately
|
|
1306
|
+
// This maintains compatibility with Sequelize-style schemas where relations are in fields
|
|
1307
|
+
if (!associations) {
|
|
1308
|
+
associations = {};
|
|
1309
|
+
console.log(`[createOrUpdateModel] Extracting associations for ${collectionName} from fields:`, Object.keys(fields));
|
|
1310
|
+
for (const [fieldName, fieldSchema] of Object.entries(fields)) {
|
|
1311
|
+
if (isRelationField(fieldSchema)) {
|
|
1312
|
+
const relSchema = fieldSchema;
|
|
1313
|
+
console.log(`[createOrUpdateModel] Found relation field ${fieldName}:`, { relType: relSchema.relType, target: relSchema.target, polymorphic: relSchema.polymorphic });
|
|
1314
|
+
// Use the 'as' name as the key if provided, otherwise use fieldName
|
|
1315
|
+
// This allows relations to be accessed by their alias (e.g., 'category' instead of 'categoryId')
|
|
1316
|
+
const relationKey = relSchema.as || fieldName;
|
|
1317
|
+
associations[relationKey] = {
|
|
1318
|
+
type: relSchema.relType,
|
|
1319
|
+
model: relSchema.target,
|
|
1320
|
+
foreignKey: relSchema.foreignKey,
|
|
1321
|
+
targetKey: relSchema.targetKey,
|
|
1322
|
+
as: relSchema.as || fieldName,
|
|
1323
|
+
// For M2A (polymorphic), target IS the junction table
|
|
1324
|
+
// For BelongsToMany, through is explicitly set
|
|
1325
|
+
through: relSchema.through || (relSchema.polymorphic ? relSchema.target : undefined),
|
|
1326
|
+
onDelete: relSchema.onDelete,
|
|
1327
|
+
onUpdate: relSchema.onUpdate,
|
|
1328
|
+
// M2A/polymorphic specific fields
|
|
1329
|
+
polymorphic: relSchema.polymorphic,
|
|
1330
|
+
tables: relSchema.tables
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
console.log(`[createOrUpdateModel] Extracted ${Object.keys(associations).length} associations for ${collectionName}:`, Object.keys(associations));
|
|
1335
|
+
}
|
|
1336
|
+
// Build column definitions
|
|
1337
|
+
const columns = {};
|
|
1338
|
+
console.log(`[createOrUpdateModel] ${collectionName} field names after tenant injection:`, Object.keys(fields));
|
|
1339
|
+
// Process each field
|
|
1340
|
+
for (const [fieldName, fieldSchema] of Object.entries(fields)) {
|
|
1341
|
+
// Skip relationship-only fields (no explicit type defined)
|
|
1342
|
+
// But process fields that have both type AND relType (e.g., foreign key columns)
|
|
1343
|
+
if (isRelationField(fieldSchema) && !fieldSchema.type) {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
try {
|
|
1347
|
+
const column = mapJsonTypeToDrizzle(fieldName, fieldSchema);
|
|
1348
|
+
if (column) {
|
|
1349
|
+
columns[fieldName] = column;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
console.warn(`Failed to map field ${fieldName}:`, error);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
// Add timestamps if enabled (default: true)
|
|
1357
|
+
const includeTimestamps = options?.timestamps !== false;
|
|
1358
|
+
if (includeTimestamps) {
|
|
1359
|
+
if (!columns.createdAt) {
|
|
1360
|
+
columns.createdAt = timestamp('createdAt', { withTimezone: true }).notNull().defaultNow();
|
|
1361
|
+
}
|
|
1362
|
+
if (!columns.updatedAt) {
|
|
1363
|
+
columns.updatedAt = timestamp('updatedAt', { withTimezone: true }).notNull().defaultNow();
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
// Add deletedAt for paranoid mode
|
|
1367
|
+
if (options?.paranoid) {
|
|
1368
|
+
if (!columns.deletedAt) {
|
|
1369
|
+
columns.deletedAt = timestamp('deletedAt', { withTimezone: true });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
// Create the table schema
|
|
1373
|
+
const tableSchema = pgTable(collectionName, columns);
|
|
1374
|
+
// Store the schema (soft-delete filtering will be applied at query time)
|
|
1375
|
+
this.schemas.set(collectionName, tableSchema);
|
|
1376
|
+
// Track paranoid mode for this table
|
|
1377
|
+
if (options?.paranoid) {
|
|
1378
|
+
this.schemas.set(`${collectionName}_paranoid`, true);
|
|
1379
|
+
}
|
|
1380
|
+
// Handle associations (store them for later query use)
|
|
1381
|
+
if (associations) {
|
|
1382
|
+
relationBuilder.storeAssociations(collectionName, associations);
|
|
1383
|
+
}
|
|
1384
|
+
// NOTE: Index creation is now handled in createTableFromSchema
|
|
1385
|
+
// to avoid DB queries during fast startup path
|
|
1386
|
+
// Register hooks if needed
|
|
1387
|
+
this.registerModelHooks(collectionName, jsonSchema);
|
|
1388
|
+
console.log(`Model ${collectionName} created successfully`);
|
|
1389
|
+
return tableSchema;
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
console.error(`Failed to create/update model ${collectionName}:`, error);
|
|
1393
|
+
throw error;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Create indexes for a table
|
|
1398
|
+
*/
|
|
1399
|
+
async createIndexes(tableName, indexes) {
|
|
1400
|
+
const sql = getSqlClient();
|
|
1401
|
+
// Check PostgreSQL version once for all indexes
|
|
1402
|
+
const supportsNullsNotDistinct = await isPgVersionAtLeast(15);
|
|
1403
|
+
for (const index of indexes) {
|
|
1404
|
+
try {
|
|
1405
|
+
const indexName = index.name || `${tableName}_${index.fields.join('_')}_idx`;
|
|
1406
|
+
const unique = index.unique ? 'UNIQUE' : '';
|
|
1407
|
+
const method = index.type || 'BTREE';
|
|
1408
|
+
const fields = index.fields.map(f => `"${f}"`).join(', ');
|
|
1409
|
+
// Support NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
|
|
1410
|
+
let nullsNotDistinct = '';
|
|
1411
|
+
if (index.unique && index.nullsNotDistinct) {
|
|
1412
|
+
if (supportsNullsNotDistinct) {
|
|
1413
|
+
nullsNotDistinct = ' NULLS NOT DISTINCT';
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
console.warn(`Index ${indexName}: NULLS NOT DISTINCT requires PostgreSQL 15+, ignoring option`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// Check if index already exists
|
|
1420
|
+
const exists = await sql `
|
|
1421
|
+
SELECT EXISTS (
|
|
1422
|
+
SELECT FROM pg_indexes
|
|
1423
|
+
WHERE tablename = ${tableName}
|
|
1424
|
+
AND indexname = ${indexName}
|
|
1425
|
+
)
|
|
1426
|
+
`;
|
|
1427
|
+
if (!exists[0].exists) {
|
|
1428
|
+
await sql.unsafe(`
|
|
1429
|
+
CREATE ${unique} INDEX "${indexName}"
|
|
1430
|
+
ON "${tableName}" USING ${method} (${fields})${nullsNotDistinct}
|
|
1431
|
+
`);
|
|
1432
|
+
console.log(`Created index ${indexName} on ${tableName}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
console.warn(`Failed to create index on ${tableName}:`, error);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Register model-specific hooks
|
|
1442
|
+
*/
|
|
1443
|
+
registerModelHooks(collectionName, jsonSchema) {
|
|
1444
|
+
// Hook registration will be implemented based on schema configuration
|
|
1445
|
+
// For now, this is a placeholder for future hook registration
|
|
1446
|
+
// const hooksManager = HooksManager.getInstance();
|
|
1447
|
+
// Example: Register audit logging hook for all models
|
|
1448
|
+
// This can be customized based on schema options
|
|
1449
|
+
if (jsonSchema.options?.paranoid) {
|
|
1450
|
+
// Add soft-delete specific hooks if needed
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Get a registered schema by collection name
|
|
1455
|
+
*/
|
|
1456
|
+
getSchema(collectionName) {
|
|
1457
|
+
return this.schemas.get(collectionName);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Get all registered schemas
|
|
1461
|
+
*/
|
|
1462
|
+
getAllSchemas() {
|
|
1463
|
+
return this.schemas;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Check if a model/collection exists
|
|
1467
|
+
*/
|
|
1468
|
+
modelExists(collectionName) {
|
|
1469
|
+
return this.schemas.has(collectionName);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Get table for a collection
|
|
1473
|
+
*/
|
|
1474
|
+
getTable(collectionName) {
|
|
1475
|
+
const schema = this.schemas.get(collectionName);
|
|
1476
|
+
if (!schema) {
|
|
1477
|
+
throw new Error(`Table not found for collection: ${collectionName}`);
|
|
1478
|
+
}
|
|
1479
|
+
return schema;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Get primary key field name for a collection
|
|
1483
|
+
*/
|
|
1484
|
+
getPrimaryKey(collectionName) {
|
|
1485
|
+
// Get schema definition from schemaDefinitions Map (loaded during initialization)
|
|
1486
|
+
const schemaDef = this.schemaDefinitions.get(collectionName);
|
|
1487
|
+
if (schemaDef && schemaDef.schema && schemaDef.schema.fields) {
|
|
1488
|
+
// Find the field with primaryKey: true
|
|
1489
|
+
for (const [fieldName, fieldSchema] of Object.entries(schemaDef.schema.fields)) {
|
|
1490
|
+
if (fieldSchema.primaryKey === true) {
|
|
1491
|
+
return fieldName;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
// Default to 'id' if no primary key is explicitly defined
|
|
1496
|
+
return 'id';
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Check if a collection has paranoid mode enabled (soft delete)
|
|
1500
|
+
*/
|
|
1501
|
+
isParanoid(collectionName) {
|
|
1502
|
+
return this.schemas.get(`${collectionName}_paranoid`) === true;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Get schema options for a collection
|
|
1506
|
+
*/
|
|
1507
|
+
getSchemaOptions(collectionName) {
|
|
1508
|
+
const schema = this.getSchema(collectionName);
|
|
1509
|
+
if (!schema)
|
|
1510
|
+
return {};
|
|
1511
|
+
// Schema options are stored in the schema definition
|
|
1512
|
+
// For now, we track paranoid mode separately
|
|
1513
|
+
return {
|
|
1514
|
+
paranoid: this.isParanoid(collectionName)
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Get schema definition with flags from baasix_SchemaDefinition table
|
|
1519
|
+
*/
|
|
1520
|
+
async getSchemaDefinition(collectionName) {
|
|
1521
|
+
try {
|
|
1522
|
+
const schemaDefTable = this.getTable('baasix_SchemaDefinition');
|
|
1523
|
+
if (!schemaDefTable)
|
|
1524
|
+
return null;
|
|
1525
|
+
const db = getDatabase();
|
|
1526
|
+
const result = await db
|
|
1527
|
+
.select()
|
|
1528
|
+
.from(schemaDefTable)
|
|
1529
|
+
.where(eq(schemaDefTable.collectionName, collectionName))
|
|
1530
|
+
.limit(1);
|
|
1531
|
+
if (result.length === 0)
|
|
1532
|
+
return null;
|
|
1533
|
+
return result[0].schema;
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
console.error(`Error getting schema definition for ${collectionName}:`, error);
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Get relation names for a collection
|
|
1542
|
+
*/
|
|
1543
|
+
getRelationNames(collectionName) {
|
|
1544
|
+
const associations = relationBuilder.getAssociations(collectionName);
|
|
1545
|
+
if (!associations)
|
|
1546
|
+
return [];
|
|
1547
|
+
return Object.keys(associations);
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Get relations for a collection
|
|
1551
|
+
*/
|
|
1552
|
+
getRelations(collectionName) {
|
|
1553
|
+
return this.relations.get(collectionName);
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Get a specific relation for a collection
|
|
1557
|
+
*/
|
|
1558
|
+
getRelation(collectionName, relationName) {
|
|
1559
|
+
const relations = this.relations.get(collectionName);
|
|
1560
|
+
return relations?.[relationName];
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Check if schema manager is initialized
|
|
1564
|
+
*/
|
|
1565
|
+
isInitialized() {
|
|
1566
|
+
return this.initialized;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Add a new schema definition to the database
|
|
1570
|
+
*/
|
|
1571
|
+
async addSchemaDefinition(collectionName, schema) {
|
|
1572
|
+
const db = getDatabase();
|
|
1573
|
+
// Check if schema already exists
|
|
1574
|
+
const existing = await db
|
|
1575
|
+
.select()
|
|
1576
|
+
.from(baasixSchemaDefinition)
|
|
1577
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName))
|
|
1578
|
+
.limit(1);
|
|
1579
|
+
if (existing.length > 0) {
|
|
1580
|
+
// Update existing schema
|
|
1581
|
+
await db
|
|
1582
|
+
.update(baasixSchemaDefinition)
|
|
1583
|
+
.set({
|
|
1584
|
+
schema: schema,
|
|
1585
|
+
updatedAt: new Date(),
|
|
1586
|
+
})
|
|
1587
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName));
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
// Insert new schema
|
|
1591
|
+
await db.insert(baasixSchemaDefinition).values({
|
|
1592
|
+
collectionName,
|
|
1593
|
+
schema: schema,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
// Reload the schema
|
|
1597
|
+
await this.createOrUpdateModel(collectionName, schema);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Remove a schema definition
|
|
1601
|
+
*/
|
|
1602
|
+
async removeSchemaDefinition(collectionName) {
|
|
1603
|
+
const db = getDatabase();
|
|
1604
|
+
// Delete the schema definition
|
|
1605
|
+
await db
|
|
1606
|
+
.delete(baasixSchemaDefinition)
|
|
1607
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName));
|
|
1608
|
+
// Remove from memory
|
|
1609
|
+
this.schemas.delete(collectionName);
|
|
1610
|
+
this.relations.delete(collectionName);
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Sync schemas - create/update tables in database
|
|
1614
|
+
* Similar to Sequelize.sync()
|
|
1615
|
+
*/
|
|
1616
|
+
async sync(options) {
|
|
1617
|
+
console.log('Syncing schemas with database...');
|
|
1618
|
+
// For now, we rely on Drizzle Kit for migrations
|
|
1619
|
+
// In production, use: drizzle-kit push:pg or drizzle-kit migrate
|
|
1620
|
+
console.warn('Schema sync is handled by Drizzle Kit. Run: npm run db:push');
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Create or update a model (for schema routes compatibility)
|
|
1624
|
+
*/
|
|
1625
|
+
async updateModel(collectionName, schema, accountability) {
|
|
1626
|
+
console.log(`Creating/updating model: ${collectionName}`);
|
|
1627
|
+
console.log(`[updateModel] Schema fields for ${collectionName}:`, Object.keys(schema.fields || {}));
|
|
1628
|
+
// Store JSON schema definition in memory
|
|
1629
|
+
this.schemaDefinitions.set(collectionName, { collectionName, schema });
|
|
1630
|
+
// Update schema definition in database
|
|
1631
|
+
const db = getDatabase();
|
|
1632
|
+
const existingSchema = await db
|
|
1633
|
+
.select()
|
|
1634
|
+
.from(baasixSchemaDefinition)
|
|
1635
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName))
|
|
1636
|
+
.limit(1);
|
|
1637
|
+
if (existingSchema.length > 0) {
|
|
1638
|
+
// Update existing schema
|
|
1639
|
+
await db
|
|
1640
|
+
.update(baasixSchemaDefinition)
|
|
1641
|
+
.set({ schema: schema, updatedAt: new Date() })
|
|
1642
|
+
.where(eq(baasixSchemaDefinition.collectionName, collectionName));
|
|
1643
|
+
console.log(`Updated schema definition in database for ${collectionName}`);
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
// Insert new schema
|
|
1647
|
+
await db.insert(baasixSchemaDefinition).values({
|
|
1648
|
+
collectionName,
|
|
1649
|
+
schema: schema,
|
|
1650
|
+
});
|
|
1651
|
+
console.log(`Inserted new schema definition in database for ${collectionName}`);
|
|
1652
|
+
}
|
|
1653
|
+
// Create/update the Drizzle schema in memory
|
|
1654
|
+
await this.createOrUpdateModel(collectionName, schema);
|
|
1655
|
+
// Create the actual PostgreSQL table
|
|
1656
|
+
await this.createTableFromSchema(collectionName, schema);
|
|
1657
|
+
console.log(`Model ${collectionName} created/updated successfully`);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Delete a model (for schema routes compatibility)
|
|
1661
|
+
*/
|
|
1662
|
+
async deleteModel(collectionName) {
|
|
1663
|
+
console.log(`Deleting model: ${collectionName}`);
|
|
1664
|
+
this.schemas.delete(collectionName);
|
|
1665
|
+
// In production, this would drop the table
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Add an index to a collection
|
|
1669
|
+
*/
|
|
1670
|
+
async addIndex(collectionName, indexData, accountability) {
|
|
1671
|
+
const sql = getSqlClient();
|
|
1672
|
+
try {
|
|
1673
|
+
const fields = indexData.fields;
|
|
1674
|
+
const indexName = indexData.name || `${collectionName}_${fields.join('_')}_idx`;
|
|
1675
|
+
const unique = indexData.unique ? 'UNIQUE' : '';
|
|
1676
|
+
// Support NULLS NOT DISTINCT for unique indexes (PostgreSQL 15+)
|
|
1677
|
+
let nullsNotDistinct = '';
|
|
1678
|
+
if (indexData.unique && indexData.nullsNotDistinct) {
|
|
1679
|
+
const supportsNullsNotDistinct = await isPgVersionAtLeast(15);
|
|
1680
|
+
if (supportsNullsNotDistinct) {
|
|
1681
|
+
nullsNotDistinct = ' NULLS NOT DISTINCT';
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
console.warn(`Index ${indexName}: NULLS NOT DISTINCT requires PostgreSQL 15+, ignoring option`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
// Check if table exists
|
|
1688
|
+
const tableExists = await sql `
|
|
1689
|
+
SELECT EXISTS (
|
|
1690
|
+
SELECT FROM information_schema.tables
|
|
1691
|
+
WHERE table_name = ${collectionName}
|
|
1692
|
+
)
|
|
1693
|
+
`;
|
|
1694
|
+
if (!tableExists[0].exists) {
|
|
1695
|
+
throw new Error(`Table ${collectionName} does not exist`);
|
|
1696
|
+
}
|
|
1697
|
+
// Check if index already exists
|
|
1698
|
+
const indexExists = await sql `
|
|
1699
|
+
SELECT EXISTS (
|
|
1700
|
+
SELECT FROM pg_indexes
|
|
1701
|
+
WHERE tablename = ${collectionName}
|
|
1702
|
+
AND indexname = ${indexName}
|
|
1703
|
+
)
|
|
1704
|
+
`;
|
|
1705
|
+
if (indexExists[0].exists) {
|
|
1706
|
+
console.log(`Index ${indexName} already exists on ${collectionName}`);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
// Build CREATE INDEX statement
|
|
1710
|
+
const fieldList = fields.map((f) => `"${f}"`).join(', ');
|
|
1711
|
+
const createIndexSQL = `CREATE ${unique} INDEX "${indexName}" ON "${collectionName}" (${fieldList})${nullsNotDistinct}`;
|
|
1712
|
+
await sql.unsafe(createIndexSQL);
|
|
1713
|
+
console.log(`Created index ${indexName} on ${collectionName}`);
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
console.error(`Failed to create index on ${collectionName}:`, error);
|
|
1717
|
+
throw error;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Remove an index (stub for compatibility)
|
|
1722
|
+
*/
|
|
1723
|
+
async removeIndex(collectionName, indexName) {
|
|
1724
|
+
console.log(`Removing index ${indexName} from ${collectionName}`);
|
|
1725
|
+
// Stub for now
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Add missing foreign key indexes to all collections
|
|
1729
|
+
* Useful for migrating existing databases to use auto-indexing
|
|
1730
|
+
*/
|
|
1731
|
+
async addMissingForeignKeyIndexes(accountability) {
|
|
1732
|
+
const sql = getSqlClient();
|
|
1733
|
+
const result = {
|
|
1734
|
+
created: [],
|
|
1735
|
+
skipped: [],
|
|
1736
|
+
errors: [],
|
|
1737
|
+
};
|
|
1738
|
+
console.log('Scanning for missing foreign key indexes...');
|
|
1739
|
+
// Fetch all schema definitions from database (not from memory cache which has Drizzle table objects)
|
|
1740
|
+
const schemaDefinitions = await sql `
|
|
1741
|
+
SELECT "collectionName", schema FROM "baasix_SchemaDefinition"
|
|
1742
|
+
`;
|
|
1743
|
+
console.log(`Found ${schemaDefinitions.length} schemas to scan for missing indexes`);
|
|
1744
|
+
// Track which schemas need to be updated
|
|
1745
|
+
const schemasToUpdate = new Map();
|
|
1746
|
+
for (const schemaDef of schemaDefinitions) {
|
|
1747
|
+
const collectionName = schemaDef.collectionName;
|
|
1748
|
+
const schema = schemaDef.schema;
|
|
1749
|
+
if (!schema?.fields)
|
|
1750
|
+
continue;
|
|
1751
|
+
const newIndexes = [];
|
|
1752
|
+
// Find all BelongsTo relationships (M2O, O2O)
|
|
1753
|
+
for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
|
|
1754
|
+
const field = fieldDef;
|
|
1755
|
+
// Skip if field definition is null/undefined
|
|
1756
|
+
if (!field)
|
|
1757
|
+
continue;
|
|
1758
|
+
// Check if this is a BelongsTo relation
|
|
1759
|
+
if (field.relType === 'BelongsTo' && field.foreignKey) {
|
|
1760
|
+
const fkColumn = field.foreignKey;
|
|
1761
|
+
const indexName = `${collectionName}_${fkColumn}_idx`;
|
|
1762
|
+
// Check if index already exists in schema definition
|
|
1763
|
+
const existsInSchema = schema.indexes?.some((idx) => idx.name === indexName);
|
|
1764
|
+
try {
|
|
1765
|
+
// Check if index already exists in database
|
|
1766
|
+
const indexExists = await sql `
|
|
1767
|
+
SELECT EXISTS (
|
|
1768
|
+
SELECT FROM pg_indexes
|
|
1769
|
+
WHERE tablename = ${collectionName}
|
|
1770
|
+
AND indexname = ${indexName}
|
|
1771
|
+
)
|
|
1772
|
+
`;
|
|
1773
|
+
if (indexExists[0].exists && existsInSchema) {
|
|
1774
|
+
result.skipped.push({
|
|
1775
|
+
collection: collectionName,
|
|
1776
|
+
indexName,
|
|
1777
|
+
reason: 'Index already exists',
|
|
1778
|
+
});
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
// Check if column exists
|
|
1782
|
+
const columnExists = await sql `
|
|
1783
|
+
SELECT EXISTS (
|
|
1784
|
+
SELECT FROM information_schema.columns
|
|
1785
|
+
WHERE table_name = ${collectionName}
|
|
1786
|
+
AND column_name = ${fkColumn}
|
|
1787
|
+
)
|
|
1788
|
+
`;
|
|
1789
|
+
if (!columnExists[0].exists) {
|
|
1790
|
+
result.skipped.push({
|
|
1791
|
+
collection: collectionName,
|
|
1792
|
+
indexName,
|
|
1793
|
+
reason: `Column ${fkColumn} does not exist`,
|
|
1794
|
+
});
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
// Create the index in database if not exists
|
|
1798
|
+
if (!indexExists[0].exists) {
|
|
1799
|
+
const createIndexSQL = `CREATE INDEX "${indexName}" ON "${collectionName}" ("${fkColumn}")`;
|
|
1800
|
+
await sql.unsafe(createIndexSQL);
|
|
1801
|
+
console.log(`Created index ${indexName} on ${collectionName}(${fkColumn})`);
|
|
1802
|
+
}
|
|
1803
|
+
// Add to schema definition if not exists
|
|
1804
|
+
if (!existsInSchema) {
|
|
1805
|
+
newIndexes.push({
|
|
1806
|
+
name: indexName,
|
|
1807
|
+
fields: [fkColumn],
|
|
1808
|
+
unique: false,
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
result.created.push({
|
|
1812
|
+
collection: collectionName,
|
|
1813
|
+
indexName,
|
|
1814
|
+
field: fkColumn,
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
catch (error) {
|
|
1818
|
+
result.errors.push({
|
|
1819
|
+
collection: collectionName,
|
|
1820
|
+
indexName,
|
|
1821
|
+
error: error.message,
|
|
1822
|
+
});
|
|
1823
|
+
console.error(`Failed to create index ${indexName}:`, error.message);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
// Check for junction tables (M2M/M2A) and add individual FK indexes
|
|
1828
|
+
if (schema.isJunction) {
|
|
1829
|
+
// Find all FK columns in junction table (columns ending with _id)
|
|
1830
|
+
for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
|
|
1831
|
+
const field = fieldDef;
|
|
1832
|
+
// Skip if field definition is null/undefined
|
|
1833
|
+
if (!field)
|
|
1834
|
+
continue;
|
|
1835
|
+
// Skip non-FK fields and primary keys
|
|
1836
|
+
if (field.primaryKey || field.relType)
|
|
1837
|
+
continue;
|
|
1838
|
+
// Check if it's a FK column (ends with _id or is item_id for M2A)
|
|
1839
|
+
if (fieldName.endsWith('_id') || fieldName === 'item_id') {
|
|
1840
|
+
const indexName = `${collectionName}_${fieldName}_idx`;
|
|
1841
|
+
const existsInSchema = schema.indexes?.some((idx) => idx.name === indexName);
|
|
1842
|
+
try {
|
|
1843
|
+
// Check if index already exists in database
|
|
1844
|
+
const indexExists = await sql `
|
|
1845
|
+
SELECT EXISTS (
|
|
1846
|
+
SELECT FROM pg_indexes
|
|
1847
|
+
WHERE tablename = ${collectionName}
|
|
1848
|
+
AND indexname = ${indexName}
|
|
1849
|
+
)
|
|
1850
|
+
`;
|
|
1851
|
+
if (indexExists[0].exists && existsInSchema) {
|
|
1852
|
+
result.skipped.push({
|
|
1853
|
+
collection: collectionName,
|
|
1854
|
+
indexName,
|
|
1855
|
+
reason: 'Index already exists',
|
|
1856
|
+
});
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
// Create the index in database if not exists
|
|
1860
|
+
if (!indexExists[0].exists) {
|
|
1861
|
+
const createIndexSQL = `CREATE INDEX "${indexName}" ON "${collectionName}" ("${fieldName}")`;
|
|
1862
|
+
await sql.unsafe(createIndexSQL);
|
|
1863
|
+
console.log(`Created index ${indexName} on ${collectionName}(${fieldName})`);
|
|
1864
|
+
}
|
|
1865
|
+
// Add to schema definition if not exists
|
|
1866
|
+
if (!existsInSchema) {
|
|
1867
|
+
newIndexes.push({
|
|
1868
|
+
name: indexName,
|
|
1869
|
+
fields: [fieldName],
|
|
1870
|
+
unique: false,
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
result.created.push({
|
|
1874
|
+
collection: collectionName,
|
|
1875
|
+
indexName,
|
|
1876
|
+
field: fieldName,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
catch (error) {
|
|
1880
|
+
result.errors.push({
|
|
1881
|
+
collection: collectionName,
|
|
1882
|
+
indexName,
|
|
1883
|
+
error: error.message,
|
|
1884
|
+
});
|
|
1885
|
+
console.error(`Failed to create index ${indexName}:`, error.message);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
// Also add index on 'collection' column for M2A junction tables
|
|
1890
|
+
if (schema.fields.collection) {
|
|
1891
|
+
const indexName = `${collectionName}_collection_idx`;
|
|
1892
|
+
const existsInSchema = schema.indexes?.some((idx) => idx.name === indexName);
|
|
1893
|
+
try {
|
|
1894
|
+
const indexExists = await sql `
|
|
1895
|
+
SELECT EXISTS (
|
|
1896
|
+
SELECT FROM pg_indexes
|
|
1897
|
+
WHERE tablename = ${collectionName}
|
|
1898
|
+
AND indexname = ${indexName}
|
|
1899
|
+
)
|
|
1900
|
+
`;
|
|
1901
|
+
if (indexExists[0].exists && existsInSchema) {
|
|
1902
|
+
result.skipped.push({
|
|
1903
|
+
collection: collectionName,
|
|
1904
|
+
indexName,
|
|
1905
|
+
reason: 'Index already exists',
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
// Create the index in database if not exists
|
|
1910
|
+
if (!indexExists[0].exists) {
|
|
1911
|
+
const createIndexSQL = `CREATE INDEX "${indexName}" ON "${collectionName}" ("collection")`;
|
|
1912
|
+
await sql.unsafe(createIndexSQL);
|
|
1913
|
+
console.log(`Created index ${indexName} on ${collectionName}(collection)`);
|
|
1914
|
+
}
|
|
1915
|
+
// Add to schema definition if not exists
|
|
1916
|
+
if (!existsInSchema) {
|
|
1917
|
+
newIndexes.push({
|
|
1918
|
+
name: indexName,
|
|
1919
|
+
fields: ['collection'],
|
|
1920
|
+
unique: false,
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
result.created.push({
|
|
1924
|
+
collection: collectionName,
|
|
1925
|
+
indexName,
|
|
1926
|
+
field: 'collection',
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
catch (error) {
|
|
1931
|
+
result.errors.push({
|
|
1932
|
+
collection: collectionName,
|
|
1933
|
+
indexName,
|
|
1934
|
+
error: error.message,
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
// Track schemas that need updating
|
|
1940
|
+
if (newIndexes.length > 0) {
|
|
1941
|
+
schemasToUpdate.set(collectionName, { schema, newIndexes });
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
// Update schema definitions with new indexes
|
|
1945
|
+
for (const [collectionName, { schema, newIndexes }] of schemasToUpdate) {
|
|
1946
|
+
try {
|
|
1947
|
+
const updatedSchema = { ...schema };
|
|
1948
|
+
if (!updatedSchema.indexes) {
|
|
1949
|
+
updatedSchema.indexes = [];
|
|
1950
|
+
}
|
|
1951
|
+
updatedSchema.indexes.push(...newIndexes);
|
|
1952
|
+
// Update the schema definition directly in the database
|
|
1953
|
+
await sql `
|
|
1954
|
+
UPDATE "baasix_SchemaDefinition"
|
|
1955
|
+
SET schema = ${JSON.stringify(updatedSchema)}::jsonb,
|
|
1956
|
+
"updatedAt" = NOW()
|
|
1957
|
+
WHERE "collectionName" = ${collectionName}
|
|
1958
|
+
`;
|
|
1959
|
+
console.log(`Updated schema definition for ${collectionName} with ${newIndexes.length} new indexes`);
|
|
1960
|
+
}
|
|
1961
|
+
catch (error) {
|
|
1962
|
+
console.error(`Failed to update schema definition for ${collectionName}:`, error.message);
|
|
1963
|
+
// Don't add to errors since the indexes were created successfully
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
console.log(`Index migration complete: ${result.created.length} created, ${result.skipped.length} skipped, ${result.errors.length} errors`);
|
|
1967
|
+
return result;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Export singleton instance
|
|
1972
|
+
*/
|
|
1973
|
+
export const schemaManager = SchemaManager.getInstance();
|
|
1974
|
+
/**
|
|
1975
|
+
* Initialize schema manager
|
|
1976
|
+
*/
|
|
1977
|
+
export async function initializeSchemas() {
|
|
1978
|
+
await schemaManager.initialize();
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Get schema by collection name
|
|
1982
|
+
*/
|
|
1983
|
+
export function getSchema(collectionName) {
|
|
1984
|
+
return schemaManager.getSchema(collectionName);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Get all schemas
|
|
1988
|
+
*/
|
|
1989
|
+
export function getAllSchemas() {
|
|
1990
|
+
return schemaManager.getAllSchemas();
|
|
1991
|
+
}
|
|
1992
|
+
//# sourceMappingURL=schemaManager.js.map
|