@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,1780 @@
|
|
|
1
|
+
/* eslint-disable no-case-declarations */
|
|
2
|
+
import { APIError } from "../utils/errorHandler.js";
|
|
3
|
+
import { db } from "../utils/db.js";
|
|
4
|
+
import { schemaManager } from "../utils/schemaManager.js";
|
|
5
|
+
import fileUpload from "express-fileupload";
|
|
6
|
+
import permissionService from "../services/PermissionService.js";
|
|
7
|
+
import { invalidateEntireCache } from "../services/CacheService.js";
|
|
8
|
+
import { adminOnly } from "../utils/auth.js";
|
|
9
|
+
import { eq, sql } from "drizzle-orm";
|
|
10
|
+
import { ItemsService } from "../services/ItemsService.js";
|
|
11
|
+
const registerEndpoint = (app, context) => {
|
|
12
|
+
async function validateRelationshipName(name, sourceCollection) {
|
|
13
|
+
/*
|
|
14
|
+
// Get all table names in the database
|
|
15
|
+
const tables = await sequelize.getQueryInterface().showAllTables();
|
|
16
|
+
|
|
17
|
+
// Check if the relationship name matches any table name
|
|
18
|
+
if (tables.includes(name)) {
|
|
19
|
+
throw new APIError(`Relationship name '${name}' cannot be the same as an existing table name`, 400);
|
|
20
|
+
}
|
|
21
|
+
*/
|
|
22
|
+
// Check if the relationship name is the same as the source collection
|
|
23
|
+
if (name === sourceCollection) {
|
|
24
|
+
throw new APIError(`Relationship name '${name}' cannot be the same as the collection name ${sourceCollection}`, 400);
|
|
25
|
+
}
|
|
26
|
+
// Check if the name is a reserved word in PostgreSQL
|
|
27
|
+
const reservedWords = [
|
|
28
|
+
"user",
|
|
29
|
+
"group",
|
|
30
|
+
"order",
|
|
31
|
+
"limit",
|
|
32
|
+
"offset",
|
|
33
|
+
"where",
|
|
34
|
+
"select",
|
|
35
|
+
"insert",
|
|
36
|
+
"update",
|
|
37
|
+
"delete",
|
|
38
|
+
"table",
|
|
39
|
+
"from",
|
|
40
|
+
"join",
|
|
41
|
+
"left",
|
|
42
|
+
"right",
|
|
43
|
+
"inner",
|
|
44
|
+
"outer",
|
|
45
|
+
"cross",
|
|
46
|
+
"natural",
|
|
47
|
+
"using",
|
|
48
|
+
"on",
|
|
49
|
+
];
|
|
50
|
+
if (reservedWords.includes(name.toLowerCase())) {
|
|
51
|
+
throw new APIError(`Relationship name '${name}' cannot be a reserved word`, 400);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Helper function to fetch schema definition by collection name
|
|
55
|
+
async function getSchemaDefinition(collectionName, accountability) {
|
|
56
|
+
const schemaService = new ItemsService('baasix_SchemaDefinition', { accountability });
|
|
57
|
+
const result = await schemaService.readByQuery({
|
|
58
|
+
filter: { collectionName },
|
|
59
|
+
limit: 1
|
|
60
|
+
}, true);
|
|
61
|
+
return result.data[0] || null;
|
|
62
|
+
}
|
|
63
|
+
// Get all schemas
|
|
64
|
+
// Access controlled by SCHEMAS_PUBLIC env variable:
|
|
65
|
+
// - true (default): Bypass permission check (for development/CLI)
|
|
66
|
+
// - false: Requires permission on baasix_SchemaDefinition (production)
|
|
67
|
+
// Admins always have access via ItemsService
|
|
68
|
+
app.get("/schemas", async (req, res, next) => {
|
|
69
|
+
try {
|
|
70
|
+
console.log('[schema.route] GET /schemas called');
|
|
71
|
+
const { search, page, limit, sort = "collectionName:asc" } = req.query;
|
|
72
|
+
// Default to public access (backwards compatible), set SCHEMAS_PUBLIC=false for production
|
|
73
|
+
const bypassPermissions = process.env.SCHEMAS_PUBLIC !== 'false';
|
|
74
|
+
// Use ItemsService - bypasses permission if public, otherwise checks permission
|
|
75
|
+
const schemaService = new ItemsService('baasix_SchemaDefinition', {
|
|
76
|
+
accountability: req.accountability
|
|
77
|
+
});
|
|
78
|
+
const query = {
|
|
79
|
+
fields: ['collectionName', 'schema']
|
|
80
|
+
};
|
|
81
|
+
// Only add sort if provided and not the default format that causes parsing issues
|
|
82
|
+
if (sort && typeof sort === 'string' && sort.includes(':')) {
|
|
83
|
+
const [field, direction] = sort.split(':');
|
|
84
|
+
query.sort = [direction?.toLowerCase() === 'desc' ? `-${field}` : field];
|
|
85
|
+
}
|
|
86
|
+
if (search) {
|
|
87
|
+
query.search = search;
|
|
88
|
+
query.searchFields = ['collectionName'];
|
|
89
|
+
}
|
|
90
|
+
if (page !== undefined || limit !== undefined) {
|
|
91
|
+
query.page = parseInt(page || 1, 10);
|
|
92
|
+
query.limit = parseInt(limit || 50, 10);
|
|
93
|
+
}
|
|
94
|
+
const result = await schemaService.readByQuery(query, bypassPermissions);
|
|
95
|
+
// Transform to expected format
|
|
96
|
+
const schemas = result.data.map((item) => ({
|
|
97
|
+
collectionName: item.collectionName,
|
|
98
|
+
schema: item.schema
|
|
99
|
+
}));
|
|
100
|
+
if (page !== undefined || limit !== undefined) {
|
|
101
|
+
const pageNum = parseInt(page || 1, 10);
|
|
102
|
+
const limitNum = parseInt(limit || 50, 10);
|
|
103
|
+
const totalPages = Math.ceil((result.totalCount || 0) / limitNum);
|
|
104
|
+
return res.status(200).json({
|
|
105
|
+
data: schemas,
|
|
106
|
+
totalCount: result.totalCount || schemas.length,
|
|
107
|
+
pagination: {
|
|
108
|
+
currentPage: pageNum,
|
|
109
|
+
totalPages,
|
|
110
|
+
limit: limitNum,
|
|
111
|
+
hasNextPage: pageNum < totalPages,
|
|
112
|
+
hasPrevPage: pageNum > 1,
|
|
113
|
+
nextPage: pageNum < totalPages ? pageNum + 1 : null,
|
|
114
|
+
prevPage: pageNum > 1 ? pageNum - 1 : null
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return res.status(200).json({
|
|
119
|
+
data: schemas,
|
|
120
|
+
totalCount: schemas.length
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('[schema.route] Error in GET /schemas:', error);
|
|
125
|
+
next(error);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// Get a specific schema
|
|
129
|
+
// Access controlled same as GET /schemas
|
|
130
|
+
app.get("/schemas/:collectionName", async (req, res, next) => {
|
|
131
|
+
try {
|
|
132
|
+
// Default to public access (backwards compatible), set SCHEMAS_PUBLIC=false for production
|
|
133
|
+
const bypassPermissions = process.env.SCHEMAS_PUBLIC !== 'false';
|
|
134
|
+
// Use ItemsService - bypasses permission if public, otherwise checks permission
|
|
135
|
+
const schemaService = new ItemsService('baasix_SchemaDefinition', {
|
|
136
|
+
accountability: req.accountability
|
|
137
|
+
});
|
|
138
|
+
const result = await schemaService.readByQuery({
|
|
139
|
+
filter: { collectionName: { eq: req.params.collectionName } },
|
|
140
|
+
limit: 1,
|
|
141
|
+
fields: ['collectionName', 'schema']
|
|
142
|
+
}, bypassPermissions);
|
|
143
|
+
if (!result.data || result.data.length === 0) {
|
|
144
|
+
throw new APIError("Schema not found", 404);
|
|
145
|
+
}
|
|
146
|
+
return res.status(200).json({
|
|
147
|
+
data: {
|
|
148
|
+
collectionName: result.data[0].collectionName,
|
|
149
|
+
schema: result.data[0].schema
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
next(error);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Process schema to handle special flags like usertrack, sortEnabled
|
|
158
|
+
function processSchemaFlags(schema, editMode = false) {
|
|
159
|
+
// Deep clone to avoid mutations
|
|
160
|
+
const processedSchema = JSON.parse(JSON.stringify(schema));
|
|
161
|
+
// Ensure fields object exists
|
|
162
|
+
if (!processedSchema.fields) {
|
|
163
|
+
processedSchema.fields = {};
|
|
164
|
+
}
|
|
165
|
+
// Handle usertrack flag
|
|
166
|
+
if (processedSchema.usertrack === true) {
|
|
167
|
+
const usertrack = {
|
|
168
|
+
userCreated_Id: { type: "UUID", SystemGenerated: true },
|
|
169
|
+
userCreated: {
|
|
170
|
+
relType: "BelongsTo",
|
|
171
|
+
target: "baasix_User",
|
|
172
|
+
foreignKey: "userCreated_Id",
|
|
173
|
+
as: "userCreated",
|
|
174
|
+
SystemGenerated: true,
|
|
175
|
+
description: "M2O",
|
|
176
|
+
},
|
|
177
|
+
userUpdated_Id: { type: "UUID", SystemGenerated: true },
|
|
178
|
+
userUpdated: {
|
|
179
|
+
relType: "BelongsTo",
|
|
180
|
+
target: "baasix_User",
|
|
181
|
+
foreignKey: "userUpdated_Id",
|
|
182
|
+
as: "userUpdated",
|
|
183
|
+
SystemGenerated: true,
|
|
184
|
+
description: "M2O",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
processedSchema.fields = { ...processedSchema.fields, ...usertrack };
|
|
188
|
+
}
|
|
189
|
+
// Handle sortEnabled flag
|
|
190
|
+
if (processedSchema.sortEnabled === true) {
|
|
191
|
+
processedSchema.fields = {
|
|
192
|
+
...processedSchema.fields,
|
|
193
|
+
sort: {
|
|
194
|
+
type: "Integer",
|
|
195
|
+
allowNull: true,
|
|
196
|
+
description: "Sort order for items",
|
|
197
|
+
SystemGenerated: true
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (editMode) {
|
|
202
|
+
// If sortEnabled is enabled, ensure the sort field is added
|
|
203
|
+
if (processedSchema.sortEnabled === true) {
|
|
204
|
+
processedSchema.fields.sort = {
|
|
205
|
+
type: "Integer",
|
|
206
|
+
allowNull: true,
|
|
207
|
+
description: "Sort order for items",
|
|
208
|
+
SystemGenerated: true
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Note: If sortEnabled is disabled, we don't remove the sort field, just keep it
|
|
212
|
+
// If timestamps are enabled, ensure they are not removed
|
|
213
|
+
if (processedSchema.timestamps === true) {
|
|
214
|
+
processedSchema.fields = {
|
|
215
|
+
...processedSchema.fields,
|
|
216
|
+
createdAt: { type: "DateTime", allowNull: true, SystemGenerated: true, defaultValue: { type: "NOW" } },
|
|
217
|
+
updatedAt: { type: "DateTime", allowNull: true, SystemGenerated: true, defaultValue: { type: "NOW" } },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
else if (processedSchema.timestamps === false) {
|
|
221
|
+
// If timestamps are explicitly disabled, remove them from the schema
|
|
222
|
+
delete processedSchema.fields.createdAt;
|
|
223
|
+
delete processedSchema.fields.updatedAt;
|
|
224
|
+
}
|
|
225
|
+
// If paranoid is enabled, ensure it is not removed
|
|
226
|
+
if (processedSchema.paranoid === true) {
|
|
227
|
+
processedSchema.fields.deletedAt = { type: "DateTime", allowNull: true, SystemGenerated: true, defaultValue: { type: "NOW" } };
|
|
228
|
+
}
|
|
229
|
+
else if (processedSchema.paranoid === false) {
|
|
230
|
+
delete processedSchema.fields.deletedAt;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return processedSchema;
|
|
234
|
+
}
|
|
235
|
+
// PostgreSQL identifier max length (63 characters)
|
|
236
|
+
const PG_MAX_IDENTIFIER_LENGTH = 63;
|
|
237
|
+
// Validate identifier length for PostgreSQL
|
|
238
|
+
function validateIdentifierLength(name, type = 'Identifier') {
|
|
239
|
+
if (name.length > PG_MAX_IDENTIFIER_LENGTH) {
|
|
240
|
+
throw new APIError(`${type} name too long`, 400, `${type} name "${name}" exceeds PostgreSQL's ${PG_MAX_IDENTIFIER_LENGTH} character limit (${name.length} chars). Please use a shorter name.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
app.post("/schemas", adminOnly, async (req, res, next) => {
|
|
244
|
+
try {
|
|
245
|
+
console.log("Creating new schema");
|
|
246
|
+
const { collectionName, schema } = req.body;
|
|
247
|
+
// Process schema flags
|
|
248
|
+
const processedSchema = processSchemaFlags(schema);
|
|
249
|
+
//Return error if collectionName is not provided or ends with _junction
|
|
250
|
+
if (!collectionName || collectionName.endsWith("_junction")) {
|
|
251
|
+
throw new APIError("Invalid collection name", 400, "Collection name cannot be empty or end with _junction");
|
|
252
|
+
}
|
|
253
|
+
// Validate collection name length
|
|
254
|
+
validateIdentifierLength(collectionName, 'Collection');
|
|
255
|
+
// Insert into baasix_SchemaDefinition table
|
|
256
|
+
const schemaDefTable = schemaManager.getTable("baasix_SchemaDefinition");
|
|
257
|
+
await db.insert(schemaDefTable).values({
|
|
258
|
+
collectionName,
|
|
259
|
+
schema: processedSchema,
|
|
260
|
+
});
|
|
261
|
+
// Update in-memory schema (creates Drizzle table)
|
|
262
|
+
await schemaManager.updateModel(collectionName, processedSchema, req.accountability);
|
|
263
|
+
// Invalidate schema definition cache after creating schema
|
|
264
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
265
|
+
// Sync realtime if the new schema has realtime enabled
|
|
266
|
+
const hasRealtime = processedSchema.realtime === true ||
|
|
267
|
+
(typeof processedSchema.realtime === 'object' && processedSchema.realtime?.enabled);
|
|
268
|
+
if (hasRealtime) {
|
|
269
|
+
try {
|
|
270
|
+
const realtimeService = (await import('../services/RealtimeService.js')).default;
|
|
271
|
+
if (realtimeService.isWalAvailable()) {
|
|
272
|
+
await realtimeService.reloadCollections([collectionName]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.warn('Could not sync realtime configuration:', error.message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
console.log("Schema created successfully");
|
|
280
|
+
res.status(201).json({ message: "Schema created successfully" });
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error("Error creating schema:", error);
|
|
284
|
+
next(new APIError("Error creating schema", 500, error.message));
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
app.patch("/schemas/:collectionName", adminOnly, async (req, res, next) => {
|
|
288
|
+
try {
|
|
289
|
+
console.log(`Updating schema for ${req.params.collectionName}`);
|
|
290
|
+
console.log("New schema:", JSON.stringify(req.body.schema, null, 2));
|
|
291
|
+
const { collectionName } = req.params;
|
|
292
|
+
const { schema } = req.body;
|
|
293
|
+
// Get existing schema to compare flags
|
|
294
|
+
const schemaDefTable = schemaManager.getTable("baasix_SchemaDefinition");
|
|
295
|
+
const existingSchemaRecords = await db
|
|
296
|
+
.select()
|
|
297
|
+
.from(schemaDefTable)
|
|
298
|
+
.where(eq(schemaDefTable.collectionName, collectionName))
|
|
299
|
+
.limit(1);
|
|
300
|
+
if (!existingSchemaRecords || existingSchemaRecords.length === 0) {
|
|
301
|
+
throw new APIError("Schema not found", 404);
|
|
302
|
+
}
|
|
303
|
+
const existingSchema = existingSchemaRecords[0].schema;
|
|
304
|
+
// Check for changes in special flags
|
|
305
|
+
const flagsChanged = existingSchema.usertrack !== schema.usertrack ||
|
|
306
|
+
existingSchema.sortEnabled !== schema.sortEnabled ||
|
|
307
|
+
existingSchema.timestamps !== schema.timestamps ||
|
|
308
|
+
existingSchema.paranoid !== schema.paranoid;
|
|
309
|
+
// Check if realtime config changed
|
|
310
|
+
const realtimeChanged = !deepEqual(existingSchema.realtime, schema.realtime);
|
|
311
|
+
console.log(`Flags changed: ${flagsChanged}`);
|
|
312
|
+
// Process schema flags
|
|
313
|
+
const processedSchema = processSchemaFlags(schema, true);
|
|
314
|
+
// If usertrack was disabled, we don't remove the fields, just keep them
|
|
315
|
+
// If sortEnabled was disabled, we don't remove the sort field, just keep it
|
|
316
|
+
// Update in database
|
|
317
|
+
await db
|
|
318
|
+
.update(schemaDefTable)
|
|
319
|
+
.set({ schema: processedSchema, updatedAt: new Date() })
|
|
320
|
+
.where(eq(schemaDefTable.collectionName, collectionName));
|
|
321
|
+
// Update in-memory schema
|
|
322
|
+
await schemaManager.updateModel(collectionName, processedSchema, req.accountability);
|
|
323
|
+
// Invalidate schema definition cache after updating schema
|
|
324
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
325
|
+
// Sync realtime if the config changed
|
|
326
|
+
if (realtimeChanged) {
|
|
327
|
+
console.log(`Realtime config changed for ${collectionName}, syncing...`);
|
|
328
|
+
console.log(` Old: ${JSON.stringify(existingSchema.realtime)}`);
|
|
329
|
+
console.log(` New: ${JSON.stringify(schema.realtime)}`);
|
|
330
|
+
try {
|
|
331
|
+
const realtimeService = (await import('../services/RealtimeService.js')).default;
|
|
332
|
+
if (realtimeService.isWalAvailable()) {
|
|
333
|
+
await realtimeService.reloadCollections([collectionName]);
|
|
334
|
+
console.log(`Realtime configuration synced for ${collectionName}`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.log(`WAL not available, skipping realtime sync for ${collectionName}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.warn('Could not sync realtime configuration:', error.message);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
console.log(`Schema for ${collectionName} updated successfully`);
|
|
345
|
+
res.status(200).json({ message: "Schema updated successfully" });
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
console.error("Error updating schema:", error);
|
|
349
|
+
next(new APIError("Error updating schema", 500, error.message));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
app.delete("/schemas/:collectionName", adminOnly, async (req, res, next) => {
|
|
353
|
+
try {
|
|
354
|
+
console.log("Deleting schema");
|
|
355
|
+
const { collectionName } = req.params;
|
|
356
|
+
// Delete from database
|
|
357
|
+
const schemaDefTable = schemaManager.getTable("baasix_SchemaDefinition");
|
|
358
|
+
await db
|
|
359
|
+
.delete(schemaDefTable)
|
|
360
|
+
.where(eq(schemaDefTable.collectionName, collectionName));
|
|
361
|
+
// Delete from memory
|
|
362
|
+
await schemaManager.deleteModel(collectionName, req.accountability);
|
|
363
|
+
// Invalidate schema definition cache after deleting schema
|
|
364
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
365
|
+
// Remove from realtime publication if it was enabled
|
|
366
|
+
try {
|
|
367
|
+
const realtimeService = (await import('../services/RealtimeService.js')).default;
|
|
368
|
+
if (realtimeService.isWalAvailable()) {
|
|
369
|
+
await realtimeService.reloadCollections([collectionName]);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
console.warn('Could not sync realtime configuration:', error.message);
|
|
374
|
+
}
|
|
375
|
+
console.log("Schema deleted successfully");
|
|
376
|
+
res.status(200).json({ message: "Schema deleted successfully" });
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
console.error("Error deleting schema:", error);
|
|
380
|
+
next(new APIError("Error deleting schema", 500, error.message));
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// Add index
|
|
384
|
+
app.post("/schemas/:collectionName/indexes", adminOnly, async (req, res, next) => {
|
|
385
|
+
try {
|
|
386
|
+
const { collectionName } = req.params;
|
|
387
|
+
const indexDefinition = req.body;
|
|
388
|
+
await schemaManager.addIndex(collectionName, indexDefinition, req.accountability);
|
|
389
|
+
// Invalidate schema definition cache after adding index
|
|
390
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
391
|
+
res.status(201).json({ message: "Index added successfully" });
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
next(new APIError("Error adding index", 500, error.message));
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
// Remove index
|
|
398
|
+
app.delete("/schemas/:collectionName/indexes/:indexName", adminOnly, async (req, res, next) => {
|
|
399
|
+
try {
|
|
400
|
+
const { collectionName, indexName } = req.params;
|
|
401
|
+
await schemaManager.removeIndex(collectionName, indexName, req.accountability);
|
|
402
|
+
// Invalidate schema definition cache after removing index
|
|
403
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
404
|
+
res.status(200).json({ message: "Index removed successfully" });
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
next(new APIError("Error removing index", 500, error.message));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
// Add missing foreign key indexes to all collections (migration utility)
|
|
411
|
+
app.post("/schemas/indexes/migrate", adminOnly, async (req, res, next) => {
|
|
412
|
+
try {
|
|
413
|
+
console.log('Starting foreign key index migration...');
|
|
414
|
+
const result = await schemaManager.addMissingForeignKeyIndexes(req.accountability);
|
|
415
|
+
// Invalidate schema definition cache after migration
|
|
416
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
417
|
+
res.status(200).json({
|
|
418
|
+
message: "Foreign key index migration completed",
|
|
419
|
+
summary: {
|
|
420
|
+
created: result.created.length,
|
|
421
|
+
skipped: result.skipped.length,
|
|
422
|
+
errors: result.errors.length,
|
|
423
|
+
},
|
|
424
|
+
details: result,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
next(new APIError("Error migrating indexes", 500, error.message));
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
// Create relationship endpoint
|
|
432
|
+
app.post("/schemas/:sourceCollection/relationships", adminOnly, async (req, res, next) => {
|
|
433
|
+
try {
|
|
434
|
+
const { sourceCollection } = req.params;
|
|
435
|
+
const relationshipData = req.body;
|
|
436
|
+
// Validate input
|
|
437
|
+
if (!["M2O", "O2O", "M2M", "M2A", "O2M"].includes(relationshipData.type)) {
|
|
438
|
+
throw new APIError("Invalid relationship type. Must be M2O, O2M, O2O, M2M, or M2A", 400);
|
|
439
|
+
}
|
|
440
|
+
// Validate relationship name
|
|
441
|
+
await validateRelationshipName(relationshipData.name, sourceCollection);
|
|
442
|
+
// If there's an alias, validate it too
|
|
443
|
+
if (relationshipData.alias && relationshipData.target) {
|
|
444
|
+
await validateRelationshipName(relationshipData.alias, relationshipData.target);
|
|
445
|
+
}
|
|
446
|
+
// Get existing schemas
|
|
447
|
+
const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, req.accountability);
|
|
448
|
+
const sourceSchema = sourceSchemaDoc?.schema;
|
|
449
|
+
if (relationshipData.type == "M2A") {
|
|
450
|
+
if (!sourceSchema) {
|
|
451
|
+
throw new APIError("Source or target collection not found", 404);
|
|
452
|
+
}
|
|
453
|
+
// Process the relationship
|
|
454
|
+
const { updatedSourceSchema } = await processRelationship(sourceCollection, sourceSchema, null, relationshipData, req.accountability, false);
|
|
455
|
+
// Apply schema updates
|
|
456
|
+
await schemaManager.updateModel(sourceCollection, updatedSourceSchema, req.accountability);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
const targetSchemaDoc = await getSchemaDefinition(relationshipData.target, req.accountability);
|
|
460
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
461
|
+
if (!sourceSchema || !targetSchema) {
|
|
462
|
+
throw new APIError("Source or target collection not found", 404);
|
|
463
|
+
}
|
|
464
|
+
//Check if the relationship is self-referential
|
|
465
|
+
const isSelfReferential = sourceCollection === relationshipData.target;
|
|
466
|
+
// Process the relationship
|
|
467
|
+
const { updatedSourceSchema, updatedTargetSchema } = await processRelationship(sourceCollection, sourceSchema, targetSchema, relationshipData, req.accountability, isSelfReferential);
|
|
468
|
+
// Apply schema updates
|
|
469
|
+
console.log(`[processRelationship] Applying schema updates for ${sourceCollection}`);
|
|
470
|
+
console.log(`[processRelationship] Updated source schema fields:`, Object.keys(updatedSourceSchema.fields));
|
|
471
|
+
await schemaManager.updateModel(sourceCollection, updatedSourceSchema, req.accountability);
|
|
472
|
+
if (updatedTargetSchema && !isSelfReferential) {
|
|
473
|
+
console.log(`[processRelationship] Applying schema updates for ${relationshipData.target}`);
|
|
474
|
+
console.log(`[processRelationship] Updated target schema fields:`, Object.keys(updatedTargetSchema.fields));
|
|
475
|
+
await schemaManager.updateModel(relationshipData.target, updatedTargetSchema, req.accountability);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Invalidate schema definition cache after creating relationship
|
|
479
|
+
// This ensures the updated schema with new fields is fetched fresh
|
|
480
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
481
|
+
res.status(201).json({ message: "Relationship created successfully" });
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.error("Error creating relationship:", error);
|
|
485
|
+
next(new APIError("Error creating relationship", 500, error.message));
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
// Update relationship endpoint
|
|
489
|
+
app.patch("/schemas/:sourceCollection/relationships/:fieldName", adminOnly, async (req, res, next) => {
|
|
490
|
+
try {
|
|
491
|
+
const { sourceCollection, fieldName } = req.params;
|
|
492
|
+
const updateData = req.body;
|
|
493
|
+
const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, req.accountability);
|
|
494
|
+
const sourceSchema = sourceSchemaDoc?.schema;
|
|
495
|
+
if (!sourceSchema.fields[fieldName]) {
|
|
496
|
+
throw new APIError("Relationship field not found", 404);
|
|
497
|
+
}
|
|
498
|
+
const updatedField = { ...sourceSchema.fields[fieldName], ...updateData };
|
|
499
|
+
// If it's an M2A relationship and tables are being added or removed
|
|
500
|
+
if (updatedField.relType === "HasMany" && updatedField.polymorphic && updateData.tables) {
|
|
501
|
+
// Pass the original field data (before update) to determine what actually changed
|
|
502
|
+
await updateM2ARelationship(sourceCollection, fieldName, updateData.tables, sourceSchema.fields[fieldName], // Pass original field data
|
|
503
|
+
updatedField, // Pass updated field data for new values
|
|
504
|
+
req.accountability);
|
|
505
|
+
}
|
|
506
|
+
// Update the source schema field AFTER processing M2A changes
|
|
507
|
+
sourceSchema.fields[fieldName] = updatedField;
|
|
508
|
+
await schemaManager.updateModel(sourceCollection, sourceSchema, req.accountability);
|
|
509
|
+
// Update the reverse relationship if it exists
|
|
510
|
+
if (updatedField.target) {
|
|
511
|
+
const targetSchemaDoc = await getSchemaDefinition(updatedField.target, req.accountability);
|
|
512
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
513
|
+
const reverseField = Object.entries(targetSchema.fields).find(([, field]) => field.target === sourceCollection && field.foreignKey === fieldName);
|
|
514
|
+
if (reverseField) {
|
|
515
|
+
const [reverseFieldName, reverseFieldData] = reverseField;
|
|
516
|
+
targetSchema.fields[reverseFieldName] = {
|
|
517
|
+
...reverseFieldData,
|
|
518
|
+
onDelete: updateData.onDelete,
|
|
519
|
+
onUpdate: updateData.onUpdate,
|
|
520
|
+
};
|
|
521
|
+
await schemaManager.updateModel(updatedField.target, targetSchema, req.accountability);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Invalidate schema definition cache after updating relationship
|
|
525
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
526
|
+
res.status(200).json({ message: "Relationship updated successfully" });
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
console.error("Error updating relationship:", error);
|
|
530
|
+
next(new APIError("Error updating relationship", 500, error.message));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
// Delete relationship endpoint
|
|
534
|
+
app.delete("/schemas/:sourceCollection/relationships/:fieldName", adminOnly, async (req, res, next) => {
|
|
535
|
+
try {
|
|
536
|
+
const { sourceCollection, fieldName } = req.params;
|
|
537
|
+
const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, req.accountability);
|
|
538
|
+
const sourceSchema = sourceSchemaDoc?.schema;
|
|
539
|
+
if (!sourceSchema.fields[fieldName]) {
|
|
540
|
+
throw new APIError("Relationship field not found", 404);
|
|
541
|
+
}
|
|
542
|
+
const fieldData = sourceSchema.fields[fieldName];
|
|
543
|
+
//if foreign key exists, delete it
|
|
544
|
+
if (sourceSchema.fields[fieldData.foreignKey] &&
|
|
545
|
+
sourceSchema.fields[fieldData.foreignKey].SystemGenerated) {
|
|
546
|
+
delete sourceSchema.fields[fieldData.foreignKey];
|
|
547
|
+
}
|
|
548
|
+
delete sourceSchema.fields[fieldName];
|
|
549
|
+
await schemaManager.updateModel(sourceCollection, sourceSchema, req.accountability);
|
|
550
|
+
// Remove the reverse relationship if it exists
|
|
551
|
+
if (fieldData.target) {
|
|
552
|
+
const targetSchemaDoc = await getSchemaDefinition(fieldData.target, req.accountability);
|
|
553
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
554
|
+
const reverseField = Object.entries(targetSchema.fields).find(([, field]) => field.target === sourceCollection && field.foreignKey === fieldData.foreignKey);
|
|
555
|
+
if (reverseField) {
|
|
556
|
+
const [reverseFieldName] = reverseField;
|
|
557
|
+
delete targetSchema.fields[reverseFieldName];
|
|
558
|
+
await schemaManager.updateModel(fieldData.target, targetSchema, req.accountability);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
//If it's an M2M relationship, delete the through table
|
|
562
|
+
if (fieldData.relType === "BelongsToMany" && fieldData.through) {
|
|
563
|
+
//Delete the through table from the database
|
|
564
|
+
await db.execute(sql `DROP TABLE IF EXISTS ${sql.raw(`"${fieldData.through}"`)}`);
|
|
565
|
+
// Delete schema definition
|
|
566
|
+
const schemaToDelete = await getSchemaDefinition(fieldData.through, req.accountability);
|
|
567
|
+
if (schemaToDelete) {
|
|
568
|
+
const schemaDefService = new ItemsService('baasix_SchemaDefinition', { accountability: req.accountability });
|
|
569
|
+
await schemaDefService.deleteOne(schemaToDelete.id);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Invalidate schema definition cache after deleting relationship
|
|
573
|
+
await invalidateEntireCache('baasix_SchemaDefinition');
|
|
574
|
+
res.status(200).json({ message: "Relationship deleted successfully" });
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
next(new APIError("Error deleting relationship", 500, error.message));
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
async function processRelationship(sourceCollection, sourceSchema, targetSchema, relationshipData, accountability, isSelfReferential) {
|
|
581
|
+
const updatedSourceSchema = { ...sourceSchema };
|
|
582
|
+
let updatedTargetSchema = { ...targetSchema };
|
|
583
|
+
const { onDelete, onUpdate } = relationshipData;
|
|
584
|
+
console.log("Processing relationship", sourceSchema);
|
|
585
|
+
switch (relationshipData.type) {
|
|
586
|
+
case "O2M":
|
|
587
|
+
updatedSourceSchema.fields[relationshipData.name] = {
|
|
588
|
+
relType: "HasMany",
|
|
589
|
+
target: relationshipData.target,
|
|
590
|
+
foreignKey: relationshipData.foreignKey,
|
|
591
|
+
as: relationshipData.name,
|
|
592
|
+
description: relationshipData.description,
|
|
593
|
+
onDelete,
|
|
594
|
+
onUpdate,
|
|
595
|
+
};
|
|
596
|
+
break;
|
|
597
|
+
case "M2O":
|
|
598
|
+
// Use provided foreignKey or default to name + "_id"
|
|
599
|
+
const m2oForeignKey = relationshipData.foreignKey || (relationshipData.name + "_id");
|
|
600
|
+
updatedSourceSchema.fields[relationshipData.name] = {
|
|
601
|
+
relType: "BelongsTo",
|
|
602
|
+
target: relationshipData.target,
|
|
603
|
+
foreignKey: m2oForeignKey,
|
|
604
|
+
as: relationshipData.name,
|
|
605
|
+
description: relationshipData.description,
|
|
606
|
+
onDelete,
|
|
607
|
+
onUpdate,
|
|
608
|
+
};
|
|
609
|
+
//Add foreign key to source collection, fetching type from target collection schema
|
|
610
|
+
// Find the actual primary key field in the target schema
|
|
611
|
+
const targetPrimaryKeyField = Object.entries(targetSchema.fields).find(([, field]) => field.primaryKey === true);
|
|
612
|
+
const targetPrimaryKeyName = targetPrimaryKeyField ? targetPrimaryKeyField[0] : 'id';
|
|
613
|
+
const targetPrimaryKeyType = targetSchema.fields[targetPrimaryKeyName]?.type || 'UUID';
|
|
614
|
+
updatedSourceSchema.fields[m2oForeignKey] = {
|
|
615
|
+
type: targetPrimaryKeyType,
|
|
616
|
+
allowNull: true,
|
|
617
|
+
SystemGenerated: true,
|
|
618
|
+
};
|
|
619
|
+
// Auto-create index on foreign key for better query performance
|
|
620
|
+
const m2oIndexName = `${sourceCollection}_${m2oForeignKey}_idx`;
|
|
621
|
+
if (!updatedSourceSchema.indexes) {
|
|
622
|
+
updatedSourceSchema.indexes = [];
|
|
623
|
+
}
|
|
624
|
+
// Only add if index doesn't already exist
|
|
625
|
+
if (!updatedSourceSchema.indexes.some((idx) => idx.name === m2oIndexName)) {
|
|
626
|
+
updatedSourceSchema.indexes.push({
|
|
627
|
+
name: m2oIndexName,
|
|
628
|
+
fields: [m2oForeignKey],
|
|
629
|
+
unique: false,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (relationshipData.alias) {
|
|
633
|
+
if (isSelfReferential) {
|
|
634
|
+
updatedSourceSchema.fields[relationshipData.alias] = {
|
|
635
|
+
relType: "HasMany",
|
|
636
|
+
target: sourceCollection,
|
|
637
|
+
foreignKey: m2oForeignKey,
|
|
638
|
+
as: relationshipData.alias,
|
|
639
|
+
description: relationshipData.description + " Alias",
|
|
640
|
+
onDelete,
|
|
641
|
+
onUpdate,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
updatedTargetSchema.fields[relationshipData.alias] = {
|
|
646
|
+
relType: "HasMany",
|
|
647
|
+
target: sourceCollection,
|
|
648
|
+
foreignKey: m2oForeignKey,
|
|
649
|
+
as: relationshipData.alias,
|
|
650
|
+
description: relationshipData.description + " Alias",
|
|
651
|
+
onDelete,
|
|
652
|
+
onUpdate,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
case "O2O":
|
|
658
|
+
// Use provided foreignKey or default to name + "_id"
|
|
659
|
+
const o2oForeignKey = relationshipData.foreignKey || (relationshipData.name + "_id");
|
|
660
|
+
updatedSourceSchema.fields[relationshipData.name] = {
|
|
661
|
+
relType: "BelongsTo",
|
|
662
|
+
target: relationshipData.target,
|
|
663
|
+
foreignKey: o2oForeignKey,
|
|
664
|
+
as: relationshipData.name,
|
|
665
|
+
description: relationshipData.description,
|
|
666
|
+
onDelete,
|
|
667
|
+
onUpdate,
|
|
668
|
+
};
|
|
669
|
+
//Add foreign key to source collection, fetching type from target collection schema
|
|
670
|
+
// Find the actual primary key field in the target schema
|
|
671
|
+
const o2oTargetPrimaryKeyField = Object.entries(targetSchema.fields).find(([, field]) => field.primaryKey === true);
|
|
672
|
+
const o2oTargetPrimaryKeyName = o2oTargetPrimaryKeyField ? o2oTargetPrimaryKeyField[0] : 'id';
|
|
673
|
+
const o2oTargetPrimaryKeyType = targetSchema.fields[o2oTargetPrimaryKeyName]?.type || 'UUID';
|
|
674
|
+
updatedSourceSchema.fields[o2oForeignKey] = {
|
|
675
|
+
type: o2oTargetPrimaryKeyType,
|
|
676
|
+
allowNull: true,
|
|
677
|
+
SystemGenerated: true,
|
|
678
|
+
};
|
|
679
|
+
// Auto-create index on foreign key for better query performance
|
|
680
|
+
const o2oIndexName = `${sourceCollection}_${o2oForeignKey}_idx`;
|
|
681
|
+
if (!updatedSourceSchema.indexes) {
|
|
682
|
+
updatedSourceSchema.indexes = [];
|
|
683
|
+
}
|
|
684
|
+
// Only add if index doesn't already exist
|
|
685
|
+
if (!updatedSourceSchema.indexes.some((idx) => idx.name === o2oIndexName)) {
|
|
686
|
+
updatedSourceSchema.indexes.push({
|
|
687
|
+
name: o2oIndexName,
|
|
688
|
+
fields: [o2oForeignKey],
|
|
689
|
+
unique: false,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (relationshipData.alias) {
|
|
693
|
+
if (isSelfReferential) {
|
|
694
|
+
updatedSourceSchema.fields[relationshipData.alias] = {
|
|
695
|
+
relType: "HasOne",
|
|
696
|
+
target: sourceCollection,
|
|
697
|
+
foreignKey: o2oForeignKey,
|
|
698
|
+
as: relationshipData.alias,
|
|
699
|
+
description: relationshipData.description + " Alias",
|
|
700
|
+
onDelete,
|
|
701
|
+
onUpdate,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
updatedTargetSchema.fields[relationshipData.alias] = {
|
|
706
|
+
relType: "HasOne",
|
|
707
|
+
target: sourceCollection,
|
|
708
|
+
foreignKey: o2oForeignKey,
|
|
709
|
+
as: relationshipData.alias,
|
|
710
|
+
description: relationshipData.description + " Alias",
|
|
711
|
+
onDelete,
|
|
712
|
+
onUpdate,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
break;
|
|
717
|
+
case "M2M":
|
|
718
|
+
// Use custom junction table name if provided, otherwise generate default
|
|
719
|
+
const through = relationshipData.through || `${sourceCollection}_${relationshipData.target}_${relationshipData.name}_junction`;
|
|
720
|
+
// Validate junction table name length
|
|
721
|
+
validateIdentifierLength(through, 'Junction table');
|
|
722
|
+
const sourceType = sourceSchema.fields.id.type;
|
|
723
|
+
const targetType = targetSchema.fields.id.type;
|
|
724
|
+
// For self-referential M2M relationships, add _2 suffix to avoid column name conflicts
|
|
725
|
+
const sourceIdColumn = `${sourceCollection}_id`;
|
|
726
|
+
const targetIdColumn = isSelfReferential
|
|
727
|
+
? `${relationshipData.target}_id_2`
|
|
728
|
+
: `${relationshipData.target}_id`;
|
|
729
|
+
const throughSchema = {
|
|
730
|
+
name: through,
|
|
731
|
+
isJunction: true, // Mark this as a junction table for M2M relationships
|
|
732
|
+
fields: {
|
|
733
|
+
id: { type: "Integer", primaryKey: true, defaultValue: { type: "AUTOINCREMENT" } },
|
|
734
|
+
[sourceIdColumn]: { type: sourceType, allowNull: false, SystemGenerated: true },
|
|
735
|
+
[targetIdColumn]: {
|
|
736
|
+
type: targetType,
|
|
737
|
+
allowNull: false,
|
|
738
|
+
SystemGenerated: true,
|
|
739
|
+
},
|
|
740
|
+
[sourceCollection]: {
|
|
741
|
+
relType: "BelongsTo",
|
|
742
|
+
target: sourceCollection,
|
|
743
|
+
foreignKey: sourceIdColumn,
|
|
744
|
+
description: "M2M Junction",
|
|
745
|
+
SystemGenerated: true,
|
|
746
|
+
},
|
|
747
|
+
[isSelfReferential ? `${relationshipData.target}_2` : relationshipData.target]: {
|
|
748
|
+
relType: "BelongsTo",
|
|
749
|
+
target: relationshipData.target,
|
|
750
|
+
foreignKey: targetIdColumn,
|
|
751
|
+
description: "M2M Junction",
|
|
752
|
+
SystemGenerated: true,
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
timestamps: true,
|
|
756
|
+
indexes: [
|
|
757
|
+
{
|
|
758
|
+
name: `${sourceCollection}_${relationshipData.target}_unique`,
|
|
759
|
+
fields: [sourceIdColumn, targetIdColumn],
|
|
760
|
+
unique: true,
|
|
761
|
+
},
|
|
762
|
+
// Individual indexes on each FK for better query performance
|
|
763
|
+
{
|
|
764
|
+
name: `${through}_${sourceIdColumn}_idx`,
|
|
765
|
+
fields: [sourceIdColumn],
|
|
766
|
+
unique: false,
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: `${through}_${targetIdColumn}_idx`,
|
|
770
|
+
fields: [targetIdColumn],
|
|
771
|
+
unique: false,
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
};
|
|
775
|
+
await schemaManager.updateModel(through, throughSchema, accountability);
|
|
776
|
+
// Add HasMany from source to junction
|
|
777
|
+
updatedSourceSchema.fields[relationshipData.name] = {
|
|
778
|
+
relType: "HasMany",
|
|
779
|
+
target: through,
|
|
780
|
+
foreignKey: sourceIdColumn,
|
|
781
|
+
as: relationshipData.name,
|
|
782
|
+
description: relationshipData.description,
|
|
783
|
+
onDelete,
|
|
784
|
+
onUpdate,
|
|
785
|
+
};
|
|
786
|
+
if (relationshipData.alias) {
|
|
787
|
+
if (isSelfReferential) {
|
|
788
|
+
updatedSourceSchema.fields[relationshipData.alias] = {
|
|
789
|
+
relType: "HasMany",
|
|
790
|
+
target: through,
|
|
791
|
+
foreignKey: targetIdColumn,
|
|
792
|
+
as: relationshipData.alias,
|
|
793
|
+
description: relationshipData.description,
|
|
794
|
+
onDelete,
|
|
795
|
+
onUpdate,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
updatedTargetSchema.fields[relationshipData.alias] = {
|
|
800
|
+
relType: "HasMany",
|
|
801
|
+
target: through,
|
|
802
|
+
foreignKey: targetIdColumn,
|
|
803
|
+
as: relationshipData.alias,
|
|
804
|
+
description: relationshipData.description,
|
|
805
|
+
onDelete,
|
|
806
|
+
onUpdate,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
break;
|
|
811
|
+
case "M2A":
|
|
812
|
+
// Use custom junction table name if provided, otherwise generate default
|
|
813
|
+
const throughTable = relationshipData.through || `${sourceCollection}_${relationshipData.name}_junction`;
|
|
814
|
+
// Validate junction table name length
|
|
815
|
+
validateIdentifierLength(throughTable, 'Junction table');
|
|
816
|
+
//Check type of id in all target tables to ensure they are the same
|
|
817
|
+
const firstTableSchemaDoc = await getSchemaDefinition(relationshipData.tables[0], accountability);
|
|
818
|
+
const firstTableSchema = firstTableSchemaDoc?.schema;
|
|
819
|
+
console.log("First table schema", firstTableSchema);
|
|
820
|
+
if (!firstTableSchema) {
|
|
821
|
+
throw new APIError("Target table not found", 404);
|
|
822
|
+
}
|
|
823
|
+
for (const table of relationshipData.tables) {
|
|
824
|
+
const tableSchemaDoc = await getSchemaDefinition(table, accountability);
|
|
825
|
+
const tableSchema = tableSchemaDoc?.schema;
|
|
826
|
+
if (tableSchema.fields.id.type !== firstTableSchema.fields.id.type) {
|
|
827
|
+
throw new APIError("Target tables must have the same id type", 400);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// Create through table schema
|
|
831
|
+
const throughSchemaM2A = {
|
|
832
|
+
name: throughTable,
|
|
833
|
+
isJunction: true, // Mark this as a junction table for M2A relationships
|
|
834
|
+
fields: {
|
|
835
|
+
id: { type: "Integer", primaryKey: true, defaultValue: { type: "AUTOINCREMENT" } },
|
|
836
|
+
[`${sourceCollection}_id`]: {
|
|
837
|
+
type: sourceSchema.fields.id.type,
|
|
838
|
+
allowNull: false,
|
|
839
|
+
SystemGenerated: true,
|
|
840
|
+
},
|
|
841
|
+
item_id: {
|
|
842
|
+
type: firstTableSchema.fields.id.type,
|
|
843
|
+
allowNull: false,
|
|
844
|
+
references: null,
|
|
845
|
+
SystemGenerated: true,
|
|
846
|
+
constraints: false,
|
|
847
|
+
},
|
|
848
|
+
collection: {
|
|
849
|
+
type: "String",
|
|
850
|
+
allowNull: false,
|
|
851
|
+
SystemGenerated: true,
|
|
852
|
+
},
|
|
853
|
+
[sourceCollection]: {
|
|
854
|
+
relType: "BelongsTo",
|
|
855
|
+
target: sourceCollection,
|
|
856
|
+
foreignKey: `${sourceCollection}_id`,
|
|
857
|
+
description: "M2A Junction Source",
|
|
858
|
+
SystemGenerated: true,
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
indexes: [
|
|
862
|
+
{
|
|
863
|
+
name: `${sourceCollection}_${relationshipData.name}_unique`,
|
|
864
|
+
fields: [`${sourceCollection}_id`, "item_id", "collection"],
|
|
865
|
+
unique: true,
|
|
866
|
+
},
|
|
867
|
+
// Individual indexes on FK columns for better query performance
|
|
868
|
+
{
|
|
869
|
+
name: `${throughTable}_${sourceCollection}_id_idx`,
|
|
870
|
+
fields: [`${sourceCollection}_id`],
|
|
871
|
+
unique: false,
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
name: `${throughTable}_item_id_idx`,
|
|
875
|
+
fields: ["item_id"],
|
|
876
|
+
unique: false,
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
name: `${throughTable}_collection_idx`,
|
|
880
|
+
fields: ["collection"],
|
|
881
|
+
unique: false,
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
timestamps: true,
|
|
885
|
+
constraints: false,
|
|
886
|
+
};
|
|
887
|
+
for (const table of relationshipData.tables) {
|
|
888
|
+
//Add one to many polymorphic relation from junction table to target tables.
|
|
889
|
+
throughSchemaM2A.fields[table] = {
|
|
890
|
+
relType: "BelongsTo",
|
|
891
|
+
description: "M2A Junction Target",
|
|
892
|
+
target: table,
|
|
893
|
+
foreignKey: "item_id",
|
|
894
|
+
as: table,
|
|
895
|
+
constraints: false,
|
|
896
|
+
SystemGenerated: true,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
// Create through table
|
|
900
|
+
await schemaManager.updateModel(throughTable, throughSchemaM2A, accountability);
|
|
901
|
+
for (const table of relationshipData.tables) {
|
|
902
|
+
// Add HasMany relation from target to junction table
|
|
903
|
+
const targetSchemaDoc = await getSchemaDefinition(table, accountability);
|
|
904
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
905
|
+
targetSchema.fields[relationshipData.alias] = {
|
|
906
|
+
relType: "HasMany",
|
|
907
|
+
target: throughTable,
|
|
908
|
+
foreignKey: "item_id",
|
|
909
|
+
as: relationshipData.alias,
|
|
910
|
+
description: relationshipData.description,
|
|
911
|
+
constraints: false,
|
|
912
|
+
scope: {
|
|
913
|
+
collection: table.toLowerCase(),
|
|
914
|
+
},
|
|
915
|
+
onDelete,
|
|
916
|
+
onUpdate,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
// Add HasMany relation from source to junction table
|
|
920
|
+
console.log(`[processRelationship] Adding M2A field '${relationshipData.name}' to ${sourceCollection}`);
|
|
921
|
+
updatedSourceSchema.fields[relationshipData.name] = {
|
|
922
|
+
relType: "HasMany",
|
|
923
|
+
target: throughTable,
|
|
924
|
+
foreignKey: `${sourceCollection}_id`,
|
|
925
|
+
as: relationshipData.name,
|
|
926
|
+
description: relationshipData.description,
|
|
927
|
+
tables: relationshipData.tables,
|
|
928
|
+
polymorphic: true,
|
|
929
|
+
onDelete,
|
|
930
|
+
onUpdate,
|
|
931
|
+
};
|
|
932
|
+
console.log(`[processRelationship] Updated source schema fields for ${sourceCollection}:`, Object.keys(updatedSourceSchema.fields));
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
return { updatedSourceSchema, updatedTargetSchema };
|
|
936
|
+
}
|
|
937
|
+
async function updateM2ARelationship(sourceCollection, fieldName, newTables, originalFieldData, updatedFieldData, accountability) {
|
|
938
|
+
console.log("Updating M2A relationship for", sourceCollection, fieldName, newTables);
|
|
939
|
+
const sourceSchemaDoc = await getSchemaDefinition(sourceCollection, accountability);
|
|
940
|
+
const sourceSchema = sourceSchemaDoc?.schema;
|
|
941
|
+
const throughTable = `${sourceCollection}_${fieldName}_junction`;
|
|
942
|
+
// Check type of id in all target tables to ensure they are the same
|
|
943
|
+
const firstTableSchemaDoc = await getSchemaDefinition(newTables[0], accountability);
|
|
944
|
+
const firstTableSchema = firstTableSchemaDoc?.schema;
|
|
945
|
+
if (!firstTableSchema) {
|
|
946
|
+
throw new APIError("Target table not found", 404);
|
|
947
|
+
}
|
|
948
|
+
for (const table of newTables) {
|
|
949
|
+
const tableSchemaDoc = await getSchemaDefinition(table, accountability);
|
|
950
|
+
const tableSchema = tableSchemaDoc?.schema;
|
|
951
|
+
if (tableSchema.fields.id.type !== firstTableSchema.fields.id.type) {
|
|
952
|
+
throw new APIError("Target tables must have the same id type", 400);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Get current tables from the existing M2A relationship
|
|
956
|
+
const currentTables = originalFieldData.tables || [];
|
|
957
|
+
console.log(`Current tables in M2A relationship: ${currentTables}`);
|
|
958
|
+
// Determine which tables are being added and removed
|
|
959
|
+
const tablesToAdd = newTables.filter((table) => !currentTables.includes(table));
|
|
960
|
+
const tablesToRemove = currentTables.filter((table) => !newTables.includes(table));
|
|
961
|
+
// Check if there's existing data that would be orphaned by removing tables
|
|
962
|
+
if (tablesToRemove.length > 0) {
|
|
963
|
+
try {
|
|
964
|
+
const junctionService = new ItemsService(throughTable, { accountability });
|
|
965
|
+
for (const table of tablesToRemove) {
|
|
966
|
+
const result = await junctionService.readByQuery({
|
|
967
|
+
filter: { collection: table.toLowerCase() },
|
|
968
|
+
aggregate: { count: { function: 'count', field: 'id' } },
|
|
969
|
+
limit: 0
|
|
970
|
+
}, true);
|
|
971
|
+
const existingCount = result.data?.[0]?.count || 0;
|
|
972
|
+
if (existingCount > 0) {
|
|
973
|
+
throw new APIError(`Cannot remove table '${table}' from M2A relationship because there are ${existingCount} existing records. Please clean up the data first.`, 400);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
catch (error) {
|
|
978
|
+
// If table doesn't exist yet, that's fine - no data to validate
|
|
979
|
+
if (!error.message?.includes('does not exist')) {
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// Also check if there's existing data that would violate new table constraints
|
|
985
|
+
if (tablesToAdd.length > 0) {
|
|
986
|
+
try {
|
|
987
|
+
const junctionService = new ItemsService(throughTable, { accountability });
|
|
988
|
+
// Get all existing data in the junction table
|
|
989
|
+
const existingDataResult = await junctionService.readByQuery({
|
|
990
|
+
fields: ['item_id', 'collection'],
|
|
991
|
+
limit: -1
|
|
992
|
+
}, true);
|
|
993
|
+
const existingData = existingDataResult.data;
|
|
994
|
+
// Check if any existing data references tables that aren't in newTables
|
|
995
|
+
for (const record of existingData) {
|
|
996
|
+
if (!newTables.map((t) => t.toLowerCase()).includes(record.collection)) {
|
|
997
|
+
throw new APIError(`Cannot update M2A relationship because there is existing data referencing table '${record.collection}' which is not in the new table list. Please clean up the data first.`, 400);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// Check if existing data has valid references to the new tables being added
|
|
1001
|
+
for (const table of tablesToAdd) {
|
|
1002
|
+
try {
|
|
1003
|
+
const tableService = new ItemsService(table, { accountability });
|
|
1004
|
+
const recordsForTable = existingData.filter((r) => r.collection === table.toLowerCase());
|
|
1005
|
+
for (const record of recordsForTable) {
|
|
1006
|
+
const result = await tableService.readByQuery({
|
|
1007
|
+
filter: { id: record.item_id },
|
|
1008
|
+
aggregate: { count: { function: 'count', field: 'id' } },
|
|
1009
|
+
limit: 0
|
|
1010
|
+
}, true);
|
|
1011
|
+
const exists = result.data?.[0]?.count || 0;
|
|
1012
|
+
if (!exists) {
|
|
1013
|
+
throw new APIError(`Cannot add table '${table}' to M2A relationship because junction table contains item_id=${record.item_id} which doesn't exist in table '${table}'. Please clean up the data first.`, 400);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
// If target table doesn't exist, skip validation for it
|
|
1019
|
+
if (!error.message?.includes('does not exist')) {
|
|
1020
|
+
throw error;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
catch (error) {
|
|
1026
|
+
// If junction table doesn't exist yet, that's fine - no data to validate
|
|
1027
|
+
if (!error.message?.includes('does not exist')) {
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
// Remove relationship from tables no longer in the list
|
|
1033
|
+
for (const table of tablesToRemove) {
|
|
1034
|
+
// Remove the inverse relationship from the target table
|
|
1035
|
+
const targetSchemaDoc = await getSchemaDefinition(table, accountability);
|
|
1036
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
1037
|
+
if (targetSchema.fields[originalFieldData.alias]) {
|
|
1038
|
+
delete targetSchema.fields[originalFieldData.alias];
|
|
1039
|
+
await schemaManager.updateModel(table, targetSchema, accountability);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Get the existing junction table schema to preserve existing structure
|
|
1043
|
+
const existingJunctionSchema = await getSchemaDefinition(throughTable, accountability);
|
|
1044
|
+
if (!existingJunctionSchema) {
|
|
1045
|
+
throw new APIError(`Junction table ${throughTable} not found. Cannot update M2A relationship on non-existent table.`, 404);
|
|
1046
|
+
}
|
|
1047
|
+
console.log(`Updating M2A junction table ${throughTable}: adding ${tablesToAdd.length} tables, removing ${tablesToRemove.length} tables`);
|
|
1048
|
+
// Preserve existing schema completely and only modify table relationships
|
|
1049
|
+
// This ensures any custom fields added to the junction table are preserved
|
|
1050
|
+
const throughSchemaM2A = JSON.parse(JSON.stringify(existingJunctionSchema.schema)); // Deep clone
|
|
1051
|
+
// Ensure item_id field has proper constraints disabled (only if it exists)
|
|
1052
|
+
if (throughSchemaM2A.fields.item_id) {
|
|
1053
|
+
throughSchemaM2A.fields.item_id = {
|
|
1054
|
+
...throughSchemaM2A.fields.item_id,
|
|
1055
|
+
references: null,
|
|
1056
|
+
constraints: false,
|
|
1057
|
+
foreignKey: false,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
// Only remove BelongsTo relationships for tables that are no longer needed
|
|
1061
|
+
// Preserve any custom fields that are not table relationships
|
|
1062
|
+
for (const table of tablesToRemove) {
|
|
1063
|
+
if (throughSchemaM2A.fields[table] &&
|
|
1064
|
+
throughSchemaM2A.fields[table].relType === "BelongsTo" &&
|
|
1065
|
+
throughSchemaM2A.fields[table].foreignKey === "item_id") {
|
|
1066
|
+
console.log(`Removing M2A table relationship: ${table}`);
|
|
1067
|
+
delete throughSchemaM2A.fields[table];
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// Add relationships for new tables only
|
|
1071
|
+
for (const table of tablesToAdd) {
|
|
1072
|
+
console.log(`Adding M2A table relationship: ${table}`);
|
|
1073
|
+
throughSchemaM2A.fields[table] = {
|
|
1074
|
+
relType: "BelongsTo",
|
|
1075
|
+
description: "M2A Junction Target",
|
|
1076
|
+
target: table,
|
|
1077
|
+
foreignKey: "item_id",
|
|
1078
|
+
as: table,
|
|
1079
|
+
constraints: false,
|
|
1080
|
+
SystemGenerated: true,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
// Update the through table
|
|
1084
|
+
await schemaManager.updateModel(throughTable, throughSchemaM2A, accountability);
|
|
1085
|
+
// Update the main HasMany relation from source to junction table
|
|
1086
|
+
sourceSchema.fields[fieldName] = {
|
|
1087
|
+
relType: "HasMany",
|
|
1088
|
+
target: throughTable,
|
|
1089
|
+
foreignKey: `${sourceCollection}_id`,
|
|
1090
|
+
as: fieldName,
|
|
1091
|
+
description: updatedFieldData.description || originalFieldData.description,
|
|
1092
|
+
onDelete: updatedFieldData.onDelete || originalFieldData.onDelete,
|
|
1093
|
+
onUpdate: updatedFieldData.onUpdate || originalFieldData.onUpdate,
|
|
1094
|
+
polymorphic: true,
|
|
1095
|
+
tables: newTables,
|
|
1096
|
+
};
|
|
1097
|
+
// Add relationships for newly added tables
|
|
1098
|
+
for (const table of tablesToAdd) {
|
|
1099
|
+
// Add or update inverse relationship in target schema
|
|
1100
|
+
const targetSchemaDoc = await getSchemaDefinition(table, accountability);
|
|
1101
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
1102
|
+
targetSchema.fields[updatedFieldData.alias || originalFieldData.alias] = {
|
|
1103
|
+
relType: "HasMany",
|
|
1104
|
+
target: throughTable,
|
|
1105
|
+
foreignKey: "item_id",
|
|
1106
|
+
as: updatedFieldData.alias || originalFieldData.alias,
|
|
1107
|
+
description: updatedFieldData.description || originalFieldData.description,
|
|
1108
|
+
constraints: false,
|
|
1109
|
+
scope: {
|
|
1110
|
+
collection: table.toLowerCase(),
|
|
1111
|
+
},
|
|
1112
|
+
onDelete: updatedFieldData.onDelete || originalFieldData.onDelete,
|
|
1113
|
+
onUpdate: updatedFieldData.onUpdate || originalFieldData.onUpdate,
|
|
1114
|
+
};
|
|
1115
|
+
await schemaManager.updateModel(table, targetSchema, accountability);
|
|
1116
|
+
}
|
|
1117
|
+
// Update existing tables in case the alias or other properties changed
|
|
1118
|
+
for (const table of newTables.filter(t => !tablesToAdd.includes(t))) {
|
|
1119
|
+
const targetSchemaDoc = await getSchemaDefinition(table, accountability);
|
|
1120
|
+
const targetSchema = targetSchemaDoc?.schema;
|
|
1121
|
+
if (targetSchema.fields[originalFieldData.alias]) {
|
|
1122
|
+
targetSchema.fields[updatedFieldData.alias || originalFieldData.alias] = {
|
|
1123
|
+
relType: "HasMany",
|
|
1124
|
+
target: throughTable,
|
|
1125
|
+
foreignKey: "item_id",
|
|
1126
|
+
as: updatedFieldData.alias || originalFieldData.alias,
|
|
1127
|
+
description: updatedFieldData.description || originalFieldData.description,
|
|
1128
|
+
constraints: false,
|
|
1129
|
+
scope: {
|
|
1130
|
+
collection: table.toLowerCase(),
|
|
1131
|
+
},
|
|
1132
|
+
onDelete: updatedFieldData.onDelete || originalFieldData.onDelete,
|
|
1133
|
+
onUpdate: updatedFieldData.onUpdate || originalFieldData.onUpdate,
|
|
1134
|
+
};
|
|
1135
|
+
await schemaManager.updateModel(table, targetSchema, accountability);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Update the source schema
|
|
1139
|
+
await schemaManager.updateModel(sourceCollection, sourceSchema, accountability);
|
|
1140
|
+
}
|
|
1141
|
+
// Export schema as JSON file
|
|
1142
|
+
app.get("/schemas-export", adminOnly, async (req, res, next) => {
|
|
1143
|
+
try {
|
|
1144
|
+
const schemaService = new ItemsService('baasix_SchemaDefinition', { accountability: req.accountability });
|
|
1145
|
+
const schemasResult = await schemaService.readByQuery({
|
|
1146
|
+
sort: ['collectionName'],
|
|
1147
|
+
limit: -1
|
|
1148
|
+
}, true);
|
|
1149
|
+
const schemas = schemasResult.data;
|
|
1150
|
+
// Create a versioned export with metadata
|
|
1151
|
+
const schemaExport = {
|
|
1152
|
+
version: "1.0",
|
|
1153
|
+
timestamp: new Date().toISOString(),
|
|
1154
|
+
schemas: schemas.map((schema) => ({
|
|
1155
|
+
collectionName: schema.collectionName,
|
|
1156
|
+
schema: schema.schema,
|
|
1157
|
+
createdAt: schema.createdAt,
|
|
1158
|
+
updatedAt: schema.updatedAt,
|
|
1159
|
+
})),
|
|
1160
|
+
};
|
|
1161
|
+
// Set headers for file download
|
|
1162
|
+
res.setHeader("Content-Type", "application/json");
|
|
1163
|
+
res.setHeader("Content-Disposition", `attachment; filename=schema-export-${Date.now()}.json`);
|
|
1164
|
+
// Send the JSON as a file download response to the client browser as buffer
|
|
1165
|
+
res.status(200).send(Buffer.from(JSON.stringify(schemaExport, null, 2)));
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
next(new APIError("Error exporting schemas", 500, error.message));
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
// Preview schema changes from uploaded file
|
|
1172
|
+
app.post("/schemas-preview-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), // 50MB limit
|
|
1173
|
+
async (req, res, next) => {
|
|
1174
|
+
try {
|
|
1175
|
+
if (!req.files || !req.files.schema) {
|
|
1176
|
+
throw new APIError("No schema file uploaded", 400);
|
|
1177
|
+
}
|
|
1178
|
+
const schemaFile = req.files.schema;
|
|
1179
|
+
// Validate file type
|
|
1180
|
+
if (!schemaFile.mimetype.includes("application/json")) {
|
|
1181
|
+
throw new APIError("Invalid file type. Please upload a JSON file", 400);
|
|
1182
|
+
}
|
|
1183
|
+
// Parse the uploaded JSON file
|
|
1184
|
+
let importData;
|
|
1185
|
+
try {
|
|
1186
|
+
importData = JSON.parse(schemaFile.data.toString());
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
throw new APIError("Invalid JSON file", 400);
|
|
1190
|
+
}
|
|
1191
|
+
const schemaService = new ItemsService('baasix_SchemaDefinition', { accountability: req.accountability });
|
|
1192
|
+
const currentSchemasResult = await schemaService.readByQuery({ limit: -1 }, true);
|
|
1193
|
+
const currentSchemas = currentSchemasResult.data;
|
|
1194
|
+
const currentSchemaMap = new Map(currentSchemas.map((s) => [s.collectionName, s]));
|
|
1195
|
+
// Analyze changes
|
|
1196
|
+
const changes = {
|
|
1197
|
+
new: [],
|
|
1198
|
+
modified: [],
|
|
1199
|
+
deleted: [],
|
|
1200
|
+
unchanged: [],
|
|
1201
|
+
};
|
|
1202
|
+
// Check for new and modified schemas
|
|
1203
|
+
for (const importSchema of importData.schemas) {
|
|
1204
|
+
const currentSchema = currentSchemaMap.get(importSchema.collectionName);
|
|
1205
|
+
if (!currentSchema) {
|
|
1206
|
+
changes.new.push({
|
|
1207
|
+
collectionName: importSchema.collectionName,
|
|
1208
|
+
details: "New schema will be created",
|
|
1209
|
+
});
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
const differences = compareSchemas(currentSchema.schema, importSchema.schema);
|
|
1213
|
+
if (Object.keys(differences).length > 0) {
|
|
1214
|
+
changes.modified.push({
|
|
1215
|
+
collectionName: importSchema.collectionName,
|
|
1216
|
+
differences,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
else {
|
|
1220
|
+
changes.unchanged.push(importSchema.collectionName);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// Check for deleted schemas
|
|
1224
|
+
for (const [collectionName, schema] of currentSchemaMap) {
|
|
1225
|
+
if (!importData.schemas.find((s) => s.collectionName === collectionName)) {
|
|
1226
|
+
changes.deleted.push(collectionName);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
res.status(200).json({
|
|
1230
|
+
importVersion: importData.version,
|
|
1231
|
+
importTimestamp: importData.timestamp,
|
|
1232
|
+
changes,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
next(new APIError("Error analyzing schema changes", 500, error.message));
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
// Import schema from uploaded file
|
|
1240
|
+
app.post("/schemas-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), // 50MB limit
|
|
1241
|
+
async (req, res, next) => {
|
|
1242
|
+
try {
|
|
1243
|
+
if (!req.files || !req.files.schema) {
|
|
1244
|
+
throw new APIError("No schema file uploaded", 400);
|
|
1245
|
+
}
|
|
1246
|
+
const schemaFile = req.files.schema;
|
|
1247
|
+
// Validate file type
|
|
1248
|
+
if (!schemaFile.mimetype.includes("application/json")) {
|
|
1249
|
+
throw new APIError("Invalid file type. Please upload a JSON file", 400);
|
|
1250
|
+
}
|
|
1251
|
+
// Parse the uploaded JSON file
|
|
1252
|
+
let importData;
|
|
1253
|
+
try {
|
|
1254
|
+
importData = JSON.parse(schemaFile.data.toString());
|
|
1255
|
+
}
|
|
1256
|
+
catch (error) {
|
|
1257
|
+
throw new APIError("Invalid JSON file", 400);
|
|
1258
|
+
}
|
|
1259
|
+
// Validate import data structure
|
|
1260
|
+
if (!importData.version || !importData.schemas) {
|
|
1261
|
+
throw new APIError("Invalid import data format", 400);
|
|
1262
|
+
}
|
|
1263
|
+
// Track all changes made during import
|
|
1264
|
+
const changes = {
|
|
1265
|
+
created: [],
|
|
1266
|
+
updated: [],
|
|
1267
|
+
unchanged: [],
|
|
1268
|
+
deleted: [],
|
|
1269
|
+
errors: [],
|
|
1270
|
+
};
|
|
1271
|
+
// Track collections with realtime config changes for efficient sync
|
|
1272
|
+
const realtimeChangedCollections = [];
|
|
1273
|
+
// Process each schema
|
|
1274
|
+
for (const schemaData of importData.schemas) {
|
|
1275
|
+
try {
|
|
1276
|
+
const existingSchema = await getSchemaDefinition(schemaData.collectionName, req.accountability);
|
|
1277
|
+
// Process schema flags
|
|
1278
|
+
const processedSchema = processSchemaFlags(schemaData.schema);
|
|
1279
|
+
if (existingSchema) {
|
|
1280
|
+
// Check if schema has actually changed
|
|
1281
|
+
const differences = compareSchemas(existingSchema.schema, processedSchema);
|
|
1282
|
+
if (Object.keys(differences).length > 0) {
|
|
1283
|
+
// Track if realtime config changed
|
|
1284
|
+
if (differences.realtime) {
|
|
1285
|
+
realtimeChangedCollections.push(schemaData.collectionName);
|
|
1286
|
+
}
|
|
1287
|
+
// Update existing schema only if there are changes
|
|
1288
|
+
await schemaManager.updateModel(schemaData.collectionName, processedSchema, req.accountability);
|
|
1289
|
+
changes.updated.push(schemaData.collectionName);
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
// Schema is unchanged, skip syncing
|
|
1293
|
+
console.log(`Schema ${schemaData.collectionName} is unchanged, skipping sync`);
|
|
1294
|
+
changes.unchanged.push(schemaData.collectionName);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
// Create new schema - check if it has realtime enabled
|
|
1299
|
+
if (processedSchema.realtime === true ||
|
|
1300
|
+
(typeof processedSchema.realtime === 'object' && processedSchema.realtime?.enabled)) {
|
|
1301
|
+
realtimeChangedCollections.push(schemaData.collectionName);
|
|
1302
|
+
}
|
|
1303
|
+
// Create new schema
|
|
1304
|
+
await schemaManager.updateModel(schemaData.collectionName, processedSchema, req.accountability);
|
|
1305
|
+
changes.created.push(schemaData.collectionName);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
catch (error) {
|
|
1309
|
+
console.error(`Error importing schema ${schemaData.collectionName}:`, error);
|
|
1310
|
+
changes.errors.push({
|
|
1311
|
+
collectionName: schemaData.collectionName,
|
|
1312
|
+
error: error.message,
|
|
1313
|
+
});
|
|
1314
|
+
throw new APIError("Schema import failed", 400, changes);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
await invalidateEntireCache();
|
|
1318
|
+
// Reload realtime configuration only for collections with realtime changes
|
|
1319
|
+
if (realtimeChangedCollections.length > 0) {
|
|
1320
|
+
try {
|
|
1321
|
+
const realtimeService = (await import('../services/RealtimeService.js')).default;
|
|
1322
|
+
if (realtimeService.isWalAvailable()) {
|
|
1323
|
+
await realtimeService.reloadCollections(realtimeChangedCollections);
|
|
1324
|
+
console.log(`Realtime configuration reloaded for ${realtimeChangedCollections.length} collections: ${realtimeChangedCollections.join(', ')}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
catch (error) {
|
|
1328
|
+
console.warn('Could not reload realtime configuration:', error.message);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
res.status(200).json({
|
|
1332
|
+
message: "Schema import completed",
|
|
1333
|
+
changes,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
catch (error) {
|
|
1337
|
+
throw new APIError("Schema import failed", 400, error);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
// Helper function for deep equality comparison (ignores property order)
|
|
1341
|
+
function deepEqual(obj1, obj2) {
|
|
1342
|
+
if (obj1 === obj2)
|
|
1343
|
+
return true;
|
|
1344
|
+
if (obj1 === null || obj2 === null)
|
|
1345
|
+
return false;
|
|
1346
|
+
if (typeof obj1 !== typeof obj2)
|
|
1347
|
+
return false;
|
|
1348
|
+
if (typeof obj1 !== 'object')
|
|
1349
|
+
return obj1 === obj2;
|
|
1350
|
+
if (Array.isArray(obj1) !== Array.isArray(obj2))
|
|
1351
|
+
return false;
|
|
1352
|
+
if (Array.isArray(obj1)) {
|
|
1353
|
+
if (obj1.length !== obj2.length)
|
|
1354
|
+
return false;
|
|
1355
|
+
for (let i = 0; i < obj1.length; i++) {
|
|
1356
|
+
if (!deepEqual(obj1[i], obj2[i]))
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
const keys1 = Object.keys(obj1);
|
|
1362
|
+
const keys2 = Object.keys(obj2);
|
|
1363
|
+
if (keys1.length !== keys2.length)
|
|
1364
|
+
return false;
|
|
1365
|
+
for (const key of keys1) {
|
|
1366
|
+
if (!keys2.includes(key))
|
|
1367
|
+
return false;
|
|
1368
|
+
if (!deepEqual(obj1[key], obj2[key]))
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
// Helper function to normalize schema for comparison
|
|
1374
|
+
// Removes SystemGenerated field as it's metadata that doesn't affect DB structure
|
|
1375
|
+
function normalizeSchemaForComparison(schema) {
|
|
1376
|
+
if (!schema)
|
|
1377
|
+
return schema;
|
|
1378
|
+
const normalized = { ...schema };
|
|
1379
|
+
if (normalized.fields) {
|
|
1380
|
+
normalized.fields = { ...normalized.fields };
|
|
1381
|
+
for (const fieldName of Object.keys(normalized.fields)) {
|
|
1382
|
+
const field = normalized.fields[fieldName];
|
|
1383
|
+
if (field && typeof field === 'object') {
|
|
1384
|
+
// Create a copy without SystemGenerated for comparison
|
|
1385
|
+
const { SystemGenerated, ...fieldWithoutSystemGenerated } = field;
|
|
1386
|
+
normalized.fields[fieldName] = fieldWithoutSystemGenerated;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return normalized;
|
|
1391
|
+
}
|
|
1392
|
+
// Helper function to compare schemas
|
|
1393
|
+
function compareSchemas(currentSchema, newSchema) {
|
|
1394
|
+
// Normalize both schemas before comparison
|
|
1395
|
+
const normalizedCurrent = normalizeSchemaForComparison(currentSchema);
|
|
1396
|
+
const normalizedNew = normalizeSchemaForComparison(newSchema);
|
|
1397
|
+
const differences = {};
|
|
1398
|
+
// Compare fields
|
|
1399
|
+
const allFields = new Set([...Object.keys(normalizedCurrent.fields || {}), ...Object.keys(normalizedNew.fields || {})]);
|
|
1400
|
+
for (const field of allFields) {
|
|
1401
|
+
if (!normalizedCurrent.fields[field]) {
|
|
1402
|
+
differences[field] = {
|
|
1403
|
+
type: "added",
|
|
1404
|
+
details: normalizedNew.fields[field],
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
else if (!normalizedNew.fields[field]) {
|
|
1408
|
+
differences[field] = {
|
|
1409
|
+
type: "removed",
|
|
1410
|
+
details: normalizedCurrent.fields[field],
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
else if (!deepEqual(normalizedCurrent.fields[field], normalizedNew.fields[field])) {
|
|
1414
|
+
differences[field] = {
|
|
1415
|
+
type: "modified",
|
|
1416
|
+
from: normalizedCurrent.fields[field],
|
|
1417
|
+
to: normalizedNew.fields[field],
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
// Compare indexes
|
|
1422
|
+
if (normalizedCurrent.indexes || normalizedNew.indexes) {
|
|
1423
|
+
const currentIndexes = normalizedCurrent.indexes || [];
|
|
1424
|
+
const newIndexes = normalizedNew.indexes || [];
|
|
1425
|
+
if (!deepEqual(currentIndexes, newIndexes)) {
|
|
1426
|
+
differences.indexes = {
|
|
1427
|
+
type: "modified",
|
|
1428
|
+
from: currentIndexes,
|
|
1429
|
+
to: newIndexes,
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// Compare realtime configuration
|
|
1434
|
+
if (normalizedCurrent.realtime !== undefined || normalizedNew.realtime !== undefined) {
|
|
1435
|
+
if (!deepEqual(normalizedCurrent.realtime, normalizedNew.realtime)) {
|
|
1436
|
+
differences.realtime = {
|
|
1437
|
+
type: "modified",
|
|
1438
|
+
from: normalizedCurrent.realtime,
|
|
1439
|
+
to: normalizedNew.realtime,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// Compare other properties
|
|
1444
|
+
const schemaProps = ["timestamps", "paranoid", "name"];
|
|
1445
|
+
for (const prop of schemaProps) {
|
|
1446
|
+
if (normalizedCurrent[prop] !== normalizedNew[prop]) {
|
|
1447
|
+
differences[prop] = {
|
|
1448
|
+
type: "modified",
|
|
1449
|
+
from: normalizedCurrent[prop],
|
|
1450
|
+
to: normalizedNew[prop],
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return differences;
|
|
1455
|
+
}
|
|
1456
|
+
// Add these endpoints to the permission.route.js file
|
|
1457
|
+
// Export roles and permissions
|
|
1458
|
+
app.get("/permissions-export", adminOnly, async (req, res, next) => {
|
|
1459
|
+
try {
|
|
1460
|
+
const roleService = new ItemsService('baasix_Role', { accountability: req.accountability });
|
|
1461
|
+
const permissionItemsService = new ItemsService('baasix_Permission', { accountability: req.accountability });
|
|
1462
|
+
// Get all roles sorted by name
|
|
1463
|
+
const rolesResult = await roleService.readByQuery({
|
|
1464
|
+
sort: ['name'],
|
|
1465
|
+
limit: -1
|
|
1466
|
+
}, true);
|
|
1467
|
+
const roles = rolesResult.data;
|
|
1468
|
+
// Get all permissions sorted by role_Id, collection, and action
|
|
1469
|
+
const permissionsResult = await permissionItemsService.readByQuery({
|
|
1470
|
+
sort: ['role_Id', 'collection', 'action'],
|
|
1471
|
+
limit: -1
|
|
1472
|
+
}, true);
|
|
1473
|
+
const permissions = permissionsResult.data;
|
|
1474
|
+
// Group permissions by role_Id
|
|
1475
|
+
const permissionsByRole = permissions.reduce((acc, permission) => {
|
|
1476
|
+
if (!acc[permission.role_Id]) {
|
|
1477
|
+
acc[permission.role_Id] = [];
|
|
1478
|
+
}
|
|
1479
|
+
acc[permission.role_Id].push(permission);
|
|
1480
|
+
return acc;
|
|
1481
|
+
}, {});
|
|
1482
|
+
// Create a versioned export with metadata
|
|
1483
|
+
const exportData = {
|
|
1484
|
+
version: "1.0",
|
|
1485
|
+
timestamp: new Date().toISOString(),
|
|
1486
|
+
roles: roles.map((role) => ({
|
|
1487
|
+
name: role.name,
|
|
1488
|
+
description: role.description,
|
|
1489
|
+
permissions: (permissionsByRole[role.id] || []).map((permission) => ({
|
|
1490
|
+
collection: permission.collection,
|
|
1491
|
+
action: permission.action,
|
|
1492
|
+
fields: permission.fields,
|
|
1493
|
+
conditions: permission.conditions,
|
|
1494
|
+
defaultValues: permission.defaultValues,
|
|
1495
|
+
relConditions: permission.relConditions,
|
|
1496
|
+
})),
|
|
1497
|
+
})),
|
|
1498
|
+
};
|
|
1499
|
+
// Set headers for file download
|
|
1500
|
+
res.setHeader("Content-Type", "application/json");
|
|
1501
|
+
res.setHeader("Content-Disposition", `attachment; filename=roles-permissions-export-${Date.now()}.json`);
|
|
1502
|
+
res.status(200).send(Buffer.from(JSON.stringify(exportData, null, 2)));
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
next(new APIError("Error exporting roles and permissions", 500, error.message));
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
// Preview roles and permissions import
|
|
1509
|
+
app.post("/permissions-preview-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), async (req, res, next) => {
|
|
1510
|
+
try {
|
|
1511
|
+
if (!req.files || !req.files.rolesPermissions) {
|
|
1512
|
+
throw new APIError("No roles & permissions file uploaded", 400);
|
|
1513
|
+
}
|
|
1514
|
+
const uploadedFile = req.files.rolesPermissions;
|
|
1515
|
+
if (!uploadedFile.mimetype.includes("application/json")) {
|
|
1516
|
+
throw new APIError("Invalid file type. Please upload a JSON file", 400);
|
|
1517
|
+
}
|
|
1518
|
+
let importData;
|
|
1519
|
+
try {
|
|
1520
|
+
importData = JSON.parse(uploadedFile.data.toString());
|
|
1521
|
+
}
|
|
1522
|
+
catch (error) {
|
|
1523
|
+
throw new APIError("Invalid JSON file", 400);
|
|
1524
|
+
}
|
|
1525
|
+
const roleService = new ItemsService('baasix_Role', { accountability: req.accountability });
|
|
1526
|
+
const permissionItemsService = new ItemsService('baasix_Permission', { accountability: req.accountability });
|
|
1527
|
+
const currentRolesResult = await roleService.readByQuery({ limit: -1 }, true);
|
|
1528
|
+
const currentRoles = currentRolesResult.data;
|
|
1529
|
+
const permissionsResult = await permissionItemsService.readByQuery({ limit: -1 }, true);
|
|
1530
|
+
const permissions = permissionsResult.data;
|
|
1531
|
+
// Group permissions by role_Id
|
|
1532
|
+
const permissionsByRole = permissions.reduce((acc, permission) => {
|
|
1533
|
+
if (!acc[permission.role_Id]) {
|
|
1534
|
+
acc[permission.role_Id] = [];
|
|
1535
|
+
}
|
|
1536
|
+
acc[permission.role_Id].push(permission);
|
|
1537
|
+
return acc;
|
|
1538
|
+
}, {});
|
|
1539
|
+
// Add permissions to roles
|
|
1540
|
+
const currentRolesWithPerms = currentRoles.map((role) => ({
|
|
1541
|
+
...role,
|
|
1542
|
+
permissions: permissionsByRole[role.id] || []
|
|
1543
|
+
}));
|
|
1544
|
+
const currentRoleMap = new Map(currentRolesWithPerms.map((r) => [r.name, r]));
|
|
1545
|
+
const changes = {
|
|
1546
|
+
new: [],
|
|
1547
|
+
modified: [],
|
|
1548
|
+
deleted: [],
|
|
1549
|
+
unchanged: [],
|
|
1550
|
+
};
|
|
1551
|
+
// Analyze changes for roles and their permissions
|
|
1552
|
+
for (const importRole of importData.roles) {
|
|
1553
|
+
const currentRole = currentRoleMap.get(importRole.name);
|
|
1554
|
+
if (!currentRole) {
|
|
1555
|
+
changes.new.push({
|
|
1556
|
+
name: importRole.name,
|
|
1557
|
+
type: "role",
|
|
1558
|
+
details: `New role with ${importRole.permissions.length} permissions`,
|
|
1559
|
+
});
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const differences = compareRoleAndPermissions(currentRole, importRole);
|
|
1563
|
+
if (Object.keys(differences).length > 0) {
|
|
1564
|
+
changes.modified.push({
|
|
1565
|
+
name: importRole.name,
|
|
1566
|
+
type: "role",
|
|
1567
|
+
differences,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
else {
|
|
1571
|
+
changes.unchanged.push(importRole.name);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
// Check for deleted roles
|
|
1575
|
+
for (const [roleName, role] of currentRoleMap) {
|
|
1576
|
+
if (!importData.roles.find((r) => r.name === roleName)) {
|
|
1577
|
+
changes.deleted.push({
|
|
1578
|
+
name: roleName,
|
|
1579
|
+
type: "role",
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
res.status(200).json({
|
|
1584
|
+
importVersion: importData.version,
|
|
1585
|
+
importTimestamp: importData.timestamp,
|
|
1586
|
+
changes,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
catch (error) {
|
|
1590
|
+
console.error("Error analyzing roles and permissions changes:", error);
|
|
1591
|
+
next(new APIError("Error analyzing roles and permissions changes", 500, error.message));
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
// Import roles and permissions
|
|
1595
|
+
app.post("/permissions-import", adminOnly, fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }), async (req, res, next) => {
|
|
1596
|
+
try {
|
|
1597
|
+
if (!req.files || !req.files.rolesPermissions) {
|
|
1598
|
+
throw new APIError("No roles & permissions file uploaded", 400);
|
|
1599
|
+
}
|
|
1600
|
+
const uploadedFile = req.files.rolesPermissions;
|
|
1601
|
+
if (!Array.isArray(uploadedFile) && !uploadedFile.mimetype.includes("application/json")) {
|
|
1602
|
+
throw new APIError("Invalid file type. Please upload a JSON file", 400);
|
|
1603
|
+
}
|
|
1604
|
+
let importData;
|
|
1605
|
+
try {
|
|
1606
|
+
const fileData = Array.isArray(uploadedFile) ? uploadedFile[0].data : uploadedFile.data;
|
|
1607
|
+
importData = JSON.parse(fileData.toString());
|
|
1608
|
+
}
|
|
1609
|
+
catch (error) {
|
|
1610
|
+
throw new APIError("Invalid JSON file", 400);
|
|
1611
|
+
}
|
|
1612
|
+
if (!importData.version || !importData.roles) {
|
|
1613
|
+
throw new APIError("Invalid import data format", 400);
|
|
1614
|
+
}
|
|
1615
|
+
const roleService = new ItemsService('baasix_Role', { accountability: req.accountability });
|
|
1616
|
+
const permissionItemsService = new ItemsService('baasix_Permission', { accountability: req.accountability });
|
|
1617
|
+
const changes = {
|
|
1618
|
+
created: [],
|
|
1619
|
+
updated: [],
|
|
1620
|
+
deleted: [],
|
|
1621
|
+
errors: [],
|
|
1622
|
+
};
|
|
1623
|
+
// Process each role and its permissions
|
|
1624
|
+
for (const roleData of importData.roles) {
|
|
1625
|
+
try {
|
|
1626
|
+
// Check if role exists
|
|
1627
|
+
const existingRolesResult = await roleService.readByQuery({
|
|
1628
|
+
filter: { name: roleData.name },
|
|
1629
|
+
limit: 1
|
|
1630
|
+
}, true);
|
|
1631
|
+
const existingRole = existingRolesResult.data[0];
|
|
1632
|
+
let roleId;
|
|
1633
|
+
if (!existingRole) {
|
|
1634
|
+
// Create new role - createOne returns only ID in Drizzle
|
|
1635
|
+
roleId = await roleService.createOne({
|
|
1636
|
+
name: roleData.name,
|
|
1637
|
+
description: roleData.description
|
|
1638
|
+
});
|
|
1639
|
+
changes.created.push(`Role: ${roleData.name}`);
|
|
1640
|
+
}
|
|
1641
|
+
else {
|
|
1642
|
+
// Update existing role
|
|
1643
|
+
await roleService.updateOne(existingRole.id, {
|
|
1644
|
+
description: roleData.description
|
|
1645
|
+
});
|
|
1646
|
+
roleId = existingRole.id;
|
|
1647
|
+
changes.updated.push(`Role: ${roleData.name}`);
|
|
1648
|
+
}
|
|
1649
|
+
// Delete existing permissions for this role
|
|
1650
|
+
const existingPermsResult = await permissionItemsService.readByQuery({
|
|
1651
|
+
filter: { role_Id: roleId },
|
|
1652
|
+
limit: -1
|
|
1653
|
+
}, true);
|
|
1654
|
+
for (const perm of existingPermsResult.data) {
|
|
1655
|
+
await permissionItemsService.deleteOne(perm.id);
|
|
1656
|
+
}
|
|
1657
|
+
// Create new permissions
|
|
1658
|
+
const permissions = roleData.permissions.map((perm) => ({
|
|
1659
|
+
...perm,
|
|
1660
|
+
role_Id: roleId,
|
|
1661
|
+
}));
|
|
1662
|
+
for (const perm of permissions) {
|
|
1663
|
+
await permissionItemsService.createOne(perm);
|
|
1664
|
+
}
|
|
1665
|
+
changes.created.push(`Permissions for ${roleData.name}: ${permissions.length}`);
|
|
1666
|
+
}
|
|
1667
|
+
catch (error) {
|
|
1668
|
+
changes.errors.push({
|
|
1669
|
+
role: roleData.name,
|
|
1670
|
+
error: error.message,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
await invalidateEntireCache();
|
|
1675
|
+
await permissionService.invalidateRoles(); // Reload roles cache
|
|
1676
|
+
await permissionService.loadPermissions(); // Reload permission cache (using imported singleton)
|
|
1677
|
+
res.status(200).json({
|
|
1678
|
+
message: "Roles and permissions import completed",
|
|
1679
|
+
changes,
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
catch (error) {
|
|
1683
|
+
next(new APIError("Error importing roles and permissions", 500, error.message));
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
// Helper function to compare roles and their permissions
|
|
1687
|
+
function compareRoleAndPermissions(currentRole, importRole) {
|
|
1688
|
+
const differences = {};
|
|
1689
|
+
// Compare basic role properties
|
|
1690
|
+
if (currentRole.description !== importRole.description) {
|
|
1691
|
+
differences.description = {
|
|
1692
|
+
type: "modified",
|
|
1693
|
+
from: currentRole.description,
|
|
1694
|
+
to: importRole.description,
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
// Compare permissions
|
|
1698
|
+
const currentPermissions = currentRole.permissions || [];
|
|
1699
|
+
const newPermissions = importRole.permissions || [];
|
|
1700
|
+
// Create maps for easier comparison
|
|
1701
|
+
const currentPermMap = new Map(currentPermissions.map((p) => [
|
|
1702
|
+
`${p.collection}:${p.action}`,
|
|
1703
|
+
{
|
|
1704
|
+
fields: p.fields,
|
|
1705
|
+
conditions: p.conditions,
|
|
1706
|
+
defaultValues: p.defaultValues,
|
|
1707
|
+
relConditions: p.relConditions,
|
|
1708
|
+
},
|
|
1709
|
+
]));
|
|
1710
|
+
const newPermMap = new Map(newPermissions.map((p) => [
|
|
1711
|
+
`${p.collection}:${p.action}`,
|
|
1712
|
+
{
|
|
1713
|
+
fields: p.fields,
|
|
1714
|
+
conditions: p.conditions,
|
|
1715
|
+
defaultValues: p.defaultValues,
|
|
1716
|
+
relConditions: p.relConditions,
|
|
1717
|
+
},
|
|
1718
|
+
]));
|
|
1719
|
+
// Check for added and modified permissions
|
|
1720
|
+
for (const [key, newPerm] of newPermMap) {
|
|
1721
|
+
if (!currentPermMap.has(key)) {
|
|
1722
|
+
if (!differences.permissions)
|
|
1723
|
+
differences.permissions = {};
|
|
1724
|
+
if (!differences.permissions.added)
|
|
1725
|
+
differences.permissions.added = [];
|
|
1726
|
+
differences.permissions.added.push(key);
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
const currentPerm = currentPermMap.get(key);
|
|
1730
|
+
if (JSON.stringify(currentPerm) !== JSON.stringify(newPerm)) {
|
|
1731
|
+
if (!differences.permissions)
|
|
1732
|
+
differences.permissions = {};
|
|
1733
|
+
if (!differences.permissions.modified)
|
|
1734
|
+
differences.permissions.modified = [];
|
|
1735
|
+
differences.permissions.modified.push({
|
|
1736
|
+
permission: key,
|
|
1737
|
+
changes: compareObjects(currentPerm, newPerm),
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
// Check for removed permissions
|
|
1743
|
+
for (const [key] of currentPermMap) {
|
|
1744
|
+
if (!newPermMap.has(key)) {
|
|
1745
|
+
if (!differences.permissions)
|
|
1746
|
+
differences.permissions = {};
|
|
1747
|
+
if (!differences.permissions.removed)
|
|
1748
|
+
differences.permissions.removed = [];
|
|
1749
|
+
differences.permissions.removed.push(key);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return differences;
|
|
1753
|
+
}
|
|
1754
|
+
// Helper function to compare objects
|
|
1755
|
+
function compareObjects(obj1, obj2) {
|
|
1756
|
+
const changes = {};
|
|
1757
|
+
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
|
|
1758
|
+
for (const key of allKeys) {
|
|
1759
|
+
if (!Object.prototype.hasOwnProperty.call(obj1, key)) {
|
|
1760
|
+
changes[key] = { type: "added", value: obj2[key] };
|
|
1761
|
+
}
|
|
1762
|
+
else if (!Object.prototype.hasOwnProperty.call(obj2, key)) {
|
|
1763
|
+
changes[key] = { type: "removed", value: obj1[key] };
|
|
1764
|
+
}
|
|
1765
|
+
else if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
|
|
1766
|
+
changes[key] = {
|
|
1767
|
+
type: "modified",
|
|
1768
|
+
from: obj1[key],
|
|
1769
|
+
to: obj2[key],
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return changes;
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
export default {
|
|
1777
|
+
id: "schemas",
|
|
1778
|
+
handler: registerEndpoint,
|
|
1779
|
+
};
|
|
1780
|
+
//# sourceMappingURL=schema.route.js.map
|