velocious 1.0.431 → 1.0.433
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/build/application.js +229 -0
- package/build/authorization/ability.js +329 -0
- package/build/authorization/base-resource.js +143 -0
- package/build/background-jobs/client.js +50 -0
- package/build/background-jobs/cron-expression.js +277 -0
- package/build/background-jobs/forked-runner-child.js +86 -0
- package/build/background-jobs/job-record.js +13 -0
- package/build/background-jobs/job-registry.js +92 -0
- package/build/background-jobs/job-runner.js +107 -0
- package/build/background-jobs/job.js +77 -0
- package/build/background-jobs/json-socket.js +78 -0
- package/build/background-jobs/main.js +926 -0
- package/build/background-jobs/normalize-error.js +26 -0
- package/build/background-jobs/scheduler.js +274 -0
- package/build/background-jobs/socket-request.js +68 -0
- package/build/background-jobs/status-reporter.js +101 -0
- package/build/background-jobs/store.js +994 -0
- package/build/background-jobs/types.js +70 -0
- package/build/background-jobs/web/authorization.js +89 -0
- package/build/background-jobs/web/controller.js +280 -0
- package/build/background-jobs/web/index.js +57 -0
- package/build/background-jobs/web/path-matcher.js +74 -0
- package/build/background-jobs/web/registry.js +49 -0
- package/build/background-jobs/worker.js +683 -0
- package/build/beacon/client.js +330 -0
- package/build/beacon/in-process-broker.js +71 -0
- package/build/beacon/in-process-client.js +139 -0
- package/build/beacon/server.js +148 -0
- package/build/beacon/types.js +55 -0
- package/build/cli/base-command.js +67 -0
- package/build/cli/browser-cli.js +45 -0
- package/build/cli/commands/background-jobs-main.js +7 -0
- package/build/cli/commands/background-jobs-runner.js +7 -0
- package/build/cli/commands/background-jobs-worker.js +7 -0
- package/build/cli/commands/beacon.js +7 -0
- package/build/cli/commands/console.js +12 -0
- package/build/cli/commands/db/base-command.js +82 -0
- package/build/cli/commands/db/create.js +64 -0
- package/build/cli/commands/db/drop.js +75 -0
- package/build/cli/commands/db/migrate.js +17 -0
- package/build/cli/commands/db/reset.js +22 -0
- package/build/cli/commands/db/rollback.js +15 -0
- package/build/cli/commands/db/schema/dump.js +12 -0
- package/build/cli/commands/db/schema/load.js +12 -0
- package/build/cli/commands/db/seed.js +12 -0
- package/build/cli/commands/db/tenants/check.js +38 -0
- package/build/cli/commands/db/tenants/create.js +33 -0
- package/build/cli/commands/db/tenants/migrate.js +49 -0
- package/build/cli/commands/destroy/migration.js +7 -0
- package/build/cli/commands/generate/base-models.js +7 -0
- package/build/cli/commands/generate/frontend-models.js +12 -0
- package/build/cli/commands/generate/migration.js +7 -0
- package/build/cli/commands/generate/model.js +7 -0
- package/build/cli/commands/init.js +11 -0
- package/build/cli/commands/routes.js +7 -0
- package/build/cli/commands/run-script.js +12 -0
- package/build/cli/commands/runner.js +12 -0
- package/build/cli/commands/server.js +7 -0
- package/build/cli/commands/test.js +9 -0
- package/build/cli/index.js +152 -0
- package/build/cli/tenant-database-command-helper.js +198 -0
- package/build/cli/use-browser-cli.js +30 -0
- package/build/configuration-resolver.js +65 -0
- package/build/configuration-types.js +429 -0
- package/build/configuration.js +2590 -0
- package/build/controller.js +421 -0
- package/build/current-configuration.js +31 -0
- package/build/current.js +80 -0
- package/build/database/annotations-async-hooks.js +47 -0
- package/build/database/annotations.js +40 -0
- package/build/database/drivers/base-column.js +182 -0
- package/build/database/drivers/base-columns-index.js +81 -0
- package/build/database/drivers/base-foreign-key.js +104 -0
- package/build/database/drivers/base-table.js +156 -0
- package/build/database/drivers/base.js +1609 -0
- package/build/database/drivers/mssql/column.js +74 -0
- package/build/database/drivers/mssql/columns-index.js +6 -0
- package/build/database/drivers/mssql/connect-connection.js +16 -0
- package/build/database/drivers/mssql/foreign-key.js +12 -0
- package/build/database/drivers/mssql/index.js +590 -0
- package/build/database/drivers/mssql/options.js +79 -0
- package/build/database/drivers/mssql/query-parser.js +6 -0
- package/build/database/drivers/mssql/sql/alter-table.js +4 -0
- package/build/database/drivers/mssql/sql/create-database.js +36 -0
- package/build/database/drivers/mssql/sql/create-index.js +4 -0
- package/build/database/drivers/mssql/sql/create-table.js +4 -0
- package/build/database/drivers/mssql/sql/delete.js +19 -0
- package/build/database/drivers/mssql/sql/drop-database.js +36 -0
- package/build/database/drivers/mssql/sql/drop-table.js +4 -0
- package/build/database/drivers/mssql/sql/insert.js +4 -0
- package/build/database/drivers/mssql/sql/update.js +31 -0
- package/build/database/drivers/mssql/sql/upsert.js +23 -0
- package/build/database/drivers/mssql/structure-sql.js +120 -0
- package/build/database/drivers/mssql/table.js +145 -0
- package/build/database/drivers/mysql/column.js +112 -0
- package/build/database/drivers/mysql/columns-index.js +22 -0
- package/build/database/drivers/mysql/foreign-key.js +12 -0
- package/build/database/drivers/mysql/index.js +473 -0
- package/build/database/drivers/mysql/options.js +34 -0
- package/build/database/drivers/mysql/query-parser.js +6 -0
- package/build/database/drivers/mysql/query.js +37 -0
- package/build/database/drivers/mysql/sql/alter-table.js +6 -0
- package/build/database/drivers/mysql/sql/create-database.js +39 -0
- package/build/database/drivers/mysql/sql/create-index.js +6 -0
- package/build/database/drivers/mysql/sql/create-table.js +6 -0
- package/build/database/drivers/mysql/sql/delete.js +21 -0
- package/build/database/drivers/mysql/sql/drop-database.js +6 -0
- package/build/database/drivers/mysql/sql/drop-table.js +6 -0
- package/build/database/drivers/mysql/sql/insert.js +6 -0
- package/build/database/drivers/mysql/sql/update.js +33 -0
- package/build/database/drivers/mysql/sql/upsert.js +13 -0
- package/build/database/drivers/mysql/structure-sql.js +93 -0
- package/build/database/drivers/mysql/table.js +121 -0
- package/build/database/drivers/pgsql/column.js +90 -0
- package/build/database/drivers/pgsql/columns-index.js +6 -0
- package/build/database/drivers/pgsql/foreign-key.js +12 -0
- package/build/database/drivers/pgsql/index.js +441 -0
- package/build/database/drivers/pgsql/options.js +32 -0
- package/build/database/drivers/pgsql/query-parser.js +6 -0
- package/build/database/drivers/pgsql/sql/alter-table.js +6 -0
- package/build/database/drivers/pgsql/sql/create-database.js +38 -0
- package/build/database/drivers/pgsql/sql/create-index.js +6 -0
- package/build/database/drivers/pgsql/sql/create-table.js +6 -0
- package/build/database/drivers/pgsql/sql/delete.js +21 -0
- package/build/database/drivers/pgsql/sql/drop-database.js +6 -0
- package/build/database/drivers/pgsql/sql/drop-table.js +6 -0
- package/build/database/drivers/pgsql/sql/insert.js +6 -0
- package/build/database/drivers/pgsql/sql/update.js +33 -0
- package/build/database/drivers/pgsql/sql/upsert.js +14 -0
- package/build/database/drivers/pgsql/structure-sql.js +126 -0
- package/build/database/drivers/pgsql/table.js +135 -0
- package/build/database/drivers/sqlite/base.js +509 -0
- package/build/database/drivers/sqlite/column.js +75 -0
- package/build/database/drivers/sqlite/columns-index.js +30 -0
- package/build/database/drivers/sqlite/connection-sql-js.js +46 -0
- package/build/database/drivers/sqlite/foreign-key.js +24 -0
- package/build/database/drivers/sqlite/index.js +394 -0
- package/build/database/drivers/sqlite/index.native.js +72 -0
- package/build/database/drivers/sqlite/index.web.js +99 -0
- package/build/database/drivers/sqlite/options.js +32 -0
- package/build/database/drivers/sqlite/query-parser.js +6 -0
- package/build/database/drivers/sqlite/query.js +35 -0
- package/build/database/drivers/sqlite/query.native.js +35 -0
- package/build/database/drivers/sqlite/query.web.js +49 -0
- package/build/database/drivers/sqlite/sql/alter-table.js +187 -0
- package/build/database/drivers/sqlite/sql/create-index.js +6 -0
- package/build/database/drivers/sqlite/sql/create-table.js +6 -0
- package/build/database/drivers/sqlite/sql/delete.js +26 -0
- package/build/database/drivers/sqlite/sql/drop-table.js +6 -0
- package/build/database/drivers/sqlite/sql/insert.js +6 -0
- package/build/database/drivers/sqlite/sql/update.js +33 -0
- package/build/database/drivers/sqlite/sql/upsert.js +14 -0
- package/build/database/drivers/sqlite/structure-sql.js +56 -0
- package/build/database/drivers/sqlite/table-rebuilder.js +96 -0
- package/build/database/drivers/sqlite/table.js +131 -0
- package/build/database/drivers/structure-sql/utils.js +35 -0
- package/build/database/handler.js +13 -0
- package/build/database/initializer-from-require-context.js +101 -0
- package/build/database/migration/index.js +438 -0
- package/build/database/migrator/files-finder.js +55 -0
- package/build/database/migrator/types.js +31 -0
- package/build/database/migrator.js +557 -0
- package/build/database/pool/async-tracked-multi-connection.js +1164 -0
- package/build/database/pool/base-methods-forward.js +52 -0
- package/build/database/pool/base.js +380 -0
- package/build/database/pool/single-multi-use.js +118 -0
- package/build/database/query/alter-table-base.js +104 -0
- package/build/database/query/base.js +49 -0
- package/build/database/query/create-database-base.js +42 -0
- package/build/database/query/create-index-base.js +117 -0
- package/build/database/query/create-table-base.js +205 -0
- package/build/database/query/delete-base.js +19 -0
- package/build/database/query/drop-database-base.js +38 -0
- package/build/database/query/drop-table-base.js +58 -0
- package/build/database/query/from-base.js +36 -0
- package/build/database/query/from-plain.js +16 -0
- package/build/database/query/from-table.js +18 -0
- package/build/database/query/index.js +533 -0
- package/build/database/query/insert-base.js +172 -0
- package/build/database/query/join-base.js +43 -0
- package/build/database/query/join-object.js +167 -0
- package/build/database/query/join-plain.js +18 -0
- package/build/database/query/join-tracker.js +93 -0
- package/build/database/query/model-class-query.js +1577 -0
- package/build/database/query/order-base.js +33 -0
- package/build/database/query/order-column.js +77 -0
- package/build/database/query/order-plain.js +28 -0
- package/build/database/query/preloader/belongs-to.js +267 -0
- package/build/database/query/preloader/ensure-model-class-initialized.js +18 -0
- package/build/database/query/preloader/has-many.js +316 -0
- package/build/database/query/preloader/has-one.js +123 -0
- package/build/database/query/preloader/selection.js +152 -0
- package/build/database/query/preloader.js +201 -0
- package/build/database/query/query-data.js +305 -0
- package/build/database/query/select-base.js +30 -0
- package/build/database/query/select-plain.js +18 -0
- package/build/database/query/select-table-and-column.js +28 -0
- package/build/database/query/update-base.js +41 -0
- package/build/database/query/upsert-base.js +103 -0
- package/build/database/query/where-base.js +38 -0
- package/build/database/query/where-combinator.js +31 -0
- package/build/database/query/where-hash.js +77 -0
- package/build/database/query/where-model-class-hash.js +505 -0
- package/build/database/query/where-not.js +23 -0
- package/build/database/query/where-plain.js +20 -0
- package/build/database/query/with-count.js +219 -0
- package/build/database/query-parser/base-query-parser.js +40 -0
- package/build/database/query-parser/from-parser.js +49 -0
- package/build/database/query-parser/group-parser.js +55 -0
- package/build/database/query-parser/joins-parser.js +37 -0
- package/build/database/query-parser/limit-parser.js +77 -0
- package/build/database/query-parser/options.js +94 -0
- package/build/database/query-parser/order-parser.js +45 -0
- package/build/database/query-parser/select-parser.js +67 -0
- package/build/database/query-parser/where-parser.js +46 -0
- package/build/database/record/acts-as-list.js +374 -0
- package/build/database/record/attachments/download.js +49 -0
- package/build/database/record/attachments/handle.js +188 -0
- package/build/database/record/attachments/normalize-input.js +213 -0
- package/build/database/record/attachments/storage-drivers/filesystem.js +114 -0
- package/build/database/record/attachments/storage-drivers/native.js +146 -0
- package/build/database/record/attachments/storage-drivers/s3.js +245 -0
- package/build/database/record/attachments/store.js +591 -0
- package/build/database/record/index.js +4119 -0
- package/build/database/record/instance-relationships/base.js +289 -0
- package/build/database/record/instance-relationships/belongs-to.js +84 -0
- package/build/database/record/instance-relationships/has-many.js +284 -0
- package/build/database/record/instance-relationships/has-one.js +117 -0
- package/build/database/record/record-not-found-error.js +3 -0
- package/build/database/record/relationships/base.js +195 -0
- package/build/database/record/relationships/belongs-to.js +57 -0
- package/build/database/record/relationships/has-many.js +46 -0
- package/build/database/record/relationships/has-one.js +46 -0
- package/build/database/record/state-machine.js +278 -0
- package/build/database/record/user-module.js +43 -0
- package/build/database/record/validators/base.js +27 -0
- package/build/database/record/validators/format.js +50 -0
- package/build/database/record/validators/presence.js +24 -0
- package/build/database/record/validators/uniqueness.js +124 -0
- package/build/database/table-data/index.js +241 -0
- package/build/database/table-data/table-column.js +416 -0
- package/build/database/table-data/table-foreign-key.js +69 -0
- package/build/database/table-data/table-index.js +46 -0
- package/build/database/table-data/table-reference.js +13 -0
- package/build/database/use-database.js +48 -0
- package/build/environment-handlers/base.js +561 -0
- package/build/environment-handlers/browser.js +338 -0
- package/build/environment-handlers/node/cli/commands/background-jobs-main.js +21 -0
- package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +24 -0
- package/build/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -0
- package/build/environment-handlers/node/cli/commands/beacon.js +21 -0
- package/build/environment-handlers/node/cli/commands/cli-command-context.js +31 -0
- package/build/environment-handlers/node/cli/commands/console.js +149 -0
- package/build/environment-handlers/node/cli/commands/db/schema/dump.js +43 -0
- package/build/environment-handlers/node/cli/commands/db/schema/load.js +69 -0
- package/build/environment-handlers/node/cli/commands/db/seed.js +79 -0
- package/build/environment-handlers/node/cli/commands/destroy/migration.js +47 -0
- package/build/environment-handlers/node/cli/commands/generate/base-models.js +396 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +872 -0
- package/build/environment-handlers/node/cli/commands/generate/migration.js +45 -0
- package/build/environment-handlers/node/cli/commands/generate/model.js +45 -0
- package/build/environment-handlers/node/cli/commands/init.js +68 -0
- package/build/environment-handlers/node/cli/commands/routes.js +63 -0
- package/build/environment-handlers/node/cli/commands/run-script.js +85 -0
- package/build/environment-handlers/node/cli/commands/runner.js +84 -0
- package/build/environment-handlers/node/cli/commands/server.js +151 -0
- package/build/environment-handlers/node/cli/commands/test.js +118 -0
- package/build/environment-handlers/node.js +887 -0
- package/build/error-logger.js +30 -0
- package/build/frontend-model-controller.js +3491 -0
- package/build/frontend-model-resource/base-resource.js +939 -0
- package/build/frontend-models/base.js +4004 -0
- package/build/frontend-models/clear-pending-debounced-callback.js +16 -0
- package/build/frontend-models/event-hook-models.js +49 -0
- package/build/frontend-models/model-registry.js +28 -0
- package/build/frontend-models/outgoing-event-buffer.js +51 -0
- package/build/frontend-models/preloader.js +169 -0
- package/build/frontend-models/query.js +2245 -0
- package/build/frontend-models/resource-config-validation.js +56 -0
- package/build/frontend-models/resource-definition.js +399 -0
- package/build/frontend-models/transport-serialization.js +369 -0
- package/build/frontend-models/use-created-event.js +21 -0
- package/build/frontend-models/use-destroyed-event.js +148 -0
- package/build/frontend-models/use-model-class-event.js +164 -0
- package/build/frontend-models/use-updated-event.js +152 -0
- package/build/frontend-models/websocket-channel.js +494 -0
- package/build/frontend-models/websocket-publishers.js +224 -0
- package/build/http-client/header.js +17 -0
- package/build/http-client/index.js +139 -0
- package/build/http-client/request.js +94 -0
- package/build/http-client/response.js +151 -0
- package/build/http-client/websocket-client.js +27 -0
- package/build/http-server/client/index.js +507 -0
- package/build/http-server/client/params-to-object.js +152 -0
- package/build/http-server/client/request-buffer/form-data-part.js +139 -0
- package/build/http-server/client/request-buffer/header.js +19 -0
- package/build/http-server/client/request-buffer/index.js +535 -0
- package/build/http-server/client/request-parser.js +195 -0
- package/build/http-server/client/request-runner.js +321 -0
- package/build/http-server/client/request-timing.js +171 -0
- package/build/http-server/client/request.js +114 -0
- package/build/http-server/client/response.js +251 -0
- package/build/http-server/client/uploaded-file/memory-uploaded-file.js +32 -0
- package/build/http-server/client/uploaded-file/temporary-uploaded-file.js +32 -0
- package/build/http-server/client/uploaded-file/uploaded-file.js +36 -0
- package/build/http-server/client/websocket-request.js +147 -0
- package/build/http-server/client/websocket-session.js +1755 -0
- package/build/http-server/cookie.js +245 -0
- package/build/http-server/development-reloader.js +240 -0
- package/build/http-server/index.js +561 -0
- package/build/http-server/remote-address.js +77 -0
- package/build/http-server/server-client.js +222 -0
- package/build/http-server/server-lock.js +178 -0
- package/build/http-server/websocket-channel-subscribers.js +110 -0
- package/build/http-server/websocket-channel.js +137 -0
- package/build/http-server/websocket-connection.js +118 -0
- package/build/http-server/websocket-event-log-store.js +433 -0
- package/build/http-server/websocket-events-host.js +170 -0
- package/build/http-server/websocket-events.js +50 -0
- package/build/http-server/worker-handler/channel-subscriber-dispatch.js +28 -0
- package/build/http-server/worker-handler/in-process.js +155 -0
- package/build/http-server/worker-handler/index.js +370 -0
- package/build/http-server/worker-handler/worker-script.js +6 -0
- package/build/http-server/worker-handler/worker-thread.js +286 -0
- package/build/initializer.js +39 -0
- package/build/jobs/mail-delivery.js +22 -0
- package/build/logger/base-logger.js +34 -0
- package/build/logger/console-logger.js +28 -0
- package/build/logger/file-logger.js +36 -0
- package/build/logger/outputs/array-output.js +50 -0
- package/build/logger/outputs/console-output.js +32 -0
- package/build/logger/outputs/file-output.js +55 -0
- package/build/logger/outputs/stdout-output.js +64 -0
- package/build/logger.js +507 -0
- package/build/mailer/backends/smtp.js +197 -0
- package/build/mailer/base.js +337 -0
- package/build/mailer/delivery.js +70 -0
- package/build/mailer/index.js +24 -0
- package/build/mailer.js +15 -0
- package/build/plugins/sqljs-wasm-route-controller.js +70 -0
- package/build/plugins/sqljs-wasm-route.js +71 -0
- package/build/record-payload-values.js +83 -0
- package/build/routes/app-routes.js +17 -0
- package/build/routes/base-route.js +133 -0
- package/build/routes/basic-route.js +109 -0
- package/build/routes/built-in/debug/controller.js +12 -0
- package/build/routes/built-in/errors/controller.js +7 -0
- package/build/routes/get-route.js +75 -0
- package/build/routes/hooks/frontend-model-command-route-hook.js +100 -0
- package/build/routes/index.js +50 -0
- package/build/routes/namespace-route.js +51 -0
- package/build/routes/plugin-routes.js +141 -0
- package/build/routes/post-route.js +74 -0
- package/build/routes/resolver.js +535 -0
- package/build/routes/resource-route.js +154 -0
- package/build/routes/root-route.js +11 -0
- package/build/src/application.js +187 -214
- package/build/src/authorization/ability.js +250 -297
- package/build/src/authorization/base-resource.js +120 -136
- package/build/src/background-jobs/client.js +43 -47
- package/build/src/background-jobs/cron-expression.js +127 -166
- package/build/src/background-jobs/forked-runner-child.js +37 -47
- package/build/src/background-jobs/job-record.js +8 -10
- package/build/src/background-jobs/job-registry.js +72 -84
- package/build/src/background-jobs/job-runner.js +74 -81
- package/build/src/background-jobs/job.js +62 -72
- package/build/src/background-jobs/json-socket.js +65 -70
- package/build/src/background-jobs/main.js +841 -900
- package/build/src/background-jobs/normalize-error.js +12 -11
- package/build/src/background-jobs/scheduler.js +205 -247
- package/build/src/background-jobs/socket-request.js +60 -65
- package/build/src/background-jobs/status-reporter.js +86 -96
- package/build/src/background-jobs/store.js +862 -980
- package/build/src/background-jobs/types.js +2 -3
- package/build/src/background-jobs/web/authorization.js +38 -50
- package/build/src/background-jobs/web/controller.js +232 -268
- package/build/src/background-jobs/web/index.js +36 -40
- package/build/src/background-jobs/web/path-matcher.js +45 -48
- package/build/src/background-jobs/web/registry.js +9 -14
- package/build/src/background-jobs/worker.js +585 -639
- package/build/src/beacon/client.js +264 -293
- package/build/src/beacon/in-process-broker.js +20 -25
- package/build/src/beacon/in-process-client.js +104 -116
- package/build/src/beacon/server.js +110 -126
- package/build/src/beacon/types.js +2 -8
- package/build/src/cli/base-command.js +49 -57
- package/build/src/cli/browser-cli.js +37 -42
- package/build/src/cli/commands/background-jobs-main.js +5 -5
- package/build/src/cli/commands/background-jobs-runner.js +5 -5
- package/build/src/cli/commands/background-jobs-worker.js +5 -5
- package/build/src/cli/commands/beacon.js +5 -5
- package/build/src/cli/commands/console.js +10 -10
- package/build/src/cli/commands/db/base-command.js +71 -76
- package/build/src/cli/commands/db/create.js +53 -61
- package/build/src/cli/commands/db/drop.js +62 -71
- package/build/src/cli/commands/db/migrate.js +13 -15
- package/build/src/cli/commands/db/reset.js +16 -19
- package/build/src/cli/commands/db/rollback.js +12 -13
- package/build/src/cli/commands/db/schema/dump.js +9 -9
- package/build/src/cli/commands/db/schema/load.js +9 -9
- package/build/src/cli/commands/db/seed.js +9 -9
- package/build/src/cli/commands/db/tenants/check.js +32 -35
- package/build/src/cli/commands/db/tenants/create.js +26 -29
- package/build/src/cli/commands/db/tenants/migrate.js +40 -44
- package/build/src/cli/commands/destroy/migration.js +5 -5
- package/build/src/cli/commands/generate/base-models.js +5 -5
- package/build/src/cli/commands/generate/frontend-models.js +9 -9
- package/build/src/cli/commands/generate/migration.js +5 -5
- package/build/src/cli/commands/generate/model.js +5 -5
- package/build/src/cli/commands/init.js +7 -9
- package/build/src/cli/commands/routes.js +6 -6
- package/build/src/cli/commands/run-script.js +9 -9
- package/build/src/cli/commands/runner.js +9 -9
- package/build/src/cli/commands/server.js +6 -6
- package/build/src/cli/commands/test.js +6 -7
- package/build/src/cli/index.js +127 -141
- package/build/src/cli/tenant-database-command-helper.js +154 -185
- package/build/src/cli/use-browser-cli.js +15 -20
- package/build/src/configuration-resolver.js +47 -54
- package/build/src/configuration-types.d.ts +5 -3
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +3 -54
- package/build/src/configuration.js +2240 -2547
- package/build/src/controller.js +363 -407
- package/build/src/current-configuration.js +9 -12
- package/build/src/current.js +70 -75
- package/build/src/database/annotations-async-hooks.js +16 -22
- package/build/src/database/annotations.js +12 -18
- package/build/src/database/drivers/base-column.js +155 -179
- package/build/src/database/drivers/base-columns-index.js +69 -78
- package/build/src/database/drivers/base-foreign-key.js +89 -101
- package/build/src/database/drivers/base-table.js +124 -149
- package/build/src/database/drivers/base.js +1306 -1489
- package/build/src/database/drivers/mssql/column.js +39 -50
- package/build/src/database/drivers/mssql/columns-index.js +2 -3
- package/build/src/database/drivers/mssql/connect-connection.js +11 -9
- package/build/src/database/drivers/mssql/foreign-key.js +8 -9
- package/build/src/database/drivers/mssql/index.js +507 -587
- package/build/src/database/drivers/mssql/options.js +68 -75
- package/build/src/database/drivers/mssql/query-parser.js +2 -3
- package/build/src/database/drivers/mssql/sql/alter-table.js +2 -2
- package/build/src/database/drivers/mssql/sql/create-database.js +24 -31
- package/build/src/database/drivers/mssql/sql/create-index.js +2 -2
- package/build/src/database/drivers/mssql/sql/create-table.js +2 -2
- package/build/src/database/drivers/mssql/sql/delete.js +14 -16
- package/build/src/database/drivers/mssql/sql/drop-database.js +24 -31
- package/build/src/database/drivers/mssql/sql/drop-table.js +2 -2
- package/build/src/database/drivers/mssql/sql/insert.js +2 -2
- package/build/src/database/drivers/mssql/sql/update.js +24 -28
- package/build/src/database/drivers/mssql/sql/upsert.js +18 -20
- package/build/src/database/drivers/mssql/structure-sql.js +102 -114
- package/build/src/database/drivers/mssql/table.js +81 -96
- package/build/src/database/drivers/mysql/column.js +75 -92
- package/build/src/database/drivers/mysql/columns-index.js +16 -19
- package/build/src/database/drivers/mysql/foreign-key.js +8 -9
- package/build/src/database/drivers/mysql/index.js +396 -457
- package/build/src/database/drivers/mysql/options.js +26 -30
- package/build/src/database/drivers/mysql/query-parser.js +2 -3
- package/build/src/database/drivers/mysql/query.js +26 -29
- package/build/src/database/drivers/mysql/sql/alter-table.js +2 -3
- package/build/src/database/drivers/mysql/sql/create-database.js +23 -28
- package/build/src/database/drivers/mysql/sql/create-index.js +2 -3
- package/build/src/database/drivers/mysql/sql/create-table.js +2 -3
- package/build/src/database/drivers/mysql/sql/delete.js +14 -17
- package/build/src/database/drivers/mysql/sql/drop-database.js +2 -3
- package/build/src/database/drivers/mysql/sql/drop-table.js +2 -3
- package/build/src/database/drivers/mysql/sql/insert.js +2 -3
- package/build/src/database/drivers/mysql/sql/update.js +24 -29
- package/build/src/database/drivers/mysql/sql/upsert.js +8 -10
- package/build/src/database/drivers/mysql/structure-sql.js +79 -88
- package/build/src/database/drivers/mysql/table.js +83 -98
- package/build/src/database/drivers/pgsql/column.js +56 -72
- package/build/src/database/drivers/pgsql/columns-index.js +2 -3
- package/build/src/database/drivers/pgsql/foreign-key.js +8 -9
- package/build/src/database/drivers/pgsql/index.js +377 -438
- package/build/src/database/drivers/pgsql/options.js +25 -28
- package/build/src/database/drivers/pgsql/query-parser.js +2 -3
- package/build/src/database/drivers/pgsql/sql/alter-table.js +2 -3
- package/build/src/database/drivers/pgsql/sql/create-database.js +19 -23
- package/build/src/database/drivers/pgsql/sql/create-index.js +2 -3
- package/build/src/database/drivers/pgsql/sql/create-table.js +2 -3
- package/build/src/database/drivers/pgsql/sql/delete.js +14 -17
- package/build/src/database/drivers/pgsql/sql/drop-database.js +2 -3
- package/build/src/database/drivers/pgsql/sql/drop-table.js +2 -3
- package/build/src/database/drivers/pgsql/sql/insert.js +2 -3
- package/build/src/database/drivers/pgsql/sql/update.js +24 -29
- package/build/src/database/drivers/pgsql/sql/upsert.js +9 -11
- package/build/src/database/drivers/pgsql/structure-sql.js +108 -120
- package/build/src/database/drivers/pgsql/table.js +60 -77
- package/build/src/database/drivers/sqlite/base.js +405 -478
- package/build/src/database/drivers/sqlite/column.js +54 -69
- package/build/src/database/drivers/sqlite/columns-index.js +22 -27
- package/build/src/database/drivers/sqlite/connection-sql-js.js +35 -42
- package/build/src/database/drivers/sqlite/foreign-key.js +18 -21
- package/build/src/database/drivers/sqlite/index.js +330 -373
- package/build/src/database/drivers/sqlite/index.native.js +55 -64
- package/build/src/database/drivers/sqlite/index.web.js +69 -87
- package/build/src/database/drivers/sqlite/options.js +25 -28
- package/build/src/database/drivers/sqlite/query-parser.js +2 -3
- package/build/src/database/drivers/sqlite/query.js +21 -24
- package/build/src/database/drivers/sqlite/query.native.js +20 -25
- package/build/src/database/drivers/sqlite/query.web.js +30 -37
- package/build/src/database/drivers/sqlite/sql/alter-table.js +159 -179
- package/build/src/database/drivers/sqlite/sql/create-index.js +2 -3
- package/build/src/database/drivers/sqlite/sql/create-table.js +2 -3
- package/build/src/database/drivers/sqlite/sql/delete.js +17 -22
- package/build/src/database/drivers/sqlite/sql/drop-table.js +2 -3
- package/build/src/database/drivers/sqlite/sql/insert.js +2 -3
- package/build/src/database/drivers/sqlite/sql/update.js +24 -29
- package/build/src/database/drivers/sqlite/sql/upsert.js +9 -11
- package/build/src/database/drivers/sqlite/structure-sql.js +49 -52
- package/build/src/database/drivers/sqlite/table-rebuilder.js +62 -75
- package/build/src/database/drivers/sqlite/table.js +102 -125
- package/build/src/database/drivers/structure-sql/utils.js +14 -17
- package/build/src/database/handler.js +9 -10
- package/build/src/database/initializer-from-require-context.js +76 -87
- package/build/src/database/migration/index.js +332 -395
- package/build/src/database/migrator/files-finder.js +40 -50
- package/build/src/database/migrator/types.js +2 -30
- package/build/src/database/migrator.js +454 -526
- package/build/src/database/pool/async-tracked-multi-connection.js +997 -1147
- package/build/src/database/pool/base-methods-forward.js +40 -43
- package/build/src/database/pool/base.js +298 -343
- package/build/src/database/pool/single-multi-use.js +93 -110
- package/build/src/database/query/alter-table-base.js +84 -99
- package/build/src/database/query/base.js +39 -46
- package/build/src/database/query/create-database-base.js +25 -30
- package/build/src/database/query/create-index-base.js +75 -94
- package/build/src/database/query/create-table-base.js +151 -193
- package/build/src/database/query/delete-base.js +14 -16
- package/build/src/database/query/drop-database-base.js +23 -28
- package/build/src/database/query/drop-table-base.js +42 -53
- package/build/src/database/query/from-base.js +30 -33
- package/build/src/database/query/from-plain.js +11 -13
- package/build/src/database/query/from-table.js +13 -15
- package/build/src/database/query/index.js +410 -472
- package/build/src/database/query/insert-base.js +143 -164
- package/build/src/database/query/join-base.js +35 -40
- package/build/src/database/query/join-object.js +128 -153
- package/build/src/database/query/join-plain.js +13 -15
- package/build/src/database/query/join-tracker.js +76 -90
- package/build/src/database/query/model-class-query.js +1134 -1370
- package/build/src/database/query/order-base.js +27 -30
- package/build/src/database/query/order-column.js +44 -53
- package/build/src/database/query/order-plain.js +20 -24
- package/build/src/database/query/preloader/belongs-to.js +210 -258
- package/build/src/database/query/preloader/ensure-model-class-initialized.js +8 -9
- package/build/src/database/query/preloader/has-many.js +240 -301
- package/build/src/database/query/preloader/has-one.js +91 -117
- package/build/src/database/query/preloader/selection.js +117 -129
- package/build/src/database/query/preloader.js +160 -185
- package/build/src/database/query/query-data.js +157 -201
- package/build/src/database/query/select-base.js +25 -27
- package/build/src/database/query/select-plain.js +13 -15
- package/build/src/database/query/select-table-and-column.js +21 -25
- package/build/src/database/query/update-base.js +35 -38
- package/build/src/database/query/upsert-base.js +93 -100
- package/build/src/database/query/where-base.js +32 -35
- package/build/src/database/query/where-combinator.js +25 -28
- package/build/src/database/query/where-hash.js +61 -68
- package/build/src/database/query/where-model-class-hash.js +414 -469
- package/build/src/database/query/where-not.js +18 -20
- package/build/src/database/query/where-plain.js +15 -17
- package/build/src/database/query/with-count.js +125 -159
- package/build/src/database/query-parser/base-query-parser.js +32 -37
- package/build/src/database/query-parser/from-parser.js +36 -45
- package/build/src/database/query-parser/group-parser.js +42 -50
- package/build/src/database/query-parser/joins-parser.js +28 -33
- package/build/src/database/query-parser/limit-parser.js +67 -70
- package/build/src/database/query-parser/options.js +75 -82
- package/build/src/database/query-parser/order-parser.js +36 -40
- package/build/src/database/query-parser/select-parser.js +49 -60
- package/build/src/database/query-parser/where-parser.js +36 -41
- package/build/src/database/record/acts-as-list.js +235 -273
- package/build/src/database/record/attachments/download.js +44 -45
- package/build/src/database/record/attachments/handle.js +141 -161
- package/build/src/database/record/attachments/normalize-input.js +128 -138
- package/build/src/database/record/attachments/storage-drivers/filesystem.js +77 -91
- package/build/src/database/record/attachments/storage-drivers/native.js +112 -121
- package/build/src/database/record/attachments/storage-drivers/s3.js +177 -208
- package/build/src/database/record/attachments/store.js +467 -539
- package/build/src/database/record/index.d.ts +109 -25
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +3502 -3898
- package/build/src/database/record/instance-relationships/base.js +234 -268
- package/build/src/database/record/instance-relationships/belongs-to.js +58 -73
- package/build/src/database/record/instance-relationships/has-many.js +225 -264
- package/build/src/database/record/instance-relationships/has-one.js +85 -105
- package/build/src/database/record/record-not-found-error.js +3 -2
- package/build/src/database/record/relationships/base.js +144 -166
- package/build/src/database/record/relationships/belongs-to.js +44 -51
- package/build/src/database/record/relationships/has-many.js +32 -40
- package/build/src/database/record/relationships/has-one.js +32 -40
- package/build/src/database/record/state-machine.js +156 -208
- package/build/src/database/record/user-module.js +32 -38
- package/build/src/database/record/validators/base.js +22 -24
- package/build/src/database/record/validators/format.js +36 -46
- package/build/src/database/record/validators/presence.js +18 -20
- package/build/src/database/record/validators/uniqueness.js +99 -117
- package/build/src/database/table-data/index.js +199 -231
- package/build/src/database/table-data/table-column.js +338 -382
- package/build/src/database/table-data/table-foreign-key.js +57 -66
- package/build/src/database/table-data/table-index.js +29 -36
- package/build/src/database/table-data/table-reference.js +10 -10
- package/build/src/database/use-database.js +32 -40
- package/build/src/environment-handlers/base.js +484 -544
- package/build/src/environment-handlers/browser.js +241 -294
- package/build/src/environment-handlers/node/cli/commands/background-jobs-main.js +16 -19
- package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +18 -21
- package/build/src/environment-handlers/node/cli/commands/background-jobs-worker.js +22 -29
- package/build/src/environment-handlers/node/cli/commands/beacon.js +16 -19
- package/build/src/environment-handlers/node/cli/commands/cli-command-context.js +14 -15
- package/build/src/environment-handlers/node/cli/commands/console.js +99 -120
- package/build/src/environment-handlers/node/cli/commands/db/schema/dump.js +34 -39
- package/build/src/environment-handlers/node/cli/commands/db/schema/load.js +57 -63
- package/build/src/environment-handlers/node/cli/commands/db/seed.js +51 -63
- package/build/src/environment-handlers/node/cli/commands/destroy/migration.js +32 -40
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts +4 -2
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +326 -358
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +10 -10
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +729 -844
- package/build/src/environment-handlers/node/cli/commands/generate/migration.js +34 -38
- package/build/src/environment-handlers/node/cli/commands/generate/model.js +34 -38
- package/build/src/environment-handlers/node/cli/commands/init.js +56 -61
- package/build/src/environment-handlers/node/cli/commands/routes.js +51 -59
- package/build/src/environment-handlers/node/cli/commands/run-script.js +54 -68
- package/build/src/environment-handlers/node/cli/commands/runner.js +56 -74
- package/build/src/environment-handlers/node/cli/commands/server.js +93 -106
- package/build/src/environment-handlers/node/cli/commands/test.js +97 -113
- package/build/src/environment-handlers/node.js +753 -874
- package/build/src/error-logger.js +22 -21
- package/build/src/frontend-model-controller.js +2791 -3291
- package/build/src/frontend-model-resource/base-resource.d.ts +8 -3
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +770 -865
- package/build/src/frontend-models/base.js +3136 -3593
- package/build/src/frontend-models/clear-pending-debounced-callback.js +7 -8
- package/build/src/frontend-models/event-hook-models.js +16 -21
- package/build/src/frontend-models/model-registry.js +9 -11
- package/build/src/frontend-models/outgoing-event-buffer.js +10 -17
- package/build/src/frontend-models/preloader.js +131 -149
- package/build/src/frontend-models/query.js +1557 -1855
- package/build/src/frontend-models/resource-config-validation.js +27 -37
- package/build/src/frontend-models/resource-definition.d.ts +6 -7
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +237 -291
- package/build/src/frontend-models/transport-serialization.js +203 -266
- package/build/src/frontend-models/use-created-event.js +5 -7
- package/build/src/frontend-models/use-destroyed-event.js +80 -93
- package/build/src/frontend-models/use-model-class-event.js +79 -91
- package/build/src/frontend-models/use-updated-event.js +84 -97
- package/build/src/frontend-models/websocket-channel.js +381 -441
- package/build/src/frontend-models/websocket-publishers.js +142 -175
- package/build/src/http-client/header.js +13 -14
- package/build/src/http-client/index.js +116 -132
- package/build/src/http-client/request.js +71 -87
- package/build/src/http-client/response.js +122 -140
- package/build/src/http-client/websocket-client.js +15 -17
- package/build/src/http-server/client/index.js +409 -465
- package/build/src/http-server/client/params-to-object.js +124 -135
- package/build/src/http-server/client/request-buffer/form-data-part.js +111 -132
- package/build/src/http-server/client/request-buffer/header.js +15 -16
- package/build/src/http-server/client/request-buffer/index.js +446 -506
- package/build/src/http-server/client/request-parser.js +163 -186
- package/build/src/http-server/client/request-runner.js +226 -259
- package/build/src/http-server/client/request-timing.js +132 -151
- package/build/src/http-server/client/request.js +96 -108
- package/build/src/http-server/client/response.js +213 -235
- package/build/src/http-server/client/uploaded-file/memory-uploaded-file.js +25 -29
- package/build/src/http-server/client/uploaded-file/temporary-uploaded-file.js +25 -29
- package/build/src/http-server/client/uploaded-file/uploaded-file.js +33 -33
- package/build/src/http-server/client/websocket-request.js +114 -137
- package/build/src/http-server/client/websocket-session.js +1452 -1657
- package/build/src/http-server/cookie.js +216 -236
- package/build/src/http-server/development-reloader.js +190 -221
- package/build/src/http-server/index.js +451 -525
- package/build/src/http-server/remote-address.js +38 -50
- package/build/src/http-server/server-client.js +181 -208
- package/build/src/http-server/server-lock.js +153 -167
- package/build/src/http-server/websocket-channel-subscribers.js +81 -93
- package/build/src/http-server/websocket-channel.js +104 -117
- package/build/src/http-server/websocket-connection.js +96 -104
- package/build/src/http-server/websocket-event-log-store.js +350 -404
- package/build/src/http-server/websocket-events-host.js +145 -164
- package/build/src/http-server/websocket-events.js +47 -47
- package/build/src/http-server/worker-handler/channel-subscriber-dispatch.js +13 -14
- package/build/src/http-server/worker-handler/in-process.js +123 -141
- package/build/src/http-server/worker-handler/index.js +313 -349
- package/build/src/http-server/worker-handler/worker-script.js +4 -5
- package/build/src/http-server/worker-handler/worker-thread.js +240 -269
- package/build/src/initializer.js +31 -36
- package/build/src/jobs/mail-delivery.js +13 -15
- package/build/src/logger/base-logger.js +24 -26
- package/build/src/logger/console-logger.js +21 -23
- package/build/src/logger/file-logger.js +29 -31
- package/build/src/logger/outputs/array-output.js +37 -42
- package/build/src/logger/outputs/console-output.js +20 -24
- package/build/src/logger/outputs/file-output.js +43 -48
- package/build/src/logger/outputs/stdout-output.js +39 -48
- package/build/src/logger.js +338 -394
- package/build/src/mailer/backends/smtp.js +134 -163
- package/build/src/mailer/base.js +211 -251
- package/build/src/mailer/delivery.js +56 -64
- package/build/src/mailer/index.js +4 -22
- package/build/src/mailer.js +4 -13
- package/build/src/plugins/sqljs-wasm-route-controller.js +42 -52
- package/build/src/plugins/sqljs-wasm-route.js +28 -38
- package/build/src/record-payload-values.js +25 -28
- package/build/src/routes/app-routes.js +12 -14
- package/build/src/routes/base-route.js +112 -130
- package/build/src/routes/basic-route.js +83 -102
- package/build/src/routes/built-in/debug/controller.js +10 -10
- package/build/src/routes/built-in/errors/controller.js +5 -5
- package/build/src/routes/get-route.js +50 -63
- package/build/src/routes/hooks/frontend-model-command-route-hook.js +66 -80
- package/build/src/routes/index.js +36 -43
- package/build/src/routes/namespace-route.js +38 -47
- package/build/src/routes/plugin-routes.js +107 -124
- package/build/src/routes/post-route.js +51 -62
- package/build/src/routes/resolver.js +422 -494
- package/build/src/routes/resource-route.js +124 -143
- package/build/src/routes/root-route.js +7 -8
- package/build/src/testing/base-expect.js +13 -14
- package/build/src/testing/browser-frontend-model-event-hook-scenarios.js +329 -405
- package/build/src/testing/browser-test-app.js +23 -29
- package/build/src/testing/expect-to-change.js +41 -50
- package/build/src/testing/expect-utils.js +139 -184
- package/build/src/testing/expect.js +638 -731
- package/build/src/testing/request-client.js +70 -85
- package/build/src/testing/test-files-finder.js +285 -339
- package/build/src/testing/test-filter-parser.js +124 -155
- package/build/src/testing/test-runner.js +883 -1020
- package/build/src/testing/test-suite-splitter.js +114 -142
- package/build/src/testing/test.js +216 -256
- package/build/src/utils/backtrace-cleaner-node.js +62 -69
- package/build/src/utils/backtrace-cleaner.js +188 -216
- package/build/src/utils/ensure-error.js +7 -7
- package/build/src/utils/event-emitter.js +4 -6
- package/build/src/utils/file-exists.js +9 -10
- package/build/src/utils/format-value.js +67 -76
- package/build/src/utils/model-scope.js +27 -31
- package/build/src/utils/nest-callbacks.js +10 -13
- package/build/src/utils/plain-object.js +5 -6
- package/build/src/utils/ransack.js +448 -563
- package/build/src/utils/rest-args-error.js +5 -6
- package/build/src/utils/singularize-model-name.js +9 -11
- package/build/src/utils/split-sql-statements.js +68 -79
- package/build/src/utils/to-import-specifier.js +24 -30
- package/build/src/utils/with-tracked-stack-async-hooks.js +60 -74
- package/build/src/utils/with-tracked-stack.js +14 -18
- package/build/src/velocious-error.js +27 -30
- package/build/templates/configuration.js +61 -0
- package/build/templates/generate-migration.js +11 -0
- package/build/templates/generate-model.js +6 -0
- package/build/templates/routes.js +11 -0
- package/build/testing/base-expect.js +17 -0
- package/build/testing/browser-frontend-model-event-hook-scenarios.js +520 -0
- package/build/testing/browser-test-app.js +32 -0
- package/build/testing/expect-to-change.js +55 -0
- package/build/testing/expect-utils.js +269 -0
- package/build/testing/expect.js +763 -0
- package/build/testing/request-client.js +90 -0
- package/build/testing/test-files-finder.js +364 -0
- package/build/testing/test-filter-parser.js +198 -0
- package/build/testing/test-runner.js +1168 -0
- package/build/testing/test-suite-splitter.js +177 -0
- package/build/testing/test.js +370 -0
- package/build/utils/backtrace-cleaner-node.js +87 -0
- package/build/utils/backtrace-cleaner.js +266 -0
- package/build/utils/ensure-error.js +15 -0
- package/build/utils/event-emitter.js +8 -0
- package/build/utils/file-exists.js +18 -0
- package/build/utils/format-value.js +101 -0
- package/build/utils/model-scope.js +56 -0
- package/build/utils/nest-callbacks.js +22 -0
- package/build/utils/plain-object.js +14 -0
- package/build/utils/ransack.js +859 -0
- package/build/utils/rest-args-error.js +14 -0
- package/build/utils/singularize-model-name.js +18 -0
- package/build/utils/split-sql-statements.js +88 -0
- package/build/utils/to-import-specifier.js +53 -0
- package/build/utils/with-tracked-stack-async-hooks.js +103 -0
- package/build/utils/with-tracked-stack.js +38 -0
- package/build/velocious-error.js +34 -0
- package/package.json +3 -3
- package/src/configuration-types.js +1 -1
- package/src/database/record/index.js +174 -25
- package/src/environment-handlers/node/cli/commands/generate/base-models.js +50 -21
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +5 -5
- package/src/frontend-model-resource/base-resource.js +6 -2
- package/src/frontend-models/resource-definition.js +3 -3
- package/src/frontend-models/websocket-publishers.js +6 -6
|
@@ -0,0 +1,3491 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import * as inflection from "inflection"
|
|
4
|
+
import Controller from "./controller.js"
|
|
5
|
+
import Response from "./http-server/client/response.js"
|
|
6
|
+
import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
|
|
7
|
+
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
|
|
8
|
+
import {normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
|
|
9
|
+
import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
|
|
10
|
+
import RoutesResolver from "./routes/resolver.js"
|
|
11
|
+
import {ValidationError} from "./database/record/index.js"
|
|
12
|
+
import VelociousError from "./velocious-error.js"
|
|
13
|
+
import isPlainObject from "./utils/plain-object.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Runs normalize frontend model preload.
|
|
17
|
+
* @param {import("./database/query/index.js").NestedPreloadRecord | string | string[] | boolean | undefined | null} preload - Preload shorthand.
|
|
18
|
+
* @returns {import("./database/query/index.js").NestedPreloadRecord | null} - Normalized preload.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeFrontendModelPreload(preload) {
|
|
21
|
+
if (!preload) return null
|
|
22
|
+
|
|
23
|
+
return normalizeQueryPreload(preload)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Runs normalize frontend model joins.
|
|
28
|
+
* @param {?} joins - Joins payload.
|
|
29
|
+
* @returns {Record<string, ?> | null} - Normalized relationship-object joins.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeFrontendModelJoins(joins) {
|
|
32
|
+
if (!joins) return null
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return normalizeQueryJoins(joins)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw frontendModelValidationErrorForError(error)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs normalize frontend model select.
|
|
43
|
+
* @param {?} select - Select payload.
|
|
44
|
+
* @param {string | null} [rootModelName] - Optional root model name for shorthand payloads.
|
|
45
|
+
* @returns {Record<string, string[]> | null} - Normalized model-name keyed select record.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeFrontendModelSelect(select, rootModelName = null) {
|
|
48
|
+
if (!select) return null
|
|
49
|
+
|
|
50
|
+
if (typeof select === "string") {
|
|
51
|
+
if (!rootModelName) {
|
|
52
|
+
throw frontendModelValidationError("Invalid select shorthand without root model name")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {[rootModelName]: [select]}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(select)) {
|
|
59
|
+
if (!rootModelName) {
|
|
60
|
+
throw frontendModelValidationError("Invalid select shorthand without root model name")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const attributeName of select) {
|
|
64
|
+
if (typeof attributeName !== "string") {
|
|
65
|
+
throw frontendModelValidationError(`Invalid select attribute for ${rootModelName}: ${typeof attributeName}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {[rootModelName]: Array.from(new Set(select))}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isPlainObject(select)) {
|
|
73
|
+
throw frontendModelValidationError(`Invalid select type: ${typeof select}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalized.
|
|
78
|
+
@type {Record<string, string[]>} */
|
|
79
|
+
const normalized = {}
|
|
80
|
+
|
|
81
|
+
for (const [modelName, selectValue] of Object.entries(select)) {
|
|
82
|
+
if (typeof selectValue === "string") {
|
|
83
|
+
normalized[modelName] = [selectValue]
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!Array.isArray(selectValue)) {
|
|
88
|
+
throw frontendModelValidationError(`Invalid select value for ${modelName}: ${typeof selectValue}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const attributeName of selectValue) {
|
|
92
|
+
if (typeof attributeName !== "string") {
|
|
93
|
+
throw frontendModelValidationError(`Invalid select attribute for ${modelName}: ${typeof attributeName}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
normalized[modelName] = Array.from(new Set(selectValue))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return normalized
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* FrontendModelSearch type.
|
|
105
|
+
* @typedef {object} FrontendModelSearch
|
|
106
|
+
* @property {string[]} path - Relationship path.
|
|
107
|
+
* @property {string} column - Column or attribute name.
|
|
108
|
+
* @property {"eq" | "like" | "notEq" | "gt" | "gteq" | "lt" | "lteq"} operator - Search operator.
|
|
109
|
+
* @property {?} value - Search value.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* FrontendModelSort type.
|
|
114
|
+
* @typedef {object} FrontendModelSort
|
|
115
|
+
* @property {string} column - Attribute name to sort by.
|
|
116
|
+
* @property {"asc" | "desc"} direction - Sort direction.
|
|
117
|
+
* @property {string[]} path - Relationship path from root model.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* FrontendModelGroup type.
|
|
122
|
+
* @typedef {object} FrontendModelGroup
|
|
123
|
+
* @property {string} column - Attribute name to group by.
|
|
124
|
+
* @property {string[]} path - Relationship path from root model.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* FrontendModelPluck type.
|
|
129
|
+
* @typedef {object} FrontendModelPluck
|
|
130
|
+
* @property {string} column - Attribute name to pluck.
|
|
131
|
+
* @property {string[]} path - Relationship path from root model.
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* FrontendModelPagination type.
|
|
136
|
+
* @typedef {object} FrontendModelPagination
|
|
137
|
+
* @property {number | null} limit - Maximum number of records.
|
|
138
|
+
* @property {number | null} offset - Number of records to skip.
|
|
139
|
+
* @property {number | null} page - 1-based page number.
|
|
140
|
+
* @property {number | null} perPage - Page size.
|
|
141
|
+
*/
|
|
142
|
+
|
|
143
|
+
const frontendModelJoinedPathsSymbol = Symbol("frontendModelJoinedPaths")
|
|
144
|
+
const frontendModelGroupedColumnsSymbol = Symbol("frontendModelGroupedColumns")
|
|
145
|
+
const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
|
|
146
|
+
const frontendModelClientSafeErrorMessage = "Request failed."
|
|
147
|
+
const frontendModelDebugErrorEnvironments = new Set(["development", "test"])
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Runs frontend model query metadata.
|
|
151
|
+
* @param {import("./database/query/model-class-query.js").default} query - Query instance.
|
|
152
|
+
* @returns {import("./database/query/model-class-query.js").default & {[frontendModelJoinedPathsSymbol]?: Set<string>, [frontendModelGroupedColumnsSymbol]?: Set<string>}} - Query metadata access helper.
|
|
153
|
+
*/
|
|
154
|
+
function frontendModelQueryMetadata(query) {
|
|
155
|
+
return /** Narrows the runtime value to the documented type. @type {import("./database/query/model-class-query.js").default & {[frontendModelJoinedPathsSymbol]?: Set<string>, [frontendModelGroupedColumnsSymbol]?: Set<string>}} */ (query)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Runs frontend model validation error.
|
|
160
|
+
* @param {string} message - Validation error message.
|
|
161
|
+
* @returns {VelociousError} - Client-safe validation error.
|
|
162
|
+
*/
|
|
163
|
+
function frontendModelValidationError(message) {
|
|
164
|
+
return VelociousError.safe(message, {code: "frontend-model-validation"})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Runs frontend model validation error for error.
|
|
169
|
+
* @param {?} error - Error raised while normalizing client query params.
|
|
170
|
+
* @returns {VelociousError} - Client-safe validation error preserving the normalizer message.
|
|
171
|
+
*/
|
|
172
|
+
function frontendModelValidationErrorForError(error) {
|
|
173
|
+
if (error instanceof VelociousError && error.safeToExpose) return error
|
|
174
|
+
|
|
175
|
+
const message = error instanceof Error
|
|
176
|
+
? error.message
|
|
177
|
+
: String(error)
|
|
178
|
+
|
|
179
|
+
return frontendModelValidationError(message)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether the error carries an `error.velocious` metadata bag. The
|
|
184
|
+
* presence of any such bag marks the error as "annotated by the
|
|
185
|
+
* developer for the frontend" — the framework treats it as
|
|
186
|
+
* user-facing: surface the message, forward the metadata, and skip
|
|
187
|
+
* the noisy endpoint-error log.
|
|
188
|
+
* @param {?} error - Caught error.
|
|
189
|
+
* @returns {boolean}
|
|
190
|
+
*/
|
|
191
|
+
function frontendModelErrorHasVelociousMetadata(error) {
|
|
192
|
+
return Boolean(error && typeof error === "object" && /**
|
|
193
|
+
* Types the following value.
|
|
194
|
+
@type {?} */ (error).velocious && typeof /**
|
|
195
|
+
* Types the following value.
|
|
196
|
+
@type {?} */ (error).velocious === "object")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Runs frontend model velocious metadata for error.
|
|
201
|
+
* @param {?} error - Caught error.
|
|
202
|
+
* @returns {Record<string, ?> | null}
|
|
203
|
+
*/
|
|
204
|
+
function frontendModelVelociousMetadataForError(error) {
|
|
205
|
+
if (!frontendModelErrorHasVelociousMetadata(error)) return null
|
|
206
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (/**
|
|
207
|
+
* Types the following value.
|
|
208
|
+
@type {?} */ (error).velocious)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Runs frontend model client message for error.
|
|
213
|
+
* @param {?} error - Caught error.
|
|
214
|
+
* @returns {string} - Message safe to return to API clients.
|
|
215
|
+
*/
|
|
216
|
+
function frontendModelClientMessageForError(error) {
|
|
217
|
+
if (error instanceof VelociousError && error.safeToExpose) {
|
|
218
|
+
return error.message
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (frontendModelErrorHasVelociousMetadata(error) && error instanceof Error) {
|
|
222
|
+
return error.message
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return frontendModelClientSafeErrorMessage
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Runs frontend model debug payload for error.
|
|
230
|
+
* @param {object} args - Arguments.
|
|
231
|
+
* @param {import("./configuration.js").default} args.configuration - Current configuration.
|
|
232
|
+
* @param {string} args.environment - Current environment.
|
|
233
|
+
* @param {?} args.error - Caught error.
|
|
234
|
+
* @returns {Record<string, ?>} - Optional debug payload for non-production environments.
|
|
235
|
+
*/
|
|
236
|
+
function frontendModelDebugPayloadForError({configuration, environment, error}) {
|
|
237
|
+
const debugAllowed = frontendModelDebugErrorEnvironments.has(environment) || environment !== "production" && configuration.getExposeInternalErrorsToClients()
|
|
238
|
+
|
|
239
|
+
if (!debugAllowed) {
|
|
240
|
+
return {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (error instanceof VelociousError && error.safeToExpose) {
|
|
244
|
+
return {}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (frontendModelErrorHasVelociousMetadata(error)) {
|
|
248
|
+
return {}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const debugErrorClass = error instanceof Error && error.name
|
|
252
|
+
? error.name
|
|
253
|
+
: typeof error
|
|
254
|
+
const debugErrorMessage = error instanceof Error
|
|
255
|
+
? error.message
|
|
256
|
+
: String(error)
|
|
257
|
+
const debugBacktrace = error instanceof Error && typeof error.stack === "string" && error.stack.length > 0
|
|
258
|
+
? error.stack.split("\n")
|
|
259
|
+
: undefined
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
debugErrorClass,
|
|
263
|
+
debugErrorMessage,
|
|
264
|
+
...(debugBacktrace ? {debugBacktrace} : {})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Runs normalize frontend model searches.
|
|
270
|
+
* @param {?} searches - Search payload.
|
|
271
|
+
* @returns {FrontendModelSearch[]} - Normalized searches.
|
|
272
|
+
*/
|
|
273
|
+
function normalizeFrontendModelSearches(searches) {
|
|
274
|
+
if (!searches) return []
|
|
275
|
+
|
|
276
|
+
if (!Array.isArray(searches)) {
|
|
277
|
+
throw frontendModelValidationError(`Invalid searches type: ${typeof searches}`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Normalized.
|
|
282
|
+
@type {FrontendModelSearch[]} */
|
|
283
|
+
const normalized = []
|
|
284
|
+
|
|
285
|
+
for (const search of searches) {
|
|
286
|
+
if (!isPlainObject(search)) {
|
|
287
|
+
throw frontendModelValidationError(`Invalid search entry type: ${typeof search}`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const path = search.path
|
|
291
|
+
const column = search.column
|
|
292
|
+
const operator = search.operator
|
|
293
|
+
|
|
294
|
+
if (!Array.isArray(path)) {
|
|
295
|
+
throw frontendModelValidationError("Invalid search path: expected an array")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const pathEntry of path) {
|
|
299
|
+
if (typeof pathEntry !== "string" || pathEntry.length < 1) {
|
|
300
|
+
throw frontendModelValidationError("Invalid search path entry: expected non-empty string")
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (typeof column !== "string" || column.length < 1) {
|
|
305
|
+
throw frontendModelValidationError("Invalid search column: expected non-empty string")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (typeof operator !== "string") {
|
|
309
|
+
throw frontendModelValidationError(`Invalid search operator: ${operator}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
normalized.push({
|
|
313
|
+
column,
|
|
314
|
+
operator: normalizeQuerySearchOperator(operator),
|
|
315
|
+
path: [...path],
|
|
316
|
+
value: search.value
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return normalized
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Runs normalize frontend model where.
|
|
325
|
+
* @param {?} where - Where payload.
|
|
326
|
+
* @returns {Record<string, ?> | null} - Normalized where hash.
|
|
327
|
+
*/
|
|
328
|
+
function normalizeFrontendModelWhere(where) {
|
|
329
|
+
if (!where) return null
|
|
330
|
+
|
|
331
|
+
if (!isPlainObject(where)) {
|
|
332
|
+
throw frontendModelValidationError(`Invalid where type: ${typeof where}`)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return where
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Runs normalize frontend model ransack.
|
|
340
|
+
* @param {?} ransack - Ransack payload.
|
|
341
|
+
* @returns {Record<string, ?> | null} - Normalized Ransack hash.
|
|
342
|
+
*/
|
|
343
|
+
function normalizeFrontendModelRansack(ransack) {
|
|
344
|
+
if (!ransack) return null
|
|
345
|
+
|
|
346
|
+
if (!isPlainObject(ransack)) {
|
|
347
|
+
throw frontendModelValidationError(`Invalid ransack type: ${typeof ransack}`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return ransack
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Runs normalize frontend model integer param.
|
|
355
|
+
* @param {?} value - Candidate integer.
|
|
356
|
+
* @param {string} name - Param name for errors.
|
|
357
|
+
* @param {number} min - Minimum allowed value.
|
|
358
|
+
* @returns {number | null} - Normalized integer.
|
|
359
|
+
*/
|
|
360
|
+
function normalizeFrontendModelIntegerParam(value, name, min) {
|
|
361
|
+
if (value == null) return null
|
|
362
|
+
|
|
363
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
364
|
+
throw frontendModelValidationError(`Invalid ${name}: expected integer number`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (value < min) {
|
|
368
|
+
throw frontendModelValidationError(`Invalid ${name}: expected value >= ${min}`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return value
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Runs normalize frontend model pagination.
|
|
376
|
+
* @param {object} args - Pagination args.
|
|
377
|
+
* @param {?} args.limit - Limit payload.
|
|
378
|
+
* @param {?} args.offset - Offset payload.
|
|
379
|
+
* @param {?} args.page - Page payload.
|
|
380
|
+
* @param {?} args.perPage - Per-page payload.
|
|
381
|
+
* @returns {FrontendModelPagination} - Normalized pagination data.
|
|
382
|
+
*/
|
|
383
|
+
function normalizeFrontendModelPagination({limit, offset, page, perPage}) {
|
|
384
|
+
return {
|
|
385
|
+
limit: normalizeFrontendModelIntegerParam(limit, "limit", 0),
|
|
386
|
+
offset: normalizeFrontendModelIntegerParam(offset, "offset", 0),
|
|
387
|
+
page: normalizeFrontendModelIntegerParam(page, "page", 1),
|
|
388
|
+
perPage: normalizeFrontendModelIntegerParam(perPage, "perPage", 1)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Runs normalize frontend model distinct.
|
|
394
|
+
* @param {?} distinct - Distinct payload.
|
|
395
|
+
* @returns {boolean | null} - Normalized distinct flag when provided.
|
|
396
|
+
*/
|
|
397
|
+
function normalizeFrontendModelDistinct(distinct) {
|
|
398
|
+
if (distinct == null) return null
|
|
399
|
+
|
|
400
|
+
if (typeof distinct !== "boolean") {
|
|
401
|
+
throw frontendModelValidationError(`Invalid distinct: expected boolean`)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return distinct
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Runs build frontend model join object from path.
|
|
409
|
+
* @param {string[]} path - Relationship path.
|
|
410
|
+
* @returns {Record<string, ?>} - Join object.
|
|
411
|
+
*/
|
|
412
|
+
function buildFrontendModelJoinObjectFromPath(path) {
|
|
413
|
+
/**
|
|
414
|
+
* Join object.
|
|
415
|
+
@type {Record<string, ?>} */
|
|
416
|
+
const joinObject = {}
|
|
417
|
+
/**
|
|
418
|
+
* Current node.
|
|
419
|
+
@type {Record<string, ?>} */
|
|
420
|
+
let currentNode = joinObject
|
|
421
|
+
|
|
422
|
+
for (const relationshipName of path) {
|
|
423
|
+
currentNode[relationshipName] = {}
|
|
424
|
+
currentNode = currentNode[relationshipName]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return joinObject
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Build a successful single-model frontend-model response payload.
|
|
432
|
+
* @param {Record<string, ?>} model - Serialized model payload.
|
|
433
|
+
* @returns {{model: Record<string, ?>, status: "success"}} - Success response payload.
|
|
434
|
+
*/
|
|
435
|
+
function frontendModelSerializedModelSuccess(model) {
|
|
436
|
+
return {model, status: "success"}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Resolve and validate attachment params shared by attachment commands.
|
|
441
|
+
* @param {Record<string, ?>} params - Frontend-model request params.
|
|
442
|
+
* @returns {{attachmentId: string | undefined, attachmentName: string} | string} - Attachment params or validation error message.
|
|
443
|
+
*/
|
|
444
|
+
function frontendModelAttachmentParams(params) {
|
|
445
|
+
const attachmentName = params.attachmentName
|
|
446
|
+
|
|
447
|
+
if (typeof attachmentName !== "string" || attachmentName.length < 1) {
|
|
448
|
+
return "Expected attachmentName."
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
attachmentId: typeof params.attachmentId === "string" ? params.attachmentId : undefined,
|
|
453
|
+
attachmentName
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Extract mutation attributes shared by create and update commands.
|
|
459
|
+
* @param {Record<string, ?>} params - Frontend-model request params.
|
|
460
|
+
* @returns {{attributes: Record<string, ?>, nestedAttributes: Record<string, ?> | null} | string} - Mutation attributes or validation error message.
|
|
461
|
+
*/
|
|
462
|
+
function frontendModelMutationAttributes(params) {
|
|
463
|
+
const attributes = params.attributes
|
|
464
|
+
|
|
465
|
+
if (!attributes || typeof attributes !== "object") {
|
|
466
|
+
return "Expected model attributes."
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
attributes,
|
|
471
|
+
nestedAttributes: params.nestedAttributes && typeof params.nestedAttributes === "object"
|
|
472
|
+
? /**
|
|
473
|
+
* Types the following value.
|
|
474
|
+
@type {Record<string, ?>} */ (params.nestedAttributes)
|
|
475
|
+
: null
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Controller with built-in frontend model resource actions. */
|
|
480
|
+
export default class FrontendModelController extends Controller {
|
|
481
|
+
/**
|
|
482
|
+
* Frontend model params.
|
|
483
|
+
@type {Record<string, ?> | undefined} */
|
|
484
|
+
_frontendModelParams = undefined
|
|
485
|
+
/**
|
|
486
|
+
* Frontend model params override.
|
|
487
|
+
@type {Record<string, ?> | undefined} */
|
|
488
|
+
_frontendModelParamsOverride = undefined
|
|
489
|
+
/**
|
|
490
|
+
* Frontend model ability override.
|
|
491
|
+
@type {import("./authorization/ability.js").default | undefined} */
|
|
492
|
+
_frontendModelAbilityOverride = undefined
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Runs frontend model params.
|
|
496
|
+
* @returns {Record<string, ?>} - Decoded request params.
|
|
497
|
+
*/
|
|
498
|
+
frontendModelParams() {
|
|
499
|
+
if (this._frontendModelParamsOverride) {
|
|
500
|
+
return this._frontendModelParamsOverride
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
this._frontendModelParams ||= /**
|
|
504
|
+
* Types the following value.
|
|
505
|
+
@type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(this.params()))
|
|
506
|
+
|
|
507
|
+
return this._frontendModelParams
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Runs with frontend model params.
|
|
512
|
+
* @template T
|
|
513
|
+
* @param {Record<string, ?>} params - Temporary frontend model params.
|
|
514
|
+
* @param {() => Promise<T>} callback - Callback executed with temporary params.
|
|
515
|
+
* @returns {Promise<T>} - Callback return value.
|
|
516
|
+
*/
|
|
517
|
+
async withFrontendModelParams(params, callback) {
|
|
518
|
+
const previousOverride = this._frontendModelParamsOverride
|
|
519
|
+
const previousParams = this._frontendModelParams
|
|
520
|
+
|
|
521
|
+
this._frontendModelParamsOverride = params
|
|
522
|
+
this._frontendModelParams = undefined
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
return await callback()
|
|
526
|
+
} finally {
|
|
527
|
+
this._frontendModelParamsOverride = previousOverride
|
|
528
|
+
this._frontendModelParams = previousParams
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Runs with frontend model request context.
|
|
534
|
+
* @template T
|
|
535
|
+
* @param {Record<string, ?>} params - Request-scoped params.
|
|
536
|
+
* @param {import("./http-server/client/response.js").default} response - Response instance.
|
|
537
|
+
* @param {() => Promise<T>} callback - Callback executed inside resolved tenant and ability context.
|
|
538
|
+
* @returns {Promise<T>} - Callback return value.
|
|
539
|
+
*/
|
|
540
|
+
async withFrontendModelRequestContext(params, response, callback) {
|
|
541
|
+
const configuration = this.getConfiguration()
|
|
542
|
+
const tenant = await configuration.resolveTenant({
|
|
543
|
+
params,
|
|
544
|
+
request: this.request(),
|
|
545
|
+
response
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
return await configuration.runWithTenant(tenant, async () => {
|
|
549
|
+
return await configuration.ensureConnections({name: "Frontend model request"}, async () => {
|
|
550
|
+
const ability = await configuration.resolveAbility({
|
|
551
|
+
params,
|
|
552
|
+
request: this.request(),
|
|
553
|
+
response
|
|
554
|
+
})
|
|
555
|
+
/**
|
|
556
|
+
* Previous ability override.
|
|
557
|
+
@type {import("./authorization/ability.js").default | undefined} */
|
|
558
|
+
const previousAbilityOverride = this._frontendModelAbilityOverride
|
|
559
|
+
|
|
560
|
+
this._frontendModelAbilityOverride = ability
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
return await configuration.runWithAbility(ability, async () => {
|
|
564
|
+
return await callback()
|
|
565
|
+
})
|
|
566
|
+
} finally {
|
|
567
|
+
this._frontendModelAbilityOverride = previousAbilityOverride
|
|
568
|
+
}
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Runs current ability.
|
|
575
|
+
* @returns {import("./authorization/ability.js").default | undefined} - Current ability for frontend-model request scope.
|
|
576
|
+
*/
|
|
577
|
+
currentAbility() {
|
|
578
|
+
return this._frontendModelAbilityOverride || super.currentAbility()
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Runs frontend model class.
|
|
583
|
+
* @returns {typeof import("./database/record/index.js").default} - Frontend model class for controller resource actions.
|
|
584
|
+
*/
|
|
585
|
+
frontendModelClass() {
|
|
586
|
+
const frontendModelClass = this.frontendModelClassFromConfiguration()
|
|
587
|
+
const params = this.frontendModelParams()
|
|
588
|
+
const modelName = typeof params.model === "string" ? params.model : undefined
|
|
589
|
+
const controllerName = typeof params.controller === "string" ? params.controller : undefined
|
|
590
|
+
|
|
591
|
+
if (frontendModelClass) return frontendModelClass
|
|
592
|
+
|
|
593
|
+
throw new Error(`No frontend model configured for model '${modelName || "unknown"}' and controller '${controllerName || "unknown"}'. Ensure a FrontendModelBaseResource subclass exists in src/resources/ or is listed in the ability resolver.`)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Runs frontend model resource configuration.
|
|
598
|
+
* @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for current controller.
|
|
599
|
+
*/
|
|
600
|
+
frontendModelResourceConfiguration() {
|
|
601
|
+
const params = this.frontendModelParams()
|
|
602
|
+
const modelName = typeof params.model === "string" ? params.model : undefined
|
|
603
|
+
const controllerName = typeof params.controller === "string" ? params.controller : undefined
|
|
604
|
+
const backendProjects = this.getConfiguration().getBackendProjects()
|
|
605
|
+
|
|
606
|
+
for (const backendProject of backendProjects) {
|
|
607
|
+
const resources = frontendModelResourcesForBackendProject(backendProject)
|
|
608
|
+
|
|
609
|
+
if (modelName && modelName.length > 0 && resources[modelName]) {
|
|
610
|
+
const resourceDefinition = resources[modelName]
|
|
611
|
+
const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
|
|
612
|
+
const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
|
|
613
|
+
|
|
614
|
+
if (!resourceConfiguration || !resourceClass) {
|
|
615
|
+
throw new Error(`Frontend model resource '${modelName}' must be a FrontendModelBaseResource subclass`)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
backendProject,
|
|
620
|
+
modelName,
|
|
621
|
+
resourceClass,
|
|
622
|
+
resourceConfiguration
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!controllerName || controllerName.length < 1) continue
|
|
627
|
+
|
|
628
|
+
for (const resourceModelName in resources) {
|
|
629
|
+
const resourceDefinition = resources[resourceModelName]
|
|
630
|
+
const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
|
|
631
|
+
const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
|
|
632
|
+
|
|
633
|
+
if (!resourceConfiguration || !resourceClass) {
|
|
634
|
+
throw new Error(`Frontend model resource '${resourceModelName}' must be a FrontendModelBaseResource subclass`)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const resourcePath = this.frontendModelResourcePath(resourceModelName, resourceDefinition)
|
|
638
|
+
|
|
639
|
+
if (this.frontendModelResourceMatchesController({controllerName, resourcePath})) {
|
|
640
|
+
return {
|
|
641
|
+
backendProject,
|
|
642
|
+
modelName: resourceModelName,
|
|
643
|
+
resourceClass,
|
|
644
|
+
resourceConfiguration
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return null
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Runs frontend model resource configuration for backend project model name.
|
|
655
|
+
* @param {object} args - Arguments.
|
|
656
|
+
* @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
|
|
657
|
+
* @param {string} args.modelName - Model name.
|
|
658
|
+
* @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for model name.
|
|
659
|
+
*/
|
|
660
|
+
frontendModelResourceConfigurationForBackendProjectModelName({backendProject, modelName}) {
|
|
661
|
+
const resources = frontendModelResourcesForBackendProject(backendProject)
|
|
662
|
+
const resourceDefinition = resources[modelName]
|
|
663
|
+
|
|
664
|
+
if (!resourceDefinition) return null
|
|
665
|
+
|
|
666
|
+
const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
|
|
667
|
+
const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
|
|
668
|
+
|
|
669
|
+
if (!resourceConfiguration || !resourceClass) return null
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
backendProject,
|
|
673
|
+
modelName,
|
|
674
|
+
resourceClass,
|
|
675
|
+
resourceConfiguration
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Runs frontend model resource configuration for model class.
|
|
681
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
682
|
+
* @returns {{backendProject: import("./configuration-types.js").BackendProjectConfiguration, modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType, resourceConfiguration: import("./configuration-types.js").NormalizedFrontendModelResourceConfiguration} | null} - Frontend model resource configuration for model class.
|
|
683
|
+
*/
|
|
684
|
+
frontendModelResourceConfigurationForModelClass(modelClass) {
|
|
685
|
+
const frontendModelResource = this.frontendModelResourceConfiguration()
|
|
686
|
+
|
|
687
|
+
if (!frontendModelResource) return null
|
|
688
|
+
|
|
689
|
+
return this.frontendModelResourceConfigurationForBackendProjectModelName({
|
|
690
|
+
backendProject: frontendModelResource.backendProject,
|
|
691
|
+
modelName: modelClass.getModelName()
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Runs frontend model resource model class.
|
|
697
|
+
* @param {{modelName: string, resourceClass: import("./configuration-types.js").FrontendModelResourceClassType}} frontendModelResource - Frontend model resource configuration.
|
|
698
|
+
* @returns {typeof import("./database/record/index.js").default | null} - Backing record class, when available.
|
|
699
|
+
*/
|
|
700
|
+
frontendModelResourceModelClass(frontendModelResource) {
|
|
701
|
+
const resourceModelClass = frontendModelResource.resourceClass.ModelClass
|
|
702
|
+
|
|
703
|
+
if (resourceModelClass) return resourceModelClass
|
|
704
|
+
|
|
705
|
+
return this.getConfiguration().getModelClasses()[frontendModelResource.modelName] || null
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Runs frontend model class from configuration.
|
|
710
|
+
* @returns {typeof import("./database/record/index.js").default | null} - Frontend model class resolved from backend project configuration.
|
|
711
|
+
*/
|
|
712
|
+
frontendModelClassFromConfiguration() {
|
|
713
|
+
const frontendModelResource = this.frontendModelResourceConfiguration()
|
|
714
|
+
|
|
715
|
+
if (!frontendModelResource) return null
|
|
716
|
+
|
|
717
|
+
const resourceModelClass = this.frontendModelResourceModelClass(frontendModelResource)
|
|
718
|
+
|
|
719
|
+
if (resourceModelClass) return resourceModelClass
|
|
720
|
+
|
|
721
|
+
const modelClasses = this.getConfiguration().getModelClasses()
|
|
722
|
+
const modelClass = modelClasses[frontendModelResource.modelName]
|
|
723
|
+
|
|
724
|
+
if (!modelClass) {
|
|
725
|
+
throw new Error(`Frontend model '${frontendModelResource.modelName}' is configured for '${this.frontendModelParams().controller}', but no model class was registered. Registered models: ${Object.keys(modelClasses).join(", ")}`)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return modelClass
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Ensures the frontend model class and requested preload target classes are initialized.
|
|
733
|
+
* This handles the case where model initialization was skipped at startup (e.g., browser tests).
|
|
734
|
+
* @returns {Promise<void>} - Resolves when the model class is ready.
|
|
735
|
+
*/
|
|
736
|
+
async ensureFrontendModelClassInitialized() {
|
|
737
|
+
const frontendModelResource = this.frontendModelResourceConfiguration()
|
|
738
|
+
const modelClass = this.frontendModelClassFromConfiguration()
|
|
739
|
+
|
|
740
|
+
if (!modelClass) return
|
|
741
|
+
|
|
742
|
+
await this.ensureFrontendModelRecordClassInitialized(modelClass)
|
|
743
|
+
|
|
744
|
+
if (!frontendModelResource) return
|
|
745
|
+
|
|
746
|
+
await this.ensureFrontendModelPreloadClassesInitialized({
|
|
747
|
+
backendProject: frontendModelResource.backendProject,
|
|
748
|
+
modelClass,
|
|
749
|
+
preload: this.frontendModelPreload()
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Runs ensure frontend model record class initialized.
|
|
755
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class to initialize.
|
|
756
|
+
* @returns {Promise<void>} - Resolves when the model class is ready.
|
|
757
|
+
*/
|
|
758
|
+
async ensureFrontendModelRecordClassInitialized(modelClass) {
|
|
759
|
+
if (!modelClass || modelClass.isInitialized()) return
|
|
760
|
+
|
|
761
|
+
const configuration = this.getConfiguration()
|
|
762
|
+
|
|
763
|
+
if (typeof modelClass.ensureInitialized === "function") {
|
|
764
|
+
await modelClass.ensureInitialized({configuration})
|
|
765
|
+
} else if (typeof modelClass.initializeRecord === "function") {
|
|
766
|
+
await modelClass.initializeRecord({configuration})
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Runs ensure frontend model preload classes initialized.
|
|
772
|
+
* @param {object} args - Arguments.
|
|
773
|
+
* @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
|
|
774
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class whose preload tree is being resolved.
|
|
775
|
+
* @param {import("./database/query/index.js").NestedPreloadRecord | null} args.preload - Normalized preload tree.
|
|
776
|
+
* @returns {Promise<void>} - Resolves when preload target classes are initialized.
|
|
777
|
+
*/
|
|
778
|
+
async ensureFrontendModelPreloadClassesInitialized({backendProject, modelClass, preload}) {
|
|
779
|
+
if (!preload) return
|
|
780
|
+
|
|
781
|
+
for (const [relationshipName, relationshipPreload] of Object.entries(preload)) {
|
|
782
|
+
if (relationshipPreload === false) continue
|
|
783
|
+
|
|
784
|
+
const relationship = modelClass.getRelationshipByName(relationshipName)
|
|
785
|
+
const targetModelClass = await this.ensureFrontendModelRelationshipTargetClassInitialized({
|
|
786
|
+
backendProject,
|
|
787
|
+
relationship
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
if (!targetModelClass || !isPlainObject(relationshipPreload)) continue
|
|
791
|
+
|
|
792
|
+
await this.ensureFrontendModelPreloadClassesInitialized({
|
|
793
|
+
backendProject,
|
|
794
|
+
modelClass: targetModelClass,
|
|
795
|
+
preload: /**
|
|
796
|
+
* Types the following value.
|
|
797
|
+
@type {import("./database/query/index.js").NestedPreloadRecord} */ (relationshipPreload)
|
|
798
|
+
})
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Runs ensure frontend model relationship target class initialized.
|
|
804
|
+
* @param {object} args - Arguments.
|
|
805
|
+
* @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
|
|
806
|
+
* @param {import("./database/record/relationships/base.js").default} args.relationship - Relationship definition.
|
|
807
|
+
* @returns {Promise<typeof import("./database/record/index.js").default | null>} - Target model class, when available.
|
|
808
|
+
*/
|
|
809
|
+
async ensureFrontendModelRelationshipTargetClassInitialized({backendProject, relationship}) {
|
|
810
|
+
if (relationship.through) {
|
|
811
|
+
const throughRelationship = relationship.getModelClass().getRelationshipByName(relationship.through)
|
|
812
|
+
await this.ensureFrontendModelRelationshipTargetClassInitialized({
|
|
813
|
+
backendProject,
|
|
814
|
+
relationship: throughRelationship
|
|
815
|
+
})
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const targetModelClass = this.frontendModelRelationshipTargetModelClass({
|
|
819
|
+
backendProject,
|
|
820
|
+
relationship
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
if (!targetModelClass) return null
|
|
824
|
+
|
|
825
|
+
await this.ensureFrontendModelRecordClassInitialized(targetModelClass)
|
|
826
|
+
|
|
827
|
+
return targetModelClass
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Runs frontend model relationship target model class.
|
|
832
|
+
* @param {object} args - Arguments.
|
|
833
|
+
* @param {import("./configuration-types.js").BackendProjectConfiguration} args.backendProject - Backend project configuration.
|
|
834
|
+
* @param {import("./database/record/relationships/base.js").default} args.relationship - Relationship definition.
|
|
835
|
+
* @returns {typeof import("./database/record/index.js").default | null} - Target model class, when available.
|
|
836
|
+
*/
|
|
837
|
+
frontendModelRelationshipTargetModelClass({backendProject, relationship}) {
|
|
838
|
+
if (relationship.getPolymorphic() && relationship.getType() === "belongsTo") return null
|
|
839
|
+
|
|
840
|
+
if (relationship.klass) return relationship.klass
|
|
841
|
+
|
|
842
|
+
if (relationship.className) {
|
|
843
|
+
const frontendModelResource = this.frontendModelResourceConfigurationForBackendProjectModelName({
|
|
844
|
+
backendProject,
|
|
845
|
+
modelName: relationship.className
|
|
846
|
+
})
|
|
847
|
+
const resourceModelClass = frontendModelResource ? this.frontendModelResourceModelClass(frontendModelResource) : null
|
|
848
|
+
|
|
849
|
+
if (resourceModelClass) return resourceModelClass
|
|
850
|
+
|
|
851
|
+
const registeredModelClass = this.getConfiguration().getModelClasses()[relationship.className]
|
|
852
|
+
|
|
853
|
+
if (registeredModelClass) return registeredModelClass
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const targetModelClass = relationship.getTargetModelClass()
|
|
857
|
+
|
|
858
|
+
return targetModelClass || null
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Runs frontend model resource path.
|
|
863
|
+
* @param {string} modelName - Model class name.
|
|
864
|
+
* @param {?} resourceDefinition - Resource definition.
|
|
865
|
+
* @returns {string} - Normalized resource path.
|
|
866
|
+
*/
|
|
867
|
+
frontendModelResourcePath(modelName, resourceDefinition) {
|
|
868
|
+
return frontendModelResourcePath(modelName, resourceDefinition)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Runs frontend model resource matches controller.
|
|
873
|
+
* @param {object} args - Arguments.
|
|
874
|
+
* @param {string} args.controllerName - Controller name from params.
|
|
875
|
+
* @param {string} args.resourcePath - Resource path from configuration.
|
|
876
|
+
* @returns {boolean} - Whether resource path matches current controller.
|
|
877
|
+
*/
|
|
878
|
+
frontendModelResourceMatchesController({controllerName, resourcePath}) {
|
|
879
|
+
const normalizedController = controllerName.replace(/^\/+|\/+$/g, "")
|
|
880
|
+
const normalizedResourcePath = resourcePath.replace(/^\/+|\/+$/g, "")
|
|
881
|
+
|
|
882
|
+
if (normalizedResourcePath === normalizedController) return true
|
|
883
|
+
|
|
884
|
+
return normalizedResourcePath.endsWith(`/${normalizedController}`)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Runs frontend model resource instance.
|
|
889
|
+
* @returns {FrontendModelBaseResource} - Backend resource instance for current frontend-model action.
|
|
890
|
+
*/
|
|
891
|
+
frontendModelResourceInstance() {
|
|
892
|
+
const frontendModelResource = this.frontendModelResourceConfiguration()
|
|
893
|
+
|
|
894
|
+
if (!frontendModelResource) {
|
|
895
|
+
throw new Error(`No frontend model resource configuration for controller '${this.frontendModelParams().controller}'`)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const resourceArgs = {
|
|
899
|
+
ability: this.currentAbility(),
|
|
900
|
+
controller: this,
|
|
901
|
+
context: {
|
|
902
|
+
...(this.currentAbility()?.getContext() || {}),
|
|
903
|
+
params: this.frontendModelParams(),
|
|
904
|
+
request: this.request()
|
|
905
|
+
},
|
|
906
|
+
locals: this.currentAbility()?.getLocals() || {},
|
|
907
|
+
modelClass: this.frontendModelClass(),
|
|
908
|
+
modelName: frontendModelResource.modelName,
|
|
909
|
+
params: this.frontendModelParams(),
|
|
910
|
+
resourceConfiguration: frontendModelResource.resourceConfiguration
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return new frontendModelResource.resourceClass(resourceArgs)
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Runs frontend model primary key.
|
|
918
|
+
* @returns {string} - Frontend model primary key.
|
|
919
|
+
*/
|
|
920
|
+
frontendModelPrimaryKey() {
|
|
921
|
+
return this.frontendModelClass().primaryKey()
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Runs frontend model ability action.
|
|
926
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
927
|
+
* @returns {string} - Ability action configured for the frontend action.
|
|
928
|
+
*/
|
|
929
|
+
frontendModelAbilityAction(action) {
|
|
930
|
+
const frontendModelResource = this.frontendModelResourceConfiguration()
|
|
931
|
+
|
|
932
|
+
if (!frontendModelResource) {
|
|
933
|
+
throw new Error(`No frontend model resource configuration for controller '${this.frontendModelParams().controller}'`)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const abilities = frontendModelResource.resourceConfiguration.abilities
|
|
937
|
+
|
|
938
|
+
if (!abilities || typeof abilities !== "object") {
|
|
939
|
+
throw new Error(`Resource '${frontendModelResource.modelName}' must define an 'abilities' object`)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const abilityKey = action === "attach"
|
|
943
|
+
? "update"
|
|
944
|
+
: ((action === "download" || action === "url") ? "find" : action)
|
|
945
|
+
const abilityAction = abilities[abilityKey]
|
|
946
|
+
|
|
947
|
+
if (typeof abilityAction !== "string" || abilityAction.length < 1) {
|
|
948
|
+
throw new Error(`Resource '${frontendModelResource.modelName}' must define abilities.${abilityKey}`)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return abilityAction
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Runs frontend model authorized query.
|
|
956
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
957
|
+
* @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Authorized query for the action.
|
|
958
|
+
*/
|
|
959
|
+
frontendModelAuthorizedQuery(action) {
|
|
960
|
+
const abilityAction = this.frontendModelAbilityAction(action)
|
|
961
|
+
|
|
962
|
+
return this.frontendModelClass().accessibleFor(abilityAction, this.currentAbility())
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Runs frontend model primary key value.
|
|
967
|
+
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
968
|
+
* @returns {string} - Primary key value as string.
|
|
969
|
+
*/
|
|
970
|
+
frontendModelPrimaryKeyValue(model) {
|
|
971
|
+
const columnName = this.frontendModelPrimaryKey()
|
|
972
|
+
const attributeNameMap = model.getModelClass().getColumnNameToAttributeNameMap()
|
|
973
|
+
const attributeName = attributeNameMap[columnName] || columnName
|
|
974
|
+
const value = model.readAttribute(attributeName)
|
|
975
|
+
|
|
976
|
+
return String(value)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Runs frontend model filter authorized models.
|
|
981
|
+
* @param {object} args - Arguments.
|
|
982
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} args.action - Frontend action.
|
|
983
|
+
* @param {import("./database/record/index.js").default[]} args.models - Candidate models.
|
|
984
|
+
* @returns {Promise<import("./database/record/index.js").default[]>} - Authorized models.
|
|
985
|
+
*/
|
|
986
|
+
async frontendModelFilterAuthorizedModels({action, models}) {
|
|
987
|
+
if (models.length === 0) return models
|
|
988
|
+
|
|
989
|
+
const primaryKey = this.frontendModelPrimaryKey()
|
|
990
|
+
const ids = models.map((model) => this.frontendModelPrimaryKeyValue(model))
|
|
991
|
+
const authorizedQuery = this.frontendModelAuthorizedQuery(action).where({[primaryKey]: ids})
|
|
992
|
+
|
|
993
|
+
const authorizedIdsRaw = await authorizedQuery.pluck(primaryKey)
|
|
994
|
+
|
|
995
|
+
const authorizedIds = new Set(authorizedIdsRaw.map((id) => String(id)))
|
|
996
|
+
|
|
997
|
+
return models.filter((model) => authorizedIds.has(this.frontendModelPrimaryKeyValue(model)))
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Runs run frontend model before action.
|
|
1002
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
1003
|
+
* @returns {Promise<boolean>} - Whether action should continue.
|
|
1004
|
+
*/
|
|
1005
|
+
async runFrontendModelBeforeAction(action) {
|
|
1006
|
+
const result = await this.frontendModelResourceInstance().beforeAction(action)
|
|
1007
|
+
|
|
1008
|
+
return result !== false
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Runs frontend model find record.
|
|
1013
|
+
* @param {"find" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
1014
|
+
* @param {string | number} id - Record id.
|
|
1015
|
+
* @returns {Promise<import("./database/record/index.js").default | null>} - Located model record.
|
|
1016
|
+
*/
|
|
1017
|
+
async frontendModelFindRecord(action, id) {
|
|
1018
|
+
const model = await this.frontendModelResourceInstance().find(action, id)
|
|
1019
|
+
|
|
1020
|
+
if (!model) return null
|
|
1021
|
+
|
|
1022
|
+
const authorizedModels = await this.frontendModelFilterAuthorizedModels({action, models: [model]})
|
|
1023
|
+
|
|
1024
|
+
return authorizedModels[0] || null
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Runs frontend model create record.
|
|
1029
|
+
* @param {Record<string, ?>} attributes - Create attributes.
|
|
1030
|
+
* @param {Record<string, ?> | null} [nestedAttributes] - Optional nested-attribute payload for cascading writes.
|
|
1031
|
+
* @returns {Promise<import("./database/record/index.js").default | null>} - Created model when authorized.
|
|
1032
|
+
*/
|
|
1033
|
+
async frontendModelCreateRecord(attributes, nestedAttributes = null) {
|
|
1034
|
+
const resource = this.frontendModelResourceInstance()
|
|
1035
|
+
const model = await resource.create(attributes, {nestedAttributes, controller: this})
|
|
1036
|
+
|
|
1037
|
+
const authorizedModels = await this.frontendModelFilterAuthorizedModels({action: "create", models: [model]})
|
|
1038
|
+
|
|
1039
|
+
if (authorizedModels.length > 0) {
|
|
1040
|
+
return authorizedModels[0]
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
await resource.handleUnauthorizedCreatedModel(model)
|
|
1044
|
+
|
|
1045
|
+
return null
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Runs frontend model records.
|
|
1050
|
+
* @returns {Promise<import("./database/record/index.js").default[]>} - Frontend model records.
|
|
1051
|
+
*/
|
|
1052
|
+
async frontendModelRecords() {
|
|
1053
|
+
const models = await this.frontendModelResourceInstance().records()
|
|
1054
|
+
|
|
1055
|
+
return await this.frontendModelFilterAuthorizedModels({action: "index", models})
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Runs frontend model preload.
|
|
1060
|
+
* @returns {import("./database/query/index.js").NestedPreloadRecord | null} - Frontend preload data.
|
|
1061
|
+
*/
|
|
1062
|
+
frontendModelPreload() {
|
|
1063
|
+
return normalizeFrontendModelPreload(this.frontendModelParams().preload)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Runs frontend model select.
|
|
1068
|
+
* @returns {Record<string, string[]> | null} - Frontend select data.
|
|
1069
|
+
*/
|
|
1070
|
+
frontendModelSelect() {
|
|
1071
|
+
return normalizeFrontendModelSelect(this.frontendModelParams().select, this.frontendModelClass().getModelName())
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Runs frontend model selects extra.
|
|
1076
|
+
* @returns {Record<string, string[]> | null} - Frontend extra-select data (defaults plus these), keyed by model name.
|
|
1077
|
+
*/
|
|
1078
|
+
frontendModelSelectsExtra() {
|
|
1079
|
+
return normalizeFrontendModelSelect(this.frontendModelParams().selectsExtra, this.frontendModelClass().getModelName())
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Runs frontend model searches.
|
|
1084
|
+
* @returns {FrontendModelSearch[]} - Frontend search filters.
|
|
1085
|
+
*/
|
|
1086
|
+
frontendModelSearches() {
|
|
1087
|
+
return normalizeFrontendModelSearches(this.frontendModelParams().searches)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Runs frontend model where.
|
|
1092
|
+
* @returns {Record<string, ?> | null} - Frontend where filters.
|
|
1093
|
+
*/
|
|
1094
|
+
frontendModelWhere() {
|
|
1095
|
+
return normalizeFrontendModelWhere(this.frontendModelParams().where)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Runs frontend model ransack.
|
|
1100
|
+
* @returns {Record<string, ?> | null} - Frontend Ransack filters.
|
|
1101
|
+
*/
|
|
1102
|
+
frontendModelRansack() {
|
|
1103
|
+
return normalizeFrontendModelRansack(this.frontendModelParams().ransack)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Runs frontend model joins.
|
|
1108
|
+
* @returns {Record<string, ?> | null} - Frontend joins descriptors.
|
|
1109
|
+
*/
|
|
1110
|
+
frontendModelJoins() {
|
|
1111
|
+
return normalizeFrontendModelJoins(this.frontendModelParams().joins)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* Runs frontend model sort.
|
|
1116
|
+
* @returns {FrontendModelSort[]} - Frontend sort definitions.
|
|
1117
|
+
*/
|
|
1118
|
+
frontendModelSort() {
|
|
1119
|
+
return normalizeQuerySort(this.frontendModelParams().sort)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Runs frontend model group.
|
|
1124
|
+
* @returns {FrontendModelGroup[]} - Frontend group definitions.
|
|
1125
|
+
*/
|
|
1126
|
+
frontendModelGroup() {
|
|
1127
|
+
try {
|
|
1128
|
+
return normalizeQueryGroup(this.frontendModelParams().group)
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
throw frontendModelValidationErrorForError(error)
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Runs frontend model pagination.
|
|
1136
|
+
* @returns {FrontendModelPagination} - Frontend pagination params.
|
|
1137
|
+
*/
|
|
1138
|
+
frontendModelPagination() {
|
|
1139
|
+
const params = this.frontendModelParams()
|
|
1140
|
+
|
|
1141
|
+
return normalizeFrontendModelPagination({
|
|
1142
|
+
limit: params.limit,
|
|
1143
|
+
offset: params.offset,
|
|
1144
|
+
page: params.page,
|
|
1145
|
+
perPage: params.perPage
|
|
1146
|
+
})
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Runs frontend model distinct.
|
|
1151
|
+
* @returns {boolean | null} - Frontend distinct flag when provided.
|
|
1152
|
+
*/
|
|
1153
|
+
frontendModelDistinct() {
|
|
1154
|
+
return normalizeFrontendModelDistinct(this.frontendModelParams().distinct)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* Runs frontend model pluck.
|
|
1159
|
+
* @returns {FrontendModelPluck[]} - Frontend pluck definitions.
|
|
1160
|
+
*/
|
|
1161
|
+
frontendModelPluck() {
|
|
1162
|
+
try {
|
|
1163
|
+
return normalizeQueryPluck(this.frontendModelParams().pluck)
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
throw frontendModelValidationErrorForError(error)
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Runs frontend model count requested.
|
|
1171
|
+
* @returns {boolean} - Whether the request asks for an aggregate count.
|
|
1172
|
+
*/
|
|
1173
|
+
frontendModelCountRequested() {
|
|
1174
|
+
return this.frontendModelParams().count === true
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Runs frontend model with count.
|
|
1179
|
+
* @returns {Array<{attributeName: string, relationshipName: string, where?: Record<string, ?>}>}
|
|
1180
|
+
* Frontend withCount entries. Empty array when not requested.
|
|
1181
|
+
*/
|
|
1182
|
+
frontendModelWithCount() {
|
|
1183
|
+
const raw = this.frontendModelParams().withCount
|
|
1184
|
+
|
|
1185
|
+
if (!Array.isArray(raw)) return []
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Entries.
|
|
1189
|
+
@type {Array<{attributeName: string, relationshipName: string, where?: Record<string, ?>}>} */
|
|
1190
|
+
const entries = []
|
|
1191
|
+
|
|
1192
|
+
for (const entry of raw) {
|
|
1193
|
+
if (!entry || typeof entry !== "object") continue
|
|
1194
|
+
if (typeof entry.attributeName !== "string" || entry.attributeName.length === 0) continue
|
|
1195
|
+
if (typeof entry.relationshipName !== "string" || entry.relationshipName.length === 0) continue
|
|
1196
|
+
|
|
1197
|
+
entries.push({
|
|
1198
|
+
attributeName: entry.attributeName,
|
|
1199
|
+
relationshipName: entry.relationshipName,
|
|
1200
|
+
where: entry.where && typeof entry.where === "object" ? entry.where : undefined
|
|
1201
|
+
})
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return entries
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Resolve an entry from the frontend-model `abilities` payload to
|
|
1209
|
+
* its backend model class by looking up the resource by modelName
|
|
1210
|
+
* across all configured backend projects. Returns null when no
|
|
1211
|
+
* resource matches — the spec entry is then silently ignored so a
|
|
1212
|
+
* caller requesting abilities for a model they cannot resolve does
|
|
1213
|
+
* not crash the request.
|
|
1214
|
+
* @param {string} modelName
|
|
1215
|
+
* @returns {typeof import("./database/record/index.js").default | null}
|
|
1216
|
+
*/
|
|
1217
|
+
_frontendModelClassForAbilities(modelName) {
|
|
1218
|
+
if (typeof modelName !== "string" || modelName.length === 0) return null
|
|
1219
|
+
|
|
1220
|
+
const configuration = this.getConfiguration()
|
|
1221
|
+
const backendProjects = configuration?.getBackendProjects?.() ?? []
|
|
1222
|
+
|
|
1223
|
+
for (const backendProject of backendProjects) {
|
|
1224
|
+
const frontendModels = backendProject?.frontendModels
|
|
1225
|
+
if (!frontendModels || typeof frontendModels !== "object") continue
|
|
1226
|
+
|
|
1227
|
+
const resourceDefinition = frontendModels[modelName]
|
|
1228
|
+
if (!resourceDefinition) continue
|
|
1229
|
+
|
|
1230
|
+
const resourceClass = frontendModelResourceClassFromDefinition(resourceDefinition)
|
|
1231
|
+
if (!resourceClass) continue
|
|
1232
|
+
|
|
1233
|
+
const modelClass = typeof resourceClass.modelClass === "function"
|
|
1234
|
+
? resourceClass.modelClass()
|
|
1235
|
+
: resourceClass.ModelClass
|
|
1236
|
+
|
|
1237
|
+
if (typeof modelClass === "function") return modelClass
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
return null
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Collect every loaded record whose `getModelName()` matches the
|
|
1245
|
+
* requested name, walking across the root-level slice plus any
|
|
1246
|
+
* preloaded relationships at any depth. Used to evaluate per-record
|
|
1247
|
+
* abilities against nested preloaded children with a single batched
|
|
1248
|
+
* query per (modelClass, action) pair.
|
|
1249
|
+
* @param {import("./database/record/index.js").default[]} rootModels
|
|
1250
|
+
* @param {string} modelName
|
|
1251
|
+
* @returns {import("./database/record/index.js").default[]}
|
|
1252
|
+
*/
|
|
1253
|
+
_frontendModelCollectRecordsForName(rootModels, modelName) {
|
|
1254
|
+
/**
|
|
1255
|
+
* Out.
|
|
1256
|
+
@type {import("./database/record/index.js").default[]} */
|
|
1257
|
+
const out = []
|
|
1258
|
+
/**
|
|
1259
|
+
* Seen.
|
|
1260
|
+
@type {Set<import("./database/record/index.js").default>} */
|
|
1261
|
+
const seen = new Set()
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Walk.
|
|
1265
|
+
@param {import("./database/record/index.js").default | null | undefined} record */
|
|
1266
|
+
const walk = (record) => {
|
|
1267
|
+
if (!record || typeof record !== "object") return
|
|
1268
|
+
if (seen.has(record)) return
|
|
1269
|
+
seen.add(record)
|
|
1270
|
+
|
|
1271
|
+
const ModelClass = typeof record.getModelClass === "function"
|
|
1272
|
+
? record.getModelClass()
|
|
1273
|
+
: null
|
|
1274
|
+
if (ModelClass && typeof ModelClass.getModelName === "function" && ModelClass.getModelName() === modelName) {
|
|
1275
|
+
out.push(record)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const relationshipsMap = typeof ModelClass?.getRelationshipsMap === "function"
|
|
1279
|
+
? ModelClass.getRelationshipsMap()
|
|
1280
|
+
: null
|
|
1281
|
+
if (!relationshipsMap) return
|
|
1282
|
+
|
|
1283
|
+
for (const relationshipName of Object.keys(relationshipsMap)) {
|
|
1284
|
+
const relationship = typeof record.getRelationshipByName === "function"
|
|
1285
|
+
? record.getRelationshipByName(relationshipName)
|
|
1286
|
+
: null
|
|
1287
|
+
if (!relationship || typeof relationship.getLoadedOrUndefined !== "function") continue
|
|
1288
|
+
|
|
1289
|
+
const loaded = relationship.getLoadedOrUndefined()
|
|
1290
|
+
if (loaded === undefined) continue
|
|
1291
|
+
|
|
1292
|
+
if (Array.isArray(loaded)) {
|
|
1293
|
+
for (const child of loaded) walk(child)
|
|
1294
|
+
} else {
|
|
1295
|
+
walk(loaded)
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
for (const root of rootModels) walk(root)
|
|
1301
|
+
|
|
1302
|
+
return out
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Evaluate every ability requested via the frontend `abilities`
|
|
1307
|
+
* param against the loaded model cohort (plus any preloaded
|
|
1308
|
+
* children), attaching the results to each record via
|
|
1309
|
+
* `_setComputedAbility`. Runs one batched `authorized query + pluck`
|
|
1310
|
+
* per (modelClass, action) pair, regardless of how many records
|
|
1311
|
+
* were loaded.
|
|
1312
|
+
* @param {import("./database/record/index.js").default[]} rootModels
|
|
1313
|
+
* @returns {Promise<void>}
|
|
1314
|
+
*/
|
|
1315
|
+
async frontendModelComputeAbilities(rootModels) {
|
|
1316
|
+
const entries = this.frontendModelAbilities()
|
|
1317
|
+
if (entries.length === 0) return
|
|
1318
|
+
if (!Array.isArray(rootModels) || rootModels.length === 0) return
|
|
1319
|
+
|
|
1320
|
+
const ability = this.currentAbility()
|
|
1321
|
+
if (!ability) return
|
|
1322
|
+
|
|
1323
|
+
for (const entry of entries) {
|
|
1324
|
+
const modelClass = this._frontendModelClassForAbilities(entry.modelName)
|
|
1325
|
+
if (!modelClass) continue
|
|
1326
|
+
|
|
1327
|
+
const candidates = this._frontendModelCollectRecordsForName(rootModels, entry.modelName)
|
|
1328
|
+
if (candidates.length === 0) continue
|
|
1329
|
+
|
|
1330
|
+
const primaryKey = modelClass.primaryKey()
|
|
1331
|
+
const ids = candidates
|
|
1332
|
+
.map((record) => record.readAttribute(primaryKey))
|
|
1333
|
+
.filter((value) => value !== null && value !== undefined)
|
|
1334
|
+
if (ids.length === 0) continue
|
|
1335
|
+
|
|
1336
|
+
for (const action of entry.actions) {
|
|
1337
|
+
let allowedIds
|
|
1338
|
+
try {
|
|
1339
|
+
const authorizedQuery = modelClass.accessibleFor(action, ability).where({[primaryKey]: ids})
|
|
1340
|
+
const plucked = await authorizedQuery.pluck(primaryKey)
|
|
1341
|
+
allowedIds = new Set(plucked.map((value) => String(value)))
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
// An ability with no allow rules for the action throws via
|
|
1344
|
+
// `accessibleFor`; treat as a universal deny so the frontend
|
|
1345
|
+
// gets `can(action) === false` for every candidate, instead
|
|
1346
|
+
// of surfacing an error that the UI can't act on.
|
|
1347
|
+
void error
|
|
1348
|
+
allowedIds = new Set()
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
for (const record of candidates) {
|
|
1352
|
+
const idValue = record.readAttribute(primaryKey)
|
|
1353
|
+
const allowed = idValue !== null && idValue !== undefined && allowedIds.has(String(idValue))
|
|
1354
|
+
record._setComputedAbility(action, allowed)
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Parse the frontend-model `abilities` param into a list of
|
|
1362
|
+
* `{modelName, actions}` entries to evaluate against loaded records.
|
|
1363
|
+
* Unknown entries are silently skipped — downstream code resolves
|
|
1364
|
+
* model names to classes when applying the check, so unresolved
|
|
1365
|
+
* names naturally become no-ops.
|
|
1366
|
+
* @returns {Array<{modelName: string, actions: string[]}>}
|
|
1367
|
+
*/
|
|
1368
|
+
frontendModelAbilities() {
|
|
1369
|
+
const raw = this.frontendModelParams().abilities
|
|
1370
|
+
|
|
1371
|
+
if (!Array.isArray(raw)) return []
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Entries.
|
|
1375
|
+
@type {Array<{modelName: string, actions: string[]}>} */
|
|
1376
|
+
const entries = []
|
|
1377
|
+
|
|
1378
|
+
for (const entry of raw) {
|
|
1379
|
+
if (!entry || typeof entry !== "object") continue
|
|
1380
|
+
if (typeof entry.modelName !== "string" || entry.modelName.length === 0) continue
|
|
1381
|
+
if (!Array.isArray(entry.actions)) continue
|
|
1382
|
+
|
|
1383
|
+
const actions = entry.actions.filter(
|
|
1384
|
+
(/**
|
|
1385
|
+
* Types the following value.
|
|
1386
|
+
@type {?} */ action) => typeof action === "string" && action.length > 0
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
if (actions.length === 0) continue
|
|
1390
|
+
|
|
1391
|
+
entries.push({actions, modelName: entry.modelName})
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
return entries
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Read the frontend-model `queryData` param. The wire format carries
|
|
1399
|
+
* only **names** (the keys the frontend wants attached) plus the
|
|
1400
|
+
* optional nested-relationship chain leading to them — the actual SQL
|
|
1401
|
+
* fragments live on the backend model as `Model.queryData(name, fn)`
|
|
1402
|
+
* registrations. Callers cannot push SQL through this endpoint.
|
|
1403
|
+
*
|
|
1404
|
+
* Returns the raw nested-record spec (shape validated by the
|
|
1405
|
+
* normalizer inside `Query.queryData`) or `null` when not requested.
|
|
1406
|
+
* @returns {import("./database/query/query-data.js").QueryDataSpec | null}
|
|
1407
|
+
*/
|
|
1408
|
+
frontendModelQueryData() {
|
|
1409
|
+
const raw = this.frontendModelParams().queryData
|
|
1410
|
+
|
|
1411
|
+
if (raw == null) return null
|
|
1412
|
+
|
|
1413
|
+
if (typeof raw === "string") return raw
|
|
1414
|
+
if (Array.isArray(raw)) return raw
|
|
1415
|
+
if (typeof raw === "object") return raw
|
|
1416
|
+
|
|
1417
|
+
return null
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Runs frontend model index query.
|
|
1422
|
+
* @returns {import("./database/query/model-class-query.js").default} - Frontend index query with normalized params applied.
|
|
1423
|
+
*/
|
|
1424
|
+
frontendModelIndexQuery() {
|
|
1425
|
+
let query = this.frontendModelAuthorizedQuery("index")
|
|
1426
|
+
const preload = this.frontendModelPreload()
|
|
1427
|
+
|
|
1428
|
+
if (preload) {
|
|
1429
|
+
query = query.preload(preload)
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const joins = this.frontendModelJoins()
|
|
1433
|
+
const where = this.frontendModelWhere()
|
|
1434
|
+
const pagination = this.frontendModelPagination()
|
|
1435
|
+
const distinct = this.frontendModelDistinct()
|
|
1436
|
+
|
|
1437
|
+
this.applyFrontendModelPagination({pagination, query})
|
|
1438
|
+
|
|
1439
|
+
if (distinct !== null) {
|
|
1440
|
+
query.distinct(distinct)
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (where) {
|
|
1444
|
+
this.applyFrontendModelWhere({query, where})
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const ransack = this.frontendModelRansack()
|
|
1448
|
+
|
|
1449
|
+
if (ransack) {
|
|
1450
|
+
query.ransack(ransack)
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (joins) {
|
|
1454
|
+
this.applyFrontendModelJoins({joins, query})
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const searches = this.frontendModelSearches()
|
|
1458
|
+
|
|
1459
|
+
for (const search of searches) {
|
|
1460
|
+
this.applyFrontendModelSearch({query, search})
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const groups = this.frontendModelGroup()
|
|
1464
|
+
|
|
1465
|
+
if (groups.length > 0) {
|
|
1466
|
+
this.applyFrontendModelRootGroupColumns({query})
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
for (const group of groups) {
|
|
1470
|
+
this.applyFrontendModelGroup({group, query})
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const sorts = this.frontendModelSort()
|
|
1474
|
+
|
|
1475
|
+
if (sorts.length > 0) {
|
|
1476
|
+
for (const sort of sorts) {
|
|
1477
|
+
this.applyFrontendModelSort({query, sort})
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const withCount = this.frontendModelWithCount()
|
|
1482
|
+
|
|
1483
|
+
for (const entry of withCount) {
|
|
1484
|
+
/**
|
|
1485
|
+
* Spec.
|
|
1486
|
+
@type {Record<string, boolean | {relationship?: string, where?: Record<string, ?>}>} */
|
|
1487
|
+
const spec = {}
|
|
1488
|
+
spec[entry.attributeName] = {relationship: entry.relationshipName, where: entry.where}
|
|
1489
|
+
query.withCount(spec)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const queryData = this.frontendModelQueryData()
|
|
1493
|
+
|
|
1494
|
+
if (queryData != null) {
|
|
1495
|
+
query.queryData(queryData)
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
query = this.applyFrontendModelTranslatedAttributePreloads({query})
|
|
1499
|
+
|
|
1500
|
+
if (query._distinct && query.driver.getType() === "mssql") {
|
|
1501
|
+
return this.frontendModelMssqlDistinctByPrimaryKeyQuery({query})
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
return query
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* MSSQL cannot apply DISTINCT over non-comparable text columns in table.* selects.
|
|
1509
|
+
* This rewrites distinct frontend-model queries to select root records by distinct PK subquery.
|
|
1510
|
+
* @param {object} args - Args.
|
|
1511
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query with distinct and filters.
|
|
1512
|
+
* @returns {import("./database/query/model-class-query.js").default} - MSSQL-safe distinct query.
|
|
1513
|
+
*/
|
|
1514
|
+
frontendModelMssqlDistinctByPrimaryKeyQuery({query}) {
|
|
1515
|
+
const modelClass = this.frontendModelClass()
|
|
1516
|
+
const primaryKey = modelClass.primaryKey()
|
|
1517
|
+
const rootTableSql = query.driver.quoteTable(modelClass.tableName())
|
|
1518
|
+
const primaryKeySql = `${rootTableSql}.${query.driver.quoteColumn(primaryKey)}`
|
|
1519
|
+
const distinctIdsQuery = query.clone()
|
|
1520
|
+
|
|
1521
|
+
distinctIdsQuery._preload = {}
|
|
1522
|
+
distinctIdsQuery._selects = []
|
|
1523
|
+
distinctIdsQuery.select(primaryKeySql)
|
|
1524
|
+
distinctIdsQuery.distinct(true)
|
|
1525
|
+
|
|
1526
|
+
const distinctRootQuery = modelClass._newQuery()
|
|
1527
|
+
|
|
1528
|
+
distinctRootQuery.where(`${primaryKeySql} IN (${distinctIdsQuery.toSql()})`)
|
|
1529
|
+
distinctRootQuery._preload = {...query._preload}
|
|
1530
|
+
|
|
1531
|
+
return distinctRootQuery
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Runs frontend model pluck values.
|
|
1536
|
+
* @param {object} args - Pluck args.
|
|
1537
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1538
|
+
* @param {FrontendModelPluck[]} args.pluck - Pluck descriptors.
|
|
1539
|
+
* @returns {Promise<Array<?>>} - Plucked values.
|
|
1540
|
+
*/
|
|
1541
|
+
async frontendModelPluckValues({query, pluck}) {
|
|
1542
|
+
if (pluck.length < 1) {
|
|
1543
|
+
throw new Error("No columns given to pluck")
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const modelClass = this.frontendModelClass()
|
|
1547
|
+
const pluckQuery = query.clone()
|
|
1548
|
+
/**
|
|
1549
|
+
* Aliases.
|
|
1550
|
+
@type {string[]} */
|
|
1551
|
+
const aliases = []
|
|
1552
|
+
const queryMetadata = frontendModelQueryMetadata(query)
|
|
1553
|
+
const pluckQueryMetadata = frontendModelQueryMetadata(pluckQuery)
|
|
1554
|
+
const joinedPaths = queryMetadata[frontendModelJoinedPathsSymbol]
|
|
1555
|
+
|
|
1556
|
+
pluckQuery._preload = {}
|
|
1557
|
+
pluckQuery._selects = []
|
|
1558
|
+
pluckQueryMetadata[frontendModelJoinedPathsSymbol] = joinedPaths ? new Set(joinedPaths) : new Set()
|
|
1559
|
+
|
|
1560
|
+
for (const [pluckIndex, pluckEntry] of pluck.entries()) {
|
|
1561
|
+
const targetModelClass = this.frontendModelSearchTargetModelClass({
|
|
1562
|
+
modelClass,
|
|
1563
|
+
path: pluckEntry.path
|
|
1564
|
+
})
|
|
1565
|
+
const columnName = this.resolveFrontendModelColumnName(targetModelClass, pluckEntry.column)
|
|
1566
|
+
|
|
1567
|
+
if (!columnName) {
|
|
1568
|
+
throw new Error(`Unknown pluck column "${pluckEntry.column}" for ${targetModelClass.name}`)
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (pluckEntry.path.length > 0) {
|
|
1572
|
+
this.ensureFrontendModelJoinPath({path: pluckEntry.path, query: pluckQuery})
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const tableReference = pluckQuery.getTableReferenceForJoin(...pluckEntry.path)
|
|
1576
|
+
const columnSql = `${pluckQuery.driver.quoteTable(tableReference)}.${pluckQuery.driver.quoteColumn(columnName)}`
|
|
1577
|
+
const alias = `frontend_model_pluck_${pluckIndex}`
|
|
1578
|
+
|
|
1579
|
+
pluckQuery.select(`${columnSql} AS ${pluckQuery.driver.quoteColumn(alias)}`)
|
|
1580
|
+
aliases.push(alias)
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
const rows = await pluckQuery.results()
|
|
1584
|
+
|
|
1585
|
+
if (aliases.length === 1) {
|
|
1586
|
+
const [alias] = aliases
|
|
1587
|
+
|
|
1588
|
+
return rows.map((row) => /**
|
|
1589
|
+
* Types the following value.
|
|
1590
|
+
@type {Record<string, ?>} */ (row)[alias])
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return rows.map((row) => {
|
|
1594
|
+
const rowHash = /**
|
|
1595
|
+
* Types the following value.
|
|
1596
|
+
@type {Record<string, ?>} */ (row)
|
|
1597
|
+
|
|
1598
|
+
return aliases.map((alias) => rowHash[alias])
|
|
1599
|
+
})
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/**
|
|
1603
|
+
* Runs frontend model search target model class.
|
|
1604
|
+
* @param {object} args - Search args.
|
|
1605
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Root model class.
|
|
1606
|
+
* @param {string[]} args.path - Relationship path.
|
|
1607
|
+
* @returns {typeof import("./database/record/index.js").default} - Target model class.
|
|
1608
|
+
*/
|
|
1609
|
+
frontendModelSearchTargetModelClass({modelClass, path}) {
|
|
1610
|
+
let targetModelClass = modelClass
|
|
1611
|
+
|
|
1612
|
+
for (const relationshipName of path) {
|
|
1613
|
+
const relationship = targetModelClass.getRelationshipsMap()[relationshipName]
|
|
1614
|
+
|
|
1615
|
+
if (!relationship) {
|
|
1616
|
+
throw new Error(`Unknown search relationship "${relationshipName}" for ${targetModelClass.name}`)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const relationshipTargetModelClass = relationship.getTargetModelClass()
|
|
1620
|
+
|
|
1621
|
+
if (!relationshipTargetModelClass) {
|
|
1622
|
+
throw new Error(`No target model class for ${targetModelClass.name}#${relationshipName}`)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
targetModelClass = relationshipTargetModelClass
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return targetModelClass
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* Runs apply frontend model search.
|
|
1633
|
+
* @param {object} args - Search args.
|
|
1634
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1635
|
+
* @param {FrontendModelSearch} args.search - Search filter.
|
|
1636
|
+
* @returns {void}
|
|
1637
|
+
*/
|
|
1638
|
+
applyFrontendModelSearch({query, search}) {
|
|
1639
|
+
const modelClass = this.frontendModelClass()
|
|
1640
|
+
const targetModelClass = this.frontendModelSearchTargetModelClass({
|
|
1641
|
+
modelClass,
|
|
1642
|
+
path: search.path
|
|
1643
|
+
})
|
|
1644
|
+
const columnName = this.resolveFrontendModelColumnName(targetModelClass, search.column)
|
|
1645
|
+
|
|
1646
|
+
if (!columnName) {
|
|
1647
|
+
throw new Error(`Unknown search column "${search.column}" for ${targetModelClass.name}`)
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
if (search.path.length > 0) {
|
|
1651
|
+
this.ensureFrontendModelJoinPath({path: search.path, query})
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const tableReference = query.getTableReferenceForJoin(...search.path)
|
|
1655
|
+
const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
|
|
1656
|
+
const operatorMap = {
|
|
1657
|
+
eq: "=",
|
|
1658
|
+
gt: ">",
|
|
1659
|
+
gteq: ">=",
|
|
1660
|
+
like: "LIKE",
|
|
1661
|
+
lt: "<",
|
|
1662
|
+
lteq: "<=",
|
|
1663
|
+
notEq: "!="
|
|
1664
|
+
}
|
|
1665
|
+
const sqlOperator = operatorMap[search.operator]
|
|
1666
|
+
|
|
1667
|
+
if (search.operator === "eq") {
|
|
1668
|
+
if (this.applyFrontendModelArraySearch({emptySql: "1=0", operatorSql: "IN", query, search, columnSql})) return
|
|
1669
|
+
|
|
1670
|
+
if (search.value === null) {
|
|
1671
|
+
query.where(`${columnSql} IS NULL`)
|
|
1672
|
+
return
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (search.operator === "notEq") {
|
|
1677
|
+
if (this.applyFrontendModelArraySearch({emptySql: "1=1", operatorSql: "NOT IN", query, search, columnSql})) return
|
|
1678
|
+
|
|
1679
|
+
if (search.value === null) {
|
|
1680
|
+
query.where(`${columnSql} IS NOT NULL`)
|
|
1681
|
+
return
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
query.where(`${columnSql} ${sqlOperator} ${query.driver.quote(search.value)}`)
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Apply array-valued equality search filters.
|
|
1690
|
+
* @param {object} args - Search arguments.
|
|
1691
|
+
* @param {string} args.columnSql - SQL for the searched column.
|
|
1692
|
+
* @param {string} args.emptySql - SQL predicate used when the array is empty.
|
|
1693
|
+
* @param {"IN" | "NOT IN"} args.operatorSql - SQL array operator.
|
|
1694
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1695
|
+
* @param {FrontendModelSearch} args.search - Search descriptor.
|
|
1696
|
+
* @returns {boolean} - Whether an array predicate was applied.
|
|
1697
|
+
*/
|
|
1698
|
+
applyFrontendModelArraySearch({columnSql, emptySql, operatorSql, query, search}) {
|
|
1699
|
+
if (!Array.isArray(search.value)) return false
|
|
1700
|
+
|
|
1701
|
+
if (search.value.length === 0) {
|
|
1702
|
+
query.where(emptySql)
|
|
1703
|
+
} else {
|
|
1704
|
+
query.where(`${columnSql} ${operatorSql} (${search.value.map((entry) => query.driver.quote(entry)).join(", ")})`)
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return true
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Runs apply frontend model pagination.
|
|
1712
|
+
* @param {object} args - Pagination args.
|
|
1713
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1714
|
+
* @param {FrontendModelPagination} args.pagination - Pagination values.
|
|
1715
|
+
* @returns {void}
|
|
1716
|
+
*/
|
|
1717
|
+
applyFrontendModelPagination({query, pagination}) {
|
|
1718
|
+
if (pagination.limit !== null) {
|
|
1719
|
+
query.limit(pagination.limit)
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
if (pagination.offset !== null) {
|
|
1723
|
+
query.offset(pagination.offset)
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
if (pagination.perPage !== null) {
|
|
1727
|
+
query.perPage(pagination.perPage)
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (pagination.page !== null) {
|
|
1731
|
+
query.page(pagination.page)
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Runs apply frontend model where.
|
|
1737
|
+
* @param {object} args - Where args.
|
|
1738
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1739
|
+
* @param {Record<string, ?>} args.where - Root-model where conditions.
|
|
1740
|
+
* @returns {void}
|
|
1741
|
+
*/
|
|
1742
|
+
applyFrontendModelWhere({query, where}) {
|
|
1743
|
+
this.applyFrontendModelWhereForPath({
|
|
1744
|
+
modelClass: this.frontendModelClass(),
|
|
1745
|
+
path: [],
|
|
1746
|
+
query,
|
|
1747
|
+
where
|
|
1748
|
+
})
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Runs apply frontend model joins.
|
|
1753
|
+
* @param {object} args - Joins args.
|
|
1754
|
+
* @param {Record<string, ?>} args.joins - Relationship-object joins.
|
|
1755
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1756
|
+
* @returns {void}
|
|
1757
|
+
*/
|
|
1758
|
+
applyFrontendModelJoins({joins, query}) {
|
|
1759
|
+
const joinPathKeys = new Set()
|
|
1760
|
+
|
|
1761
|
+
this.applyFrontendModelJoinsForPath({
|
|
1762
|
+
joins,
|
|
1763
|
+
joinPathKeys,
|
|
1764
|
+
modelClass: this.frontendModelClass(),
|
|
1765
|
+
path: [],
|
|
1766
|
+
query
|
|
1767
|
+
})
|
|
1768
|
+
|
|
1769
|
+
query.joins(joins)
|
|
1770
|
+
|
|
1771
|
+
const queryMetadata = frontendModelQueryMetadata(query)
|
|
1772
|
+
const joinedPaths = queryMetadata[frontendModelJoinedPathsSymbol] || new Set()
|
|
1773
|
+
|
|
1774
|
+
for (const joinPathKey of joinPathKeys) {
|
|
1775
|
+
joinedPaths.add(joinPathKey)
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
queryMetadata[frontendModelJoinedPathsSymbol] = joinedPaths
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Runs apply frontend model joins for path.
|
|
1783
|
+
* @param {object} args - Joins args.
|
|
1784
|
+
* @param {Record<string, ?>} args.joins - Joins for current path.
|
|
1785
|
+
* @param {Set<string>} args.joinPathKeys - Joined path keys.
|
|
1786
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class for current path.
|
|
1787
|
+
* @param {string[]} args.path - Relationship path.
|
|
1788
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1789
|
+
* @returns {void}
|
|
1790
|
+
*/
|
|
1791
|
+
applyFrontendModelJoinsForPath({joins, joinPathKeys, modelClass, path, query}) {
|
|
1792
|
+
void query
|
|
1793
|
+
|
|
1794
|
+
for (const [relationshipName, relationshipJoin] of Object.entries(joins)) {
|
|
1795
|
+
const relationship = modelClass.getRelationshipsMap()[relationshipName]
|
|
1796
|
+
|
|
1797
|
+
if (!relationship) {
|
|
1798
|
+
throw new Error(`Unknown join relationship "${relationshipName}" for ${modelClass.name}`)
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const targetModelClass = relationship.getTargetModelClass()
|
|
1802
|
+
|
|
1803
|
+
if (!targetModelClass) {
|
|
1804
|
+
throw new Error(`No target model class for join relationship "${relationshipName}" on ${modelClass.name}`)
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const relationshipPath = [...path, relationshipName]
|
|
1808
|
+
joinPathKeys.add(relationshipPath.join("."))
|
|
1809
|
+
|
|
1810
|
+
if (relationshipJoin === true) continue
|
|
1811
|
+
|
|
1812
|
+
this.applyFrontendModelJoinsForPath({
|
|
1813
|
+
joins: relationshipJoin,
|
|
1814
|
+
joinPathKeys,
|
|
1815
|
+
modelClass: targetModelClass,
|
|
1816
|
+
path: relationshipPath,
|
|
1817
|
+
query
|
|
1818
|
+
})
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* Resolves a key that may be either a camelCase attribute name or a raw DB
|
|
1824
|
+
* column name to its canonical column name. Returns `undefined` when the
|
|
1825
|
+
* key matches neither map.
|
|
1826
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
1827
|
+
* @param {string} key - Attribute name or column name to resolve.
|
|
1828
|
+
* @returns {string | undefined} - Resolved DB column name, or `undefined`.
|
|
1829
|
+
*/
|
|
1830
|
+
resolveFrontendModelColumnName(modelClass, key) {
|
|
1831
|
+
const attributeNameToColumnNameMap = modelClass.getAttributeNameToColumnNameMap()
|
|
1832
|
+
const columnName = attributeNameToColumnNameMap[key]
|
|
1833
|
+
|
|
1834
|
+
if (columnName) return columnName
|
|
1835
|
+
|
|
1836
|
+
// Fall back: check whether the key is already a raw DB column name.
|
|
1837
|
+
const columnNameToAttributeNameMap = modelClass.getColumnNameToAttributeNameMap()
|
|
1838
|
+
|
|
1839
|
+
if (columnNameToAttributeNameMap[key]) return key
|
|
1840
|
+
|
|
1841
|
+
return undefined
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
/**
|
|
1845
|
+
* Runs apply frontend model where for path.
|
|
1846
|
+
* @param {object} args - Where args.
|
|
1847
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class for current where scope.
|
|
1848
|
+
* @param {string[]} args.path - Relationship path from root.
|
|
1849
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1850
|
+
* @param {Record<string, ?>} args.where - Where conditions for current scope.
|
|
1851
|
+
* @returns {void}
|
|
1852
|
+
*/
|
|
1853
|
+
applyFrontendModelWhereForPath({modelClass, path, query, where}) {
|
|
1854
|
+
for (const [attributeName, value] of Object.entries(where)) {
|
|
1855
|
+
const columnName = this.resolveFrontendModelColumnName(modelClass, attributeName)
|
|
1856
|
+
|
|
1857
|
+
if (columnName) {
|
|
1858
|
+
this.ensureFrontendModelJoinPath({path, query})
|
|
1859
|
+
|
|
1860
|
+
const tableReference = query.getTableReferenceForJoin(...path)
|
|
1861
|
+
const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
|
|
1862
|
+
|
|
1863
|
+
if (Array.isArray(value)) {
|
|
1864
|
+
if (value.length === 0) {
|
|
1865
|
+
query.where("1=0")
|
|
1866
|
+
} else {
|
|
1867
|
+
const normalizedValues = value.map((entry) => this.normalizeFrontendModelWhereColumnValue({columnName, modelClass, value: entry}))
|
|
1868
|
+
|
|
1869
|
+
if (normalizedValues.includes(frontendModelWhereNoMatchSymbol)) {
|
|
1870
|
+
query.where("1=0")
|
|
1871
|
+
} else {
|
|
1872
|
+
query.where(`${columnSql} IN (${normalizedValues.map((entry) => query.driver.quote(entry)).join(", ")})`)
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
continue
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
if (value == null) {
|
|
1880
|
+
query.where(`${columnSql} IS NULL`)
|
|
1881
|
+
} else {
|
|
1882
|
+
const normalizedValue = this.normalizeFrontendModelWhereColumnValue({columnName, modelClass, value})
|
|
1883
|
+
|
|
1884
|
+
if (normalizedValue === frontendModelWhereNoMatchSymbol) {
|
|
1885
|
+
query.where("1=0")
|
|
1886
|
+
} else {
|
|
1887
|
+
query.where(`${columnSql} = ${query.driver.quote(normalizedValue)}`)
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
continue
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (isPlainObject(value)) {
|
|
1895
|
+
const relationship = modelClass.getRelationshipsMap()[attributeName]
|
|
1896
|
+
|
|
1897
|
+
if (!relationship) {
|
|
1898
|
+
throw new Error(`Unknown where relationship "${attributeName}" for ${modelClass.name}`)
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const targetModelClass = relationship.getTargetModelClass()
|
|
1902
|
+
|
|
1903
|
+
if (!targetModelClass) {
|
|
1904
|
+
throw new Error(`No target model class for where relationship "${attributeName}" on ${modelClass.name}`)
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
const relationshipPath = [...path, attributeName]
|
|
1908
|
+
|
|
1909
|
+
this.applyFrontendModelWhereForPath({
|
|
1910
|
+
modelClass: targetModelClass,
|
|
1911
|
+
path: relationshipPath,
|
|
1912
|
+
query,
|
|
1913
|
+
where: value
|
|
1914
|
+
})
|
|
1915
|
+
|
|
1916
|
+
continue
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
throw new Error(`Unknown where column "${attributeName}" for ${modelClass.name}`)
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Runs normalize frontend model where column value.
|
|
1925
|
+
* @param {object} args - Args.
|
|
1926
|
+
* @param {typeof import("./database/record/index.js").default} args.modelClass - Model class.
|
|
1927
|
+
* @param {string} args.columnName - Column name.
|
|
1928
|
+
* @param {?} args.value - Where value.
|
|
1929
|
+
* @returns {? | symbol} - SQL-safe where value.
|
|
1930
|
+
*/
|
|
1931
|
+
normalizeFrontendModelWhereColumnValue({columnName, modelClass, value}) {
|
|
1932
|
+
if (typeof value === "string") {
|
|
1933
|
+
const columnType = modelClass.getColumnTypeByName(columnName)?.toLowerCase()
|
|
1934
|
+
const isDateTimeColumn = typeof columnType === "string" && ["date", "datetime", "timestamp"].some((type) => columnType.includes(type))
|
|
1935
|
+
|
|
1936
|
+
if (isDateTimeColumn) {
|
|
1937
|
+
const parsedDate = new Date(value)
|
|
1938
|
+
|
|
1939
|
+
if (!Number.isNaN(parsedDate.getTime())) {
|
|
1940
|
+
return parsedDate
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
if (isPlainObject(value)) {
|
|
1946
|
+
const columnType = modelClass.getColumnTypeByName(columnName)
|
|
1947
|
+
|
|
1948
|
+
if (typeof columnType !== "string") {
|
|
1949
|
+
return frontendModelWhereNoMatchSymbol
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
const normalizedType = columnType.toLowerCase()
|
|
1953
|
+
const objectValueTypes = new Set(["char", "varchar", "nvarchar", "string", "enum", "json", "jsonb", "citext", "binary", "varbinary"])
|
|
1954
|
+
const supportsObjectValues = normalizedType.includes("text") || objectValueTypes.has(normalizedType)
|
|
1955
|
+
|
|
1956
|
+
if (!supportsObjectValues) {
|
|
1957
|
+
return frontendModelWhereNoMatchSymbol
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
return JSON.stringify(value)
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
return value
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* Runs apply frontend model group.
|
|
1968
|
+
* @param {object} args - Group args.
|
|
1969
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1970
|
+
* @param {FrontendModelGroup} args.group - Group definition.
|
|
1971
|
+
* @returns {void}
|
|
1972
|
+
*/
|
|
1973
|
+
applyFrontendModelGroup({query, group}) {
|
|
1974
|
+
const modelClass = this.frontendModelClass()
|
|
1975
|
+
const targetModelClass = this.frontendModelSearchTargetModelClass({
|
|
1976
|
+
modelClass,
|
|
1977
|
+
path: group.path
|
|
1978
|
+
})
|
|
1979
|
+
const columnName = this.resolveFrontendModelColumnName(targetModelClass, group.column)
|
|
1980
|
+
|
|
1981
|
+
if (!columnName) {
|
|
1982
|
+
throw new Error(`Unknown group column "${group.column}" for ${targetModelClass.name}`)
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
this.ensureFrontendModelJoinPath({path: group.path, query})
|
|
1986
|
+
|
|
1987
|
+
const tableReference = query.getTableReferenceForJoin(...group.path)
|
|
1988
|
+
const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
|
|
1989
|
+
|
|
1990
|
+
this.ensureFrontendModelGroupColumn({columnSql, query})
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Adds root-model columns to GROUP BY so strict SQL engines accept default root-table selects.
|
|
1995
|
+
* @param {object} args - Args.
|
|
1996
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
1997
|
+
* @returns {void}
|
|
1998
|
+
*/
|
|
1999
|
+
applyFrontendModelRootGroupColumns({query}) {
|
|
2000
|
+
const modelClass = this.frontendModelClass()
|
|
2001
|
+
const attributeNameToColumnNameMap = modelClass.getAttributeNameToColumnNameMap()
|
|
2002
|
+
const rootTableReference = query.getTableReferenceForJoin()
|
|
2003
|
+
|
|
2004
|
+
for (const columnName of Object.values(attributeNameToColumnNameMap)) {
|
|
2005
|
+
const columnSql = `${query.driver.quoteTable(rootTableReference)}.${query.driver.quoteColumn(columnName)}`
|
|
2006
|
+
|
|
2007
|
+
this.ensureFrontendModelGroupColumn({columnSql, query})
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/**
|
|
2012
|
+
* Ensures a group-by SQL column is only appended once.
|
|
2013
|
+
* @param {object} args - Args.
|
|
2014
|
+
* @param {string} args.columnSql - Fully-qualified column SQL.
|
|
2015
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
2016
|
+
* @returns {void}
|
|
2017
|
+
*/
|
|
2018
|
+
ensureFrontendModelGroupColumn({columnSql, query}) {
|
|
2019
|
+
const queryMetadata = frontendModelQueryMetadata(query)
|
|
2020
|
+
const groupedColumns = queryMetadata[frontendModelGroupedColumnsSymbol] || new Set()
|
|
2021
|
+
|
|
2022
|
+
if (groupedColumns.has(columnSql)) return
|
|
2023
|
+
|
|
2024
|
+
query.group(columnSql)
|
|
2025
|
+
groupedColumns.add(columnSql)
|
|
2026
|
+
queryMetadata[frontendModelGroupedColumnsSymbol] = groupedColumns
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Runs apply frontend model translated attribute preloads.
|
|
2031
|
+
* @param {object} args - Args.
|
|
2032
|
+
* @param {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} args.query - Query instance.
|
|
2033
|
+
* @returns {import("./database/query/model-class-query.js").default<typeof import("./database/record/index.js").default>} - Query with translations preloaded if needed.
|
|
2034
|
+
*/
|
|
2035
|
+
applyFrontendModelTranslatedAttributePreloads({query}) {
|
|
2036
|
+
const modelClass = this.frontendModelClass()
|
|
2037
|
+
const selectedAttributes = this.frontendModelEffectiveSelectedAttributesForModelClass(modelClass, this.frontendModelDefaultAttributesForModelClass(modelClass) || [])
|
|
2038
|
+
|| this.frontendModelDefaultAttributesForModelClass(modelClass)
|
|
2039
|
+
|
|
2040
|
+
if (!selectedAttributes) return query
|
|
2041
|
+
|
|
2042
|
+
const resource = this.frontendModelResourceInstance()
|
|
2043
|
+
const resourceClass = /**
|
|
2044
|
+
* Types the following value.
|
|
2045
|
+
@type {typeof import("./frontend-model-resource/base-resource.js").default} */ (resource.constructor)
|
|
2046
|
+
const translatedSet = new Set(resourceClass.translatedAttributes || [])
|
|
2047
|
+
let needsTranslations = false
|
|
2048
|
+
|
|
2049
|
+
for (const attributeName of selectedAttributes) {
|
|
2050
|
+
const hookName = `${attributeName}AttributeSelected`
|
|
2051
|
+
const dynamicResource = /**
|
|
2052
|
+
* Types the following value.
|
|
2053
|
+
@type {Record<string, ?>} */ (/**
|
|
2054
|
+
* Types the following value.
|
|
2055
|
+
@type {?} */ (resource))
|
|
2056
|
+
|
|
2057
|
+
if (typeof dynamicResource[hookName] === "function") {
|
|
2058
|
+
const result = dynamicResource[hookName]({query})
|
|
2059
|
+
|
|
2060
|
+
if (result) {
|
|
2061
|
+
query = result
|
|
2062
|
+
}
|
|
2063
|
+
} else if (translatedSet.has(attributeName)) {
|
|
2064
|
+
needsTranslations = true
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
if (needsTranslations) {
|
|
2069
|
+
query = query.preload({translations: {}})
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
return query
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* Runs apply frontend model sort.
|
|
2077
|
+
* @param {object} args - Sort args.
|
|
2078
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
2079
|
+
* @param {FrontendModelSort} args.sort - Sort definition.
|
|
2080
|
+
* @returns {void}
|
|
2081
|
+
*/
|
|
2082
|
+
applyFrontendModelSort({query, sort}) {
|
|
2083
|
+
const modelClass = this.frontendModelClass()
|
|
2084
|
+
const targetModelClass = this.frontendModelSearchTargetModelClass({
|
|
2085
|
+
modelClass,
|
|
2086
|
+
path: sort.path
|
|
2087
|
+
})
|
|
2088
|
+
const translatedAttributesMap = targetModelClass.getTranslationsMap()
|
|
2089
|
+
const translatedAttributeNames = Object.keys(translatedAttributesMap)
|
|
2090
|
+
const isTranslatedSortAttribute = translatedAttributeNames.includes(sort.column)
|
|
2091
|
+
|
|
2092
|
+
const columnName = this.resolveFrontendModelColumnName(targetModelClass, sort.column)
|
|
2093
|
+
const direction = sort.direction.toUpperCase()
|
|
2094
|
+
|
|
2095
|
+
if (isTranslatedSortAttribute) {
|
|
2096
|
+
const translationModelClass = targetModelClass.getTranslationClass()
|
|
2097
|
+
const translationAttributeNameToColumnNameMap = translationModelClass.getAttributeNameToColumnNameMap()
|
|
2098
|
+
const translationColumnName = translationAttributeNameToColumnNameMap[sort.column]
|
|
2099
|
+
const translationPath = sort.path.concat(["currentTranslation"])
|
|
2100
|
+
|
|
2101
|
+
if (!translationColumnName) {
|
|
2102
|
+
throw new Error(`Unknown translated sort column "${sort.column}" for ${targetModelClass.name}`)
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
this.ensureFrontendModelSortJoinPath({path: translationPath, query})
|
|
2106
|
+
|
|
2107
|
+
const translationTableReference = query.getTableReferenceForJoin(...translationPath)
|
|
2108
|
+
const translationColumnSql = `${query.driver.quoteTable(translationTableReference)}.${query.driver.quoteColumn(translationColumnName)}`
|
|
2109
|
+
|
|
2110
|
+
query.order(`${translationColumnSql} ${direction}`)
|
|
2111
|
+
|
|
2112
|
+
return
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (!columnName) {
|
|
2116
|
+
throw new Error(`Unknown sort column "${sort.column}" for ${targetModelClass.name}`)
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
this.ensureFrontendModelSortJoinPath({path: sort.path, query})
|
|
2120
|
+
|
|
2121
|
+
const tableReference = query.getTableReferenceForJoin(...sort.path)
|
|
2122
|
+
const columnSql = `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnName)}`
|
|
2123
|
+
|
|
2124
|
+
query.order(`${columnSql} ${direction}`)
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* Ensures a sort join path has been joined on query.
|
|
2129
|
+
* @param {object} args - Join args.
|
|
2130
|
+
* @param {string[]} args.path - Relationship join path.
|
|
2131
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
2132
|
+
* @returns {void}
|
|
2133
|
+
*/
|
|
2134
|
+
ensureFrontendModelSortJoinPath({path, query}) {
|
|
2135
|
+
this.ensureFrontendModelJoinPath({path, query})
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/**
|
|
2139
|
+
* Ensures a relationship path has exactly one SQL join.
|
|
2140
|
+
* @param {object} args - Join args.
|
|
2141
|
+
* @param {string[]} args.path - Relationship join path.
|
|
2142
|
+
* @param {import("./database/query/model-class-query.js").default} args.query - Query instance.
|
|
2143
|
+
* @returns {void}
|
|
2144
|
+
*/
|
|
2145
|
+
ensureFrontendModelJoinPath({path, query}) {
|
|
2146
|
+
if (path.length < 1) return
|
|
2147
|
+
|
|
2148
|
+
const queryMetadata = frontendModelQueryMetadata(query)
|
|
2149
|
+
const joinedPaths = queryMetadata[frontendModelJoinedPathsSymbol] || new Set()
|
|
2150
|
+
const pathKey = path.join(".")
|
|
2151
|
+
|
|
2152
|
+
if (joinedPaths.has(pathKey)) return
|
|
2153
|
+
|
|
2154
|
+
query.joins(buildFrontendModelJoinObjectFromPath(path))
|
|
2155
|
+
joinedPaths.add(pathKey)
|
|
2156
|
+
queryMetadata[frontendModelJoinedPathsSymbol] = joinedPaths
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/**
|
|
2160
|
+
* Runs frontend model selected attributes for model class.
|
|
2161
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2162
|
+
* @returns {string[] | null} - Selected attributes for model class.
|
|
2163
|
+
*/
|
|
2164
|
+
frontendModelSelectedAttributesForModelClass(modelClass) {
|
|
2165
|
+
const select = this.frontendModelSelect()
|
|
2166
|
+
|
|
2167
|
+
if (!select) return null
|
|
2168
|
+
|
|
2169
|
+
return select[modelClass.getModelName()] || null
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Runs frontend model selects extra for model class.
|
|
2174
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2175
|
+
* @returns {string[] | null} - Extra attributes (loaded in addition to the defaults) for the model class.
|
|
2176
|
+
*/
|
|
2177
|
+
frontendModelSelectsExtraForModelClass(modelClass) {
|
|
2178
|
+
const selectsExtra = this.frontendModelSelectsExtra()
|
|
2179
|
+
|
|
2180
|
+
if (!selectsExtra) return null
|
|
2181
|
+
|
|
2182
|
+
return selectsExtra[modelClass.getModelName()] || null
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/**
|
|
2186
|
+
* Resolves the final set of attribute names to serialize for a model class:
|
|
2187
|
+
* an explicit narrowing `select` wins; otherwise, when `selectsExtra` is given,
|
|
2188
|
+
* the default attributes plus the extras; otherwise null (default behavior).
|
|
2189
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2190
|
+
* @param {string[]} fallbackAttributeNames - Attribute names to treat as the defaults when the resource declares none.
|
|
2191
|
+
* @returns {string[] | null} - Effective selected attribute names, or null for default serialization.
|
|
2192
|
+
*/
|
|
2193
|
+
frontendModelEffectiveSelectedAttributesForModelClass(modelClass, fallbackAttributeNames) {
|
|
2194
|
+
const selectedAttributes = this.frontendModelSelectedAttributesForModelClass(modelClass)
|
|
2195
|
+
|
|
2196
|
+
if (selectedAttributes) return selectedAttributes
|
|
2197
|
+
|
|
2198
|
+
const extraAttributes = this.frontendModelSelectsExtraForModelClass(modelClass)
|
|
2199
|
+
|
|
2200
|
+
if (!extraAttributes) return null
|
|
2201
|
+
|
|
2202
|
+
const defaultAttributes = this.frontendModelDefaultAttributesForModelClass(modelClass) || fallbackAttributeNames
|
|
2203
|
+
|
|
2204
|
+
return Array.from(new Set([...defaultAttributes, ...extraAttributes]))
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* Runs frontend model default attributes for model class.
|
|
2209
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2210
|
+
* @returns {string[] | null} - Default frontend-model attributes declared on the resource.
|
|
2211
|
+
*/
|
|
2212
|
+
frontendModelDefaultAttributesForModelClass(modelClass) {
|
|
2213
|
+
const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
|
|
2214
|
+
const attributes = frontendModelResource?.resourceConfiguration.attributes
|
|
2215
|
+
|
|
2216
|
+
if (!attributes) return null
|
|
2217
|
+
|
|
2218
|
+
if (Array.isArray(attributes)) {
|
|
2219
|
+
return attributes
|
|
2220
|
+
.filter((entry) => {
|
|
2221
|
+
if (typeof entry === "string") return true
|
|
2222
|
+
|
|
2223
|
+
const config = /**
|
|
2224
|
+
* Types the following value.
|
|
2225
|
+
@type {Record<string, ?>} */ (entry)
|
|
2226
|
+
|
|
2227
|
+
if (config && config.selectedByDefault === false) return false
|
|
2228
|
+
|
|
2229
|
+
return true
|
|
2230
|
+
})
|
|
2231
|
+
.map((entry) => typeof entry === "string" ? entry : /**
|
|
2232
|
+
* Types the following value.
|
|
2233
|
+
@type {Record<string, ?>} */ (entry).name)
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
if (typeof attributes === "object") {
|
|
2237
|
+
return Object.entries(attributes)
|
|
2238
|
+
.filter(([, config]) => {
|
|
2239
|
+
if (!config || typeof config !== "object") return true
|
|
2240
|
+
|
|
2241
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (config).selectedByDefault !== false
|
|
2242
|
+
})
|
|
2243
|
+
.map(([name]) => name)
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
return null
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* Runs frontend model non default attributes for model class.
|
|
2251
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2252
|
+
* @returns {string[]} - Attribute names explicitly marked selectedByDefault: false.
|
|
2253
|
+
*/
|
|
2254
|
+
frontendModelNonDefaultAttributesForModelClass(modelClass) {
|
|
2255
|
+
const frontendModelResource = this.frontendModelResourceConfigurationForModelClass(modelClass)
|
|
2256
|
+
const attributes = frontendModelResource?.resourceConfiguration.attributes
|
|
2257
|
+
|
|
2258
|
+
if (!attributes) return []
|
|
2259
|
+
|
|
2260
|
+
if (Array.isArray(attributes)) {
|
|
2261
|
+
return attributes
|
|
2262
|
+
.filter((entry) => {
|
|
2263
|
+
if (typeof entry !== "object" || !entry) return false
|
|
2264
|
+
|
|
2265
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (entry).selectedByDefault === false
|
|
2266
|
+
})
|
|
2267
|
+
.map((entry) => /**
|
|
2268
|
+
* Types the following value.
|
|
2269
|
+
@type {Record<string, ?>} */ (/**
|
|
2270
|
+
* Types the following value.
|
|
2271
|
+
@type {?} */ (entry)).name)
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (typeof attributes === "object") {
|
|
2275
|
+
return Object.entries(attributes)
|
|
2276
|
+
.filter(([, config]) => typeof config === "object" && config && /**
|
|
2277
|
+
* Types the following value.
|
|
2278
|
+
@type {Record<string, ?>} */ (config).selectedByDefault === false)
|
|
2279
|
+
.map(([name]) => name)
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
return []
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* Runs serialize frontend model attributes.
|
|
2287
|
+
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
2288
|
+
* @returns {Promise<Record<string, ?>>} - Serialized attributes filtered by select map.
|
|
2289
|
+
*/
|
|
2290
|
+
async serializeFrontendModelAttributes(model) {
|
|
2291
|
+
const modelClass = /**
|
|
2292
|
+
* Types the following value.
|
|
2293
|
+
@type {typeof import("./database/record/index.js").default} */ (model.constructor)
|
|
2294
|
+
const modelAttributes = model.attributes()
|
|
2295
|
+
const selectedAttributes = this.frontendModelEffectiveSelectedAttributesForModelClass(modelClass, Object.keys(modelAttributes))
|
|
2296
|
+
const defaultAttributes = this.frontendModelDefaultAttributesForModelClass(modelClass)
|
|
2297
|
+
const resourceInstance = this._serializationResourceInstanceForModel(model)
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* Resource attribute method name.
|
|
2301
|
+
* @param {string} attributeName - Attribute name.
|
|
2302
|
+
*/
|
|
2303
|
+
const resourceAttributeMethodName = (attributeName) => `${attributeName}Attribute`
|
|
2304
|
+
|
|
2305
|
+
/**
|
|
2306
|
+
* Resource has attribute.
|
|
2307
|
+
* @param {string} attributeName - Attribute name.
|
|
2308
|
+
*/
|
|
2309
|
+
const resourceHasAttribute = (attributeName) => {
|
|
2310
|
+
const methodName = resourceAttributeMethodName(attributeName)
|
|
2311
|
+
|
|
2312
|
+
return resourceInstance && typeof /**
|
|
2313
|
+
* Types the following value.
|
|
2314
|
+
@type {Record<string, ?>} */ (/**
|
|
2315
|
+
* Types the following value.
|
|
2316
|
+
@type {?} */ (resourceInstance))[methodName] === "function"
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* Prototype attribute method.
|
|
2321
|
+
* @param {string} attributeName - Attribute name.
|
|
2322
|
+
*/
|
|
2323
|
+
const prototypeAttributeMethod = (attributeName) => {
|
|
2324
|
+
let currentPrototype = Object.getPrototypeOf(model)
|
|
2325
|
+
|
|
2326
|
+
while (currentPrototype && currentPrototype !== Object.prototype) {
|
|
2327
|
+
const candidate = Object.getOwnPropertyDescriptor(currentPrototype, attributeName)?.value
|
|
2328
|
+
|
|
2329
|
+
if (typeof candidate === "function") {
|
|
2330
|
+
return {
|
|
2331
|
+
method: candidate,
|
|
2332
|
+
ownerName: currentPrototype.constructor?.name
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
currentPrototype = Object.getPrototypeOf(currentPrototype)
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
/**
|
|
2341
|
+
* Serialized attribute value.
|
|
2342
|
+
* @param {string} attributeName - Attribute name.
|
|
2343
|
+
*/
|
|
2344
|
+
const serializedAttributeValue = async (attributeName) => {
|
|
2345
|
+
// Check resource instance first (virtual/computed attributes via ${name}Attribute convention)
|
|
2346
|
+
if (resourceHasAttribute(attributeName)) {
|
|
2347
|
+
const methodName = resourceAttributeMethodName(attributeName)
|
|
2348
|
+
|
|
2349
|
+
return await /**
|
|
2350
|
+
* Types the following value.
|
|
2351
|
+
@type {Record<string, Function>} */ (/**
|
|
2352
|
+
* Types the following value.
|
|
2353
|
+
@type {?} */ (resourceInstance))[methodName](model)
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Fall back to model method
|
|
2357
|
+
const attributeMethodLookup = prototypeAttributeMethod(attributeName)
|
|
2358
|
+
const attributeMethod = attributeMethodLookup?.method
|
|
2359
|
+
|
|
2360
|
+
if (typeof attributeMethod === "function") {
|
|
2361
|
+
return await attributeMethod.call(model)
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
return modelAttributes[attributeName]
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* Attribute exists.
|
|
2369
|
+
* @param {string} attributeName - Attribute name.
|
|
2370
|
+
*/
|
|
2371
|
+
const attributeExists = (attributeName) => {
|
|
2372
|
+
return (attributeName in modelAttributes) || (attributeName in /**
|
|
2373
|
+
* Types the following value.
|
|
2374
|
+
@type {Record<string, ?>} */ (model)) || resourceHasAttribute(attributeName)
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (!selectedAttributes) {
|
|
2378
|
+
if (!defaultAttributes || defaultAttributes.length < 1) {
|
|
2379
|
+
return modelAttributes
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const excludedAttributes = this.frontendModelNonDefaultAttributesForModelClass(modelClass)
|
|
2383
|
+
const serializedAttributes = {...modelAttributes}
|
|
2384
|
+
|
|
2385
|
+
for (const excludedName of excludedAttributes) {
|
|
2386
|
+
delete serializedAttributes[excludedName]
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
for (const attributeName of defaultAttributes) {
|
|
2390
|
+
if (!attributeExists(attributeName)) continue
|
|
2391
|
+
serializedAttributes[attributeName] = await serializedAttributeValue(attributeName)
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
return serializedAttributes
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* Serialized attributes.
|
|
2399
|
+
@type {Record<string, ?>} */
|
|
2400
|
+
const serializedAttributes = {}
|
|
2401
|
+
|
|
2402
|
+
for (const attributeName of selectedAttributes) {
|
|
2403
|
+
if (!attributeExists(attributeName)) continue
|
|
2404
|
+
serializedAttributes[attributeName] = await serializedAttributeValue(attributeName)
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
return serializedAttributes
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
/**
|
|
2411
|
+
* Runs serialization resource instance for model.
|
|
2412
|
+
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
2413
|
+
* @returns {import("./frontend-model-resource/base-resource.js").default | null} - Resource instance or null.
|
|
2414
|
+
*/
|
|
2415
|
+
_serializationResourceInstanceForModel(model) {
|
|
2416
|
+
const resource = this.frontendModelResourceInstance()
|
|
2417
|
+
|
|
2418
|
+
if (resource.modelClass() === model.constructor) {
|
|
2419
|
+
return resource
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const configuration = this.getConfiguration()
|
|
2423
|
+
const backendProjects = configuration.getBackendProjects?.() || []
|
|
2424
|
+
const modelClassName = /**
|
|
2425
|
+
* Types the following value.
|
|
2426
|
+
@type {typeof import("./database/record/index.js").default} */ (model.constructor).getModelName()
|
|
2427
|
+
|
|
2428
|
+
for (const backendProject of backendProjects) {
|
|
2429
|
+
const resources = frontendModelResourcesForBackendProject(backendProject)
|
|
2430
|
+
const resourceDefinition = resources[modelClassName]
|
|
2431
|
+
const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
|
|
2432
|
+
|
|
2433
|
+
if (resourceClass) {
|
|
2434
|
+
return new resourceClass({
|
|
2435
|
+
ability: this.currentAbility(),
|
|
2436
|
+
context: this.currentAbility()?.getContext() || {},
|
|
2437
|
+
locals: this.currentAbility()?.getLocals() || {},
|
|
2438
|
+
modelClass: /**
|
|
2439
|
+
* Types the following value.
|
|
2440
|
+
@type {typeof import("./database/record/index.js").default} */ (model.constructor),
|
|
2441
|
+
modelName: modelClassName,
|
|
2442
|
+
params: {},
|
|
2443
|
+
resourceConfiguration: /**
|
|
2444
|
+
* Types the following value.
|
|
2445
|
+
@type {import("./configuration-types.js").FrontendModelResourceConfiguration | undefined} */ (typeof resourceClass.resourceConfig === "function" ? resourceClass.resourceConfig() : undefined)
|
|
2446
|
+
})
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
return null
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/**
|
|
2454
|
+
* Runs frontend model filter serializable related models.
|
|
2455
|
+
* @param {object} args - Arguments.
|
|
2456
|
+
* @param {import("./database/record/index.js").default[]} args.models - Frontend model records.
|
|
2457
|
+
* @param {boolean} args.relationshipIsCollection - Whether relation is has-many.
|
|
2458
|
+
* @returns {Promise<import("./database/record/index.js").default[]>} - Serializable related models.
|
|
2459
|
+
*/
|
|
2460
|
+
async frontendModelFilterSerializableRelatedModels({models, relationshipIsCollection}) {
|
|
2461
|
+
if (!this.currentAbility()) return models
|
|
2462
|
+
if (models.length === 0) return models
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Models by class.
|
|
2466
|
+
@type {Map<typeof import("./database/record/index.js").default, import("./database/record/index.js").default[]>} */
|
|
2467
|
+
const modelsByClass = new Map()
|
|
2468
|
+
|
|
2469
|
+
for (const model of models) {
|
|
2470
|
+
const relatedModelClass = /**
|
|
2471
|
+
* Types the following value.
|
|
2472
|
+
@type {typeof import("./database/record/index.js").default} */ (model.constructor)
|
|
2473
|
+
const existingModelsForClass = modelsByClass.get(relatedModelClass) || []
|
|
2474
|
+
|
|
2475
|
+
existingModelsForClass.push(model)
|
|
2476
|
+
modelsByClass.set(relatedModelClass, existingModelsForClass)
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/**
|
|
2480
|
+
* Authorized ids by class.
|
|
2481
|
+
@type {Map<typeof import("./database/record/index.js").default, Set<string>>} */
|
|
2482
|
+
const authorizedIdsByClass = new Map()
|
|
2483
|
+
/**
|
|
2484
|
+
* Primary keys by class.
|
|
2485
|
+
@type {Map<typeof import("./database/record/index.js").default, string>} */
|
|
2486
|
+
const primaryKeysByClass = new Map()
|
|
2487
|
+
|
|
2488
|
+
for (const [relatedModelClass, relatedModels] of modelsByClass.entries()) {
|
|
2489
|
+
const relatedResource = this.frontendModelResourceConfigurationForModelClass(relatedModelClass)
|
|
2490
|
+
|
|
2491
|
+
if (!relatedResource) {
|
|
2492
|
+
authorizedIdsByClass.set(relatedModelClass, new Set())
|
|
2493
|
+
continue
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
const abilityAction = relationshipIsCollection
|
|
2497
|
+
? relatedResource.resourceConfiguration.abilities?.index
|
|
2498
|
+
: relatedResource.resourceConfiguration.abilities?.find
|
|
2499
|
+
|
|
2500
|
+
if (typeof abilityAction !== "string" || abilityAction.length < 1) {
|
|
2501
|
+
authorizedIdsByClass.set(relatedModelClass, new Set())
|
|
2502
|
+
continue
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
const primaryKey = relatedModelClass.primaryKey()
|
|
2506
|
+
const ids = relatedModels
|
|
2507
|
+
.map((model) => model.attributes()[primaryKey])
|
|
2508
|
+
.filter((id) => id !== undefined && id !== null)
|
|
2509
|
+
|
|
2510
|
+
if (ids.length < 1) {
|
|
2511
|
+
authorizedIdsByClass.set(relatedModelClass, new Set())
|
|
2512
|
+
continue
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
const authorizedIdsRaw = await relatedModelClass
|
|
2516
|
+
.accessibleFor(abilityAction)
|
|
2517
|
+
.where({[primaryKey]: ids})
|
|
2518
|
+
.pluck(primaryKey)
|
|
2519
|
+
|
|
2520
|
+
primaryKeysByClass.set(relatedModelClass, primaryKey)
|
|
2521
|
+
authorizedIdsByClass.set(relatedModelClass, new Set(authorizedIdsRaw.map((id) => String(id))))
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
return models.filter((model) => {
|
|
2525
|
+
const relatedModelClass = /**
|
|
2526
|
+
* Types the following value.
|
|
2527
|
+
@type {typeof import("./database/record/index.js").default} */ (model.constructor)
|
|
2528
|
+
const authorizedIds = authorizedIdsByClass.get(relatedModelClass)
|
|
2529
|
+
const primaryKey = primaryKeysByClass.get(relatedModelClass)
|
|
2530
|
+
|
|
2531
|
+
if (!authorizedIds || !primaryKey) return false
|
|
2532
|
+
|
|
2533
|
+
const primaryKeyValue = model.attributes()[primaryKey]
|
|
2534
|
+
|
|
2535
|
+
if (primaryKeyValue === undefined || primaryKeyValue === null) return false
|
|
2536
|
+
|
|
2537
|
+
return authorizedIds.has(String(primaryKeyValue))
|
|
2538
|
+
})
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/**
|
|
2542
|
+
* Runs is serializable frontend model.
|
|
2543
|
+
* @param {?} value - Candidate preloaded value.
|
|
2544
|
+
* @returns {value is import("./database/record/index.js").default} - Whether value behaves like a model.
|
|
2545
|
+
*/
|
|
2546
|
+
isSerializableFrontendModel(value) {
|
|
2547
|
+
return Boolean(value && typeof value === "object" && typeof /**
|
|
2548
|
+
* Types the following value.
|
|
2549
|
+
@type {?} */ (value).attributes === "function")
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
/**
|
|
2553
|
+
* Runs serialize frontend models.
|
|
2554
|
+
* @param {import("./database/record/index.js").default[]} models - Models to serialize.
|
|
2555
|
+
* @returns {Promise<Record<string, ?>[]>} - Serialized model payloads.
|
|
2556
|
+
*/
|
|
2557
|
+
async serializeFrontendModels(models) {
|
|
2558
|
+
if (models.length < 1) return []
|
|
2559
|
+
|
|
2560
|
+
/**
|
|
2561
|
+
* Preloaded relationships per model.
|
|
2562
|
+
@type {Array<Record<string, ?>>} */
|
|
2563
|
+
const preloadedRelationshipsPerModel = Array.from({length: models.length}, () => ({}))
|
|
2564
|
+
|
|
2565
|
+
/**
|
|
2566
|
+
* Collection relationship entries.
|
|
2567
|
+
@type {Array<{loadedModels: import("./database/record/index.js").default[], modelIndex: number, relationshipName: string}>} */
|
|
2568
|
+
const collectionRelationshipEntries = []
|
|
2569
|
+
/**
|
|
2570
|
+
* Singular relationship entries.
|
|
2571
|
+
@type {Array<{loadedModel: import("./database/record/index.js").default, modelIndex: number, relationshipName: string}>} */
|
|
2572
|
+
const singularRelationshipEntries = []
|
|
2573
|
+
|
|
2574
|
+
models.forEach((model, modelIndex) => {
|
|
2575
|
+
const modelClass = /**
|
|
2576
|
+
* Types the following value.
|
|
2577
|
+
@type {typeof import("./database/record/index.js").default} */ (model.constructor)
|
|
2578
|
+
const relationshipsMap = modelClass.getRelationshipsMap()
|
|
2579
|
+
|
|
2580
|
+
for (const relationshipName in relationshipsMap) {
|
|
2581
|
+
const relationship = model.getRelationshipByName(relationshipName)
|
|
2582
|
+
|
|
2583
|
+
if (!relationship.getPreloaded()) continue
|
|
2584
|
+
|
|
2585
|
+
const loadedRelationship = relationship.loaded()
|
|
2586
|
+
|
|
2587
|
+
if (Array.isArray(loadedRelationship)) {
|
|
2588
|
+
collectionRelationshipEntries.push({loadedModels: loadedRelationship, modelIndex, relationshipName})
|
|
2589
|
+
continue
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
if (this.isSerializableFrontendModel(loadedRelationship)) {
|
|
2593
|
+
singularRelationshipEntries.push({loadedModel: loadedRelationship, modelIndex, relationshipName})
|
|
2594
|
+
continue
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
preloadedRelationshipsPerModel[modelIndex][relationshipName] = loadedRelationship == undefined ? null : loadedRelationship
|
|
2598
|
+
}
|
|
2599
|
+
})
|
|
2600
|
+
|
|
2601
|
+
if (collectionRelationshipEntries.length > 0) {
|
|
2602
|
+
const allCollectionModels = collectionRelationshipEntries.flatMap((entry) => entry.loadedModels)
|
|
2603
|
+
const serializableCollectionModels = await this.frontendModelFilterSerializableRelatedModels({
|
|
2604
|
+
models: allCollectionModels,
|
|
2605
|
+
relationshipIsCollection: true
|
|
2606
|
+
})
|
|
2607
|
+
const serializableCollectionModelsSet = new Set(serializableCollectionModels)
|
|
2608
|
+
|
|
2609
|
+
for (const relationshipEntry of collectionRelationshipEntries) {
|
|
2610
|
+
const allowedModels = relationshipEntry.loadedModels.filter((relatedModel) => serializableCollectionModelsSet.has(relatedModel))
|
|
2611
|
+
const serializedRelatedModels = await this.serializeFrontendModels(allowedModels)
|
|
2612
|
+
|
|
2613
|
+
preloadedRelationshipsPerModel[relationshipEntry.modelIndex][relationshipEntry.relationshipName] = serializedRelatedModels
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
if (singularRelationshipEntries.length > 0) {
|
|
2618
|
+
const allSingularModels = singularRelationshipEntries.map((entry) => entry.loadedModel)
|
|
2619
|
+
const serializableSingularModels = await this.frontendModelFilterSerializableRelatedModels({
|
|
2620
|
+
models: allSingularModels,
|
|
2621
|
+
relationshipIsCollection: false
|
|
2622
|
+
})
|
|
2623
|
+
const serializableSingularModelsSet = new Set(serializableSingularModels)
|
|
2624
|
+
|
|
2625
|
+
for (const relationshipEntry of singularRelationshipEntries) {
|
|
2626
|
+
if (!serializableSingularModelsSet.has(relationshipEntry.loadedModel)) {
|
|
2627
|
+
preloadedRelationshipsPerModel[relationshipEntry.modelIndex][relationshipEntry.relationshipName] = null
|
|
2628
|
+
continue
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
const serializedModel = (await this.serializeFrontendModels([relationshipEntry.loadedModel]))[0]
|
|
2632
|
+
preloadedRelationshipsPerModel[relationshipEntry.modelIndex][relationshipEntry.relationshipName] = serializedModel
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/**
|
|
2637
|
+
* Serialized models.
|
|
2638
|
+
@type {Record<string, ?>[]} */
|
|
2639
|
+
const serializedModels = []
|
|
2640
|
+
|
|
2641
|
+
for (const [modelIndex, model] of models.entries()) {
|
|
2642
|
+
const serializedAttributes = await this.serializeFrontendModelAttributes(model)
|
|
2643
|
+
const preloadedRelationships = preloadedRelationshipsPerModel[modelIndex]
|
|
2644
|
+
const associationCounts = typeof model.associationCounts === "function"
|
|
2645
|
+
? model.associationCounts()
|
|
2646
|
+
: {}
|
|
2647
|
+
const queryDataValues = typeof model.queryDataValues === "function"
|
|
2648
|
+
? model.queryDataValues()
|
|
2649
|
+
: {}
|
|
2650
|
+
const computedAbilities = typeof model.computedAbilities === "function"
|
|
2651
|
+
? model.computedAbilities()
|
|
2652
|
+
: {}
|
|
2653
|
+
const hasCounts = Object.keys(associationCounts).length > 0
|
|
2654
|
+
const hasQueryData = Object.keys(queryDataValues).length > 0
|
|
2655
|
+
const hasAbilities = Object.keys(computedAbilities).length > 0
|
|
2656
|
+
const hasPreloaded = Object.keys(preloadedRelationships).length > 0
|
|
2657
|
+
|
|
2658
|
+
if (!hasPreloaded && !hasCounts && !hasQueryData && !hasAbilities) {
|
|
2659
|
+
serializedModels.push(serializedAttributes)
|
|
2660
|
+
continue
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/**
|
|
2664
|
+
* Serialized.
|
|
2665
|
+
@type {Record<string, ?>} */
|
|
2666
|
+
const serialized = {...serializedAttributes}
|
|
2667
|
+
|
|
2668
|
+
if (hasPreloaded) serialized.__preloadedRelationships = preloadedRelationships
|
|
2669
|
+
if (hasCounts) serialized.__associationCounts = associationCounts
|
|
2670
|
+
if (hasQueryData) serialized.__queryData = queryDataValues
|
|
2671
|
+
if (hasAbilities) serialized.__abilities = computedAbilities
|
|
2672
|
+
|
|
2673
|
+
serializedModels.push(serialized)
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
return serializedModels
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/**
|
|
2680
|
+
* Runs serialize frontend model.
|
|
2681
|
+
* @param {import("./database/record/index.js").default} model - Frontend model record.
|
|
2682
|
+
* @returns {Promise<Record<string, ?>>} - Serialized frontend model payload.
|
|
2683
|
+
*/
|
|
2684
|
+
async serializeFrontendModel(model) {
|
|
2685
|
+
const serializedModels = await this.serializeFrontendModels([model])
|
|
2686
|
+
|
|
2687
|
+
return serializedModels[0]
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
/**
|
|
2691
|
+
* Runs frontend model render error.
|
|
2692
|
+
* @param {string} errorMessage - Error message.
|
|
2693
|
+
* @returns {Promise<void>} - Resolves when error has been rendered.
|
|
2694
|
+
*/
|
|
2695
|
+
async frontendModelRenderError(errorMessage) {
|
|
2696
|
+
await this.logger.error(`Frontend model request failed: ${errorMessage}`)
|
|
2697
|
+
|
|
2698
|
+
const renderError = /**
|
|
2699
|
+
* Types the following value.
|
|
2700
|
+
@type {((errorMessage: string) => Promise<void>) | undefined} */ (
|
|
2701
|
+
/**
|
|
2702
|
+
* Types the following value.
|
|
2703
|
+
@type {?} */ (this).renderError
|
|
2704
|
+
)
|
|
2705
|
+
|
|
2706
|
+
if (typeof renderError === "function") {
|
|
2707
|
+
await renderError.call(this, frontendModelClientSafeErrorMessage)
|
|
2708
|
+
return
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
await this.render({
|
|
2712
|
+
json: /**
|
|
2713
|
+
* Types the following value.
|
|
2714
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
|
|
2715
|
+
errorMessage: frontendModelClientSafeErrorMessage,
|
|
2716
|
+
status: "error"
|
|
2717
|
+
}))
|
|
2718
|
+
})
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Runs frontend model error payload.
|
|
2723
|
+
* @param {string} errorMessage - Error message.
|
|
2724
|
+
* @returns {Record<string, ?>} - Error payload.
|
|
2725
|
+
*/
|
|
2726
|
+
frontendModelErrorPayload(errorMessage) {
|
|
2727
|
+
return {
|
|
2728
|
+
errorMessage,
|
|
2729
|
+
status: "error"
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
/**
|
|
2734
|
+
* Runs frontend model client safe error payload.
|
|
2735
|
+
* @returns {Record<string, ?>} - Client-safe error payload.
|
|
2736
|
+
*/
|
|
2737
|
+
frontendModelClientSafeErrorPayload() {
|
|
2738
|
+
return this.frontendModelErrorPayload(frontendModelClientSafeErrorMessage)
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
/**
|
|
2742
|
+
* Runs frontend model client error payload for error.
|
|
2743
|
+
* @param {?} error - Caught error.
|
|
2744
|
+
* @returns {Record<string, ?>} - Client payload for the current environment.
|
|
2745
|
+
*/
|
|
2746
|
+
frontendModelClientErrorPayloadForError(error) {
|
|
2747
|
+
const velociousMetadata = frontendModelVelociousMetadataForError(error)
|
|
2748
|
+
|
|
2749
|
+
let validationErrorsPayload = {}
|
|
2750
|
+
|
|
2751
|
+
if (error instanceof ValidationError) {
|
|
2752
|
+
const validationErrors = error.getValidationErrors()
|
|
2753
|
+
const model = error.getModel()
|
|
2754
|
+
/**
|
|
2755
|
+
* Structured errors.
|
|
2756
|
+
@type {Record<string, {type: string, message: string, fullMessage: string}[]>} */
|
|
2757
|
+
const structuredErrors = {}
|
|
2758
|
+
|
|
2759
|
+
for (const attributeName in validationErrors) {
|
|
2760
|
+
structuredErrors[attributeName] = validationErrors[attributeName].map(err => ({
|
|
2761
|
+
type: err.type,
|
|
2762
|
+
message: err.message,
|
|
2763
|
+
fullMessage: `${model.getModelClass().humanAttributeName(attributeName)} ${err.message}`
|
|
2764
|
+
}))
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
validationErrorsPayload = {
|
|
2768
|
+
errorType: "validation_error",
|
|
2769
|
+
validationErrors: structuredErrors
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
return {
|
|
2774
|
+
...this.frontendModelErrorPayload(frontendModelClientMessageForError(error)),
|
|
2775
|
+
...frontendModelDebugPayloadForError({
|
|
2776
|
+
configuration: this.getConfiguration(),
|
|
2777
|
+
environment: this.getConfiguration().getEnvironment(),
|
|
2778
|
+
error
|
|
2779
|
+
}),
|
|
2780
|
+
...(velociousMetadata ? {velocious: velociousMetadata} : {}),
|
|
2781
|
+
...validationErrorsPayload
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
/**
|
|
2786
|
+
* Runs frontend model log endpoint error.
|
|
2787
|
+
* @param {object} args - Error log args.
|
|
2788
|
+
* @param {string} args.action - Endpoint/action label.
|
|
2789
|
+
* @param {?} args.error - Caught error.
|
|
2790
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url" | "custom-command"} [args.commandType] - Frontend-model command type.
|
|
2791
|
+
* @param {string | undefined} [args.model] - Request model name when available.
|
|
2792
|
+
* @param {string | undefined} [args.requestId] - Batch request id when available.
|
|
2793
|
+
* @returns {Promise<void>} - Resolves after logging.
|
|
2794
|
+
*/
|
|
2795
|
+
async frontendModelLogEndpointError({action, error, commandType, model, requestId}) {
|
|
2796
|
+
// Errors annotated with `error.velocious = {...}` are user-flow
|
|
2797
|
+
// failures the developer has marked as expected (bad password,
|
|
2798
|
+
// validation message, etc.). Surface the message + metadata to
|
|
2799
|
+
// the client (handled by frontendModelClientErrorPayloadForError),
|
|
2800
|
+
// but skip the error log so monitoring stays focused on real
|
|
2801
|
+
// backend failures.
|
|
2802
|
+
if (frontendModelErrorHasVelociousMetadata(error)) return
|
|
2803
|
+
|
|
2804
|
+
let resolvedModel = model
|
|
2805
|
+
|
|
2806
|
+
if (!resolvedModel) {
|
|
2807
|
+
try {
|
|
2808
|
+
resolvedModel = this.frontendModelParams().model
|
|
2809
|
+
} catch {
|
|
2810
|
+
resolvedModel = undefined
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
const errorMessage = error instanceof Error
|
|
2815
|
+
? `${error.message}\n${error.stack || ""}`
|
|
2816
|
+
: String(error)
|
|
2817
|
+
|
|
2818
|
+
await this.logger.error(() => ["Frontend model endpoint request failed", {
|
|
2819
|
+
action,
|
|
2820
|
+
commandType,
|
|
2821
|
+
error: errorMessage,
|
|
2822
|
+
model: resolvedModel,
|
|
2823
|
+
requestId
|
|
2824
|
+
}])
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/**
|
|
2828
|
+
* Runs frontend model render command response.
|
|
2829
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
2830
|
+
* @returns {Promise<void>} - Resolves when response has been rendered.
|
|
2831
|
+
*/
|
|
2832
|
+
async frontendModelRenderCommandResponse(action) {
|
|
2833
|
+
try {
|
|
2834
|
+
const responsePayload = await this.frontendModelCommandPayload(action)
|
|
2835
|
+
if (!responsePayload) return
|
|
2836
|
+
|
|
2837
|
+
await this.render({
|
|
2838
|
+
json: /**
|
|
2839
|
+
* Types the following value.
|
|
2840
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
|
|
2841
|
+
})
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
await this.frontendModelLogEndpointError({action, commandType: action, error})
|
|
2844
|
+
|
|
2845
|
+
await this.render({
|
|
2846
|
+
json: /**
|
|
2847
|
+
* Types the following value.
|
|
2848
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue(this.frontendModelClientErrorPayloadForError(error)))
|
|
2849
|
+
})
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/**
|
|
2854
|
+
* Runs frontend model command payload.
|
|
2855
|
+
* @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Frontend action.
|
|
2856
|
+
* @returns {Promise<Record<string, ?> | null>} - Response payload.
|
|
2857
|
+
*/
|
|
2858
|
+
async frontendModelCommandPayload(action) {
|
|
2859
|
+
await this.ensureFrontendModelClassInitialized()
|
|
2860
|
+
|
|
2861
|
+
if (!(await this.runFrontendModelBeforeAction(action))) {
|
|
2862
|
+
return null
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
const resource = this.frontendModelResourceInstance()
|
|
2866
|
+
|
|
2867
|
+
if (action === "index") {
|
|
2868
|
+
if (this.frontendModelCountRequested()) {
|
|
2869
|
+
if (!(await resource.supportsCount("index"))) {
|
|
2870
|
+
throw new Error("count is not supported when resource records are customized")
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
return {
|
|
2874
|
+
count: await resource.count(),
|
|
2875
|
+
status: "success"
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
const pluck = this.frontendModelPluck()
|
|
2880
|
+
|
|
2881
|
+
if (pluck.length > 0) {
|
|
2882
|
+
if (!(await resource.supportsPluck("index"))) {
|
|
2883
|
+
throw new Error("pluck is not supported when resource records are customized")
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
const values = await this.frontendModelPluckValues({
|
|
2887
|
+
pluck,
|
|
2888
|
+
query: this.frontendModelIndexQuery()
|
|
2889
|
+
})
|
|
2890
|
+
|
|
2891
|
+
return {
|
|
2892
|
+
status: "success",
|
|
2893
|
+
values
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
const models = await this.frontendModelRecords()
|
|
2898
|
+
await this.frontendModelComputeAbilities(models)
|
|
2899
|
+
const serializedModels = await Promise.all(models.map(async (model) => await resource.serialize(model, "index")))
|
|
2900
|
+
|
|
2901
|
+
return {
|
|
2902
|
+
models: serializedModels,
|
|
2903
|
+
status: "success"
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
const params = this.frontendModelParams()
|
|
2908
|
+
const modelClass = this.frontendModelClass()
|
|
2909
|
+
const id = params.id
|
|
2910
|
+
|
|
2911
|
+
if (action === "create") {
|
|
2912
|
+
const mutationAttributes = frontendModelMutationAttributes(params)
|
|
2913
|
+
if (typeof mutationAttributes === "string") return this.frontendModelErrorPayload(mutationAttributes)
|
|
2914
|
+
|
|
2915
|
+
const model = await this.frontendModelCreateRecord(mutationAttributes.attributes, mutationAttributes.nestedAttributes)
|
|
2916
|
+
|
|
2917
|
+
if (!model) {
|
|
2918
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
const serializedModel = await resource.serialize(model, "create")
|
|
2922
|
+
|
|
2923
|
+
return frontendModelSerializedModelSuccess(serializedModel)
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
if ((typeof id !== "string" && typeof id !== "number") || `${id}`.length < 1) {
|
|
2927
|
+
return this.frontendModelErrorPayload("Expected model id.")
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
if (action === "attach") {
|
|
2931
|
+
const attachmentName = params.attachmentName
|
|
2932
|
+
const attachmentInput = params.attachment
|
|
2933
|
+
|
|
2934
|
+
if (typeof attachmentName !== "string" || attachmentName.length < 1) {
|
|
2935
|
+
return this.frontendModelErrorPayload("Expected attachmentName.")
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
if (typeof attachmentInput === "undefined") {
|
|
2939
|
+
return this.frontendModelErrorPayload("Expected attachment input.")
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
const model = await this.frontendModelFindRecord("attach", id)
|
|
2943
|
+
|
|
2944
|
+
if (!model) {
|
|
2945
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
await model.getAttachmentByName(attachmentName).attach(attachmentInput)
|
|
2949
|
+
const serializedModel = await this.serializeFrontendModel(model)
|
|
2950
|
+
|
|
2951
|
+
return frontendModelSerializedModelSuccess(serializedModel)
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (action === "download") {
|
|
2955
|
+
const attachmentParams = frontendModelAttachmentParams(params)
|
|
2956
|
+
if (typeof attachmentParams === "string") return this.frontendModelErrorPayload(attachmentParams)
|
|
2957
|
+
|
|
2958
|
+
const model = await this.frontendModelFindRecord("download", id)
|
|
2959
|
+
|
|
2960
|
+
if (!model) {
|
|
2961
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
const downloadedAttachment = await model.getAttachmentByName(attachmentParams.attachmentName).download(attachmentParams.attachmentId)
|
|
2965
|
+
|
|
2966
|
+
if (!downloadedAttachment) {
|
|
2967
|
+
return this.frontendModelErrorPayload("Attachment not found.")
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
return {
|
|
2971
|
+
attachment: {
|
|
2972
|
+
byteSize: downloadedAttachment.byteSize(),
|
|
2973
|
+
contentBase64: downloadedAttachment.content().toString("base64"),
|
|
2974
|
+
contentType: downloadedAttachment.contentType(),
|
|
2975
|
+
filename: downloadedAttachment.filename(),
|
|
2976
|
+
id: downloadedAttachment.id(),
|
|
2977
|
+
url: downloadedAttachment.url()
|
|
2978
|
+
},
|
|
2979
|
+
status: "success"
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
if (action === "url") {
|
|
2984
|
+
const attachmentParams = frontendModelAttachmentParams(params)
|
|
2985
|
+
if (typeof attachmentParams === "string") return this.frontendModelErrorPayload(attachmentParams)
|
|
2986
|
+
|
|
2987
|
+
const model = await this.frontendModelFindRecord("url", id)
|
|
2988
|
+
|
|
2989
|
+
if (!model) {
|
|
2990
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
const url = await model.getAttachmentByName(attachmentParams.attachmentName).url(attachmentParams.attachmentId)
|
|
2994
|
+
|
|
2995
|
+
if (!url) {
|
|
2996
|
+
return this.frontendModelErrorPayload("Attachment URL not available.")
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
return {
|
|
3000
|
+
status: "success",
|
|
3001
|
+
url
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
if (action === "find") {
|
|
3006
|
+
const model = await this.frontendModelFindRecord("find", id)
|
|
3007
|
+
|
|
3008
|
+
if (!model) {
|
|
3009
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
await this.frontendModelComputeAbilities([model])
|
|
3013
|
+
const serializedModel = await resource.serialize(model, "find")
|
|
3014
|
+
|
|
3015
|
+
return frontendModelSerializedModelSuccess(serializedModel)
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
if (action === "update") {
|
|
3019
|
+
const mutationAttributes = frontendModelMutationAttributes(params)
|
|
3020
|
+
if (typeof mutationAttributes === "string") return this.frontendModelErrorPayload(mutationAttributes)
|
|
3021
|
+
|
|
3022
|
+
const model = await this.frontendModelFindRecord("update", id)
|
|
3023
|
+
|
|
3024
|
+
if (!model) {
|
|
3025
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
const updatedModel = await resource.update(model, mutationAttributes.attributes, {nestedAttributes: mutationAttributes.nestedAttributes, controller: this})
|
|
3029
|
+
const serializedModel = await resource.serialize(updatedModel, "update")
|
|
3030
|
+
|
|
3031
|
+
return frontendModelSerializedModelSuccess(serializedModel)
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
const model = await this.frontendModelFindRecord("destroy", id)
|
|
3035
|
+
|
|
3036
|
+
if (!model) {
|
|
3037
|
+
return this.frontendModelErrorPayload(`${modelClass.name} not found.`)
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
await resource.destroy(model)
|
|
3041
|
+
|
|
3042
|
+
return {status: "success"}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
/**
|
|
3046
|
+
* Runs frontend api.
|
|
3047
|
+
* @returns {Promise<void>} - Shared frontend model API action with batch support.
|
|
3048
|
+
*/
|
|
3049
|
+
async frontendApi() {
|
|
3050
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3051
|
+
await this.render({status: 204, json: {}})
|
|
3052
|
+
return
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
const params = /**
|
|
3056
|
+
* Types the following value.
|
|
3057
|
+
@type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(this.params()))
|
|
3058
|
+
const requests = Array.isArray(params.requests) ? params.requests : [params]
|
|
3059
|
+
/**
|
|
3060
|
+
* Responses.
|
|
3061
|
+
@type {Array<Record<string, ?>>} */
|
|
3062
|
+
const responses = []
|
|
3063
|
+
|
|
3064
|
+
for (const requestEntry of requests) {
|
|
3065
|
+
const commandType = requestEntry?.commandType
|
|
3066
|
+
const customPath = requestEntry?.customPath
|
|
3067
|
+
const model = requestEntry?.model
|
|
3068
|
+
const payload = requestEntry?.payload
|
|
3069
|
+
const requestId = requestEntry?.requestId
|
|
3070
|
+
|
|
3071
|
+
if (typeof model !== "string" || model.length < 1) {
|
|
3072
|
+
responses.push({
|
|
3073
|
+
requestId,
|
|
3074
|
+
response: this.frontendModelErrorPayload("Expected request model.")
|
|
3075
|
+
})
|
|
3076
|
+
continue
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
const isBuiltInCommand = ["index", "find", "create", "update", "destroy", "attach", "download", "url"].includes(commandType)
|
|
3080
|
+
|
|
3081
|
+
if (!isBuiltInCommand && (typeof customPath !== "string" || !customPath.startsWith("/"))) {
|
|
3082
|
+
responses.push({
|
|
3083
|
+
requestId,
|
|
3084
|
+
response: this.frontendModelErrorPayload("Expected request customPath.")
|
|
3085
|
+
})
|
|
3086
|
+
continue
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
try {
|
|
3090
|
+
let responsePayload
|
|
3091
|
+
|
|
3092
|
+
if (isBuiltInCommand) {
|
|
3093
|
+
const commandParams = {
|
|
3094
|
+
...(payload && typeof payload === "object" ? payload : {}),
|
|
3095
|
+
model
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
responsePayload = await this.withFrontendModelParams(commandParams, async () => {
|
|
3099
|
+
return await this.withFrontendModelRequestContext(commandParams, this.response(), async () => {
|
|
3100
|
+
return await this.frontendModelCommandPayload(commandType)
|
|
3101
|
+
})
|
|
3102
|
+
})
|
|
3103
|
+
} else {
|
|
3104
|
+
responsePayload = await this.frontendApiCustomCommandPayload({
|
|
3105
|
+
customPath,
|
|
3106
|
+
payload
|
|
3107
|
+
})
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
responses.push({
|
|
3111
|
+
requestId,
|
|
3112
|
+
response: responsePayload || this.frontendModelErrorPayload("Action halted by beforeAction.")
|
|
3113
|
+
})
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
await this.frontendModelLogEndpointError({
|
|
3116
|
+
action: "frontendApi",
|
|
3117
|
+
commandType,
|
|
3118
|
+
error,
|
|
3119
|
+
model,
|
|
3120
|
+
requestId
|
|
3121
|
+
})
|
|
3122
|
+
|
|
3123
|
+
responses.push({
|
|
3124
|
+
requestId,
|
|
3125
|
+
response: this.frontendModelClientErrorPayloadForError(error)
|
|
3126
|
+
})
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
await this.render({
|
|
3131
|
+
json: /**
|
|
3132
|
+
* Types the following value.
|
|
3133
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
|
|
3134
|
+
responses,
|
|
3135
|
+
status: "success"
|
|
3136
|
+
}))
|
|
3137
|
+
})
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
/**
|
|
3141
|
+
* Dispatches a custom frontend-model command through the shared frontend-model API endpoint.
|
|
3142
|
+
* @param {object} args - Arguments.
|
|
3143
|
+
* @param {string} args.customPath - Custom backend route path.
|
|
3144
|
+
* @param {?} args.payload - Request payload.
|
|
3145
|
+
* @returns {Promise<Record<string, ?>>} - Parsed JSON response payload.
|
|
3146
|
+
*/
|
|
3147
|
+
async frontendApiCustomCommandPayload({customPath, payload}) {
|
|
3148
|
+
const configuration = this.getConfiguration()
|
|
3149
|
+
const response = new Response({configuration})
|
|
3150
|
+
const resolver = new RoutesResolver({
|
|
3151
|
+
configuration,
|
|
3152
|
+
request: this.getRequest(),
|
|
3153
|
+
response
|
|
3154
|
+
})
|
|
3155
|
+
resolver.params = {}
|
|
3156
|
+
const routeHookMatch = await resolver.resolveRouteResolverHooks(customPath)
|
|
3157
|
+
const configurationRoutes = configuration.getRoutes()
|
|
3158
|
+
const routeMatch = routeHookMatch || !configurationRoutes?.rootRoute ? undefined : resolver.matchPathWithRoutes(configurationRoutes.rootRoute, customPath)
|
|
3159
|
+
|
|
3160
|
+
if (!routeHookMatch && !routeMatch) {
|
|
3161
|
+
throw new Error(`No custom frontend model route matched '${customPath}'`)
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
const actionParam = routeHookMatch?.action || resolver.params.action
|
|
3165
|
+
const controllerParam = routeHookMatch?.controller || resolver.params.controller
|
|
3166
|
+
const actionValue = typeof actionParam === "string" ? actionParam : (Array.isArray(actionParam) ? actionParam[0] : undefined)
|
|
3167
|
+
const controllerValue = typeof controllerParam === "string" ? controllerParam : (Array.isArray(controllerParam) ? controllerParam[0] : undefined)
|
|
3168
|
+
|
|
3169
|
+
if (typeof actionValue !== "string" || actionValue.length < 1 || typeof controllerValue !== "string" || controllerValue.length < 1) {
|
|
3170
|
+
throw new Error(`Custom frontend model route matched '${customPath}' without controller/action params`)
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
const action = inflection.camelize(actionValue.replaceAll("-", "_").replaceAll("/", "_"), true)
|
|
3174
|
+
const controller = controllerValue
|
|
3175
|
+
const controllerPath = routeHookMatch?.controllerPath || `${configuration.getDirectory()}/src/routes/${controller}/controller.js`
|
|
3176
|
+
const viewPath = routeHookMatch?.viewPath || `${configuration.getDirectory()}/src/routes/${controller}`
|
|
3177
|
+
resolver.routeHookControllerClass = routeHookMatch?.controllerClass
|
|
3178
|
+
const controllerClass = await resolver.resolveControllerClass({controllerPath})
|
|
3179
|
+
const controllerParams = {
|
|
3180
|
+
...((payload && typeof payload === "object") ? payload : {}),
|
|
3181
|
+
...resolver.params
|
|
3182
|
+
}
|
|
3183
|
+
const controllerInstance = new controllerClass({
|
|
3184
|
+
action,
|
|
3185
|
+
configuration,
|
|
3186
|
+
controller,
|
|
3187
|
+
params: controllerParams,
|
|
3188
|
+
request: /**
|
|
3189
|
+
* Types the following value.
|
|
3190
|
+
@type {import("./http-server/client/request.js").default} */ (this.getRequest()),
|
|
3191
|
+
response,
|
|
3192
|
+
viewPath
|
|
3193
|
+
})
|
|
3194
|
+
|
|
3195
|
+
await this.withFrontendModelRequestContext(controllerParams, response, async () => {
|
|
3196
|
+
await controllerInstance._runBeforeCallbacks()
|
|
3197
|
+
const controllerMethods = /**
|
|
3198
|
+
* Types the following value.
|
|
3199
|
+
@type {Record<string, () => Promise<void> | void>} */ (/**
|
|
3200
|
+
* Types the following value.
|
|
3201
|
+
@type {?} */ (controllerInstance))
|
|
3202
|
+
|
|
3203
|
+
await controllerMethods[action]()
|
|
3204
|
+
})
|
|
3205
|
+
|
|
3206
|
+
const setCookieHeaders = response.headers["Set-Cookie"] || []
|
|
3207
|
+
|
|
3208
|
+
for (const setCookieHeader of setCookieHeaders) {
|
|
3209
|
+
this.response().addHeader("Set-Cookie", setCookieHeader)
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
const responseBody = response.getBody()
|
|
3213
|
+
|
|
3214
|
+
if (typeof responseBody !== "string" || responseBody.length < 1) {
|
|
3215
|
+
return {}
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// Preserve nested transport markers so the outer shared frontend-model API
|
|
3219
|
+
// can return them unchanged and let the client hydrate once at the edge.
|
|
3220
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (JSON.parse(responseBody))
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
/**
|
|
3224
|
+
* Runs frontend index.
|
|
3225
|
+
* @returns {Promise<void>} - Collection action for frontend model resources.
|
|
3226
|
+
*/
|
|
3227
|
+
async frontendIndex() {
|
|
3228
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3229
|
+
await this.render({status: 204, json: {}})
|
|
3230
|
+
return
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
await this.frontendModelRenderCommandResponse("index")
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
/**
|
|
3237
|
+
* Runs frontend find.
|
|
3238
|
+
* @returns {Promise<void>} - Member find action for frontend model resources.
|
|
3239
|
+
*/
|
|
3240
|
+
async frontendFind() {
|
|
3241
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3242
|
+
await this.render({status: 204, json: {}})
|
|
3243
|
+
return
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
await this.frontendModelRenderCommandResponse("find")
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* Runs frontend update.
|
|
3251
|
+
* @returns {Promise<void>} - Member update action for frontend model resources.
|
|
3252
|
+
*/
|
|
3253
|
+
async frontendUpdate() {
|
|
3254
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3255
|
+
await this.render({status: 204, json: {}})
|
|
3256
|
+
return
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
await this.frontendModelRenderCommandResponse("update")
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
/**
|
|
3263
|
+
* Runs frontend attach.
|
|
3264
|
+
* @returns {Promise<void>} - Member attach action for frontend model resources.
|
|
3265
|
+
*/
|
|
3266
|
+
async frontendAttach() {
|
|
3267
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3268
|
+
await this.render({status: 204, json: {}})
|
|
3269
|
+
return
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
await this.frontendModelRenderCommandResponse("attach")
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
/**
|
|
3276
|
+
* Runs frontend download.
|
|
3277
|
+
* @returns {Promise<void>} - Member download action for frontend model resources.
|
|
3278
|
+
*/
|
|
3279
|
+
async frontendDownload() {
|
|
3280
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3281
|
+
await this.render({status: 204, json: {}})
|
|
3282
|
+
return
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
await this.frontendModelRenderCommandResponse("download")
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
/**
|
|
3289
|
+
* Runs frontend url.
|
|
3290
|
+
* @returns {Promise<void>} - Member URL action for frontend model resources.
|
|
3291
|
+
*/
|
|
3292
|
+
async frontendUrl() {
|
|
3293
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3294
|
+
await this.render({status: 204, json: {}})
|
|
3295
|
+
return
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
await this.frontendModelRenderCommandResponse("url")
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
/**
|
|
3302
|
+
* Runs frontend create.
|
|
3303
|
+
* @returns {Promise<void>} - Member create action for frontend model resources.
|
|
3304
|
+
*/
|
|
3305
|
+
async frontendCreate() {
|
|
3306
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3307
|
+
await this.render({status: 204, json: {}})
|
|
3308
|
+
return
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
await this.frontendModelRenderCommandResponse("create")
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
/**
|
|
3315
|
+
* Runs frontend destroy.
|
|
3316
|
+
* @returns {Promise<void>} - Member destroy action for frontend model resources.
|
|
3317
|
+
*/
|
|
3318
|
+
async frontendDestroy() {
|
|
3319
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3320
|
+
await this.render({status: 204, json: {}})
|
|
3321
|
+
return
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
await this.frontendModelRenderCommandResponse("destroy")
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
/**
|
|
3328
|
+
* Runs frontend custom command.
|
|
3329
|
+
* @returns {Promise<void>} - Custom collection/member command action for frontend-model resources.
|
|
3330
|
+
*/
|
|
3331
|
+
async frontendCustomCommand() {
|
|
3332
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3333
|
+
await this.render({status: 204, json: {}})
|
|
3334
|
+
return
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
try {
|
|
3338
|
+
const responsePayload = await this.frontendModelCustomCommandPayload()
|
|
3339
|
+
|
|
3340
|
+
await this.render({
|
|
3341
|
+
json: /**
|
|
3342
|
+
* Types the following value.
|
|
3343
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
|
|
3344
|
+
})
|
|
3345
|
+
} catch (error) {
|
|
3346
|
+
await this.frontendModelLogEndpointError({action: "frontendCustomCommand", commandType: "custom-command", error})
|
|
3347
|
+
|
|
3348
|
+
await this.render({
|
|
3349
|
+
json: /**
|
|
3350
|
+
* Types the following value.
|
|
3351
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue(this.frontendModelClientErrorPayloadForError(error)))
|
|
3352
|
+
})
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
/**
|
|
3357
|
+
* Runs frontend model custom command payload.
|
|
3358
|
+
* @returns {Promise<Record<string, ?>>} - Response payload.
|
|
3359
|
+
*/
|
|
3360
|
+
async frontendModelCustomCommandPayload() {
|
|
3361
|
+
const params = this.frontendModelParams()
|
|
3362
|
+
const methodName = params.frontendModelCustomCommandMethodName
|
|
3363
|
+
const scope = params.frontendModelCustomCommandScope
|
|
3364
|
+
|
|
3365
|
+
if (typeof methodName !== "string" || methodName.length < 1) {
|
|
3366
|
+
return this.frontendModelErrorPayload("Expected frontend-model custom command method name.")
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
if (scope !== "collection" && scope !== "member") {
|
|
3370
|
+
return this.frontendModelErrorPayload("Expected frontend-model custom command scope.")
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
const resource = /**
|
|
3374
|
+
* Types the following value.
|
|
3375
|
+
@type {Record<string, ?>} */ (this.frontendModelResourceInstance())
|
|
3376
|
+
const commandMethod = resource[methodName]
|
|
3377
|
+
|
|
3378
|
+
if (typeof commandMethod !== "function") {
|
|
3379
|
+
return this.frontendModelErrorPayload(`Missing frontend-model custom command '${methodName}'.`)
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
const responsePayload = await commandMethod.call(resource)
|
|
3383
|
+
|
|
3384
|
+
if (!responsePayload || typeof responsePayload !== "object") {
|
|
3385
|
+
return {status: "success"}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (
|
|
3389
|
+
await this.autoSerializeFrontendModelsInPayload(
|
|
3390
|
+
responsePayload,
|
|
3391
|
+
/**
|
|
3392
|
+
* Types the following value.
|
|
3393
|
+
@type {{serialize: (model: ?, action: string) => Promise<Record<string, ?>>}} */ (resource),
|
|
3394
|
+
methodName
|
|
3395
|
+
)
|
|
3396
|
+
)
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
/**
|
|
3400
|
+
* Walks a custom-command response payload and replaces any backend `Record`
|
|
3401
|
+
* instance with the resource's per-action serialized form so handlers can
|
|
3402
|
+
* return `{record, status: "ok"}` instead of explicitly calling
|
|
3403
|
+
* `await this.serialize(record, action)`. Plain objects, arrays, and
|
|
3404
|
+
* primitive values pass through and are later encoded by
|
|
3405
|
+
* `serializeFrontendModelTransportValue`.
|
|
3406
|
+
* @param {?} value - Payload value.
|
|
3407
|
+
* @param {{serialize: (model: ?, action: string) => Promise<Record<string, ?>>}} resource - Resource instance providing `serialize`.
|
|
3408
|
+
* @param {string} action - Custom command method name passed to `resource.serialize` for per-action authorization filtering.
|
|
3409
|
+
* @param {WeakSet<object>} [seen] - Recursion stack of plain-object containers currently being walked. Membership is added on entry and removed on exit so a container shared between siblings (i.e. referenced twice but not cyclically) is walked on each reference instead of being short-circuited the second time, which would let backend `Record` instances inside it bypass `resource.serialize`.
|
|
3410
|
+
* @returns {Promise<?>} - Payload with backend `Record` instances replaced by serialized markers.
|
|
3411
|
+
*/
|
|
3412
|
+
async autoSerializeFrontendModelsInPayload(value, resource, action, seen = new WeakSet()) {
|
|
3413
|
+
if (value === null || value === undefined) {
|
|
3414
|
+
return value
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
if (isBackendModelInstance(value)) {
|
|
3418
|
+
const richSerialized = await resource.serialize(value, action)
|
|
3419
|
+
const modelClass = /**
|
|
3420
|
+
* Types the following value.
|
|
3421
|
+
@type {{constructor: {getModelName?: () => string, name?: string}}} */ (value).constructor
|
|
3422
|
+
const modelName = typeof modelClass.getModelName === "function" ? modelClass.getModelName() : (modelClass.name || "")
|
|
3423
|
+
|
|
3424
|
+
// Wrap the resource-serialized payload in the frontend_model transport
|
|
3425
|
+
// marker. Marker-based decoding routes through `instantiateFromResponse`,
|
|
3426
|
+
// so abilities / queryData / associationCounts / preloadedRelationships
|
|
3427
|
+
// baked into the rich attributes by `resource.serialize` are restored on
|
|
3428
|
+
// the client without callers needing to wrap models manually.
|
|
3429
|
+
return {
|
|
3430
|
+
__velocious_type: "frontend_model",
|
|
3431
|
+
attributes: richSerialized,
|
|
3432
|
+
modelName
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
if (Array.isArray(value)) {
|
|
3437
|
+
/**
|
|
3438
|
+
* Result.
|
|
3439
|
+
@type {Array<?>} */
|
|
3440
|
+
const result = []
|
|
3441
|
+
|
|
3442
|
+
for (const entry of value) {
|
|
3443
|
+
result.push(await this.autoSerializeFrontendModelsInPayload(entry, resource, action, seen))
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
return result
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
3450
|
+
const container = /**
|
|
3451
|
+
* Types the following value.
|
|
3452
|
+
@type {Record<string, ?>} */ (value)
|
|
3453
|
+
|
|
3454
|
+
if (seen.has(container)) {
|
|
3455
|
+
// Cyclic back-reference along the current recursion path; the
|
|
3456
|
+
// ancestor frame is still walking this container and will produce
|
|
3457
|
+
// its serialized form. Returning the original container here
|
|
3458
|
+
// breaks the cycle without bypassing the walker for siblings that
|
|
3459
|
+
// share a non-cyclic reference (those re-enter the branch below
|
|
3460
|
+
// because the container is removed from `seen` on stack exit).
|
|
3461
|
+
return container
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
seen.add(container)
|
|
3465
|
+
|
|
3466
|
+
try {
|
|
3467
|
+
/**
|
|
3468
|
+
* Result.
|
|
3469
|
+
@type {Record<string, ?>} */
|
|
3470
|
+
const result = {}
|
|
3471
|
+
|
|
3472
|
+
for (const [key, nested] of Object.entries(container)) {
|
|
3473
|
+
// `assignSafeProperty` stores keys like `__proto__` as own
|
|
3474
|
+
// data properties instead of invoking the prototype setter,
|
|
3475
|
+
// so a custom-command response that echoes parsed client
|
|
3476
|
+
// input cannot pollute `Object.prototype` here. The transport
|
|
3477
|
+
// serializer applies the same protection on its own pass; we
|
|
3478
|
+
// just preserve it across the auto-serialize walk.
|
|
3479
|
+
assignSafeProperty(result, key, await this.autoSerializeFrontendModelsInPayload(nested, resource, action, seen))
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
return result
|
|
3483
|
+
} finally {
|
|
3484
|
+
seen.delete(container)
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
return value
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
}
|