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,4004 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import * as inflection from "inflection"
|
|
4
|
+
import timeout from "awaitery/build/timeout.js"
|
|
5
|
+
import wait from "awaitery/build/wait.js"
|
|
6
|
+
import FrontendModelQuery, {frontendModelEventOptionsPayload} from "./query.js"
|
|
7
|
+
import FrontendModelPreloader from "./preloader.js"
|
|
8
|
+
import {registerFrontendModel, resolveFrontendModelClass} from "./model-registry.js"
|
|
9
|
+
import {validateFrontendModelResourceCommandName, validateFrontendModelResourcePath} from "./resource-config-validation.js"
|
|
10
|
+
import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "./transport-serialization.js"
|
|
11
|
+
import VelociousWebsocketClient from "../http-client/websocket-client.js"
|
|
12
|
+
import {bufferOutgoingEvent, drainBufferedOutgoingEvents} from "./outgoing-event-buffer.js"
|
|
13
|
+
import {defineModelScope} from "../utils/model-scope.js"
|
|
14
|
+
import isPlainObject from "../utils/plain-object.js"
|
|
15
|
+
import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQueryData, setPayloadAssociationCount, setPayloadComputedAbility, setPayloadQueryData} from "../record-payload-values.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* FrontendModelCommandType type.
|
|
19
|
+
@typedef {"create" | "find" | "index" | "update" | "destroy" | "attach" | "download" | "url"} FrontendModelCommandType */
|
|
20
|
+
/**
|
|
21
|
+
* FrontendModelRequestCommandType type.
|
|
22
|
+
@typedef {FrontendModelCommandType | string} FrontendModelRequestCommandType */
|
|
23
|
+
/**
|
|
24
|
+
* Defines this typedef.
|
|
25
|
+
* @typedef {{type: "hasOne" | "hasMany"}} FrontendModelAttachmentDefinition
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Defines this typedef.
|
|
29
|
+
* @typedef {{attributes?: string[], builtInCollectionCommands?: string[], builtInMemberCommands?: string[], collectionCommands?: string[], commands?: string[], memberCommands?: string[], attachments?: Record<string, FrontendModelAttachmentDefinition>, modelName?: string, nestedAttributes?: Record<string, {allowDestroy?: boolean, limit?: number}>, primaryKey?: string, relationships?: string[]}} FrontendModelResourceConfig
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* FrontendModelTransportConfig type.
|
|
33
|
+
* @typedef {object} FrontendModelTransportConfig
|
|
34
|
+
* @property {string | (() => string | undefined | null)} [url] - Optional frontend-model URL. This should be the shared endpoint (for example `"/frontend-models"` or `"https://example.com/frontend-models"`).
|
|
35
|
+
* @property {boolean} [shared] - Deprecated shared-endpoint flag retained for compatibility. Frontend-model CRUD/custom commands use the shared frontend-model API envelope by default.
|
|
36
|
+
* @property {string | (() => string | undefined | null)} [websocketUrl] - Optional websocket URL. When set, Velocious creates and manages its own websocket client internally. Subscriptions use the websocket; CRUD uses HTTP and falls back gracefully. Example: `"ws://localhost:3006/websocket"`.
|
|
37
|
+
* @property {{post: (path: string, body?: ?, options?: {headers?: Record<string, string>}) => Promise<{json: () => ?}>, subscribe: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => (() => void), subscribeAndWait?: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => Promise<(() => void)>}} [websocketClient] - Optional websocket client for shared frontend-model API requests and subscriptions.
|
|
38
|
+
* @property {Record<string, string> | (() => Record<string, string>)} [requestHeaders] - Extra HTTP/WS headers to attach to every frontend-model API request. Pass a function to compute them at request time (for example to include the current locale).
|
|
39
|
+
* @property {{get: () => string | null | undefined | Promise<string | null | undefined>, set: (sessionId: string) => void | Promise<void>, clear: () => void | Promise<void>}} [sessionStore] - Optional sessionId persistence hook forwarded to the internal `VelociousWebsocketClient` so WS sessions can be resumed across page reloads / app restarts.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* FrontendModelIdleWaitArgs type.
|
|
43
|
+
* @typedef {object} FrontendModelIdleWaitArgs
|
|
44
|
+
* @property {number} [quietMs] - Milliseconds the transport must stay idle before resolving.
|
|
45
|
+
* @property {number} [timeout] - Timeout in milliseconds.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Frontend model transport config.
|
|
50
|
+
@type {FrontendModelTransportConfig} */
|
|
51
|
+
const frontendModelTransportConfig = {}
|
|
52
|
+
const SHARED_FRONTEND_MODEL_API_PATH = "/frontend-models"
|
|
53
|
+
const PRELOADED_RELATIONSHIPS_KEY = "__preloadedRelationships"
|
|
54
|
+
const SELECTED_ATTRIBUTES_KEY = "__selectedAttributes"
|
|
55
|
+
const ASSOCIATION_COUNTS_KEY = "__associationCounts"
|
|
56
|
+
const QUERY_DATA_KEY = "__queryData"
|
|
57
|
+
const ABILITIES_KEY = "__abilities"
|
|
58
|
+
/**
|
|
59
|
+
* Pending shared frontend model requests.
|
|
60
|
+
@type {Array<{commandName?: string, commandType: FrontendModelRequestCommandType, customPath?: string, modelClass: typeof FrontendModelBase, payload: Record<string, ?>, requestId: string, resolve: (response: Record<string, ?>) => void, reject: (error: ?) => void, resourcePath?: string | null}>} */
|
|
61
|
+
let pendingSharedFrontendModelRequests = []
|
|
62
|
+
let sharedFrontendModelRequestId = 0
|
|
63
|
+
let sharedFrontendModelFlushScheduled = false
|
|
64
|
+
let activeFrontendModelTransportRequestCount = 0
|
|
65
|
+
/**
|
|
66
|
+
* Frontend model idle resolvers.
|
|
67
|
+
@type {Array<() => void>} */
|
|
68
|
+
let frontendModelIdleResolvers = []
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Internal websocket client.
|
|
72
|
+
@type {VelociousWebsocketClient | null} */
|
|
73
|
+
let internalWebsocketClient = null
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Runs frontend model transport is idle.
|
|
77
|
+
* @returns {boolean} - Whether all queued and active frontend-model transport requests are done.
|
|
78
|
+
*/
|
|
79
|
+
function frontendModelTransportIsIdle() {
|
|
80
|
+
return activeFrontendModelTransportRequestCount === 0
|
|
81
|
+
&& pendingSharedFrontendModelRequests.length === 0
|
|
82
|
+
&& !sharedFrontendModelFlushScheduled
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Runs resolve frontend model idle waiters.
|
|
87
|
+
@returns {void} */
|
|
88
|
+
function resolveFrontendModelIdleWaiters() {
|
|
89
|
+
if (!frontendModelTransportIsIdle()) return
|
|
90
|
+
|
|
91
|
+
const resolvers = frontendModelIdleResolvers
|
|
92
|
+
frontendModelIdleResolvers = []
|
|
93
|
+
|
|
94
|
+
for (const resolve of resolvers) {
|
|
95
|
+
resolve()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Runs wait for frontend model transport quiet period.
|
|
101
|
+
* @param {number} milliseconds - Quiet period length.
|
|
102
|
+
* @returns {Promise<void>} Resolves after the quiet period.
|
|
103
|
+
*/
|
|
104
|
+
async function waitForFrontendModelTransportQuietPeriod(milliseconds) {
|
|
105
|
+
if (milliseconds <= 0) return
|
|
106
|
+
|
|
107
|
+
await wait(milliseconds)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Runs wait for frontend model transport idle.
|
|
112
|
+
* @param {number} quietMs - Milliseconds the transport must stay idle before resolving.
|
|
113
|
+
* @returns {Promise<void>} Resolves when transport stays idle.
|
|
114
|
+
*/
|
|
115
|
+
async function waitForFrontendModelTransportIdle(quietMs = 0) {
|
|
116
|
+
while (true) {
|
|
117
|
+
if (frontendModelTransportIsIdle()) {
|
|
118
|
+
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)))
|
|
119
|
+
|
|
120
|
+
if (frontendModelTransportIsIdle()) {
|
|
121
|
+
await waitForFrontendModelTransportQuietPeriod(quietMs)
|
|
122
|
+
|
|
123
|
+
if (frontendModelTransportIsIdle()) return
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
await new Promise((resolve) => {
|
|
127
|
+
frontendModelIdleResolvers.push(() => resolve(undefined))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Runs track frontend model transport request.
|
|
135
|
+
* @template T
|
|
136
|
+
* @param {() => Promise<T>} callback - Transport callback.
|
|
137
|
+
* @returns {Promise<T>} - Callback result.
|
|
138
|
+
*/
|
|
139
|
+
async function trackFrontendModelTransportRequest(callback) {
|
|
140
|
+
activeFrontendModelTransportRequestCount += 1
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
return await callback()
|
|
144
|
+
} finally {
|
|
145
|
+
activeFrontendModelTransportRequestCount -= 1
|
|
146
|
+
resolveFrontendModelIdleWaiters()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve the internal websocket client from websocketUrl config.
|
|
152
|
+
* Creates the client lazily on first call. Returns null if WebSocket
|
|
153
|
+
* is not available or websocketUrl is not configured.
|
|
154
|
+
* @returns {VelociousWebsocketClient | null} Websocket client or null.
|
|
155
|
+
*/
|
|
156
|
+
function resolveInternalWebsocketClient() {
|
|
157
|
+
if (internalWebsocketClient) return internalWebsocketClient
|
|
158
|
+
|
|
159
|
+
const websocketUrl = frontendModelTransportConfig.websocketUrl
|
|
160
|
+
|
|
161
|
+
if (!websocketUrl) return null
|
|
162
|
+
if (typeof globalThis.WebSocket === "undefined") return null
|
|
163
|
+
|
|
164
|
+
const resolvedUrl = typeof websocketUrl === "function" ? websocketUrl() : websocketUrl
|
|
165
|
+
|
|
166
|
+
if (!resolvedUrl) return null
|
|
167
|
+
|
|
168
|
+
internalWebsocketClient = new VelociousWebsocketClient({
|
|
169
|
+
autoReconnect: true,
|
|
170
|
+
sessionStore: frontendModelTransportConfig.sessionStore,
|
|
171
|
+
url: resolvedUrl
|
|
172
|
+
})
|
|
173
|
+
internalWebsocketClient.onReconnect = flushBufferedOutgoingEventsAfterReconnect
|
|
174
|
+
|
|
175
|
+
return internalWebsocketClient
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Runs flush buffered outgoing events after reconnect.
|
|
180
|
+
@returns {Promise<void>} */
|
|
181
|
+
async function flushBufferedOutgoingEventsAfterReconnect() {
|
|
182
|
+
if (!internalWebsocketClient) return
|
|
183
|
+
|
|
184
|
+
const events = drainBufferedOutgoingEvents()
|
|
185
|
+
|
|
186
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
187
|
+
try {
|
|
188
|
+
await internalWebsocketClient.post(events[index].customPath, events[index].payload)
|
|
189
|
+
} catch {
|
|
190
|
+
const socketOpen = internalWebsocketClient.socket?.readyState === internalWebsocketClient.socket?.OPEN
|
|
191
|
+
|
|
192
|
+
if (socketOpen) {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (let remaining = index; remaining < events.length; remaining += 1) {
|
|
197
|
+
bufferOutgoingEvent(events[remaining])
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Runs default frontend model resource path.
|
|
207
|
+
* @param {typeof FrontendModelBase} modelClass - Frontend model class.
|
|
208
|
+
* @returns {string} - Default resource path for the model class.
|
|
209
|
+
*/
|
|
210
|
+
function defaultFrontendModelResourcePath(modelClass) {
|
|
211
|
+
return `/${inflection.dasherize(inflection.pluralize(inflection.underscore(modelClass.getModelName())))}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Error raised when reading an attribute that was not selected in query payloads. */
|
|
215
|
+
export class AttributeNotSelectedError extends Error {
|
|
216
|
+
/**
|
|
217
|
+
* Runs constructor.
|
|
218
|
+
* @param {string} modelName - Model class name.
|
|
219
|
+
* @param {string} attributeName - Attribute that was requested.
|
|
220
|
+
*/
|
|
221
|
+
constructor(modelName, attributeName) {
|
|
222
|
+
super(`${modelName}#${attributeName} was not selected`)
|
|
223
|
+
this.name = "AttributeNotSelectedError"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Lightweight singular relationship state holder for frontend model instances.
|
|
229
|
+
* @template {typeof FrontendModelBase} S
|
|
230
|
+
* @template {typeof FrontendModelBase} T
|
|
231
|
+
*/
|
|
232
|
+
export class FrontendModelSingularRelationship {
|
|
233
|
+
/**
|
|
234
|
+
* Runs constructor.
|
|
235
|
+
* @param {InstanceType<S>} model - Parent model.
|
|
236
|
+
* @param {string} relationshipName - Relationship name.
|
|
237
|
+
* @param {T | null} targetModelClass - Target model class.
|
|
238
|
+
*/
|
|
239
|
+
constructor(model, relationshipName, targetModelClass) {
|
|
240
|
+
this.model = model
|
|
241
|
+
this.relationshipName = relationshipName
|
|
242
|
+
this.targetModelClass = targetModelClass
|
|
243
|
+
this._preloaded = false
|
|
244
|
+
this._loadedValue = null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Runs set loaded.
|
|
249
|
+
* @param {?} loadedValue - Loaded relationship value.
|
|
250
|
+
* @returns {void}
|
|
251
|
+
*/
|
|
252
|
+
setLoaded(loadedValue) {
|
|
253
|
+
this._loadedValue = loadedValue == undefined ? null : loadedValue
|
|
254
|
+
this._preloaded = true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Runs get preloaded.
|
|
259
|
+
* @returns {boolean} - Whether relationship is preloaded.
|
|
260
|
+
*/
|
|
261
|
+
getPreloaded() {
|
|
262
|
+
return this._preloaded
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Runs loaded.
|
|
267
|
+
* @returns {?} - Loaded relationship value.
|
|
268
|
+
*/
|
|
269
|
+
loaded() {
|
|
270
|
+
if (!this._preloaded) {
|
|
271
|
+
throw new Error(`${this.model.constructor.name}#${this.relationshipName} hasn't been preloaded`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return this._loadedValue
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Runs build.
|
|
279
|
+
* @param {Record<string, ?>} [attributes] - New model attributes.
|
|
280
|
+
* @returns {InstanceType<T>} - Built model.
|
|
281
|
+
*/
|
|
282
|
+
build(attributes = {}) {
|
|
283
|
+
if (!this.targetModelClass) {
|
|
284
|
+
throw new Error(`No target model class configured for ${this.model.constructor.name}#${this.relationshipName}`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const model = /**
|
|
288
|
+
* Narrows the runtime value to the documented type.
|
|
289
|
+
@type {InstanceType<T>} */ (new this.targetModelClass(attributes))
|
|
290
|
+
|
|
291
|
+
this.setLoaded(model)
|
|
292
|
+
|
|
293
|
+
return model
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Lightweight has-many relationship state holder for frontend model instances.
|
|
299
|
+
* @template {typeof FrontendModelBase} S
|
|
300
|
+
* @template {typeof FrontendModelBase} T
|
|
301
|
+
*/
|
|
302
|
+
export class FrontendModelHasManyRelationship {
|
|
303
|
+
/**
|
|
304
|
+
* Narrows the runtime value to the documented type.
|
|
305
|
+
@type {Array<InstanceType<T>>} */
|
|
306
|
+
_loadedValue
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Runs constructor.
|
|
310
|
+
* @param {InstanceType<S>} model - Parent model.
|
|
311
|
+
* @param {string} relationshipName - Relationship name.
|
|
312
|
+
* @param {T | null} targetModelClass - Target model class.
|
|
313
|
+
*/
|
|
314
|
+
constructor(model, relationshipName, targetModelClass) {
|
|
315
|
+
this.model = model
|
|
316
|
+
this.relationshipName = relationshipName
|
|
317
|
+
this.targetModelClass = targetModelClass
|
|
318
|
+
this._preloaded = false
|
|
319
|
+
this._loadedValue = []
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Runs set loaded.
|
|
324
|
+
* @param {Array<InstanceType<T>>} loadedValue - Loaded relationship value.
|
|
325
|
+
* @returns {void}
|
|
326
|
+
*/
|
|
327
|
+
setLoaded(loadedValue) {
|
|
328
|
+
this._loadedValue = Array.isArray(loadedValue) ? loadedValue : []
|
|
329
|
+
this._preloaded = true
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Runs get preloaded.
|
|
334
|
+
* @returns {boolean} - Whether relationship is preloaded.
|
|
335
|
+
*/
|
|
336
|
+
getPreloaded() {
|
|
337
|
+
return this._preloaded
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Runs loaded.
|
|
342
|
+
* @returns {Array<InstanceType<T>>} - Loaded relationship values.
|
|
343
|
+
*/
|
|
344
|
+
loaded() {
|
|
345
|
+
if (!this._preloaded) {
|
|
346
|
+
throw new Error(`${this.model.constructor.name}#${this.relationshipName} hasn't been preloaded`)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return this._loadedValue
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Runs add to loaded.
|
|
354
|
+
* @param {Array<InstanceType<T>>} models - Models to append.
|
|
355
|
+
* @returns {void}
|
|
356
|
+
*/
|
|
357
|
+
addToLoaded(models) {
|
|
358
|
+
const loadedModels = this.getPreloaded() ? this.loaded() : []
|
|
359
|
+
|
|
360
|
+
this.setLoaded([...loadedModels, ...models])
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Runs build.
|
|
365
|
+
* @param {Record<string, ?>} [attributes] - New model attributes.
|
|
366
|
+
* @returns {InstanceType<T>} - Built model.
|
|
367
|
+
*/
|
|
368
|
+
build(attributes = {}) {
|
|
369
|
+
if (!this.targetModelClass) {
|
|
370
|
+
throw new Error(`No target model class configured for ${this.model.constructor.name}#${this.relationshipName}`)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const model = /**
|
|
374
|
+
* Narrows the runtime value to the documented type.
|
|
375
|
+
@type {InstanceType<T>} */ (new this.targetModelClass(attributes))
|
|
376
|
+
|
|
377
|
+
this.addToLoaded([model])
|
|
378
|
+
|
|
379
|
+
return model
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Force-reload the relationship. When the parent record was loaded as part
|
|
384
|
+
* of a batch, siblings that have not preloaded this relationship get
|
|
385
|
+
* batched into one request via the cohort preloader. The scoped query path
|
|
386
|
+
* (`Model.where(...).preload([name]).toArray()` directly from user code)
|
|
387
|
+
* bypasses cohort batching by design.
|
|
388
|
+
* @returns {Promise<Array<InstanceType<T>>>} - Loaded relationship models.
|
|
389
|
+
*/
|
|
390
|
+
async load() {
|
|
391
|
+
// Reset so the cohort preloader (or single-record fallback) repopulates.
|
|
392
|
+
this._preloaded = false
|
|
393
|
+
this._loadedValue = []
|
|
394
|
+
|
|
395
|
+
const batched = await /**
|
|
396
|
+
* Narrows the runtime value to the documented type.
|
|
397
|
+
@type {?} */ (this.model)._tryCohortPreload(this.relationshipName)
|
|
398
|
+
|
|
399
|
+
if (batched) return this._loadedValue
|
|
400
|
+
|
|
401
|
+
return /** Narrows the runtime value to the documented type. @type {Promise<Array<InstanceType<T>>>} */ (this.model.loadRelationship(this.relationshipName))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Runs to array.
|
|
406
|
+
* @returns {Promise<Array<InstanceType<T>>>} - Loaded relationship models.
|
|
407
|
+
*/
|
|
408
|
+
async toArray() {
|
|
409
|
+
if (this.getPreloaded() || this._loadedValue.length > 0) {
|
|
410
|
+
return this._loadedValue
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return await this.load()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Runs relationship type is collection.
|
|
419
|
+
* @param {string} relationshipType - Relationship type.
|
|
420
|
+
* @returns {boolean} - Whether relationship type is has-many.
|
|
421
|
+
*/
|
|
422
|
+
function relationshipTypeIsCollection(relationshipType) {
|
|
423
|
+
return relationshipType == "hasMany"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Downloaded frontend-model attachment payload wrapper.
|
|
428
|
+
*/
|
|
429
|
+
export class FrontendModelAttachmentDownload {
|
|
430
|
+
/**
|
|
431
|
+
* Runs constructor.
|
|
432
|
+
* @param {object} args - Options.
|
|
433
|
+
* @param {string} args.id - Attachment id.
|
|
434
|
+
* @param {string} args.filename - Filename.
|
|
435
|
+
* @param {string | null} args.contentType - Content type.
|
|
436
|
+
* @param {number} args.byteSize - File size in bytes.
|
|
437
|
+
* @param {Uint8Array} args.content - File content bytes.
|
|
438
|
+
* @param {string | null} [args.url] - Resolvable attachment URL.
|
|
439
|
+
*/
|
|
440
|
+
constructor({byteSize, content, contentType, filename, id, url = null}) {
|
|
441
|
+
this.idValue = id
|
|
442
|
+
this.filenameValue = filename
|
|
443
|
+
this.contentTypeValue = contentType
|
|
444
|
+
this.byteSizeValue = byteSize
|
|
445
|
+
this.contentValue = content
|
|
446
|
+
this.urlValue = url
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Runs byte size.
|
|
451
|
+
* @returns {number} - File size in bytes.
|
|
452
|
+
*/
|
|
453
|
+
byteSize() { return this.byteSizeValue }
|
|
454
|
+
/**
|
|
455
|
+
* Runs content.
|
|
456
|
+
* @returns {Uint8Array} - File content bytes.
|
|
457
|
+
*/
|
|
458
|
+
content() { return this.contentValue }
|
|
459
|
+
/**
|
|
460
|
+
* Runs content type.
|
|
461
|
+
* @returns {string | null} - Content type.
|
|
462
|
+
*/
|
|
463
|
+
contentType() { return this.contentTypeValue }
|
|
464
|
+
/**
|
|
465
|
+
* Runs filename.
|
|
466
|
+
* @returns {string} - Filename.
|
|
467
|
+
*/
|
|
468
|
+
filename() { return this.filenameValue }
|
|
469
|
+
/**
|
|
470
|
+
* Runs id.
|
|
471
|
+
* @returns {string} - Attachment id.
|
|
472
|
+
*/
|
|
473
|
+
id() { return this.idValue }
|
|
474
|
+
/**
|
|
475
|
+
* Runs url.
|
|
476
|
+
* @returns {string | null} - Resolvable attachment URL.
|
|
477
|
+
*/
|
|
478
|
+
url() { return this.urlValue }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Runs frontend model attachment command payload.
|
|
483
|
+
* @param {FrontendModelAttachmentHandle} attachment - Attachment wrapper.
|
|
484
|
+
* @param {string | undefined} attachmentId - Optional has-many attachment id.
|
|
485
|
+
* @returns {Record<string, ?>} - Command payload.
|
|
486
|
+
*/
|
|
487
|
+
function frontendModelAttachmentCommandPayload(attachment, attachmentId) {
|
|
488
|
+
/**
|
|
489
|
+
* Payload.
|
|
490
|
+
@type {Record<string, ?>} */
|
|
491
|
+
const payload = {
|
|
492
|
+
attachmentName: attachment.attachmentName,
|
|
493
|
+
id: attachment.model.primaryKeyValue()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (attachmentId) payload.attachmentId = attachmentId
|
|
497
|
+
|
|
498
|
+
return payload
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Runs frontend attachment value is bytes.
|
|
503
|
+
* @param {?} value - Candidate value.
|
|
504
|
+
* @returns {boolean} - Whether value looks like byte data.
|
|
505
|
+
*/
|
|
506
|
+
function frontendAttachmentValueIsBytes(value) {
|
|
507
|
+
return value instanceof Uint8Array || value instanceof ArrayBuffer || (typeof Buffer !== "undefined" && Buffer.isBuffer(value))
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Runs frontend attachment value supports array buffer.
|
|
512
|
+
* @param {?} value - Candidate value.
|
|
513
|
+
* @returns {value is {arrayBuffer: () => Promise<ArrayBuffer>}} - Whether candidate supports arrayBuffer().
|
|
514
|
+
*/
|
|
515
|
+
function frontendAttachmentValueSupportsArrayBuffer(value) {
|
|
516
|
+
return Boolean(value && typeof value === "object" && typeof /**
|
|
517
|
+
* Narrows the runtime value to the documented type.
|
|
518
|
+
@type {?} */ (value).arrayBuffer === "function")
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Runs frontend attachment normalize bytes.
|
|
523
|
+
* @param {Uint8Array | Buffer | ArrayBuffer} value - Byte-like value.
|
|
524
|
+
* @returns {Uint8Array} - Uint8Array bytes.
|
|
525
|
+
*/
|
|
526
|
+
function frontendAttachmentNormalizeBytes(value) {
|
|
527
|
+
if (value instanceof Uint8Array) return value
|
|
528
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value)
|
|
529
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(/**
|
|
530
|
+
* Narrows the runtime value to the documented type.
|
|
531
|
+
@type {?} */ (value))) {
|
|
532
|
+
return new Uint8Array(/**
|
|
533
|
+
* Narrows the runtime value to the documented type.
|
|
534
|
+
@type {Buffer} */ (value))
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
throw new Error("Unsupported attachment bytes value")
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Runs frontend attachment bytes to base64.
|
|
542
|
+
* @param {Uint8Array} bytes - Bytes.
|
|
543
|
+
* @returns {string} - Base64 value.
|
|
544
|
+
*/
|
|
545
|
+
function frontendAttachmentBytesToBase64(bytes) {
|
|
546
|
+
if (typeof Buffer !== "undefined") {
|
|
547
|
+
return Buffer.from(bytes).toString("base64")
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let binary = ""
|
|
551
|
+
|
|
552
|
+
for (const byte of bytes) {
|
|
553
|
+
binary += String.fromCharCode(byte)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (typeof btoa !== "function") throw new Error("Missing base64 encoder")
|
|
557
|
+
|
|
558
|
+
return btoa(binary)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Runs frontend attachment base64 to bytes.
|
|
563
|
+
* @param {string} value - Base64 value.
|
|
564
|
+
* @returns {Uint8Array} - Decoded bytes.
|
|
565
|
+
*/
|
|
566
|
+
function frontendAttachmentBase64ToBytes(value) {
|
|
567
|
+
if (typeof Buffer !== "undefined") {
|
|
568
|
+
return new Uint8Array(Buffer.from(value, "base64"))
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (typeof atob !== "function") throw new Error("Missing base64 decoder")
|
|
572
|
+
|
|
573
|
+
const binary = atob(value)
|
|
574
|
+
const bytes = new Uint8Array(binary.length)
|
|
575
|
+
|
|
576
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
577
|
+
bytes[index] = binary.charCodeAt(index)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return bytes
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Runs frontend attachment value is plain object.
|
|
585
|
+
* @param {?} value - Candidate value.
|
|
586
|
+
* @returns {value is Record<string, ?>} - Whether value is plain object.
|
|
587
|
+
*/
|
|
588
|
+
function frontendAttachmentValueIsPlainObject(value) {
|
|
589
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false
|
|
590
|
+
|
|
591
|
+
const prototype = Object.getPrototypeOf(value)
|
|
592
|
+
|
|
593
|
+
return prototype === Object.prototype || prototype === null
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Runs frontend model payload contains attachment upload.
|
|
598
|
+
* @param {?} value - Payload candidate.
|
|
599
|
+
* @returns {boolean} - Whether payload contains an attachment upload body.
|
|
600
|
+
*/
|
|
601
|
+
function frontendModelPayloadContainsAttachmentUpload(value) {
|
|
602
|
+
if (!value || typeof value !== "object") return false
|
|
603
|
+
|
|
604
|
+
if (Array.isArray(value)) {
|
|
605
|
+
return value.some((entry) => frontendModelPayloadContainsAttachmentUpload(entry))
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!frontendAttachmentValueIsPlainObject(value)) return false
|
|
609
|
+
|
|
610
|
+
if (typeof value.contentBase64 === "string") {
|
|
611
|
+
return true
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return Object.values(value).some((entry) => frontendModelPayloadContainsAttachmentUpload(entry))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Runs normalize frontend attachment input.
|
|
619
|
+
* @param {?} input - Attachment input.
|
|
620
|
+
* @returns {Promise<Record<string, ?>>} - Transport-safe attachment payload.
|
|
621
|
+
*/
|
|
622
|
+
async function normalizeFrontendAttachmentInput(input) {
|
|
623
|
+
if (frontendAttachmentValueIsPlainObject(input) && "file" in input) {
|
|
624
|
+
const normalizedFile = await normalizeFrontendAttachmentInput(input.file)
|
|
625
|
+
const merged = {
|
|
626
|
+
...normalizedFile
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (typeof input.filename === "string" && input.filename.length > 0) merged.filename = input.filename
|
|
630
|
+
if (typeof input.contentType === "string" && input.contentType.length > 0) merged.contentType = input.contentType
|
|
631
|
+
|
|
632
|
+
return merged
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (frontendAttachmentValueIsPlainObject(input)) {
|
|
636
|
+
if (typeof input.path === "string" && input.path.length > 0) {
|
|
637
|
+
throw new Error("Attachment path input is not supported in frontend models")
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (typeof input.contentBase64 === "string") {
|
|
641
|
+
return {
|
|
642
|
+
contentBase64: input.contentBase64,
|
|
643
|
+
contentType: typeof input.contentType === "string" && input.contentType.length > 0 ? input.contentType : null,
|
|
644
|
+
filename: typeof input.filename === "string" && input.filename.length > 0 ? input.filename : undefined
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (frontendAttachmentValueSupportsArrayBuffer(input)) {
|
|
650
|
+
const bytes = new Uint8Array(await input.arrayBuffer())
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
contentBase64: frontendAttachmentBytesToBase64(bytes),
|
|
654
|
+
contentType: typeof /**
|
|
655
|
+
* Narrows the runtime value to the documented type.
|
|
656
|
+
@type {?} */ (input).type === "string" && /**
|
|
657
|
+
* Narrows the runtime value to the documented type.
|
|
658
|
+
@type {?} */ (input).type.length > 0
|
|
659
|
+
? /**
|
|
660
|
+
* Narrows the runtime value to the documented type.
|
|
661
|
+
@type {?} */ (input).type
|
|
662
|
+
: null,
|
|
663
|
+
filename: typeof /**
|
|
664
|
+
* Narrows the runtime value to the documented type.
|
|
665
|
+
@type {?} */ (input).name === "string" && /**
|
|
666
|
+
* Narrows the runtime value to the documented type.
|
|
667
|
+
@type {?} */ (input).name.length > 0
|
|
668
|
+
? /**
|
|
669
|
+
* Narrows the runtime value to the documented type.
|
|
670
|
+
@type {?} */ (input).name
|
|
671
|
+
: "attachment.bin"
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (frontendAttachmentValueIsBytes(input)) {
|
|
676
|
+
const bytes = frontendAttachmentNormalizeBytes(/**
|
|
677
|
+
* Narrows the runtime value to the documented type.
|
|
678
|
+
@type {Uint8Array | Buffer | ArrayBuffer} */ (input))
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
contentBase64: frontendAttachmentBytesToBase64(bytes),
|
|
682
|
+
contentType: null,
|
|
683
|
+
filename: "attachment.bin"
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
throw new Error("Unsupported frontend attachment input")
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Frontend-model attachment helper for one attachment name.
|
|
692
|
+
*/
|
|
693
|
+
export class FrontendModelAttachmentHandle {
|
|
694
|
+
/**
|
|
695
|
+
* Runs constructor.
|
|
696
|
+
* @param {object} args - Options.
|
|
697
|
+
* @param {FrontendModelBase} args.model - Model instance.
|
|
698
|
+
* @param {string} args.attachmentName - Attachment name.
|
|
699
|
+
*/
|
|
700
|
+
constructor({attachmentName, model}) {
|
|
701
|
+
this.model = model
|
|
702
|
+
this.attachmentName = attachmentName
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Runs attach.
|
|
707
|
+
* @param {?} input - Attachment input.
|
|
708
|
+
* @returns {Promise<void>} - Resolves when attached.
|
|
709
|
+
*/
|
|
710
|
+
async attach(input) {
|
|
711
|
+
const ModelClass = /**
|
|
712
|
+
* Narrows the runtime value to the documented type.
|
|
713
|
+
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
714
|
+
const normalizedInput = await normalizeFrontendAttachmentInput(input)
|
|
715
|
+
const response = await ModelClass.executeCommand("attach", {
|
|
716
|
+
attachment: normalizedInput,
|
|
717
|
+
attachmentName: this.attachmentName,
|
|
718
|
+
id: this.model.primaryKeyValue()
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
this.model.assignAttributes(ModelClass.attributesFromResponse(response))
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Runs download.
|
|
726
|
+
* @param {string} [attachmentId] - Optional attachment id for has-many attachments.
|
|
727
|
+
* @returns {Promise<FrontendModelAttachmentDownload | null>} - Downloaded attachment payload.
|
|
728
|
+
*/
|
|
729
|
+
async download(attachmentId) {
|
|
730
|
+
const ModelClass = /**
|
|
731
|
+
* Narrows the runtime value to the documented type.
|
|
732
|
+
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
733
|
+
const response = await ModelClass.executeCommand("download", frontendModelAttachmentCommandPayload(this, attachmentId))
|
|
734
|
+
const attachmentPayload = response.attachment
|
|
735
|
+
|
|
736
|
+
if (!attachmentPayload || typeof attachmentPayload !== "object") return null
|
|
737
|
+
|
|
738
|
+
const contentBase64 = typeof attachmentPayload.contentBase64 === "string" ? attachmentPayload.contentBase64 : ""
|
|
739
|
+
const content = frontendAttachmentBase64ToBytes(contentBase64)
|
|
740
|
+
const byteSize = Number(attachmentPayload.byteSize)
|
|
741
|
+
|
|
742
|
+
return new FrontendModelAttachmentDownload({
|
|
743
|
+
byteSize: Number.isFinite(byteSize) ? byteSize : content.length,
|
|
744
|
+
content,
|
|
745
|
+
contentType: typeof attachmentPayload.contentType === "string" && attachmentPayload.contentType.length > 0 ? attachmentPayload.contentType : null,
|
|
746
|
+
filename: typeof attachmentPayload.filename === "string" && attachmentPayload.filename.length > 0 ? attachmentPayload.filename : "attachment.bin",
|
|
747
|
+
id: typeof attachmentPayload.id === "string" ? attachmentPayload.id : "",
|
|
748
|
+
url: typeof attachmentPayload.url === "string" && attachmentPayload.url.length > 0 ? attachmentPayload.url : null
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Runs url.
|
|
754
|
+
* @param {string} [attachmentId] - Optional attachment id for has-many attachments.
|
|
755
|
+
* @returns {Promise<string | null>} - Resolvable attachment URL.
|
|
756
|
+
*/
|
|
757
|
+
async url(attachmentId) {
|
|
758
|
+
const ModelClass = /**
|
|
759
|
+
* Narrows the runtime value to the documented type.
|
|
760
|
+
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
761
|
+
const response = await ModelClass.executeCommand("url", frontendModelAttachmentCommandPayload(this, attachmentId))
|
|
762
|
+
|
|
763
|
+
if (typeof response.url === "string" && response.url.length > 0) {
|
|
764
|
+
return response.url
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return null
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Runs download url.
|
|
772
|
+
* @returns {string} - Download URL for this attachment on the configured backend.
|
|
773
|
+
*/
|
|
774
|
+
downloadUrl() {
|
|
775
|
+
const ModelClass = /**
|
|
776
|
+
* Narrows the runtime value to the documented type.
|
|
777
|
+
@type {typeof FrontendModelBase} */ (this.model.constructor)
|
|
778
|
+
const commandName = ModelClass.commandName("download")
|
|
779
|
+
const resourcePath = ModelClass.resourcePath()
|
|
780
|
+
const commandUrl = frontendModelCommandUrl(resourcePath, commandName)
|
|
781
|
+
const params = new URLSearchParams({
|
|
782
|
+
attachmentName: this.attachmentName,
|
|
783
|
+
id: String(this.model.primaryKeyValue())
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
return `${commandUrl}?${params.toString()}`
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Runs normalize frontend model transport url.
|
|
792
|
+
* @param {string | undefined | null} value - URL candidate.
|
|
793
|
+
* @returns {string} - Normalized URL without trailing slash.
|
|
794
|
+
*/
|
|
795
|
+
function normalizeFrontendModelTransportUrl(value) {
|
|
796
|
+
if (typeof value !== "string") return ""
|
|
797
|
+
|
|
798
|
+
const trimmed = value.trim()
|
|
799
|
+
|
|
800
|
+
if (!trimmed.length) return ""
|
|
801
|
+
|
|
802
|
+
return trimmed.replace(/\/+$/, "")
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Runs frontend model transport url.
|
|
807
|
+
* @returns {string} - Resolved frontend-model transport URL.
|
|
808
|
+
*/
|
|
809
|
+
function frontendModelTransportUrl() {
|
|
810
|
+
const configuredUrl = typeof frontendModelTransportConfig.url === "function"
|
|
811
|
+
? frontendModelTransportConfig.url()
|
|
812
|
+
: frontendModelTransportConfig.url
|
|
813
|
+
|
|
814
|
+
return normalizeFrontendModelTransportUrl(configuredUrl)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Runs clone frontend model attributes.
|
|
819
|
+
* @param {Record<string, ?>} value - Attributes hash.
|
|
820
|
+
* @returns {Record<string, ?>} - Cloned attributes hash.
|
|
821
|
+
*/
|
|
822
|
+
function cloneFrontendModelAttributes(value) {
|
|
823
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(serializeFrontendModelTransportValue(value)))
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Shared channel name for model lifecycle events (Phase 3).
|
|
828
|
+
* Matches the backend `FRONTEND_MODELS_CHANNEL_NAME`.
|
|
829
|
+
*/
|
|
830
|
+
const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Defines this typedef.
|
|
834
|
+
* @typedef {{callback: (payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void, eventFilterKey: string | null, eventFilterPayload: import("./query.js").FrontendModelEventFilterPayload | null, projectionPayload: import("./query.js").FrontendModelProjectionPayload}} FrontendModelModelEventCallbackEntry
|
|
835
|
+
*/
|
|
836
|
+
/**
|
|
837
|
+
* Defines this typedef.
|
|
838
|
+
* @typedef {{callback: (payload: {id: string}) => void}} FrontendModelDestroyEventCallbackEntry
|
|
839
|
+
*/
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Runs merge frontend model event preload.
|
|
843
|
+
* @param {Record<string, import("./query.js").FrontendModelTransportValue>} target - Target preload payload.
|
|
844
|
+
* @param {Record<string, import("./query.js").FrontendModelTransportValue>} source - Source preload payload.
|
|
845
|
+
* @returns {void}
|
|
846
|
+
*/
|
|
847
|
+
function mergeFrontendModelEventPreload(target, source) {
|
|
848
|
+
for (const [relationshipName, value] of Object.entries(source)) {
|
|
849
|
+
const existingValue = target[relationshipName]
|
|
850
|
+
|
|
851
|
+
if (value === true || value === false) {
|
|
852
|
+
if (existingValue === undefined) target[relationshipName] = value
|
|
853
|
+
continue
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
857
|
+
target[relationshipName] = value
|
|
858
|
+
continue
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!existingValue || typeof existingValue !== "object" || Array.isArray(existingValue)) {
|
|
862
|
+
target[relationshipName] = {}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
mergeFrontendModelEventPreload(
|
|
866
|
+
/**
|
|
867
|
+
* Narrows the runtime value to the documented type.
|
|
868
|
+
@type {Record<string, import("./query.js").FrontendModelTransportValue>} */ (target[relationshipName]),
|
|
869
|
+
/**
|
|
870
|
+
* Narrows the runtime value to the documented type.
|
|
871
|
+
@type {Record<string, import("./query.js").FrontendModelTransportValue>} */ (value)
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Runs merge frontend model event select.
|
|
878
|
+
* @param {Record<string, string[]>} target - Target select map.
|
|
879
|
+
* @param {Record<string, string[]>} source - Source select map.
|
|
880
|
+
* @returns {void}
|
|
881
|
+
*/
|
|
882
|
+
function mergeFrontendModelEventSelect(target, source) {
|
|
883
|
+
for (const [modelName, attributes] of Object.entries(source)) {
|
|
884
|
+
const existingAttributes = target[modelName] || []
|
|
885
|
+
|
|
886
|
+
target[modelName] = Array.from(new Set(existingAttributes.concat(attributes)))
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Runs merge unique frontend model event entries.
|
|
892
|
+
* @param {Array<import("./query.js").FrontendModelWithCountPayloadEntry | import("./query.js").FrontendModelAbilitiesPayloadEntry>} target - Target array.
|
|
893
|
+
* @param {Array<import("./query.js").FrontendModelWithCountPayloadEntry | import("./query.js").FrontendModelAbilitiesPayloadEntry>} source - Source array.
|
|
894
|
+
* @returns {void}
|
|
895
|
+
*/
|
|
896
|
+
function mergeUniqueFrontendModelEventEntries(target, source) {
|
|
897
|
+
const existingKeys = new Set(target.map((entry) => JSON.stringify(entry)))
|
|
898
|
+
|
|
899
|
+
for (const entry of source) {
|
|
900
|
+
const key = JSON.stringify(entry)
|
|
901
|
+
|
|
902
|
+
if (existingKeys.has(key)) continue
|
|
903
|
+
|
|
904
|
+
target.push(entry)
|
|
905
|
+
existingKeys.add(key)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Runs merge frontend model event projection payload.
|
|
911
|
+
* @param {import("./query.js").FrontendModelProjectionPayload} target - Target payload.
|
|
912
|
+
* @param {import("./query.js").FrontendModelProjectionPayload} source - Source payload.
|
|
913
|
+
* @returns {void}
|
|
914
|
+
*/
|
|
915
|
+
function mergeFrontendModelEventProjectionPayload(target, source) {
|
|
916
|
+
if (source.preload) {
|
|
917
|
+
if (!target.preload) target.preload = {}
|
|
918
|
+
mergeFrontendModelEventPreload(target.preload, source.preload)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (source.select) {
|
|
922
|
+
if (!target.select) target.select = {}
|
|
923
|
+
mergeFrontendModelEventSelect(target.select, source.select)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (source.selectsExtra) {
|
|
927
|
+
if (!target.selectsExtra) target.selectsExtra = {}
|
|
928
|
+
mergeFrontendModelEventSelect(target.selectsExtra, source.selectsExtra)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (source.withCount) {
|
|
932
|
+
if (!target.withCount) target.withCount = []
|
|
933
|
+
mergeUniqueFrontendModelEventEntries(target.withCount, source.withCount)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (source.abilities) {
|
|
937
|
+
if (!target.abilities) target.abilities = []
|
|
938
|
+
mergeUniqueFrontendModelEventEntries(target.abilities, source.abilities)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (source.queryData !== undefined) {
|
|
942
|
+
const targetQueryData = Array.isArray(target.queryData) ? target.queryData : []
|
|
943
|
+
|
|
944
|
+
target.queryData = targetQueryData
|
|
945
|
+
const queryDataEntries = Array.isArray(source.queryData) ? source.queryData : [source.queryData]
|
|
946
|
+
|
|
947
|
+
for (const entry of queryDataEntries) {
|
|
948
|
+
targetQueryData.push(entry)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Runs frontend model matched event filter keys.
|
|
955
|
+
* @param {?} body - Raw websocket event body.
|
|
956
|
+
* @returns {Set<string>} - Matched event filter keys delivered by the backend.
|
|
957
|
+
*/
|
|
958
|
+
function frontendModelMatchedEventFilterKeys(body) {
|
|
959
|
+
if (!body || typeof body !== "object") return new Set()
|
|
960
|
+
|
|
961
|
+
const keys = /**
|
|
962
|
+
* Narrows the runtime value to the documented type.
|
|
963
|
+
@type {{matchedEventFilterKeys?: ?}} */ (body).matchedEventFilterKeys
|
|
964
|
+
|
|
965
|
+
if (!Array.isArray(keys)) return new Set()
|
|
966
|
+
|
|
967
|
+
return new Set(keys.map((key) => String(key)))
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Runs frontend model event entry matches.
|
|
972
|
+
* @param {FrontendModelModelEventCallbackEntry} entry - Callback entry.
|
|
973
|
+
* @param {Set<string>} matchedEventFilterKeys - Backend matched filter keys.
|
|
974
|
+
* @returns {boolean} Whether the callback should receive the event.
|
|
975
|
+
*/
|
|
976
|
+
function frontendModelEventEntryMatches(entry, matchedEventFilterKeys) {
|
|
977
|
+
if (!entry.eventFilterKey) return true
|
|
978
|
+
|
|
979
|
+
return matchedEventFilterKeys.has(entry.eventFilterKey)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Runs assert no destroy event filter.
|
|
984
|
+
* @param {typeof FrontendModelBase} ModelClass - Event model class.
|
|
985
|
+
* @param {import("./query.js").FrontendModelEventOptions} options - Event options.
|
|
986
|
+
* @returns {void}
|
|
987
|
+
*/
|
|
988
|
+
function assertNoDestroyEventFilter(ModelClass, options) {
|
|
989
|
+
const eventOptionsPayload = frontendModelEventOptionsPayload(ModelClass, options)
|
|
990
|
+
|
|
991
|
+
if (!eventOptionsPayload.eventFilterKey) return
|
|
992
|
+
|
|
993
|
+
throw new Error("Frontend model destroy event subscriptions do not support query filters")
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Per-model class singleton that multiplexes all registered onCreate /
|
|
998
|
+
* onUpdate / onDestroy callbacks — class-level + instance-level —
|
|
999
|
+
* over one WebsocketChannelV2 subscription. Subscription opens on the
|
|
1000
|
+
* first listener and closes when the last one unsubscribes.
|
|
1001
|
+
*
|
|
1002
|
+
* Instance-level listeners also receive auto-merge: when an `update`
|
|
1003
|
+
* event arrives for a registered instance id, the instance's
|
|
1004
|
+
* attributes are updated in place before the callback fires, so
|
|
1005
|
+
* callers can read fresh values from the same instance handle.
|
|
1006
|
+
*/
|
|
1007
|
+
class FrontendModelEventSubscription {
|
|
1008
|
+
/**
|
|
1009
|
+
* Runs constructor.
|
|
1010
|
+
* @param {typeof FrontendModelBase} ModelClass - Frontend model class for this subscription bucket.
|
|
1011
|
+
*/
|
|
1012
|
+
constructor(ModelClass) {
|
|
1013
|
+
this.ModelClass = ModelClass
|
|
1014
|
+
/**
|
|
1015
|
+
* Narrows the runtime value to the documented type.
|
|
1016
|
+
@type {Set<FrontendModelModelEventCallbackEntry>} */
|
|
1017
|
+
this.classCreateCallbacks = new Set()
|
|
1018
|
+
/**
|
|
1019
|
+
* Narrows the runtime value to the documented type.
|
|
1020
|
+
@type {Set<FrontendModelModelEventCallbackEntry>} */
|
|
1021
|
+
this.classUpdateCallbacks = new Set()
|
|
1022
|
+
/**
|
|
1023
|
+
* Narrows the runtime value to the documented type.
|
|
1024
|
+
@type {Set<FrontendModelDestroyEventCallbackEntry>} */
|
|
1025
|
+
this.classDestroyCallbacks = new Set()
|
|
1026
|
+
/**
|
|
1027
|
+
* Narrows the runtime value to the documented type.
|
|
1028
|
+
@type {Map<string, {instance: InstanceType<typeof FrontendModelBase>, updateCallbacks: Set<FrontendModelModelEventCallbackEntry>, destroyCallbacks: Set<FrontendModelDestroyEventCallbackEntry>}>} */
|
|
1029
|
+
this.instanceListeners = new Map()
|
|
1030
|
+
/**
|
|
1031
|
+
* Narrows the runtime value to the documented type.
|
|
1032
|
+
@type {?} */
|
|
1033
|
+
this.channelHandle = null
|
|
1034
|
+
/**
|
|
1035
|
+
* Narrows the runtime value to the documented type.
|
|
1036
|
+
@type {Promise<void> | null} */
|
|
1037
|
+
this.readyPromise = null
|
|
1038
|
+
/**
|
|
1039
|
+
* Narrows the runtime value to the documented type.
|
|
1040
|
+
@type {string | null} */
|
|
1041
|
+
this.subscriptionParamsKey = null
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Runs subscription params.
|
|
1046
|
+
* @returns {{model: string, eventFilters?: import("./query.js").FrontendModelEventFilterPayloadEntry[], unfilteredEventDelivery?: boolean} & import("./query.js").FrontendModelProjectionPayload} - Current websocket subscription params.
|
|
1047
|
+
*/
|
|
1048
|
+
subscriptionParams() {
|
|
1049
|
+
/**
|
|
1050
|
+
* Projection payload.
|
|
1051
|
+
@type {import("./query.js").FrontendModelProjectionPayload} */
|
|
1052
|
+
const projectionPayload = {}
|
|
1053
|
+
/**
|
|
1054
|
+
* Event filters by key.
|
|
1055
|
+
@type {Record<string, import("./query.js").FrontendModelEventFilterPayloadEntry>} */
|
|
1056
|
+
const eventFiltersByKey = {}
|
|
1057
|
+
const projectionEntries = []
|
|
1058
|
+
let hasUnfilteredEventDelivery = this.classDestroyCallbacks.size > 0
|
|
1059
|
+
|
|
1060
|
+
for (const entry of this.classCreateCallbacks) projectionEntries.push(entry)
|
|
1061
|
+
for (const entry of this.classUpdateCallbacks) projectionEntries.push(entry)
|
|
1062
|
+
|
|
1063
|
+
for (const listener of this.instanceListeners.values()) {
|
|
1064
|
+
for (const entry of listener.updateCallbacks) projectionEntries.push(entry)
|
|
1065
|
+
if (listener.destroyCallbacks.size > 0) hasUnfilteredEventDelivery = true
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
for (const entry of projectionEntries) {
|
|
1069
|
+
mergeFrontendModelEventProjectionPayload(projectionPayload, entry.projectionPayload)
|
|
1070
|
+
|
|
1071
|
+
if (entry.eventFilterKey && entry.eventFilterPayload) {
|
|
1072
|
+
eventFiltersByKey[entry.eventFilterKey] = {
|
|
1073
|
+
...entry.eventFilterPayload,
|
|
1074
|
+
key: entry.eventFilterKey
|
|
1075
|
+
}
|
|
1076
|
+
} else {
|
|
1077
|
+
hasUnfilteredEventDelivery = true
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const eventFilters = Object.values(eventFiltersByKey)
|
|
1082
|
+
const eventFilterParams = eventFilters.length > 0
|
|
1083
|
+
? {
|
|
1084
|
+
eventFilters,
|
|
1085
|
+
...(hasUnfilteredEventDelivery ? {unfilteredEventDelivery: true} : {})
|
|
1086
|
+
}
|
|
1087
|
+
: {}
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
model: this.ModelClass.getModelName(),
|
|
1091
|
+
...eventFilterParams,
|
|
1092
|
+
...projectionPayload
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Runs subscription params json.
|
|
1098
|
+
* @returns {string} - Stable key for current subscription params.
|
|
1099
|
+
*/
|
|
1100
|
+
subscriptionParamsJson() {
|
|
1101
|
+
return JSON.stringify(this.subscriptionParams())
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Runs ensure subscribed.
|
|
1106
|
+
@returns {Promise<void>} */
|
|
1107
|
+
async ensureSubscribed() {
|
|
1108
|
+
const paramsJson = this.subscriptionParamsJson()
|
|
1109
|
+
|
|
1110
|
+
if (this.channelHandle && !this.channelHandle.isClosed()) {
|
|
1111
|
+
if (this.subscriptionParamsKey !== paramsJson) {
|
|
1112
|
+
this.channelHandle.close()
|
|
1113
|
+
this.channelHandle = null
|
|
1114
|
+
this.readyPromise = null
|
|
1115
|
+
} else {
|
|
1116
|
+
if (this.readyPromise) await this.readyPromise
|
|
1117
|
+
return
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Serialize parallel calls (e.g. Promise.all([onCreate, onUpdate,
|
|
1122
|
+
// onDestroy])) so we open exactly one subscription per model class
|
|
1123
|
+
// instead of racing three concurrent subscribeChannel calls.
|
|
1124
|
+
if (this.readyPromise) {
|
|
1125
|
+
await this.readyPromise
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const client = /**
|
|
1130
|
+
* Narrows the runtime value to the documented type.
|
|
1131
|
+
@type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
|
|
1132
|
+
|
|
1133
|
+
if (!client || typeof client.subscribeChannel !== "function") {
|
|
1134
|
+
throw new Error("Frontend model event subscriptions require configureTransport({websocketUrl}) or configureTransport({websocketClient})")
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
this.readyPromise = (async () => {
|
|
1138
|
+
if (typeof client.connect === "function") await client.connect()
|
|
1139
|
+
|
|
1140
|
+
const params = this.subscriptionParams()
|
|
1141
|
+
|
|
1142
|
+
this.subscriptionParamsKey = JSON.stringify(params)
|
|
1143
|
+
this.channelHandle = client.subscribeChannel(FRONTEND_MODELS_CHANNEL_NAME, {
|
|
1144
|
+
params,
|
|
1145
|
+
onMessage: (/**
|
|
1146
|
+
* Narrows the runtime value to the documented type.
|
|
1147
|
+
@type {?} */ body) => this._dispatchEvent(body),
|
|
1148
|
+
onClose: () => {
|
|
1149
|
+
this.channelHandle = null
|
|
1150
|
+
this.readyPromise = null
|
|
1151
|
+
this.subscriptionParamsKey = null
|
|
1152
|
+
this.instanceListeners.clear()
|
|
1153
|
+
|
|
1154
|
+
const hasCallbacks = this.classCreateCallbacks.size > 0
|
|
1155
|
+
|| this.classUpdateCallbacks.size > 0
|
|
1156
|
+
|| this.classDestroyCallbacks.size > 0
|
|
1157
|
+
|
|
1158
|
+
if (hasCallbacks && client.autoReconnect) {
|
|
1159
|
+
void this.ensureSubscribed()
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
})
|
|
1163
|
+
await this.channelHandle.ready
|
|
1164
|
+
})()
|
|
1165
|
+
|
|
1166
|
+
await this.readyPromise
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Runs dispatch event.
|
|
1171
|
+
* @param {?} body - WebSocket event payload.
|
|
1172
|
+
*/
|
|
1173
|
+
_dispatchEvent(body) {
|
|
1174
|
+
if (!body || typeof body !== "object") return
|
|
1175
|
+
|
|
1176
|
+
const action = body.action
|
|
1177
|
+
const rawId = body.id
|
|
1178
|
+
|
|
1179
|
+
if (action !== "create" && action !== "update" && action !== "destroy") return
|
|
1180
|
+
if (rawId === undefined || rawId === null) return
|
|
1181
|
+
|
|
1182
|
+
const id = String(rawId)
|
|
1183
|
+
const matchedEventFilterKeys = frontendModelMatchedEventFilterKeys(body)
|
|
1184
|
+
|
|
1185
|
+
if (action === "destroy") {
|
|
1186
|
+
const listener = this.instanceListeners.get(id)
|
|
1187
|
+
|
|
1188
|
+
if (listener) {
|
|
1189
|
+
for (const entry of listener.destroyCallbacks) {
|
|
1190
|
+
try { entry.callback({id}) } catch (error) { console.error(error) }
|
|
1191
|
+
}
|
|
1192
|
+
this.instanceListeners.delete(id)
|
|
1193
|
+
}
|
|
1194
|
+
for (const entry of this.classDestroyCallbacks) {
|
|
1195
|
+
try { entry.callback({id}) } catch (error) { console.error(error) }
|
|
1196
|
+
}
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (!body.record || typeof body.record !== "object") return
|
|
1201
|
+
|
|
1202
|
+
const deserializedRecord = /**
|
|
1203
|
+
* Narrows the runtime value to the documented type.
|
|
1204
|
+
@type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(body.record))
|
|
1205
|
+
const freshModel = /**
|
|
1206
|
+
* Narrows the runtime value to the documented type.
|
|
1207
|
+
@type {?} */ (this.ModelClass).instantiateFromResponse(deserializedRecord)
|
|
1208
|
+
const listener = this.instanceListeners.get(id)
|
|
1209
|
+
|
|
1210
|
+
if (action === "update" && listener) {
|
|
1211
|
+
const matchingUpdateCallbacks = Array.from(listener.updateCallbacks).filter((entry) =>
|
|
1212
|
+
frontendModelEventEntryMatches(entry, matchedEventFilterKeys)
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
if (matchingUpdateCallbacks.length > 0) {
|
|
1216
|
+
// Auto-merge into the registered instance so callers reading
|
|
1217
|
+
// through the same handle see fresh attributes.
|
|
1218
|
+
const instanceAny = /**
|
|
1219
|
+
* Narrows the runtime value to the documented type.
|
|
1220
|
+
@type {?} */ (listener.instance)
|
|
1221
|
+
|
|
1222
|
+
instanceAny.assignAttributes(freshModel.attributes())
|
|
1223
|
+
instanceAny._persistedAttributes = cloneFrontendModelAttributes(listener.instance.attributes())
|
|
1224
|
+
|
|
1225
|
+
for (const entry of matchingUpdateCallbacks) {
|
|
1226
|
+
try { entry.callback({id, model: listener.instance}) } catch (error) { console.error(error) }
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const classCallbacks = action === "create" ? this.classCreateCallbacks : this.classUpdateCallbacks
|
|
1232
|
+
|
|
1233
|
+
for (const entry of classCallbacks) {
|
|
1234
|
+
if (!frontendModelEventEntryMatches(entry, matchedEventFilterKeys)) continue
|
|
1235
|
+
|
|
1236
|
+
try { entry.callback({id, model: freshModel}) } catch (error) { console.error(error) }
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Runs maybe teardown.
|
|
1242
|
+
@returns {void} */
|
|
1243
|
+
maybeTeardown() {
|
|
1244
|
+
const hasAnyListener = this.classCreateCallbacks.size > 0
|
|
1245
|
+
|| this.classUpdateCallbacks.size > 0
|
|
1246
|
+
|| this.classDestroyCallbacks.size > 0
|
|
1247
|
+
|| this.instanceListeners.size > 0
|
|
1248
|
+
|
|
1249
|
+
if (hasAnyListener) return
|
|
1250
|
+
if (!this.channelHandle) return
|
|
1251
|
+
|
|
1252
|
+
try {
|
|
1253
|
+
this.channelHandle.close()
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
console.error(error)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
this.channelHandle = null
|
|
1259
|
+
this.readyPromise = null
|
|
1260
|
+
this.subscriptionParamsKey = null
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Frontend model event subscriptions.
|
|
1266
|
+
@type {WeakMap<typeof FrontendModelBase, FrontendModelEventSubscription>} */
|
|
1267
|
+
const frontendModelEventSubscriptions = new WeakMap()
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Runs ensure frontend model event subscription.
|
|
1271
|
+
* @param {typeof FrontendModelBase} ModelClass - Model class.
|
|
1272
|
+
* @returns {FrontendModelEventSubscription} - Per-class subscription helper.
|
|
1273
|
+
*/
|
|
1274
|
+
function ensureFrontendModelEventSubscription(ModelClass) {
|
|
1275
|
+
let sub = frontendModelEventSubscriptions.get(ModelClass)
|
|
1276
|
+
|
|
1277
|
+
if (!sub) {
|
|
1278
|
+
sub = new FrontendModelEventSubscription(ModelClass)
|
|
1279
|
+
frontendModelEventSubscriptions.set(ModelClass, sub)
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return sub
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Runs ensure frontend model instance listener.
|
|
1287
|
+
* @param {FrontendModelEventSubscription} sub - Event subscription bucket.
|
|
1288
|
+
* @param {string} id - Model id.
|
|
1289
|
+
* @param {InstanceType<typeof FrontendModelBase>} instance - Listener instance.
|
|
1290
|
+
* @returns {{instance: InstanceType<typeof FrontendModelBase>, updateCallbacks: Set<FrontendModelModelEventCallbackEntry>, destroyCallbacks: Set<FrontendModelDestroyEventCallbackEntry>}} - Instance listener bucket.
|
|
1291
|
+
*/
|
|
1292
|
+
function ensureFrontendModelInstanceListener(sub, id, instance) {
|
|
1293
|
+
let listener = sub.instanceListeners.get(id)
|
|
1294
|
+
|
|
1295
|
+
if (!listener) {
|
|
1296
|
+
listener = {instance, updateCallbacks: new Set(), destroyCallbacks: new Set()}
|
|
1297
|
+
sub.instanceListeners.set(id, listener)
|
|
1298
|
+
} else {
|
|
1299
|
+
listener.instance = instance
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return listener
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Runs frontend model command url.
|
|
1307
|
+
* @param {string} resourcePath - Resource path prefix.
|
|
1308
|
+
* @param {string} commandName - Command path segment.
|
|
1309
|
+
* @returns {string} - Frontend model API URL.
|
|
1310
|
+
*/
|
|
1311
|
+
function frontendModelCommandUrl(resourcePath, commandName) {
|
|
1312
|
+
const configuredUrl = frontendModelTransportUrl()
|
|
1313
|
+
const normalizedResourcePath = resourcePath.startsWith("/") ? resourcePath : `/${resourcePath}`
|
|
1314
|
+
|
|
1315
|
+
return `${configuredUrl}${normalizedResourcePath}/${commandName}`
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Runs frontend model api url.
|
|
1320
|
+
* @returns {string} - Shared frontend-model API URL.
|
|
1321
|
+
*/
|
|
1322
|
+
function frontendModelApiUrl() {
|
|
1323
|
+
return `${frontendModelTransportUrl()}${SHARED_FRONTEND_MODEL_API_PATH}`
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Runs frontend model transport path.
|
|
1328
|
+
* @param {string} url - Request URL or path.
|
|
1329
|
+
* @returns {string} - Websocket-safe request path.
|
|
1330
|
+
*/
|
|
1331
|
+
function frontendModelTransportPath(url) {
|
|
1332
|
+
if (typeof url !== "string" || url.length < 1) {
|
|
1333
|
+
throw new Error(`Expected frontend model transport URL/path, got: ${url}`)
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (url.startsWith("/")) {
|
|
1337
|
+
return url
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
try {
|
|
1341
|
+
const parsedUrl = new URL(url)
|
|
1342
|
+
|
|
1343
|
+
return `${parsedUrl.pathname}${parsedUrl.search}`
|
|
1344
|
+
} catch {
|
|
1345
|
+
return url
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Runs frontend model request headers.
|
|
1351
|
+
* @returns {Record<string, string>} - Headers for frontend-model HTTP requests.
|
|
1352
|
+
*/
|
|
1353
|
+
function frontendModelRequestHeaders() {
|
|
1354
|
+
const dynamicHeaders = typeof frontendModelTransportConfig.requestHeaders === "function"
|
|
1355
|
+
? (frontendModelTransportConfig.requestHeaders() || {})
|
|
1356
|
+
: (frontendModelTransportConfig.requestHeaders || {})
|
|
1357
|
+
|
|
1358
|
+
return {"Content-Type": "application/json", ...dynamicHeaders}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Runs perform shared frontend model api request.
|
|
1363
|
+
* @param {Record<string, ?>} requestPayload - Shared request payload.
|
|
1364
|
+
* @returns {Promise<Record<string, ?>>} - Decoded shared frontend-model API response.
|
|
1365
|
+
*/
|
|
1366
|
+
async function performSharedFrontendModelApiRequest(requestPayload) {
|
|
1367
|
+
const serializedRequestPayload = serializeFrontendModelTransportValue(requestPayload)
|
|
1368
|
+
const websocketClient = frontendModelTransportConfig.websocketClient
|
|
1369
|
+
const url = frontendModelApiUrl()
|
|
1370
|
+
const mergedHeaders = frontendModelRequestHeaders()
|
|
1371
|
+
|
|
1372
|
+
if (websocketClient) {
|
|
1373
|
+
const response = await websocketClient.post(frontendModelTransportPath(url), serializedRequestPayload, {
|
|
1374
|
+
headers: mergedHeaders
|
|
1375
|
+
})
|
|
1376
|
+
const responseJson = response.json()
|
|
1377
|
+
|
|
1378
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(responseJson))
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const response = await fetch(url, {
|
|
1382
|
+
body: JSON.stringify(serializedRequestPayload),
|
|
1383
|
+
credentials: "include",
|
|
1384
|
+
headers: mergedHeaders,
|
|
1385
|
+
method: "POST"
|
|
1386
|
+
})
|
|
1387
|
+
|
|
1388
|
+
const responseText = await response.text()
|
|
1389
|
+
|
|
1390
|
+
if (!response.ok) {
|
|
1391
|
+
throwFrontendModelHttpError({
|
|
1392
|
+
commandLabel: "shared frontend model API",
|
|
1393
|
+
response,
|
|
1394
|
+
responseText
|
|
1395
|
+
})
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const json = responseText.length > 0 ? JSON.parse(responseText) : {}
|
|
1399
|
+
|
|
1400
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(json))
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Throws a frontend-model HTTP error with backend-provided envelope details when available.
|
|
1405
|
+
* @param {{commandLabel: string, response: Response, responseText: string}} args - Error response details.
|
|
1406
|
+
* @returns {never}
|
|
1407
|
+
*/
|
|
1408
|
+
function throwFrontendModelHttpError({commandLabel, response, responseText}) {
|
|
1409
|
+
// Surface the backend's friendly errorMessage envelope (the
|
|
1410
|
+
// `{status: "error", errorMessage: "..."}` shape every controller
|
|
1411
|
+
// ships on its 4xx/5xx responses) instead of the generic status
|
|
1412
|
+
// string. Fall through to the status-only message when the body is
|
|
1413
|
+
// missing, non-JSON, or has no usable errorMessage field.
|
|
1414
|
+
const responseContentType = response.headers.get("content-type")
|
|
1415
|
+
|
|
1416
|
+
if (responseContentType && responseContentType.includes("application/json") && responseText.length > 0) {
|
|
1417
|
+
/**
|
|
1418
|
+
* Defines errorBody.
|
|
1419
|
+
@type {Record<string, ?> | null} */
|
|
1420
|
+
let errorBody
|
|
1421
|
+
|
|
1422
|
+
try {
|
|
1423
|
+
errorBody = JSON.parse(responseText)
|
|
1424
|
+
} catch {
|
|
1425
|
+
errorBody = null
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (errorBody && typeof errorBody.errorMessage === "string" && errorBody.errorMessage.trim().length > 0) {
|
|
1429
|
+
throw new Error(errorBody.errorMessage.trim())
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
throw new Error(`Request failed (${response.status}) for ${commandLabel}`)
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Runs flush pending shared frontend model requests.
|
|
1438
|
+
* @returns {Promise<void>} - Resolves after pending shared frontend-model requests flush.
|
|
1439
|
+
*/
|
|
1440
|
+
async function flushPendingSharedFrontendModelRequests() {
|
|
1441
|
+
sharedFrontendModelFlushScheduled = false
|
|
1442
|
+
|
|
1443
|
+
if (pendingSharedFrontendModelRequests.length < 1) {
|
|
1444
|
+
resolveFrontendModelIdleWaiters()
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const batchedRequests = pendingSharedFrontendModelRequests
|
|
1449
|
+
pendingSharedFrontendModelRequests = []
|
|
1450
|
+
|
|
1451
|
+
const url = frontendModelApiUrl()
|
|
1452
|
+
const requestPayload = {
|
|
1453
|
+
requests: batchedRequests.map((request) => {
|
|
1454
|
+
if (request.customPath) {
|
|
1455
|
+
return {
|
|
1456
|
+
commandType: request.commandType,
|
|
1457
|
+
customPath: request.customPath,
|
|
1458
|
+
model: request.modelClass.getModelName(),
|
|
1459
|
+
payload: request.payload,
|
|
1460
|
+
requestId: request.requestId
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return {
|
|
1465
|
+
commandType: request.commandType,
|
|
1466
|
+
model: request.modelClass.getModelName(),
|
|
1467
|
+
payload: request.payload,
|
|
1468
|
+
requestId: request.requestId
|
|
1469
|
+
}
|
|
1470
|
+
})
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
await trackFrontendModelTransportRequest(async () => {
|
|
1474
|
+
try {
|
|
1475
|
+
void url
|
|
1476
|
+
const decodedResponse = await performSharedFrontendModelApiRequest(requestPayload)
|
|
1477
|
+
const responses = Array.isArray(decodedResponse.responses) ? decodedResponse.responses : []
|
|
1478
|
+
const responsesById = new Map(responses.map((entry) => [entry.requestId, entry.response]))
|
|
1479
|
+
|
|
1480
|
+
for (const request of batchedRequests) {
|
|
1481
|
+
const responsePayload = responsesById.get(request.requestId)
|
|
1482
|
+
|
|
1483
|
+
if (!responsePayload || typeof responsePayload !== "object") {
|
|
1484
|
+
request.reject(new Error(`Missing batched response for ${request.modelClass.name}#${request.commandType}`))
|
|
1485
|
+
continue
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
request.resolve(/**
|
|
1489
|
+
* Narrows the runtime value to the documented type.
|
|
1490
|
+
@type {Record<string, ?>} */ (responsePayload))
|
|
1491
|
+
}
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
for (const request of batchedRequests) {
|
|
1494
|
+
request.reject(error)
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
})
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/**
|
|
1501
|
+
* Runs schedule shared frontend model request flush.
|
|
1502
|
+
@returns {void} */
|
|
1503
|
+
function scheduleSharedFrontendModelRequestFlush() {
|
|
1504
|
+
if (sharedFrontendModelFlushScheduled) return
|
|
1505
|
+
|
|
1506
|
+
sharedFrontendModelFlushScheduled = true
|
|
1507
|
+
queueMicrotask(() => {
|
|
1508
|
+
void flushPendingSharedFrontendModelRequests()
|
|
1509
|
+
})
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Custom commands still use the shared frontend-model API. This helper only builds the backend route path the server should dispatch after validating the segments.
|
|
1514
|
+
* @param {object} args - Arguments.
|
|
1515
|
+
* @param {string} args.commandName - Command path segment.
|
|
1516
|
+
* @param {string} args.modelName - Frontend model class name.
|
|
1517
|
+
* @param {string | number | null | undefined} [args.memberId] - Optional member id.
|
|
1518
|
+
* @param {string} args.resourcePath - Resource path prefix.
|
|
1519
|
+
* @returns {string} - Custom backend route path.
|
|
1520
|
+
*/
|
|
1521
|
+
function frontendModelCustomCommandPath({commandName, memberId, modelName, resourcePath}) {
|
|
1522
|
+
const validatedResourcePath = validateFrontendModelResourcePath({modelName, resourcePath})
|
|
1523
|
+
const validatedCommandName = validateFrontendModelResourceCommandName({commandName, commandType: commandName, modelName})
|
|
1524
|
+
|
|
1525
|
+
if (memberId === undefined || memberId === null || memberId === "") {
|
|
1526
|
+
return `${validatedResourcePath}/${validatedCommandName}`
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return `${validatedResourcePath}/${encodeURIComponent(String(memberId))}/${validatedCommandName}`
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Runs assert find by conditions shape.
|
|
1534
|
+
* @param {?} conditions - findBy conditions.
|
|
1535
|
+
* @returns {void}
|
|
1536
|
+
*/
|
|
1537
|
+
function assertFindByConditionsShape(conditions) {
|
|
1538
|
+
if (!conditions || typeof conditions !== "object" || Array.isArray(conditions)) {
|
|
1539
|
+
throw new Error(`findBy expects conditions to be a plain object, got: ${conditions}`)
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const conditionsPrototype = Object.getPrototypeOf(conditions)
|
|
1543
|
+
|
|
1544
|
+
if (conditionsPrototype !== Object.prototype && conditionsPrototype !== null) {
|
|
1545
|
+
throw new Error(`findBy expects conditions to be a plain object, got: ${conditions}`)
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const symbolKeys = Object.getOwnPropertySymbols(conditions)
|
|
1549
|
+
|
|
1550
|
+
if (symbolKeys.length > 0) {
|
|
1551
|
+
throw new Error(`findBy does not support symbol condition keys (keys: ${symbolKeys.map((key) => key.toString()).join(", ")})`)
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Runs assert defined find by condition value.
|
|
1557
|
+
* @param {?} value - Condition value to validate.
|
|
1558
|
+
* @param {string} keyPath - Key path for error output.
|
|
1559
|
+
* @returns {void}
|
|
1560
|
+
*/
|
|
1561
|
+
function assertDefinedFindByConditionValue(value, keyPath) {
|
|
1562
|
+
if (value === undefined) {
|
|
1563
|
+
throw new Error(`findBy does not support undefined condition values (key: ${keyPath})`)
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (typeof value === "function") {
|
|
1567
|
+
throw new Error(`findBy does not support function condition values (key: ${keyPath})`)
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (typeof value === "symbol") {
|
|
1571
|
+
throw new Error(`findBy does not support symbol condition values (key: ${keyPath})`)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
if (typeof value === "bigint") {
|
|
1575
|
+
throw new Error(`findBy does not support bigint condition values (key: ${keyPath})`)
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
1579
|
+
throw new Error(`findBy does not support non-finite number condition values (key: ${keyPath})`)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (Array.isArray(value)) {
|
|
1583
|
+
value.forEach((entry, index) => {
|
|
1584
|
+
assertDefinedFindByConditionValue(entry, `${keyPath}[${index}]`)
|
|
1585
|
+
})
|
|
1586
|
+
return
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (value && typeof value === "object") {
|
|
1590
|
+
if (value instanceof Date) {
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const objectValue = /**
|
|
1595
|
+
* Narrows the runtime value to the documented type.
|
|
1596
|
+
@type {Record<string, ?>} */ (value)
|
|
1597
|
+
const prototype = Object.getPrototypeOf(objectValue)
|
|
1598
|
+
|
|
1599
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1600
|
+
throw new Error(`findBy does not support non-plain object condition values (key: ${keyPath})`)
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const symbolKeys = Object.getOwnPropertySymbols(objectValue)
|
|
1604
|
+
|
|
1605
|
+
if (symbolKeys.length > 0) {
|
|
1606
|
+
throw new Error(`findBy does not support symbol condition keys (key: ${keyPath})`)
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const valueObject = /**
|
|
1610
|
+
* Narrows the runtime value to the documented type.
|
|
1611
|
+
@type {Record<string, ?>} */ (value)
|
|
1612
|
+
|
|
1613
|
+
Object.keys(valueObject).forEach((nestedKey) => {
|
|
1614
|
+
assertDefinedFindByConditionValue(valueObject[nestedKey], `${keyPath}.${nestedKey}`)
|
|
1615
|
+
})
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** Base class for generated frontend model classes. */
|
|
1620
|
+
export default class FrontendModelBase {
|
|
1621
|
+
/**
|
|
1622
|
+
* Narrows the runtime value to the documented type.
|
|
1623
|
+
@type {string | undefined} */
|
|
1624
|
+
static modelName
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Autoload.
|
|
1628
|
+
* @type {boolean} - Global auto-batch-preload toggle. Apps can opt out via FrontendModelBase.setAutoload(false).
|
|
1629
|
+
*/
|
|
1630
|
+
static _autoload = true
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Runs get autoload.
|
|
1634
|
+
* @returns {boolean} Whether auto-batch-preload of relationships on lazy access is enabled globally.
|
|
1635
|
+
*/
|
|
1636
|
+
static getAutoload() { return FrontendModelBase._autoload }
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Runs set autoload.
|
|
1640
|
+
* @param {boolean} newValue - Whether auto-batch-preload of relationships is enabled.
|
|
1641
|
+
* @returns {void}
|
|
1642
|
+
*/
|
|
1643
|
+
static setAutoload(newValue) { FrontendModelBase._autoload = newValue }
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Narrows the runtime value to the documented type.
|
|
1647
|
+
@type {Record<string, ?>} */
|
|
1648
|
+
_attributes
|
|
1649
|
+
/**
|
|
1650
|
+
* Narrows the runtime value to the documented type.
|
|
1651
|
+
@type {Record<string, FrontendModelHasManyRelationship<typeof FrontendModelBase, typeof FrontendModelBase> | FrontendModelSingularRelationship<typeof FrontendModelBase, typeof FrontendModelBase>>} */
|
|
1652
|
+
_relationships
|
|
1653
|
+
/**
|
|
1654
|
+
* Narrows the runtime value to the documented type.
|
|
1655
|
+
@type {Record<string, FrontendModelAttachmentHandle>} */
|
|
1656
|
+
_attachments
|
|
1657
|
+
/**
|
|
1658
|
+
* Narrows the runtime value to the documented type.
|
|
1659
|
+
@type {Set<string> | null} */
|
|
1660
|
+
_selectedAttributes
|
|
1661
|
+
/**
|
|
1662
|
+
* Narrows the runtime value to the documented type.
|
|
1663
|
+
@type {boolean} */
|
|
1664
|
+
_isNewRecord
|
|
1665
|
+
/**
|
|
1666
|
+
* Narrows the runtime value to the documented type.
|
|
1667
|
+
@type {boolean} */
|
|
1668
|
+
_markedForDestruction
|
|
1669
|
+
/**
|
|
1670
|
+
* Narrows the runtime value to the documented type.
|
|
1671
|
+
@type {Record<string, ?>} */
|
|
1672
|
+
_persistedAttributes
|
|
1673
|
+
/**
|
|
1674
|
+
* Narrows the runtime value to the documented type.
|
|
1675
|
+
* @type {Array<FrontendModelBase> | undefined} - Shared reference to sibling records loaded in the same batch. Used by auto-batch-preload.
|
|
1676
|
+
*/
|
|
1677
|
+
_loadCohort
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Runs constructor.
|
|
1681
|
+
* @param {Record<string, ?>} [attributes] - Initial attributes.
|
|
1682
|
+
*/
|
|
1683
|
+
constructor(attributes = {}) {
|
|
1684
|
+
const ModelClass = /**
|
|
1685
|
+
* Narrows the runtime value to the documented type.
|
|
1686
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
1687
|
+
|
|
1688
|
+
ModelClass.ensureGeneratedAttachmentMethods()
|
|
1689
|
+
this._attributes = {}
|
|
1690
|
+
this._relationships = {}
|
|
1691
|
+
this._attachments = {}
|
|
1692
|
+
this._selectedAttributes = null
|
|
1693
|
+
this._isNewRecord = true
|
|
1694
|
+
this._markedForDestruction = false
|
|
1695
|
+
this._persistedAttributes = {}
|
|
1696
|
+
this.assignAttributes(attributes)
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Runs ensure generated attachment methods.
|
|
1701
|
+
* @this {typeof FrontendModelBase}
|
|
1702
|
+
* @returns {void} - Ensures attachment helper methods exist on the prototype.
|
|
1703
|
+
*/
|
|
1704
|
+
static ensureGeneratedAttachmentMethods() {
|
|
1705
|
+
if (this._generatedAttachmentMethods) return
|
|
1706
|
+
|
|
1707
|
+
const attachments = this.attachmentDefinitions()
|
|
1708
|
+
const prototype = /**
|
|
1709
|
+
* Narrows the runtime value to the documented type.
|
|
1710
|
+
@type {Record<string, ?>} */ (this.prototype)
|
|
1711
|
+
|
|
1712
|
+
for (const attachmentName of Object.keys(attachments)) {
|
|
1713
|
+
if (!(attachmentName in prototype)) {
|
|
1714
|
+
prototype[attachmentName] = function() {
|
|
1715
|
+
return this.getAttachmentByName(attachmentName)
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
this._generatedAttachmentMethods = true
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Runs resource config.
|
|
1725
|
+
* @returns {FrontendModelResourceConfig} - Resource configuration.
|
|
1726
|
+
*/
|
|
1727
|
+
static resourceConfig() {
|
|
1728
|
+
throw new Error("resourceConfig() must be implemented by subclasses")
|
|
1729
|
+
// eslint-disable-next-line no-unreachable
|
|
1730
|
+
return {}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Runs relationship model classes.
|
|
1735
|
+
* @this {typeof FrontendModelBase}
|
|
1736
|
+
* @returns {Record<string, typeof FrontendModelBase | string>} - Relationship model classes (or class name strings) keyed by relationship name.
|
|
1737
|
+
*/
|
|
1738
|
+
static relationshipModelClasses() {
|
|
1739
|
+
return {}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Register a frontend model class so it can be resolved by name in relationship lookups.
|
|
1744
|
+
* @param {typeof FrontendModelBase} modelClass - Model class to register.
|
|
1745
|
+
* @returns {void}
|
|
1746
|
+
*/
|
|
1747
|
+
static registerModel(modelClass) {
|
|
1748
|
+
registerFrontendModel(modelClass)
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Runs define scope.
|
|
1753
|
+
* @param {(...args: Array<?>) => ?} callback - Scope callback.
|
|
1754
|
+
* @returns {((...args: Array<?>) => import("./query.js").default<typeof FrontendModelBase>) & {scope: (...args: Array<?>) => import("../utils/model-scope.js").ModelScopeDescriptor}} - Scope helper.
|
|
1755
|
+
*/
|
|
1756
|
+
static defineScope(callback) {
|
|
1757
|
+
return defineModelScope({
|
|
1758
|
+
callback,
|
|
1759
|
+
modelClass: this,
|
|
1760
|
+
startQuery: () => this.query()
|
|
1761
|
+
})
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* Resolve a relationship model class value that may be a class reference or a string name.
|
|
1766
|
+
* @param {typeof FrontendModelBase | string | null | undefined} value - Class or class name.
|
|
1767
|
+
* @returns {typeof FrontendModelBase | null} - Resolved model class.
|
|
1768
|
+
*/
|
|
1769
|
+
static resolveModelClass(value) {
|
|
1770
|
+
return resolveFrontendModelClass(value)
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Runs relationship definitions.
|
|
1775
|
+
* @this {typeof FrontendModelBase}
|
|
1776
|
+
* @returns {Record<string, {type: "belongsTo" | "hasOne" | "hasMany", autoload?: boolean}>} - Relationship definitions keyed by relationship name.
|
|
1777
|
+
*/
|
|
1778
|
+
static relationshipDefinitions() {
|
|
1779
|
+
return {}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Runs attachment definitions.
|
|
1784
|
+
* @this {typeof FrontendModelBase}
|
|
1785
|
+
* @returns {Record<string, FrontendModelAttachmentDefinition>} - Attachment definitions keyed by attachment name.
|
|
1786
|
+
*/
|
|
1787
|
+
static attachmentDefinitions() {
|
|
1788
|
+
return this.resourceConfig().attachments || {}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Runs attachment definition.
|
|
1793
|
+
* @this {typeof FrontendModelBase}
|
|
1794
|
+
* @param {string} attachmentName - Attachment name.
|
|
1795
|
+
* @returns {FrontendModelAttachmentDefinition | null} - Attachment definition.
|
|
1796
|
+
*/
|
|
1797
|
+
static attachmentDefinition(attachmentName) {
|
|
1798
|
+
return this.attachmentDefinitions()[attachmentName] || null
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
/**
|
|
1802
|
+
* Runs relationship definition.
|
|
1803
|
+
* @this {typeof FrontendModelBase}
|
|
1804
|
+
* @param {string} relationshipName - Relationship name.
|
|
1805
|
+
* @returns {{type: "belongsTo" | "hasOne" | "hasMany", autoload?: boolean} | null} - Relationship definition.
|
|
1806
|
+
*/
|
|
1807
|
+
static relationshipDefinition(relationshipName) {
|
|
1808
|
+
const definitions = this.relationshipDefinitions()
|
|
1809
|
+
|
|
1810
|
+
return definitions[relationshipName] || null
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Runs relationship model class.
|
|
1815
|
+
* @this {typeof FrontendModelBase}
|
|
1816
|
+
* @param {string} relationshipName - Relationship name.
|
|
1817
|
+
* @returns {typeof FrontendModelBase | null} - Target relationship model class.
|
|
1818
|
+
*/
|
|
1819
|
+
static relationshipModelClass(relationshipName) {
|
|
1820
|
+
const relationshipModelClasses = this.relationshipModelClasses()
|
|
1821
|
+
const value = relationshipModelClasses[relationshipName]
|
|
1822
|
+
|
|
1823
|
+
return FrontendModelBase.resolveModelClass(value)
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Runs attributes.
|
|
1828
|
+
* @returns {Record<string, ?>} - Attributes hash.
|
|
1829
|
+
*/
|
|
1830
|
+
attributes() {
|
|
1831
|
+
return this._attributes
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Runs is new record.
|
|
1836
|
+
* @returns {boolean} - Whether this model has not yet been persisted.
|
|
1837
|
+
*/
|
|
1838
|
+
isNewRecord() {
|
|
1839
|
+
return this._isNewRecord
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Runs is persisted.
|
|
1844
|
+
* @returns {boolean} - Whether this model has been persisted.
|
|
1845
|
+
*/
|
|
1846
|
+
isPersisted() {
|
|
1847
|
+
return !this.isNewRecord()
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* Runs set is new record.
|
|
1852
|
+
* @param {boolean} newIsNewRecord - New persisted-state flag.
|
|
1853
|
+
* @returns {void}
|
|
1854
|
+
*/
|
|
1855
|
+
setIsNewRecord(newIsNewRecord) {
|
|
1856
|
+
this._isNewRecord = newIsNewRecord
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Marks this record for destruction when its parent is next saved through
|
|
1861
|
+
* nested-attribute support. The record is not removed from the parent's
|
|
1862
|
+
* relationship collection until the server confirms the delete.
|
|
1863
|
+
* @returns {void} - No return value.
|
|
1864
|
+
*/
|
|
1865
|
+
markForDestruction() {
|
|
1866
|
+
this._markedForDestruction = true
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Runs marked for destruction.
|
|
1871
|
+
* @returns {boolean} - Whether this record is queued for nested destruction on next parent save.
|
|
1872
|
+
*/
|
|
1873
|
+
markedForDestruction() {
|
|
1874
|
+
return this._markedForDestruction
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Runs changes.
|
|
1879
|
+
* @returns {Record<string, Array<?>>} - Changed attributes as `[oldValue, newValue]`.
|
|
1880
|
+
*/
|
|
1881
|
+
changes() {
|
|
1882
|
+
/**
|
|
1883
|
+
* Changed attributes.
|
|
1884
|
+
@type {Record<string, Array<?>>} */
|
|
1885
|
+
const changedAttributes = {}
|
|
1886
|
+
const attributeNames = new Set([
|
|
1887
|
+
...Object.keys(this._persistedAttributes),
|
|
1888
|
+
...Object.keys(this._attributes)
|
|
1889
|
+
])
|
|
1890
|
+
|
|
1891
|
+
for (const attributeName of attributeNames) {
|
|
1892
|
+
const previousValue = this._persistedAttributes[attributeName]
|
|
1893
|
+
const currentValue = this._attributes[attributeName]
|
|
1894
|
+
|
|
1895
|
+
if (JSON.stringify(serializeFrontendModelTransportValue(previousValue)) !== JSON.stringify(serializeFrontendModelTransportValue(currentValue))) {
|
|
1896
|
+
changedAttributes[attributeName] = [previousValue, currentValue]
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
return changedAttributes
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Runs is changed.
|
|
1905
|
+
* @returns {boolean} - Whether any tracked attribute has changed.
|
|
1906
|
+
*/
|
|
1907
|
+
isChanged() {
|
|
1908
|
+
return Object.keys(this.changes()).length > 0
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/**
|
|
1912
|
+
* Runs get relationship by name.
|
|
1913
|
+
* @param {string} relationshipName - Relationship name.
|
|
1914
|
+
* @returns {FrontendModelHasManyRelationship<typeof FrontendModelBase, typeof FrontendModelBase> | FrontendModelSingularRelationship<typeof FrontendModelBase, typeof FrontendModelBase>} - Relationship state object.
|
|
1915
|
+
*/
|
|
1916
|
+
getRelationshipByName(relationshipName) {
|
|
1917
|
+
if (!this._relationships[relationshipName]) {
|
|
1918
|
+
const ModelClass = /**
|
|
1919
|
+
* Narrows the runtime value to the documented type.
|
|
1920
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
1921
|
+
const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
|
|
1922
|
+
const targetModelClass = ModelClass.relationshipModelClass(relationshipName)
|
|
1923
|
+
|
|
1924
|
+
if (relationshipDefinition && relationshipTypeIsCollection(relationshipDefinition.type)) {
|
|
1925
|
+
this._relationships[relationshipName] = new FrontendModelHasManyRelationship(this, relationshipName, targetModelClass)
|
|
1926
|
+
} else {
|
|
1927
|
+
this._relationships[relationshipName] = new FrontendModelSingularRelationship(this, relationshipName, targetModelClass)
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
return this._relationships[relationshipName]
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/**
|
|
1935
|
+
* Runs get attachment by name.
|
|
1936
|
+
* @param {string} attachmentName - Attachment name.
|
|
1937
|
+
* @returns {FrontendModelAttachmentHandle} - Attachment helper.
|
|
1938
|
+
*/
|
|
1939
|
+
getAttachmentByName(attachmentName) {
|
|
1940
|
+
const ModelClass = /**
|
|
1941
|
+
* Narrows the runtime value to the documented type.
|
|
1942
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
1943
|
+
const attachmentDefinition = ModelClass.attachmentDefinition(attachmentName)
|
|
1944
|
+
|
|
1945
|
+
if (!attachmentDefinition) {
|
|
1946
|
+
throw new Error(`Unknown attachment: ${ModelClass.name}#${attachmentName}`)
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
if (!this._attachments[attachmentName]) {
|
|
1950
|
+
this._attachments[attachmentName] = new FrontendModelAttachmentHandle({
|
|
1951
|
+
attachmentName,
|
|
1952
|
+
model: this
|
|
1953
|
+
})
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
return this._attachments[attachmentName]
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Runs load relationship.
|
|
1961
|
+
* @param {string} relationshipName - Relationship name.
|
|
1962
|
+
* @returns {Promise<?>} - Loaded relationship value.
|
|
1963
|
+
*/
|
|
1964
|
+
async loadRelationship(relationshipName) {
|
|
1965
|
+
const ModelClass = /**
|
|
1966
|
+
* Narrows the runtime value to the documented type.
|
|
1967
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
1968
|
+
const id = this.primaryKeyValue()
|
|
1969
|
+
const reloadedModel = await ModelClass
|
|
1970
|
+
.preload([relationshipName])
|
|
1971
|
+
.find(id)
|
|
1972
|
+
const loadedValue = reloadedModel.getRelationshipByName(relationshipName).loaded()
|
|
1973
|
+
|
|
1974
|
+
this.getRelationshipByName(relationshipName).setLoaded(loadedValue)
|
|
1975
|
+
|
|
1976
|
+
return loadedValue
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Preloads relationship(s) onto this already-loaded record. Accepts either a
|
|
1981
|
+
* query built via `Model.preload(...).select(...)` or a raw preload spec
|
|
1982
|
+
* (string / array / nested object). Relationships already preloaded with the
|
|
1983
|
+
* required columns present are left untouched unless `force` is set. Carries
|
|
1984
|
+
* the query's preload graph, select, selectsExtra, withCount, abilities, and
|
|
1985
|
+
* queryData when re-fetching.
|
|
1986
|
+
* @param {import("./query.js").default<typeof FrontendModelBase> | import("../database/query/index.js").NestedPreloadRecord | string | Array<string | import("../database/query/index.js").NestedPreloadRecord>} queryOrSpec - Preload source.
|
|
1987
|
+
* @param {{force?: boolean}} [options] - Options.
|
|
1988
|
+
* @returns {Promise<void>} - Resolves when preloading completes.
|
|
1989
|
+
*/
|
|
1990
|
+
async preload(queryOrSpec, options = {}) {
|
|
1991
|
+
await FrontendModelPreloader.preload([this], queryOrSpec, options)
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Runs relationship or load.
|
|
1996
|
+
* @param {string} relationshipName - Relationship name.
|
|
1997
|
+
* @returns {Promise<?>} - Loaded relationship value.
|
|
1998
|
+
*/
|
|
1999
|
+
async relationshipOrLoad(relationshipName) {
|
|
2000
|
+
const relationship = this.getRelationshipByName(relationshipName)
|
|
2001
|
+
|
|
2002
|
+
if (relationship.getPreloaded()) {
|
|
2003
|
+
return relationship.loaded()
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
const batched = await this._tryCohortPreload(relationshipName)
|
|
2007
|
+
|
|
2008
|
+
if (batched) return relationship.loaded()
|
|
2009
|
+
|
|
2010
|
+
return await this.loadRelationship(relationshipName)
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Attempts to batch-load `relationshipName` across cohort siblings via a
|
|
2015
|
+
* single `preload([name]).where({pk: [ids]}).toArray()` request, then copies
|
|
2016
|
+
* the preloaded relationship state onto each sibling. Returns true when a
|
|
2017
|
+
* batch ran, false when autoload is off, there is no cohort, or no batch
|
|
2018
|
+
* candidates remain. Siblings whose relationship state is already set
|
|
2019
|
+
* (preloaded or locally manipulated via `build` / `setRelationship`) are
|
|
2020
|
+
* skipped so their cached/edited value is preserved.
|
|
2021
|
+
* @param {string} relationshipName - Relationship name.
|
|
2022
|
+
* @returns {Promise<boolean>} - Whether a cohort batch preload ran.
|
|
2023
|
+
*/
|
|
2024
|
+
async _tryCohortPreload(relationshipName) {
|
|
2025
|
+
if (!FrontendModelBase.getAutoload()) return false
|
|
2026
|
+
|
|
2027
|
+
const ModelClass = /**
|
|
2028
|
+
* Narrows the runtime value to the documented type.
|
|
2029
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2030
|
+
const cohort = this._loadCohort
|
|
2031
|
+
|
|
2032
|
+
if (!cohort || cohort.length <= 1) return false
|
|
2033
|
+
|
|
2034
|
+
const definition = ModelClass.relationshipDefinition(relationshipName)
|
|
2035
|
+
|
|
2036
|
+
if (!definition) return false
|
|
2037
|
+
if (definition.autoload === false) return false
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Batch.
|
|
2041
|
+
@type {Array<FrontendModelBase>} */
|
|
2042
|
+
const batch = []
|
|
2043
|
+
|
|
2044
|
+
// Exact same class, persisted, no existing in-memory relationship state.
|
|
2045
|
+
// `setLoaded` sets `_preloaded = true` on every mutation path (preload,
|
|
2046
|
+
// setRelationship, build, addToLoaded), so `getPreloaded()` alone is a
|
|
2047
|
+
// reliable "already touched" signal on the frontend.
|
|
2048
|
+
for (const sibling of cohort) {
|
|
2049
|
+
if (sibling.constructor !== ModelClass) continue
|
|
2050
|
+
if (sibling.isNewRecord()) continue
|
|
2051
|
+
|
|
2052
|
+
const siblingRelationship = sibling.getRelationshipByName(relationshipName)
|
|
2053
|
+
|
|
2054
|
+
if (siblingRelationship.getPreloaded()) continue
|
|
2055
|
+
|
|
2056
|
+
batch.push(sibling)
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
if (batch.length === 0) return false
|
|
2060
|
+
|
|
2061
|
+
const primaryKey = ModelClass.primaryKey()
|
|
2062
|
+
const batchIds = batch.map((sibling) => sibling.primaryKeyValue())
|
|
2063
|
+
const reloadedBatch = await ModelClass
|
|
2064
|
+
.preload([relationshipName])
|
|
2065
|
+
.where({[primaryKey]: batchIds})
|
|
2066
|
+
.toArray()
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Reloaded by id.
|
|
2070
|
+
@type {Map<string, FrontendModelBase>} */
|
|
2071
|
+
const reloadedById = new Map()
|
|
2072
|
+
|
|
2073
|
+
for (const reloaded of reloadedBatch) {
|
|
2074
|
+
reloadedById.set(String(reloaded.primaryKeyValue()), reloaded)
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
for (const sibling of batch) {
|
|
2078
|
+
const key = String(sibling.primaryKeyValue())
|
|
2079
|
+
const reloaded = reloadedById.get(key)
|
|
2080
|
+
|
|
2081
|
+
if (!reloaded) continue
|
|
2082
|
+
|
|
2083
|
+
const reloadedValue = reloaded.getRelationshipByName(relationshipName).loaded()
|
|
2084
|
+
|
|
2085
|
+
sibling.getRelationshipByName(relationshipName).setLoaded(reloadedValue)
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// If the caller itself was not populated (record deleted/filtered between
|
|
2089
|
+
// the list fetch and this preload request), fall back to per-record load
|
|
2090
|
+
// so the caller gets a real not-found error instead of a misleading
|
|
2091
|
+
// "hasn't been preloaded" throw from loaded().
|
|
2092
|
+
if (!this.getRelationshipByName(relationshipName).getPreloaded()) return false
|
|
2093
|
+
|
|
2094
|
+
return true
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Runs set relationship.
|
|
2099
|
+
* @param {string} relationshipName - Relationship name.
|
|
2100
|
+
* @param {?} relationshipValue - Relationship value.
|
|
2101
|
+
* @returns {?} - Assigned relationship value.
|
|
2102
|
+
*/
|
|
2103
|
+
setRelationship(relationshipName, relationshipValue) {
|
|
2104
|
+
const ModelClass = /**
|
|
2105
|
+
* Narrows the runtime value to the documented type.
|
|
2106
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2107
|
+
const relationshipDefinition = ModelClass.relationshipDefinition(relationshipName)
|
|
2108
|
+
|
|
2109
|
+
if (!relationshipDefinition) {
|
|
2110
|
+
throw new Error(`Unknown relationship: ${ModelClass.name}#${relationshipName}`)
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
if (relationshipTypeIsCollection(relationshipDefinition.type)) {
|
|
2114
|
+
throw new Error(`Cannot set has-many relationship with setRelationship(): ${ModelClass.name}#${relationshipName}`)
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
this.getRelationshipByName(relationshipName).setLoaded(relationshipValue)
|
|
2118
|
+
|
|
2119
|
+
return relationshipValue
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/**
|
|
2123
|
+
* Runs assign attributes.
|
|
2124
|
+
* @param {Record<string, ?>} attributes - Attributes to assign.
|
|
2125
|
+
* @returns {void} - No return value.
|
|
2126
|
+
*/
|
|
2127
|
+
assignAttributes(attributes) {
|
|
2128
|
+
for (const key in attributes) {
|
|
2129
|
+
this.setAttribute(key, attributes[key])
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Runs clear relationship cache.
|
|
2135
|
+
* @returns {void} - Clears cached relationship state.
|
|
2136
|
+
*/
|
|
2137
|
+
clearRelationshipCache() {
|
|
2138
|
+
this._relationships = {}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/**
|
|
2142
|
+
* Runs primary key.
|
|
2143
|
+
* @this {typeof FrontendModelBase}
|
|
2144
|
+
* @returns {string} - Primary key name.
|
|
2145
|
+
*/
|
|
2146
|
+
static primaryKey() {
|
|
2147
|
+
return this.resourceConfig().primaryKey || "id"
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Runs primary key value.
|
|
2152
|
+
* @returns {number | string} - Primary key value.
|
|
2153
|
+
*/
|
|
2154
|
+
primaryKeyValue() {
|
|
2155
|
+
const ModelClass = /**
|
|
2156
|
+
* Narrows the runtime value to the documented type.
|
|
2157
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2158
|
+
const value = this.readAttribute(ModelClass.primaryKey())
|
|
2159
|
+
|
|
2160
|
+
if (value === undefined || value === null) {
|
|
2161
|
+
throw new Error(`Missing primary key '${ModelClass.primaryKey()}' on ${ModelClass.name}`)
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
return value
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Runs read attribute.
|
|
2169
|
+
* @param {string} attributeName - Attribute name.
|
|
2170
|
+
* @returns {?} - Attribute value.
|
|
2171
|
+
*/
|
|
2172
|
+
readAttribute(attributeName) {
|
|
2173
|
+
if (this._selectedAttributes && !this._selectedAttributes.has(attributeName)) {
|
|
2174
|
+
throw new AttributeNotSelectedError(this.constructor.name, attributeName)
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
return this._attributes[attributeName]
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Whether an attribute value is currently loaded on this record. Used by the
|
|
2182
|
+
* preloader to decide whether a relationship can be skipped because the
|
|
2183
|
+
* requested columns are already present.
|
|
2184
|
+
* @param {string} attributeName - Attribute name.
|
|
2185
|
+
* @returns {boolean} - Whether the attribute is loaded.
|
|
2186
|
+
*/
|
|
2187
|
+
hasLoadedAttribute(attributeName) {
|
|
2188
|
+
if (!this._selectedAttributes) return true
|
|
2189
|
+
|
|
2190
|
+
return this._selectedAttributes.has(attributeName)
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* Read an association count attached by `.withCount(...)`. Counts
|
|
2195
|
+
* live on a dedicated map separate from the record's attributes so
|
|
2196
|
+
* a virtual count like `tasksCount` can't silently shadow a real
|
|
2197
|
+
* column of the same name. Returns the attached value, or 0 when
|
|
2198
|
+
* `.withCount(...)` wasn't requested for this attribute.
|
|
2199
|
+
* @param {string} attributeName - Attribute name, e.g. `"tasksCount"` or a custom name from `.withCount({customName: {...}})`.
|
|
2200
|
+
* @returns {number}
|
|
2201
|
+
*/
|
|
2202
|
+
readCount(attributeName) {
|
|
2203
|
+
return readPayloadAssociationCount(/**
|
|
2204
|
+
* Narrows the runtime value to the documented type.
|
|
2205
|
+
@type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
|
|
2206
|
+
* Narrows the runtime value to the documented type.
|
|
2207
|
+
@type {?} */ (this)), attributeName)
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* Internal setter called by `instantiateFromResponse` when hydrating
|
|
2212
|
+
* association counts that rode along with the record payload.
|
|
2213
|
+
* @param {string} attributeName - Attribute name.
|
|
2214
|
+
* @param {number} value - Count value.
|
|
2215
|
+
* @returns {void}
|
|
2216
|
+
*/
|
|
2217
|
+
_setAssociationCount(attributeName, value) {
|
|
2218
|
+
setPayloadAssociationCount(/**
|
|
2219
|
+
* Narrows the runtime value to the documented type.
|
|
2220
|
+
@type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
|
|
2221
|
+
* Narrows the runtime value to the documented type.
|
|
2222
|
+
@type {?} */ (this)), attributeName, value)
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Read a per-record ability result attached by `.abilities(...)`. The
|
|
2227
|
+
* backend evaluates each requested action against the current
|
|
2228
|
+
* ability for this record instance and ships the result alongside
|
|
2229
|
+
* the record's attributes. Returns `false` when the action wasn't
|
|
2230
|
+
* requested (or the ability denied it), so UI code can safely branch
|
|
2231
|
+
* on `record.can("update")` without first checking whether the
|
|
2232
|
+
* ability was loaded.
|
|
2233
|
+
* @param {string} action - Ability action name, e.g. `"update"`.
|
|
2234
|
+
* @returns {boolean}
|
|
2235
|
+
*/
|
|
2236
|
+
can(action) {
|
|
2237
|
+
return readPayloadComputedAbility(/**
|
|
2238
|
+
* Narrows the runtime value to the documented type.
|
|
2239
|
+
@type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
|
|
2240
|
+
* Narrows the runtime value to the documented type.
|
|
2241
|
+
@type {?} */ (this)), action)
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* Internal setter called by `instantiateFromResponse` when hydrating
|
|
2246
|
+
* per-record ability results that rode along with the record
|
|
2247
|
+
* payload.
|
|
2248
|
+
* @param {string} action - Ability action name.
|
|
2249
|
+
* @param {boolean} value - Whether the current ability permits the action on this record.
|
|
2250
|
+
* @returns {void}
|
|
2251
|
+
*/
|
|
2252
|
+
_setComputedAbility(action, value) {
|
|
2253
|
+
setPayloadComputedAbility(/**
|
|
2254
|
+
* Narrows the runtime value to the documented type.
|
|
2255
|
+
@type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
|
|
2256
|
+
* Narrows the runtime value to the documented type.
|
|
2257
|
+
@type {?} */ (this)), action, value)
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/**
|
|
2261
|
+
* Read a consumer-defined value attached by `.queryData(...)`. Stored
|
|
2262
|
+
* on a dedicated map rather than `_attributes`, so a virtual alias
|
|
2263
|
+
* like `tasksCount` cannot silently shadow a real column of the same
|
|
2264
|
+
* name. Returns `null` when no registered fn produced that alias for
|
|
2265
|
+
* this record (e.g. no child rows matched the aggregate).
|
|
2266
|
+
* @param {string} name - queryData alias name.
|
|
2267
|
+
* @returns {?}
|
|
2268
|
+
*/
|
|
2269
|
+
queryData(name) {
|
|
2270
|
+
return readPayloadQueryData(/**
|
|
2271
|
+
* Narrows the runtime value to the documented type.
|
|
2272
|
+
@type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
|
|
2273
|
+
* Narrows the runtime value to the documented type.
|
|
2274
|
+
@type {?} */ (this)), name)
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* Internal setter used by `instantiateFromResponse` when hydrating
|
|
2279
|
+
* queryData values that rode along with the record payload.
|
|
2280
|
+
* @param {string} name - queryData alias name.
|
|
2281
|
+
* @param {?} value - Attached value.
|
|
2282
|
+
* @returns {void}
|
|
2283
|
+
*/
|
|
2284
|
+
_setQueryData(name, value) {
|
|
2285
|
+
setPayloadQueryData(/**
|
|
2286
|
+
* Narrows the runtime value to the documented type.
|
|
2287
|
+
@type {import("../record-payload-values.js").RecordPayloadValuesTarget} */ (/**
|
|
2288
|
+
* Narrows the runtime value to the documented type.
|
|
2289
|
+
@type {?} */ (this)), name, value)
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Runs set attribute.
|
|
2294
|
+
* @param {string} attributeName - Attribute name.
|
|
2295
|
+
* @param {?} newValue - New value.
|
|
2296
|
+
* @returns {?} - Assigned value.
|
|
2297
|
+
*/
|
|
2298
|
+
setAttribute(attributeName, newValue) {
|
|
2299
|
+
const previousValue = this._attributes[attributeName]
|
|
2300
|
+
|
|
2301
|
+
this._attributes[attributeName] = newValue
|
|
2302
|
+
|
|
2303
|
+
if (this._selectedAttributes) {
|
|
2304
|
+
this._selectedAttributes.add(attributeName)
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// Only invalidate relationship cache entries whose foreign key matches the changed attribute.
|
|
2308
|
+
// Blanket-clearing all relationships on any attribute change destroys nested-save state
|
|
2309
|
+
// and preloaded children the caller never asked to invalidate.
|
|
2310
|
+
if (!Object.is(previousValue, newValue)) {
|
|
2311
|
+
this._invalidateRelationshipsForAttribute(attributeName)
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
return newValue
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* Invalidates any cached belongsTo relationship whose foreign key matches the
|
|
2319
|
+
* changed attribute. HasMany / hasOne relationships are left untouched because
|
|
2320
|
+
* their foreign key lives on the child, not on this model, and blanket-clearing
|
|
2321
|
+
* them would destroy nested-save state and preloaded children the caller never
|
|
2322
|
+
* asked to invalidate.
|
|
2323
|
+
*
|
|
2324
|
+
* Foreign keys are inferred when not declared: for belongsTo `projectId` is
|
|
2325
|
+
* inferred from relationship name `project`. Explicit `foreignKey` on the
|
|
2326
|
+
* relationship definition takes precedence.
|
|
2327
|
+
* @param {string} attributeName - Attribute name that changed.
|
|
2328
|
+
* @returns {void}
|
|
2329
|
+
*/
|
|
2330
|
+
_invalidateRelationshipsForAttribute(attributeName) {
|
|
2331
|
+
if (!this._relationships || Object.keys(this._relationships).length === 0) return
|
|
2332
|
+
|
|
2333
|
+
const ModelClass = /**
|
|
2334
|
+
* Narrows the runtime value to the documented type.
|
|
2335
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
2336
|
+
const definitions = typeof ModelClass.relationshipDefinitions === "function" ? ModelClass.relationshipDefinitions() : {}
|
|
2337
|
+
|
|
2338
|
+
for (const relationshipName of Object.keys(this._relationships)) {
|
|
2339
|
+
const definition = /**
|
|
2340
|
+
* Narrows the runtime value to the documented type.
|
|
2341
|
+
@type {?} */ (definitions[relationshipName])
|
|
2342
|
+
|
|
2343
|
+
if (!definition || definition.type !== "belongsTo") continue
|
|
2344
|
+
|
|
2345
|
+
const foreignKey = definition.foreignKey || `${relationshipName}Id`
|
|
2346
|
+
|
|
2347
|
+
if (foreignKey === attributeName) {
|
|
2348
|
+
delete this._relationships[relationshipName]
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/**
|
|
2354
|
+
* Runs resource path.
|
|
2355
|
+
* @this {typeof FrontendModelBase}
|
|
2356
|
+
* @returns {string} - Derived resource path.
|
|
2357
|
+
*/
|
|
2358
|
+
static resourcePath() {
|
|
2359
|
+
return validateFrontendModelResourcePath({
|
|
2360
|
+
modelName: this.getModelName(),
|
|
2361
|
+
resourcePath: defaultFrontendModelResourcePath(this)
|
|
2362
|
+
})
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* Runs command name.
|
|
2367
|
+
* @this {typeof FrontendModelBase}
|
|
2368
|
+
* @param {FrontendModelCommandType} commandType - Command type.
|
|
2369
|
+
* @returns {string} - Resolved command name.
|
|
2370
|
+
*/
|
|
2371
|
+
static commandName(commandType) {
|
|
2372
|
+
const resourceConfig = this.resourceConfig()
|
|
2373
|
+
const builtInCollectionCommands = resourceConfig.builtInCollectionCommands || []
|
|
2374
|
+
const builtInMemberCommands = resourceConfig.builtInMemberCommands || []
|
|
2375
|
+
const commands = resourceConfig.commands || []
|
|
2376
|
+
const isExposed = builtInCollectionCommands.includes(commandType) || builtInMemberCommands.includes(commandType) || commands.includes(commandType)
|
|
2377
|
+
const commandName = isExposed ? inflection.dasherize(inflection.underscore(commandType)) : commandType
|
|
2378
|
+
|
|
2379
|
+
return validateFrontendModelResourceCommandName({
|
|
2380
|
+
commandName,
|
|
2381
|
+
commandType,
|
|
2382
|
+
modelName: this.getModelName()
|
|
2383
|
+
})
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Runs normalize custom command payload arguments.
|
|
2388
|
+
* @this {typeof FrontendModelBase}
|
|
2389
|
+
* @param {Array<?>} args - Command arguments.
|
|
2390
|
+
* @returns {Record<string, ?>} - Command payload.
|
|
2391
|
+
*/
|
|
2392
|
+
static normalizeCustomCommandPayloadArguments(args) {
|
|
2393
|
+
if (args.length === 0) return {}
|
|
2394
|
+
if (args.length === 1) {
|
|
2395
|
+
const payload = args[0]
|
|
2396
|
+
if (payload === undefined) {
|
|
2397
|
+
return {}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
if (typeof payload !== "object" || payload === null) {
|
|
2401
|
+
return {arg1: payload}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
return /** Narrows the runtime value to the documented type. @type {Record<string, ?>} */ (payload)
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* Payload.
|
|
2409
|
+
@type {Record<string, number | string | Array<?>>} */
|
|
2410
|
+
const payload = {}
|
|
2411
|
+
|
|
2412
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2413
|
+
payload[`arg${index + 1}`] = args[index]
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
return payload
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/**
|
|
2420
|
+
* Returns the model name, preferring an explicit `static modelName` declaration
|
|
2421
|
+
* over the JavaScript class `.name` property. This allows minified builds to
|
|
2422
|
+
* preserve correct model names without relying on `keep_classnames`.
|
|
2423
|
+
* @this {typeof FrontendModelBase}
|
|
2424
|
+
* @returns {string} - The model name.
|
|
2425
|
+
*/
|
|
2426
|
+
static getModelName() {
|
|
2427
|
+
const resourceConfig = typeof this.resourceConfig === "function" ? this.resourceConfig() : null
|
|
2428
|
+
const modelName = resourceConfig?.modelName
|
|
2429
|
+
|
|
2430
|
+
return (typeof modelName === "string" && modelName.length > 0) ? modelName : this.name
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Runs configure transport.
|
|
2435
|
+
* @param {FrontendModelTransportConfig} config - Frontend model transport configuration.
|
|
2436
|
+
* @returns {void} - No return value.
|
|
2437
|
+
*/
|
|
2438
|
+
static configureTransport(config) {
|
|
2439
|
+
if (!config || typeof config !== "object") {
|
|
2440
|
+
return
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
if (Object.prototype.hasOwnProperty.call(config, "url")) {
|
|
2444
|
+
frontendModelTransportConfig.url = config.url
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
if (Object.prototype.hasOwnProperty.call(config, "shared")) {
|
|
2448
|
+
frontendModelTransportConfig.shared = config.shared
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
if (Object.prototype.hasOwnProperty.call(config, "websocketClient")) {
|
|
2452
|
+
frontendModelTransportConfig.websocketClient = config.websocketClient
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
if (Object.prototype.hasOwnProperty.call(config, "websocketUrl")) {
|
|
2456
|
+
frontendModelTransportConfig.websocketUrl = config.websocketUrl
|
|
2457
|
+
// Reset cached internal client so the new URL takes effect on next subscribe
|
|
2458
|
+
internalWebsocketClient = null
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
if (Object.prototype.hasOwnProperty.call(config, "requestHeaders")) {
|
|
2462
|
+
frontendModelTransportConfig.requestHeaders = config.requestHeaders
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (Object.prototype.hasOwnProperty.call(config, "sessionStore")) {
|
|
2466
|
+
frontendModelTransportConfig.sessionStore = config.sessionStore
|
|
2467
|
+
// Reset cached internal client so the new sessionStore is picked up.
|
|
2468
|
+
internalWebsocketClient = null
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* Connect the internal WebSocket and enable auto-reconnect.
|
|
2474
|
+
* @returns {Promise<void>} - Resolves when connected.
|
|
2475
|
+
*/
|
|
2476
|
+
static async connectWebsocket() {
|
|
2477
|
+
const client = resolveInternalWebsocketClient()
|
|
2478
|
+
|
|
2479
|
+
if (!client) {
|
|
2480
|
+
throw new Error("connectWebsocket requires configureTransport({websocketUrl})")
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
await client.connect()
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
/**
|
|
2487
|
+
* Disconnect the internal WebSocket and disable auto-reconnect.
|
|
2488
|
+
* @returns {Promise<void>} - Resolves when closed.
|
|
2489
|
+
*/
|
|
2490
|
+
static async disconnectWebsocket() {
|
|
2491
|
+
if (!internalWebsocketClient) return
|
|
2492
|
+
|
|
2493
|
+
await internalWebsocketClient.disconnectAndStopReconnect()
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
/**
|
|
2497
|
+
* Waits until queued and active frontend-model transport requests finish.
|
|
2498
|
+
* @param {FrontendModelIdleWaitArgs} [args] - Wait options.
|
|
2499
|
+
* @returns {Promise<void>} - Resolves when transport is idle.
|
|
2500
|
+
*/
|
|
2501
|
+
static async waitForIdle(args = {}) {
|
|
2502
|
+
const {quietMs = 0, timeout: timeoutMs = 5000, ...restArgs} = args
|
|
2503
|
+
const restArgKeys = Object.keys(restArgs)
|
|
2504
|
+
|
|
2505
|
+
if (restArgKeys.length > 0) {
|
|
2506
|
+
throw new Error(`Unknown waitForIdle args: ${restArgKeys.join(", ")}`)
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
if (!Number.isFinite(quietMs) || quietMs < 0) {
|
|
2510
|
+
throw new Error(`Expected waitForIdle quietMs to be a non-negative number, got: ${quietMs}`)
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
await timeout(
|
|
2514
|
+
{timeout: timeoutMs, errorMessage: "Timed out waiting for frontend model transport to become idle"},
|
|
2515
|
+
async () => await waitForFrontendModelTransportIdle(quietMs)
|
|
2516
|
+
)
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
/**
|
|
2520
|
+
* Returns the current WebSocket connection state.
|
|
2521
|
+
* @returns {{disconnectedSince: number | null, hasClient: boolean, isOpen: boolean, listenerCount: number}} - Snapshot of the managed websocket connection state.
|
|
2522
|
+
*/
|
|
2523
|
+
static websocketState() {
|
|
2524
|
+
if (!internalWebsocketClient) {
|
|
2525
|
+
return {disconnectedSince: null, hasClient: false, isOpen: false, listenerCount: 0}
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
return {
|
|
2529
|
+
...internalWebsocketClient.state(),
|
|
2530
|
+
hasClient: true
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* Close the raw WebSocket without disabling auto-reconnect. Used by tests to
|
|
2536
|
+
* simulate an unexpected network drop and verify reconnection behavior.
|
|
2537
|
+
* @returns {Promise<void>} - Resolves when the socket has closed.
|
|
2538
|
+
*/
|
|
2539
|
+
static async dropWebsocket() {
|
|
2540
|
+
if (!internalWebsocketClient) return
|
|
2541
|
+
|
|
2542
|
+
await internalWebsocketClient.dropConnection()
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* Sets global metadata on the WebSocket connection. Sent to the server immediately
|
|
2547
|
+
* over WebSocket and exposed to WebSocket-borne requests as request metadata.
|
|
2548
|
+
* @param {string} key - Metadata key.
|
|
2549
|
+
* @param {?} value - Metadata value (null to clear).
|
|
2550
|
+
* @returns {void}
|
|
2551
|
+
*/
|
|
2552
|
+
static setWebsocketMetadata(key, value) {
|
|
2553
|
+
const client = /**
|
|
2554
|
+
* Narrows the runtime value to the documented type.
|
|
2555
|
+
@type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
|
|
2556
|
+
|
|
2557
|
+
if (!client || typeof client.setMetadata !== "function") return
|
|
2558
|
+
|
|
2559
|
+
client.setMetadata(key, value)
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
/**
|
|
2563
|
+
* Opens a managed connection that auto-opens, auto-closes, and
|
|
2564
|
+
* auto-reconnects based on `shouldConnect()` and `params()`.
|
|
2565
|
+
* Call `handle.sync()` whenever the inputs that drive those
|
|
2566
|
+
* functions change (e.g. current-user sign-in/out). The handle
|
|
2567
|
+
* retries when the WS client isn't ready and reopens on close.
|
|
2568
|
+
* @param {string} connectionType - Connection class name registered on the server.
|
|
2569
|
+
* @param {{shouldConnect: () => boolean, params: () => Record<string, ?>, onMessage?: (body: ?) => void}} options - Connection lifecycle and payload callbacks.
|
|
2570
|
+
* @returns {{sync: () => void, close: () => void}} - Handle used to resync or close the managed connection.
|
|
2571
|
+
*/
|
|
2572
|
+
static openManagedConnection(connectionType, options) {
|
|
2573
|
+
/**
|
|
2574
|
+
* Connection.
|
|
2575
|
+
@type {?} */
|
|
2576
|
+
let connection = null
|
|
2577
|
+
let closed = false
|
|
2578
|
+
/**
|
|
2579
|
+
* Retry timer.
|
|
2580
|
+
@type {ReturnType<typeof setTimeout> | null} */
|
|
2581
|
+
let retryTimer = null
|
|
2582
|
+
let lastParamsJson = ""
|
|
2583
|
+
|
|
2584
|
+
const sync = () => {
|
|
2585
|
+
if (closed) return
|
|
2586
|
+
|
|
2587
|
+
if (!options.shouldConnect()) {
|
|
2588
|
+
if (connection && !connection.isClosed()) connection.close()
|
|
2589
|
+
connection = null
|
|
2590
|
+
lastParamsJson = ""
|
|
2591
|
+
return
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
const nextParams = options.params()
|
|
2595
|
+
const nextParamsJson = JSON.stringify(nextParams)
|
|
2596
|
+
|
|
2597
|
+
// Already connected with same params — nothing to do.
|
|
2598
|
+
if (connection && !connection.isClosed() && nextParamsJson === lastParamsJson) return
|
|
2599
|
+
|
|
2600
|
+
// Connected but params changed — send update message.
|
|
2601
|
+
// Guard with try/catch: the connection handle stays live during
|
|
2602
|
+
// reconnect but the underlying socket may be closed.
|
|
2603
|
+
if (connection && !connection.isClosed()) {
|
|
2604
|
+
try {
|
|
2605
|
+
connection.sendMessage(nextParams)
|
|
2606
|
+
lastParamsJson = nextParamsJson
|
|
2607
|
+
return
|
|
2608
|
+
} catch {
|
|
2609
|
+
connection = null
|
|
2610
|
+
lastParamsJson = ""
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// WS client not ready — retry. Check the actual client (which
|
|
2615
|
+
// may be an injected websocketClient) instead of websocketState()
|
|
2616
|
+
// which only reflects the internal client.
|
|
2617
|
+
const client = /**
|
|
2618
|
+
* Narrows the runtime value to the documented type.
|
|
2619
|
+
@type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
|
|
2620
|
+
|
|
2621
|
+
if (!client || !client.isOpen()) {
|
|
2622
|
+
if (retryTimer === null) {
|
|
2623
|
+
retryTimer = globalThis.setTimeout(() => {
|
|
2624
|
+
retryTimer = null
|
|
2625
|
+
sync()
|
|
2626
|
+
}, 250)
|
|
2627
|
+
}
|
|
2628
|
+
return
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
lastParamsJson = nextParamsJson
|
|
2632
|
+
connection = FrontendModelBase.openWebsocketConnection(connectionType, {
|
|
2633
|
+
params: nextParams,
|
|
2634
|
+
onMessage: options.onMessage,
|
|
2635
|
+
onClose: () => {
|
|
2636
|
+
if (connection?.isClosed()) {
|
|
2637
|
+
connection = null
|
|
2638
|
+
lastParamsJson = ""
|
|
2639
|
+
sync()
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
})
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
const close = () => {
|
|
2646
|
+
closed = true
|
|
2647
|
+
if (retryTimer !== null) globalThis.clearTimeout(retryTimer)
|
|
2648
|
+
if (connection && !connection.isClosed()) connection.close()
|
|
2649
|
+
connection = null
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
sync()
|
|
2653
|
+
|
|
2654
|
+
return {sync, close}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/**
|
|
2658
|
+
* Opens a 1:1 `WebsocketConnection` of the given type. Thin
|
|
2659
|
+
* convenience wrapper around the internal WS client's
|
|
2660
|
+
* `openConnection`. Apps use this for per-session state/messaging
|
|
2661
|
+
* that doesn't fit the pub/sub Channel model (locale, presence).
|
|
2662
|
+
* @param {string} connectionType - Name the server registered the class under.
|
|
2663
|
+
* @param {{params?: Record<string, ?>, onConnect?: () => void, onMessage?: (body: ?) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Connection options and event handlers.
|
|
2664
|
+
* @returns {?} - VelociousWebsocketClientConnection handle (typed loosely to avoid a cross-module import cycle).
|
|
2665
|
+
*/
|
|
2666
|
+
static openWebsocketConnection(connectionType, options) {
|
|
2667
|
+
const client = /**
|
|
2668
|
+
* Narrows the runtime value to the documented type.
|
|
2669
|
+
@type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
|
|
2670
|
+
|
|
2671
|
+
if (!client || typeof client.openConnection !== "function") {
|
|
2672
|
+
throw new Error("openWebsocketConnection requires configureTransport({websocketUrl})")
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
return client.openConnection(connectionType, options || {})
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/**
|
|
2679
|
+
* Subscribes to a pub/sub `WebsocketChannel`. Thin wrapper around
|
|
2680
|
+
* the internal client's `subscribeChannel`.
|
|
2681
|
+
* @param {string} channelType - Channel class name registered on the server.
|
|
2682
|
+
* @param {{params?: Record<string, ?>, onMessage?: (body: ?) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Channel subscription options and event handlers.
|
|
2683
|
+
* @returns {?} - Websocket channel handle from the configured client.
|
|
2684
|
+
*/
|
|
2685
|
+
static subscribeWebsocketChannel(channelType, options) {
|
|
2686
|
+
const client = /**
|
|
2687
|
+
* Narrows the runtime value to the documented type.
|
|
2688
|
+
@type {?} */ (frontendModelTransportConfig.websocketClient || resolveInternalWebsocketClient())
|
|
2689
|
+
|
|
2690
|
+
if (!client || typeof client.subscribeChannel !== "function") {
|
|
2691
|
+
throw new Error("subscribeWebsocketChannel requires configureTransport({websocketUrl})")
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
return client.subscribeChannel(channelType, options || {})
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* Installs WebSocket lifecycle hooks on globalThis for system test access.
|
|
2699
|
+
* Tests can call `globalThis.__velocious_websocket_hooks.connect()` etc.
|
|
2700
|
+
* @returns {void}
|
|
2701
|
+
*/
|
|
2702
|
+
static installWebsocketTestHooks() {
|
|
2703
|
+
if (typeof globalThis === "undefined") return
|
|
2704
|
+
|
|
2705
|
+
/** Narrows the runtime value to the documented type. @type {?} */ (globalThis).__velocious_websocket_hooks = {
|
|
2706
|
+
connect: () => this.connectWebsocket(),
|
|
2707
|
+
disconnect: () => this.disconnectWebsocket(),
|
|
2708
|
+
drop: () => this.dropWebsocket(),
|
|
2709
|
+
state: () => this.websocketState()
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* Runs attributes from response.
|
|
2715
|
+
* @this {typeof FrontendModelBase}
|
|
2716
|
+
* @param {object} response - Response payload.
|
|
2717
|
+
* @returns {Record<string, ?>} - Attributes from payload.
|
|
2718
|
+
*/
|
|
2719
|
+
static attributesFromResponse(response) {
|
|
2720
|
+
const modelData = this.modelDataFromResponse(response)
|
|
2721
|
+
|
|
2722
|
+
return modelData.attributes
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/**
|
|
2726
|
+
* Runs model data from response.
|
|
2727
|
+
* @this {typeof FrontendModelBase}
|
|
2728
|
+
* @param {object} response - Response payload.
|
|
2729
|
+
* @returns {{abilities: Record<string, boolean>, attributes: Record<string, ?>, associationCounts: Record<string, number>, queryData: Record<string, ?>, preloadedRelationships: Record<string, ?>, selectedAttributes: Set<string>}} - Attributes, preloaded relationships, association counts, queryData, abilities, and the selected-attributes set.
|
|
2730
|
+
*/
|
|
2731
|
+
static modelDataFromResponse(response) {
|
|
2732
|
+
if (!response || typeof response !== "object") {
|
|
2733
|
+
throw new Error(`Expected object response but got: ${response}`)
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
const responseObject = /**
|
|
2737
|
+
* Narrows the runtime value to the documented type.
|
|
2738
|
+
@type {Record<string, ?>} */ (response)
|
|
2739
|
+
|
|
2740
|
+
/**
|
|
2741
|
+
* Defines modelData.
|
|
2742
|
+
@type {Record<string, ?>} */
|
|
2743
|
+
let modelData
|
|
2744
|
+
|
|
2745
|
+
if (responseObject.model && typeof responseObject.model === "object") {
|
|
2746
|
+
modelData = /**
|
|
2747
|
+
* Narrows the runtime value to the documented type.
|
|
2748
|
+
@type {Record<string, ?>} */ (responseObject.model)
|
|
2749
|
+
} else if (responseObject.attributes && typeof responseObject.attributes === "object") {
|
|
2750
|
+
modelData = /**
|
|
2751
|
+
* Narrows the runtime value to the documented type.
|
|
2752
|
+
@type {Record<string, ?>} */ (responseObject.attributes)
|
|
2753
|
+
} else {
|
|
2754
|
+
modelData = responseObject
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
const attributes = {...modelData}
|
|
2758
|
+
const preloadedRelationships = isPlainObject(attributes[PRELOADED_RELATIONSHIPS_KEY])
|
|
2759
|
+
? /**
|
|
2760
|
+
* Narrows the runtime value to the documented type.
|
|
2761
|
+
@type {Record<string, ?>} */ (attributes[PRELOADED_RELATIONSHIPS_KEY])
|
|
2762
|
+
: {}
|
|
2763
|
+
const associationCounts = isPlainObject(attributes[ASSOCIATION_COUNTS_KEY])
|
|
2764
|
+
? /**
|
|
2765
|
+
* Narrows the runtime value to the documented type.
|
|
2766
|
+
@type {Record<string, number>} */ (attributes[ASSOCIATION_COUNTS_KEY])
|
|
2767
|
+
: {}
|
|
2768
|
+
const queryData = isPlainObject(attributes[QUERY_DATA_KEY])
|
|
2769
|
+
? /**
|
|
2770
|
+
* Narrows the runtime value to the documented type.
|
|
2771
|
+
@type {Record<string, ?>} */ (attributes[QUERY_DATA_KEY])
|
|
2772
|
+
: {}
|
|
2773
|
+
const abilities = isPlainObject(attributes[ABILITIES_KEY])
|
|
2774
|
+
? /**
|
|
2775
|
+
* Narrows the runtime value to the documented type.
|
|
2776
|
+
@type {Record<string, boolean>} */ (attributes[ABILITIES_KEY])
|
|
2777
|
+
: {}
|
|
2778
|
+
const selectedAttributesFromPayload = Array.isArray(attributes[SELECTED_ATTRIBUTES_KEY])
|
|
2779
|
+
? new Set(/**
|
|
2780
|
+
* Narrows the runtime value to the documented type.
|
|
2781
|
+
@type {string[]} */ (attributes[SELECTED_ATTRIBUTES_KEY]).filter((attributeName) => typeof attributeName === "string"))
|
|
2782
|
+
: null
|
|
2783
|
+
|
|
2784
|
+
delete attributes[PRELOADED_RELATIONSHIPS_KEY]
|
|
2785
|
+
delete attributes[SELECTED_ATTRIBUTES_KEY]
|
|
2786
|
+
delete attributes[ASSOCIATION_COUNTS_KEY]
|
|
2787
|
+
delete attributes[QUERY_DATA_KEY]
|
|
2788
|
+
delete attributes[ABILITIES_KEY]
|
|
2789
|
+
|
|
2790
|
+
const selectedAttributes = selectedAttributesFromPayload || new Set(Object.keys(attributes))
|
|
2791
|
+
|
|
2792
|
+
return {abilities, attributes, associationCounts, queryData, preloadedRelationships, selectedAttributes}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
/**
|
|
2796
|
+
* Runs apply preloaded relationships.
|
|
2797
|
+
* @this {typeof FrontendModelBase}
|
|
2798
|
+
* @param {FrontendModelBase} model - Model instance.
|
|
2799
|
+
* @param {Record<string, ?>} preloadedRelationships - Preloaded relationship payload.
|
|
2800
|
+
* @returns {void}
|
|
2801
|
+
*/
|
|
2802
|
+
static applyPreloadedRelationships(model, preloadedRelationships) {
|
|
2803
|
+
for (const [relationshipName, relationshipPayload] of Object.entries(preloadedRelationships)) {
|
|
2804
|
+
const relationship = model.getRelationshipByName(relationshipName)
|
|
2805
|
+
const targetModelClass = this.relationshipModelClass(relationshipName)
|
|
2806
|
+
|
|
2807
|
+
if (Array.isArray(relationshipPayload)) {
|
|
2808
|
+
relationship.setLoaded(relationshipPayload.map((entry) => this.instantiateRelationshipValue(entry, targetModelClass)))
|
|
2809
|
+
continue
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
relationship.setLoaded(this.instantiateRelationshipValue(relationshipPayload, targetModelClass))
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Runs instantiate relationship value.
|
|
2818
|
+
* @this {typeof FrontendModelBase}
|
|
2819
|
+
* @param {?} relationshipPayload - Relationship payload value.
|
|
2820
|
+
* @param {typeof FrontendModelBase | null} targetModelClass - Target model class.
|
|
2821
|
+
* @returns {?} - Instantiated relationship value.
|
|
2822
|
+
*/
|
|
2823
|
+
static instantiateRelationshipValue(relationshipPayload, targetModelClass) {
|
|
2824
|
+
if (!targetModelClass) return relationshipPayload
|
|
2825
|
+
|
|
2826
|
+
if (!relationshipPayload || typeof relationshipPayload !== "object") return relationshipPayload
|
|
2827
|
+
|
|
2828
|
+
return targetModelClass.instantiateFromResponse(relationshipPayload)
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
/**
|
|
2832
|
+
* Runs instantiate from response.
|
|
2833
|
+
* @template {typeof FrontendModelBase} T
|
|
2834
|
+
* @this {T}
|
|
2835
|
+
* @param {Record<string, ?> | InstanceType<T>} response - Response payload, or an already-hydrated instance of this class.
|
|
2836
|
+
* @returns {InstanceType<T>} - New model instance, or the same instance unchanged if it was already hydrated.
|
|
2837
|
+
*/
|
|
2838
|
+
static instantiateFromResponse(response) {
|
|
2839
|
+
// Idempotent: if a caller hands us an already-hydrated instance of this
|
|
2840
|
+
// class (now common because the shared frontend-model API auto-serializes
|
|
2841
|
+
// backend `Record` instances returned from custom commands and the
|
|
2842
|
+
// transport deserializer hydrates them into models before the call site
|
|
2843
|
+
// sees the response), return it as-is. Without this, code that has
|
|
2844
|
+
// historically wrapped custom-command responses in
|
|
2845
|
+
// `Model.instantiateFromResponse(response.field)` would spread the live
|
|
2846
|
+
// model instance into a new constructor call and produce a broken model
|
|
2847
|
+
// with internal state keys promoted to attributes.
|
|
2848
|
+
if (response instanceof this) {
|
|
2849
|
+
return /** Narrows the runtime value to the documented type. @type {InstanceType<T>} */ (response)
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const modelData = this.modelDataFromResponse(response)
|
|
2853
|
+
const attributes = modelData.attributes
|
|
2854
|
+
const preloadedRelationships = modelData.preloadedRelationships
|
|
2855
|
+
const associationCounts = modelData.associationCounts
|
|
2856
|
+
const queryData = modelData.queryData
|
|
2857
|
+
const abilities = modelData.abilities
|
|
2858
|
+
const selectedAttributes = modelData.selectedAttributes
|
|
2859
|
+
const model = /**
|
|
2860
|
+
* Narrows the runtime value to the documented type.
|
|
2861
|
+
@type {InstanceType<T>} */ (new this(attributes))
|
|
2862
|
+
model._selectedAttributes = selectedAttributes ? new Set(selectedAttributes) : null
|
|
2863
|
+
|
|
2864
|
+
this.applyPreloadedRelationships(model, preloadedRelationships)
|
|
2865
|
+
|
|
2866
|
+
for (const [attributeName, value] of Object.entries(associationCounts || {})) {
|
|
2867
|
+
model._setAssociationCount(attributeName, Number(value) || 0)
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
for (const [name, value] of Object.entries(queryData || {})) {
|
|
2871
|
+
model._setQueryData(name, value)
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
for (const [action, value] of Object.entries(abilities || {})) {
|
|
2875
|
+
model._setComputedAbility(action, Boolean(value))
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
model.setIsNewRecord(false)
|
|
2879
|
+
model._persistedAttributes = cloneFrontendModelAttributes(model.attributes())
|
|
2880
|
+
|
|
2881
|
+
return model
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* Runs find.
|
|
2886
|
+
* @template {typeof FrontendModelBase} T
|
|
2887
|
+
* @this {T}
|
|
2888
|
+
* @param {number | string} id - Record identifier.
|
|
2889
|
+
* @returns {Promise<InstanceType<T>>} - Resolved model.
|
|
2890
|
+
*/
|
|
2891
|
+
static async find(id) {
|
|
2892
|
+
return await this.query().find(id)
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
/**
|
|
2896
|
+
* Runs find by.
|
|
2897
|
+
* @template {typeof FrontendModelBase} T
|
|
2898
|
+
* @this {T}
|
|
2899
|
+
* @param {Record<string, ?>} conditions - Attribute match conditions.
|
|
2900
|
+
* @returns {Promise<InstanceType<T> | null>} - Found model or null.
|
|
2901
|
+
*/
|
|
2902
|
+
static async findBy(conditions) {
|
|
2903
|
+
return await this.query().findBy(conditions)
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
/**
|
|
2907
|
+
* Runs find by or fail.
|
|
2908
|
+
* @template {typeof FrontendModelBase} T
|
|
2909
|
+
* @this {T}
|
|
2910
|
+
* @param {Record<string, ?>} conditions - Attribute match conditions.
|
|
2911
|
+
* @returns {Promise<InstanceType<T>>} - Found model.
|
|
2912
|
+
*/
|
|
2913
|
+
static async findByOrFail(conditions) {
|
|
2914
|
+
return await this.query().findByOrFail(conditions)
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
* Runs to array.
|
|
2919
|
+
* @template {typeof FrontendModelBase} T
|
|
2920
|
+
* @this {T}
|
|
2921
|
+
* @returns {Promise<InstanceType<T>[]>} - Loaded model instances.
|
|
2922
|
+
*/
|
|
2923
|
+
static async toArray() {
|
|
2924
|
+
return await this.query().toArray()
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
/**
|
|
2928
|
+
* Runs load.
|
|
2929
|
+
* @template {typeof FrontendModelBase} T
|
|
2930
|
+
* @this {T}
|
|
2931
|
+
* @returns {Promise<InstanceType<T>[]>} - Loaded model instances.
|
|
2932
|
+
*/
|
|
2933
|
+
static async load() {
|
|
2934
|
+
return await this.query().load()
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* Runs all.
|
|
2939
|
+
* @template {typeof FrontendModelBase} T
|
|
2940
|
+
* @this {T}
|
|
2941
|
+
* @returns {FrontendModelQuery<T>} - Query builder.
|
|
2942
|
+
*/
|
|
2943
|
+
static all() {
|
|
2944
|
+
return this.query()
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Runs where.
|
|
2949
|
+
* @template {typeof FrontendModelBase} T
|
|
2950
|
+
* @this {T}
|
|
2951
|
+
* @param {Record<string, ?>} conditions - Root-model where conditions.
|
|
2952
|
+
* @returns {import("./query.js").default<T>} - Query with where conditions.
|
|
2953
|
+
*/
|
|
2954
|
+
static where(conditions) {
|
|
2955
|
+
return this.query().where(conditions)
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
/**
|
|
2959
|
+
* Runs joins.
|
|
2960
|
+
* @template {typeof FrontendModelBase} T
|
|
2961
|
+
* @this {T}
|
|
2962
|
+
* @param {Record<string, ?> | Array<Record<string, ?>>} joins - Relationship descriptor joins.
|
|
2963
|
+
* @returns {import("./query.js").default<T>} - Query with joins.
|
|
2964
|
+
*/
|
|
2965
|
+
static joins(joins) {
|
|
2966
|
+
return this.query().joins(joins)
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
/**
|
|
2970
|
+
* Runs limit.
|
|
2971
|
+
* @template {typeof FrontendModelBase} T
|
|
2972
|
+
* @this {T}
|
|
2973
|
+
* @param {number} value - Maximum number of records.
|
|
2974
|
+
* @returns {import("./query.js").default<T>} - Query with limit.
|
|
2975
|
+
*/
|
|
2976
|
+
static limit(value) {
|
|
2977
|
+
return this.query().limit(value)
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* Runs offset.
|
|
2982
|
+
* @template {typeof FrontendModelBase} T
|
|
2983
|
+
* @this {T}
|
|
2984
|
+
* @param {number} value - Number of records to skip.
|
|
2985
|
+
* @returns {import("./query.js").default<T>} - Query with offset.
|
|
2986
|
+
*/
|
|
2987
|
+
static offset(value) {
|
|
2988
|
+
return this.query().offset(value)
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
/**
|
|
2992
|
+
* Runs page.
|
|
2993
|
+
* @template {typeof FrontendModelBase} T
|
|
2994
|
+
* @this {T}
|
|
2995
|
+
* @param {number} pageNumber - 1-based page number.
|
|
2996
|
+
* @returns {import("./query.js").default<T>} - Query with page applied.
|
|
2997
|
+
*/
|
|
2998
|
+
static page(pageNumber) {
|
|
2999
|
+
return this.query().page(pageNumber)
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
/**
|
|
3003
|
+
* Runs per page.
|
|
3004
|
+
* @template {typeof FrontendModelBase} T
|
|
3005
|
+
* @this {T}
|
|
3006
|
+
* @param {number} value - Number of records per page.
|
|
3007
|
+
* @returns {import("./query.js").default<T>} - Query with page size.
|
|
3008
|
+
*/
|
|
3009
|
+
static perPage(value) {
|
|
3010
|
+
return this.query().perPage(value)
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* Runs count.
|
|
3015
|
+
* @template {typeof FrontendModelBase} T
|
|
3016
|
+
* @this {T}
|
|
3017
|
+
* @returns {Promise<number>} - Number of loaded model instances.
|
|
3018
|
+
*/
|
|
3019
|
+
static async count() {
|
|
3020
|
+
return await this.query().count()
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
/**
|
|
3024
|
+
* Class-level hook fired when any record of this model is created.
|
|
3025
|
+
* Subscribe-time authorization only — once a subscription is
|
|
3026
|
+
* accepted, future `create` events for this model are delivered
|
|
3027
|
+
* without re-checking per-record visibility. Query options can still
|
|
3028
|
+
* narrow which events reach this callback.
|
|
3029
|
+
* @this {typeof FrontendModelBase}
|
|
3030
|
+
* @param {(payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void} callback - Event callback.
|
|
3031
|
+
* @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
|
|
3032
|
+
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3033
|
+
*/
|
|
3034
|
+
static async onCreate(callback, options = {}) {
|
|
3035
|
+
const sub = ensureFrontendModelEventSubscription(this)
|
|
3036
|
+
const entry = {callback, ...frontendModelEventOptionsPayload(this, options)}
|
|
3037
|
+
|
|
3038
|
+
sub.classCreateCallbacks.add(entry)
|
|
3039
|
+
await sub.ensureSubscribed()
|
|
3040
|
+
|
|
3041
|
+
return () => {
|
|
3042
|
+
sub.classCreateCallbacks.delete(entry)
|
|
3043
|
+
sub.maybeTeardown()
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
/**
|
|
3048
|
+
* Class-level hook fired when any record of this model is updated.
|
|
3049
|
+
* @this {typeof FrontendModelBase}
|
|
3050
|
+
* @param {(payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void} callback - Event callback.
|
|
3051
|
+
* @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
|
|
3052
|
+
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3053
|
+
*/
|
|
3054
|
+
static async onUpdate(callback, options = {}) {
|
|
3055
|
+
const sub = ensureFrontendModelEventSubscription(this)
|
|
3056
|
+
const entry = {callback, ...frontendModelEventOptionsPayload(this, options)}
|
|
3057
|
+
|
|
3058
|
+
sub.classUpdateCallbacks.add(entry)
|
|
3059
|
+
await sub.ensureSubscribed()
|
|
3060
|
+
|
|
3061
|
+
return () => {
|
|
3062
|
+
sub.classUpdateCallbacks.delete(entry)
|
|
3063
|
+
sub.maybeTeardown()
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
/**
|
|
3068
|
+
* Class-level hook fired when any record of this model is destroyed.
|
|
3069
|
+
* @this {typeof FrontendModelBase}
|
|
3070
|
+
* @param {(payload: {id: string}) => void} callback - Event callback.
|
|
3071
|
+
* @param {import("./query.js").FrontendModelEventOptions} [options] - Accepted for API symmetry; destroy events carry ids only.
|
|
3072
|
+
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3073
|
+
*/
|
|
3074
|
+
static async onDestroy(callback, options = {}) {
|
|
3075
|
+
assertNoDestroyEventFilter(this, options)
|
|
3076
|
+
|
|
3077
|
+
const sub = ensureFrontendModelEventSubscription(this)
|
|
3078
|
+
const entry = {callback}
|
|
3079
|
+
|
|
3080
|
+
sub.classDestroyCallbacks.add(entry)
|
|
3081
|
+
await sub.ensureSubscribed()
|
|
3082
|
+
|
|
3083
|
+
return () => {
|
|
3084
|
+
sub.classDestroyCallbacks.delete(entry)
|
|
3085
|
+
sub.maybeTeardown()
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
/**
|
|
3090
|
+
* Instance-level hook fired when THIS record is updated. The
|
|
3091
|
+
* instance's attributes are auto-merged with the broadcast payload
|
|
3092
|
+
* before the callback runs, so callers can read fresh values via
|
|
3093
|
+
* `this.someAttr()` without re-fetching.
|
|
3094
|
+
* @param {(payload: {id: string, model: InstanceType<typeof FrontendModelBase>}) => void} callback - Event callback.
|
|
3095
|
+
* @param {import("./query.js").FrontendModelEventOptions} [options] - Event query or record projection options.
|
|
3096
|
+
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3097
|
+
*/
|
|
3098
|
+
async onUpdate(callback, options = {}) {
|
|
3099
|
+
const self = /**
|
|
3100
|
+
* Narrows the runtime value to the documented type.
|
|
3101
|
+
@type {?} */ (this)
|
|
3102
|
+
const ModelClass = /**
|
|
3103
|
+
* Narrows the runtime value to the documented type.
|
|
3104
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3105
|
+
const sub = ensureFrontendModelEventSubscription(ModelClass)
|
|
3106
|
+
const id = String(self.id())
|
|
3107
|
+
const entry = {callback, ...frontendModelEventOptionsPayload(ModelClass, options)}
|
|
3108
|
+
const listener = ensureFrontendModelInstanceListener(sub, id, this)
|
|
3109
|
+
|
|
3110
|
+
listener.updateCallbacks.add(entry)
|
|
3111
|
+
await sub.ensureSubscribed()
|
|
3112
|
+
|
|
3113
|
+
return () => {
|
|
3114
|
+
const current = sub.instanceListeners.get(id)
|
|
3115
|
+
|
|
3116
|
+
if (!current) return
|
|
3117
|
+
current.updateCallbacks.delete(entry)
|
|
3118
|
+
|
|
3119
|
+
if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
|
|
3120
|
+
sub.instanceListeners.delete(id)
|
|
3121
|
+
}
|
|
3122
|
+
sub.maybeTeardown()
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
/**
|
|
3127
|
+
* Instance-level hook fired when THIS record is destroyed.
|
|
3128
|
+
* @param {(payload: {id: string}) => void} callback - Event callback.
|
|
3129
|
+
* @param {import("./query.js").FrontendModelEventOptions} [options] - Accepted for API symmetry; destroy events carry ids only.
|
|
3130
|
+
* @returns {Promise<() => void>} - Unsubscribe callback.
|
|
3131
|
+
*/
|
|
3132
|
+
async onDestroy(callback, options = {}) {
|
|
3133
|
+
const self = /**
|
|
3134
|
+
* Narrows the runtime value to the documented type.
|
|
3135
|
+
@type {?} */ (this)
|
|
3136
|
+
const ModelClass = /**
|
|
3137
|
+
* Narrows the runtime value to the documented type.
|
|
3138
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3139
|
+
|
|
3140
|
+
assertNoDestroyEventFilter(ModelClass, options)
|
|
3141
|
+
|
|
3142
|
+
const sub = ensureFrontendModelEventSubscription(ModelClass)
|
|
3143
|
+
const id = String(self.id())
|
|
3144
|
+
const entry = {callback}
|
|
3145
|
+
const listener = ensureFrontendModelInstanceListener(sub, id, this)
|
|
3146
|
+
|
|
3147
|
+
listener.destroyCallbacks.add(entry)
|
|
3148
|
+
await sub.ensureSubscribed()
|
|
3149
|
+
|
|
3150
|
+
return () => {
|
|
3151
|
+
const current = sub.instanceListeners.get(id)
|
|
3152
|
+
|
|
3153
|
+
if (!current) return
|
|
3154
|
+
current.destroyCallbacks.delete(entry)
|
|
3155
|
+
|
|
3156
|
+
if (current.updateCallbacks.size === 0 && current.destroyCallbacks.size === 0) {
|
|
3157
|
+
sub.instanceListeners.delete(id)
|
|
3158
|
+
}
|
|
3159
|
+
sub.maybeTeardown()
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
/**
|
|
3164
|
+
* Runs pluck.
|
|
3165
|
+
* @template {typeof FrontendModelBase} T
|
|
3166
|
+
* @this {T}
|
|
3167
|
+
* @param {...(string | string[] | Record<string, ?> | Array<Record<string, ?>>)} columns - Pluck definition(s).
|
|
3168
|
+
* @returns {Promise<Array<?>>} - Plucked values.
|
|
3169
|
+
*/
|
|
3170
|
+
static async pluck(...columns) {
|
|
3171
|
+
return await this.query().pluck(...columns)
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/**
|
|
3175
|
+
* Runs search.
|
|
3176
|
+
* @template {typeof FrontendModelBase} T
|
|
3177
|
+
* @this {T}
|
|
3178
|
+
* @param {string[]} path - Relationship path.
|
|
3179
|
+
* @param {string} column - Column or attribute name.
|
|
3180
|
+
* @param {"eq" | "like" | "notEq" | "gt" | "gteq" | "lt" | "lteq" | ">" | ">=" | "<" | "<="} operator - Search operator.
|
|
3181
|
+
* @param {?} value - Search value.
|
|
3182
|
+
* @returns {FrontendModelQuery<T>} - Query builder with search filter.
|
|
3183
|
+
*/
|
|
3184
|
+
static search(path, column, operator, value) {
|
|
3185
|
+
return this.query().search(path, column, operator, value)
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
/**
|
|
3189
|
+
* Runs ransack.
|
|
3190
|
+
* @template {typeof FrontendModelBase} T
|
|
3191
|
+
* @this {T}
|
|
3192
|
+
* @param {Record<string, ?>} params - Ransack-style params hash.
|
|
3193
|
+
* @returns {FrontendModelQuery<T>} - Query builder with Ransack filters applied.
|
|
3194
|
+
*/
|
|
3195
|
+
static ransack(params) {
|
|
3196
|
+
return this.query().ransack(params)
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/**
|
|
3200
|
+
* Runs sort.
|
|
3201
|
+
* @template {typeof FrontendModelBase} T
|
|
3202
|
+
* @this {T}
|
|
3203
|
+
* @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
|
|
3204
|
+
* @returns {FrontendModelQuery<T>} - Query builder with sort definitions.
|
|
3205
|
+
*/
|
|
3206
|
+
static sort(sort) {
|
|
3207
|
+
return this.query().sort(sort)
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
/**
|
|
3211
|
+
* Runs order.
|
|
3212
|
+
* @template {typeof FrontendModelBase} T
|
|
3213
|
+
* @this {T}
|
|
3214
|
+
* @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
|
|
3215
|
+
* @returns {FrontendModelQuery<T>} - Query builder with sort definitions.
|
|
3216
|
+
*/
|
|
3217
|
+
static order(sort) {
|
|
3218
|
+
return this.query().order(sort)
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
/**
|
|
3222
|
+
* Runs group.
|
|
3223
|
+
* @template {typeof FrontendModelBase} T
|
|
3224
|
+
* @this {T}
|
|
3225
|
+
* @param {string | string[] | Record<string, ?> | Array<Record<string, ?>>} group - Group definition(s).
|
|
3226
|
+
* @returns {FrontendModelQuery<T>} - Query builder with group definitions.
|
|
3227
|
+
*/
|
|
3228
|
+
static group(group) {
|
|
3229
|
+
return this.query().group(group)
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
/**
|
|
3233
|
+
* Runs distinct.
|
|
3234
|
+
* @template {typeof FrontendModelBase} T
|
|
3235
|
+
* @this {T}
|
|
3236
|
+
* @param {boolean} [value] - Whether to request distinct rows.
|
|
3237
|
+
* @returns {FrontendModelQuery<T>} - Query builder with distinct flag.
|
|
3238
|
+
*/
|
|
3239
|
+
static distinct(value = true) {
|
|
3240
|
+
return this.query().distinct(value)
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
/**
|
|
3244
|
+
* Runs query.
|
|
3245
|
+
* @template {typeof FrontendModelBase} T
|
|
3246
|
+
* @this {T}
|
|
3247
|
+
* @returns {FrontendModelQuery<T>} - Query builder.
|
|
3248
|
+
*/
|
|
3249
|
+
static query() {
|
|
3250
|
+
return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (new FrontendModelQuery({modelClass: this}))
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
/**
|
|
3254
|
+
* Runs preload.
|
|
3255
|
+
* @template {typeof FrontendModelBase} T
|
|
3256
|
+
* @this {T}
|
|
3257
|
+
* @param {import("../database/query/index.js").NestedPreloadRecord | string | Array<string | import("../database/query/index.js").NestedPreloadRecord>} preload - Preload graph.
|
|
3258
|
+
* @returns {FrontendModelQuery<T>} - Query with preload.
|
|
3259
|
+
*/
|
|
3260
|
+
static preload(preload) {
|
|
3261
|
+
return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (this.query().preload(preload))
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
/**
|
|
3265
|
+
* Runs select.
|
|
3266
|
+
* @template {typeof FrontendModelBase} T
|
|
3267
|
+
* @this {T}
|
|
3268
|
+
* @param {Record<string, string[] | string> | string | string[]} select - Model-aware attribute select map or root-model shorthand.
|
|
3269
|
+
* @returns {FrontendModelQuery<T>} - Query with selected attributes.
|
|
3270
|
+
*/
|
|
3271
|
+
static select(select) {
|
|
3272
|
+
return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (this.query().select(select))
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
/**
|
|
3276
|
+
* Runs selects extra.
|
|
3277
|
+
* @template {typeof FrontendModelBase} T
|
|
3278
|
+
* @this {T}
|
|
3279
|
+
* @param {Record<string, string[] | string> | string | string[]} select - Extra attributes to load in addition to the defaults, keyed by model name or root-model shorthand.
|
|
3280
|
+
* @returns {FrontendModelQuery<T>} - Query with extra selected attributes.
|
|
3281
|
+
*/
|
|
3282
|
+
static selectsExtra(select) {
|
|
3283
|
+
return /** Narrows the runtime value to the documented type. @type {FrontendModelQuery<T>} */ (this.query().selectsExtra(select))
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
/**
|
|
3287
|
+
* Runs first.
|
|
3288
|
+
* @template {typeof FrontendModelBase} T
|
|
3289
|
+
* @this {T}
|
|
3290
|
+
* @returns {Promise<InstanceType<T> | null>} - First model or null.
|
|
3291
|
+
*/
|
|
3292
|
+
static async first() {
|
|
3293
|
+
return await this.query().first()
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
/**
|
|
3297
|
+
* Runs last.
|
|
3298
|
+
* @template {typeof FrontendModelBase} T
|
|
3299
|
+
* @this {T}
|
|
3300
|
+
* @returns {Promise<InstanceType<T> | null>} - Last model or null.
|
|
3301
|
+
*/
|
|
3302
|
+
static async last() {
|
|
3303
|
+
return await this.query().last()
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/**
|
|
3307
|
+
* Runs find or initialize by.
|
|
3308
|
+
* @template {typeof FrontendModelBase} T
|
|
3309
|
+
* @this {T}
|
|
3310
|
+
* @param {Record<string, ?>} conditions - Attribute match conditions.
|
|
3311
|
+
* @returns {Promise<InstanceType<T>>} - Existing or initialized model.
|
|
3312
|
+
*/
|
|
3313
|
+
static async findOrInitializeBy(conditions) {
|
|
3314
|
+
return await this.query().findOrInitializeBy(conditions)
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/**
|
|
3318
|
+
* Runs find or create by.
|
|
3319
|
+
* @template {typeof FrontendModelBase} T
|
|
3320
|
+
* @this {T}
|
|
3321
|
+
* @param {Record<string, ?>} conditions - Attribute match conditions.
|
|
3322
|
+
* @param {(model: InstanceType<T>) => Promise<void> | void} [callback] - Optional callback before save when created.
|
|
3323
|
+
* @returns {Promise<InstanceType<T>>} - Existing or newly created model.
|
|
3324
|
+
*/
|
|
3325
|
+
static async findOrCreateBy(conditions, callback) {
|
|
3326
|
+
return await this.query().findOrCreateBy(conditions, callback)
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* Runs create.
|
|
3331
|
+
* @template {typeof FrontendModelBase} T
|
|
3332
|
+
* @this {T}
|
|
3333
|
+
* @param {Record<string, ?>} [attributes] - Initial attributes.
|
|
3334
|
+
* @returns {Promise<InstanceType<T>>} - Persisted model.
|
|
3335
|
+
*/
|
|
3336
|
+
static async create(attributes = {}) {
|
|
3337
|
+
const model = /**
|
|
3338
|
+
* Narrows the runtime value to the documented type.
|
|
3339
|
+
@type {InstanceType<T>} */ (new this(attributes))
|
|
3340
|
+
|
|
3341
|
+
await model.save()
|
|
3342
|
+
|
|
3343
|
+
return model
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
/**
|
|
3347
|
+
* Runs assert find by conditions.
|
|
3348
|
+
* @this {typeof FrontendModelBase}
|
|
3349
|
+
* @param {Record<string, ?>} conditions - findBy conditions.
|
|
3350
|
+
* @returns {void}
|
|
3351
|
+
*/
|
|
3352
|
+
static assertFindByConditions(conditions) {
|
|
3353
|
+
assertFindByConditionsShape(conditions)
|
|
3354
|
+
|
|
3355
|
+
Object.keys(conditions).forEach((key) => {
|
|
3356
|
+
assertDefinedFindByConditionValue(conditions[key], key)
|
|
3357
|
+
})
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
/**
|
|
3361
|
+
* Runs matches find by conditions.
|
|
3362
|
+
* @this {typeof FrontendModelBase}
|
|
3363
|
+
* @param {FrontendModelBase} model - Candidate model.
|
|
3364
|
+
* @param {Record<string, ?>} conditions - Match conditions.
|
|
3365
|
+
* @returns {boolean} - Whether the model matches all conditions.
|
|
3366
|
+
*/
|
|
3367
|
+
static matchesFindByConditions(model, conditions) {
|
|
3368
|
+
const modelAttributes = model.attributes()
|
|
3369
|
+
|
|
3370
|
+
for (const key of Object.keys(conditions)) {
|
|
3371
|
+
const expectedValue = conditions[key]
|
|
3372
|
+
const actualValue = modelAttributes[key]
|
|
3373
|
+
|
|
3374
|
+
if (Array.isArray(expectedValue)) {
|
|
3375
|
+
if (Array.isArray(actualValue)) {
|
|
3376
|
+
if (!this.findByConditionValueMatches(actualValue, expectedValue)) {
|
|
3377
|
+
return false
|
|
3378
|
+
}
|
|
3379
|
+
} else if (!expectedValue.some((entry) => this.findByConditionValueMatches(actualValue, entry))) {
|
|
3380
|
+
return false
|
|
3381
|
+
}
|
|
3382
|
+
} else if (!this.findByConditionValueMatches(actualValue, expectedValue)) {
|
|
3383
|
+
return false
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
return true
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
/**
|
|
3391
|
+
* Runs find by condition value matches.
|
|
3392
|
+
* @this {typeof FrontendModelBase}
|
|
3393
|
+
* @param {?} actualValue - Actual model value.
|
|
3394
|
+
* @param {?} expectedValue - Expected find condition value.
|
|
3395
|
+
* @returns {boolean} - Whether values match.
|
|
3396
|
+
*/
|
|
3397
|
+
static findByConditionValueMatches(actualValue, expectedValue) {
|
|
3398
|
+
if (expectedValue === null) {
|
|
3399
|
+
return actualValue === null
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
if (Array.isArray(expectedValue)) {
|
|
3403
|
+
if (!Array.isArray(actualValue)) {
|
|
3404
|
+
return false
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
if (actualValue.length !== expectedValue.length) {
|
|
3408
|
+
return false
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
for (let index = 0; index < expectedValue.length; index += 1) {
|
|
3412
|
+
if (!this.findByConditionValueMatches(actualValue[index], expectedValue[index])) {
|
|
3413
|
+
return false
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
return true
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
if (expectedValue && typeof expectedValue === "object") {
|
|
3421
|
+
if (!actualValue || typeof actualValue !== "object" || Array.isArray(actualValue)) {
|
|
3422
|
+
return false
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
const actualObject = /**
|
|
3426
|
+
* Narrows the runtime value to the documented type.
|
|
3427
|
+
@type {Record<string, ?>} */ (actualValue)
|
|
3428
|
+
const expectedObject = /**
|
|
3429
|
+
* Narrows the runtime value to the documented type.
|
|
3430
|
+
@type {Record<string, ?>} */ (expectedValue)
|
|
3431
|
+
const actualKeys = Object.keys(actualObject)
|
|
3432
|
+
const expectedKeys = Object.keys(expectedObject)
|
|
3433
|
+
|
|
3434
|
+
if (actualKeys.length !== expectedKeys.length) {
|
|
3435
|
+
return false
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
for (const key of expectedKeys) {
|
|
3439
|
+
if (!Object.prototype.hasOwnProperty.call(actualObject, key)) {
|
|
3440
|
+
return false
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
if (!this.findByConditionValueMatches(actualObject[key], expectedObject[key])) {
|
|
3444
|
+
return false
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
return true
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
if (actualValue === expectedValue) {
|
|
3452
|
+
return true
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
return this.findByPrimitiveValuesMatch(actualValue, expectedValue)
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
/**
|
|
3459
|
+
* Runs find by primitive values match.
|
|
3460
|
+
* @this {typeof FrontendModelBase}
|
|
3461
|
+
* @param {?} actualValue - Actual model value.
|
|
3462
|
+
* @param {?} expectedValue - Expected find condition value.
|
|
3463
|
+
* @returns {boolean} - Whether primitive values match after safe coercion.
|
|
3464
|
+
*/
|
|
3465
|
+
static findByPrimitiveValuesMatch(actualValue, expectedValue) {
|
|
3466
|
+
if (actualValue instanceof Date && typeof expectedValue === "string") {
|
|
3467
|
+
return actualValue.toISOString() === expectedValue
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
if (typeof actualValue === "string" && expectedValue instanceof Date) {
|
|
3471
|
+
return actualValue === expectedValue.toISOString()
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
if (actualValue instanceof Date && expectedValue instanceof Date) {
|
|
3475
|
+
return actualValue.toISOString() === expectedValue.toISOString()
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
if (typeof actualValue === "number" && typeof expectedValue === "string") {
|
|
3479
|
+
return this.findByNumericStringMatchesNumber(expectedValue, actualValue)
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
if (typeof actualValue === "string" && typeof expectedValue === "number") {
|
|
3483
|
+
return this.findByNumericStringMatchesNumber(actualValue, expectedValue)
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
return false
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
/**
|
|
3490
|
+
* Runs find by numeric string matches number.
|
|
3491
|
+
* @this {typeof FrontendModelBase}
|
|
3492
|
+
* @param {string} numericString - Numeric string value.
|
|
3493
|
+
* @param {number} expectedNumber - Number value.
|
|
3494
|
+
* @returns {boolean} - Whether values represent the same number.
|
|
3495
|
+
*/
|
|
3496
|
+
static findByNumericStringMatchesNumber(numericString, expectedNumber) {
|
|
3497
|
+
if (!Number.isFinite(expectedNumber)) {
|
|
3498
|
+
return false
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
if (!/^-?\d+(?:\.\d+)?$/.test(numericString)) {
|
|
3502
|
+
return false
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
return Number(numericString) === expectedNumber
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
/**
|
|
3509
|
+
* Runs update.
|
|
3510
|
+
* @param {Record<string, ?>} [newAttributes] - New values to assign before update.
|
|
3511
|
+
* @returns {Promise<this>} - Updated model.
|
|
3512
|
+
*/
|
|
3513
|
+
async update(newAttributes = {}) {
|
|
3514
|
+
const ModelClass = /**
|
|
3515
|
+
* Narrows the runtime value to the documented type.
|
|
3516
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3517
|
+
const attachmentDefinitions = ModelClass.attachmentDefinitions()
|
|
3518
|
+
/**
|
|
3519
|
+
* Regular attributes.
|
|
3520
|
+
@type {Record<string, ?>} */
|
|
3521
|
+
const regularAttributes = {}
|
|
3522
|
+
/**
|
|
3523
|
+
* Pending attachments.
|
|
3524
|
+
@type {Array<{attachmentName: string, value: ?}>} */
|
|
3525
|
+
const pendingAttachments = []
|
|
3526
|
+
|
|
3527
|
+
for (const [attributeName, attributeValue] of Object.entries(newAttributes)) {
|
|
3528
|
+
if (attachmentDefinitions[attributeName]) {
|
|
3529
|
+
if (attributeValue !== undefined && attributeValue !== null) {
|
|
3530
|
+
pendingAttachments.push({attachmentName: attributeName, value: attributeValue})
|
|
3531
|
+
}
|
|
3532
|
+
} else {
|
|
3533
|
+
regularAttributes[attributeName] = attributeValue
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
if (Object.keys(regularAttributes).length > 0) {
|
|
3538
|
+
this.assignAttributes(regularAttributes)
|
|
3539
|
+
const changedAttributes = Object.fromEntries(
|
|
3540
|
+
Object.entries(this.changes()).map(([attributeName, [, currentValue]]) => [attributeName, currentValue])
|
|
3541
|
+
)
|
|
3542
|
+
|
|
3543
|
+
const response = await ModelClass.executeCommand("update", {
|
|
3544
|
+
attributes: changedAttributes,
|
|
3545
|
+
id: this.primaryKeyValue()
|
|
3546
|
+
})
|
|
3547
|
+
|
|
3548
|
+
this.assignAttributes(ModelClass.attributesFromResponse(response))
|
|
3549
|
+
this.setIsNewRecord(false)
|
|
3550
|
+
this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
for (const pendingAttachment of pendingAttachments) {
|
|
3554
|
+
await this.getAttachmentByName(pendingAttachment.attachmentName).attach(pendingAttachment.value)
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
return this
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
/**
|
|
3561
|
+
* Runs attach.
|
|
3562
|
+
* @param {?} attachmentInput - Attachment input or named attachment payload.
|
|
3563
|
+
* @returns {Promise<void>} - Resolves when attached.
|
|
3564
|
+
*/
|
|
3565
|
+
async attach(attachmentInput) {
|
|
3566
|
+
const ModelClass = /**
|
|
3567
|
+
* Narrows the runtime value to the documented type.
|
|
3568
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3569
|
+
const attachmentDefinitions = ModelClass.attachmentDefinitions()
|
|
3570
|
+
const attachmentNames = Object.keys(attachmentDefinitions)
|
|
3571
|
+
let attachmentName = attachmentNames[0]
|
|
3572
|
+
let actualAttachmentInput = attachmentInput
|
|
3573
|
+
|
|
3574
|
+
if (frontendAttachmentValueIsPlainObject(attachmentInput)) {
|
|
3575
|
+
if ("file" in attachmentInput && attachmentDefinitions.file) {
|
|
3576
|
+
attachmentName = "file"
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
for (const candidateName of attachmentNames) {
|
|
3580
|
+
if (candidateName in attachmentInput) {
|
|
3581
|
+
attachmentName = candidateName
|
|
3582
|
+
actualAttachmentInput = attachmentInput[candidateName]
|
|
3583
|
+
break
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
if (!attachmentName) {
|
|
3589
|
+
throw new Error(`No attachment definitions on ${ModelClass.name}`)
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
await this.getAttachmentByName(attachmentName).attach(actualAttachmentInput)
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
/**
|
|
3596
|
+
* Runs save.
|
|
3597
|
+
* @returns {Promise<this>} - Saved model.
|
|
3598
|
+
*/
|
|
3599
|
+
async save() {
|
|
3600
|
+
const ModelClass = /**
|
|
3601
|
+
* Narrows the runtime value to the documented type.
|
|
3602
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3603
|
+
const isNew = this.isNewRecord()
|
|
3604
|
+
const commandType = isNew ? "create" : "update"
|
|
3605
|
+
/**
|
|
3606
|
+
* Payload.
|
|
3607
|
+
@type {Record<string, ?>} */
|
|
3608
|
+
const payload = {
|
|
3609
|
+
attributes: this._changedAttributesForSave()
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
if (!isNew) {
|
|
3613
|
+
payload.id = this.primaryKeyValue()
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
const nestedAttributes = this._buildNestedAttributesPayload()
|
|
3617
|
+
|
|
3618
|
+
if (nestedAttributes && Object.keys(nestedAttributes).length > 0) {
|
|
3619
|
+
payload.nestedAttributes = nestedAttributes
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
const response = await ModelClass.executeCommand(commandType, payload)
|
|
3623
|
+
|
|
3624
|
+
this.assignAttributes(ModelClass.attributesFromResponse(response))
|
|
3625
|
+
this.setIsNewRecord(false)
|
|
3626
|
+
this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
|
|
3627
|
+
|
|
3628
|
+
this._reconcileNestedAttributesFromResponse(response)
|
|
3629
|
+
|
|
3630
|
+
return this
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
/**
|
|
3634
|
+
* Returns the subset of `_attributes` whose value has diverged from
|
|
3635
|
+
* `_persistedAttributes`. Used by `save()` so the server receives only the
|
|
3636
|
+
* fields the caller actually changed — avoiding strict permit rejections on
|
|
3637
|
+
* framework-managed fields like `id`, `createdAt`, `updatedAt`, or owner
|
|
3638
|
+
* foreign keys that the resource never lists in `permittedParams`.
|
|
3639
|
+
* @returns {Record<string, ?>} - Changed attributes hash.
|
|
3640
|
+
*/
|
|
3641
|
+
_changedAttributesForSave() {
|
|
3642
|
+
/**
|
|
3643
|
+
* Changed attributes.
|
|
3644
|
+
@type {Record<string, ?>} */
|
|
3645
|
+
const changedAttributes = {}
|
|
3646
|
+
|
|
3647
|
+
for (const [attributeName, [previousValue, currentValue]] of Object.entries(this.changes())) {
|
|
3648
|
+
if (this.isNewRecord() && previousValue === undefined && currentValue === null) continue
|
|
3649
|
+
|
|
3650
|
+
changedAttributes[attributeName] = currentValue
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
return changedAttributes
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
/**
|
|
3657
|
+
* Marks the current value for an attribute as already persisted so the next
|
|
3658
|
+
* save does not send it unless the caller changes it again.
|
|
3659
|
+
* @param {string} attributeName - Attribute to mark unchanged.
|
|
3660
|
+
* @returns {void}
|
|
3661
|
+
*/
|
|
3662
|
+
markAttributeUnchanged(attributeName) {
|
|
3663
|
+
this._persistedAttributes[attributeName] = cloneFrontendModelAttributes({value: this._attributes[attributeName]}).value
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
/**
|
|
3667
|
+
* Runs destroy.
|
|
3668
|
+
* @returns {Promise<void>} - Resolves when destroyed on backend.
|
|
3669
|
+
*/
|
|
3670
|
+
async destroy() {
|
|
3671
|
+
const ModelClass = /**
|
|
3672
|
+
* Narrows the runtime value to the documented type.
|
|
3673
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3674
|
+
|
|
3675
|
+
await ModelClass.executeCommand("destroy", {
|
|
3676
|
+
id: this.primaryKeyValue()
|
|
3677
|
+
})
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3680
|
+
/**
|
|
3681
|
+
* Walks relationships declared in this resource's `nestedAttributes` config
|
|
3682
|
+
* and builds the per-relationship payload of dirty children for a parent save.
|
|
3683
|
+
*
|
|
3684
|
+
* Included children:
|
|
3685
|
+
* - new records (isNewRecord()) → create entry with attributes
|
|
3686
|
+
* - records marked for destruction (markedForDestruction()) → destroy entry
|
|
3687
|
+
* - records with changed attributes (isChanged()) → update entry with attributes
|
|
3688
|
+
* - records with dirty descendants in their own nestedAttributes → recurse
|
|
3689
|
+
*
|
|
3690
|
+
* Loaded but untouched records are omitted so nested save preserves Rails-style
|
|
3691
|
+
* "children not referenced in payload are left alone" semantics.
|
|
3692
|
+
* @returns {Record<string, Array<Record<string, ?>>>} - Per-relationship list of nested-attribute entries.
|
|
3693
|
+
*/
|
|
3694
|
+
_buildNestedAttributesPayload() {
|
|
3695
|
+
const ModelClass = /**
|
|
3696
|
+
* Narrows the runtime value to the documented type.
|
|
3697
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3698
|
+
const resourceConfig = typeof ModelClass.resourceConfig === "function" ? ModelClass.resourceConfig() : null
|
|
3699
|
+
const nestedAttributesConfig = resourceConfig?.nestedAttributes
|
|
3700
|
+
|
|
3701
|
+
if (!nestedAttributesConfig) return {}
|
|
3702
|
+
|
|
3703
|
+
/**
|
|
3704
|
+
* Payload.
|
|
3705
|
+
@type {Record<string, Array<Record<string, ?>>>} */
|
|
3706
|
+
const payload = {}
|
|
3707
|
+
|
|
3708
|
+
for (const relationshipName of Object.keys(nestedAttributesConfig)) {
|
|
3709
|
+
const relationship = this._relationships[relationshipName]
|
|
3710
|
+
|
|
3711
|
+
if (!relationship || !(relationship instanceof FrontendModelHasManyRelationship)) continue
|
|
3712
|
+
if (!Array.isArray(relationship._loadedValue) || relationship._loadedValue.length === 0) continue
|
|
3713
|
+
|
|
3714
|
+
/**
|
|
3715
|
+
* Entries.
|
|
3716
|
+
@type {Array<Record<string, ?>>} */
|
|
3717
|
+
const entries = []
|
|
3718
|
+
|
|
3719
|
+
for (const child of relationship._loadedValue) {
|
|
3720
|
+
const childEntry = child._nestedAttributesEntryForParentSave()
|
|
3721
|
+
|
|
3722
|
+
if (childEntry) entries.push(childEntry)
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
if (entries.length > 0) {
|
|
3726
|
+
payload[relationshipName] = entries
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
return payload
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
/**
|
|
3734
|
+
* Builds the payload entry for this child when walked by a parent's
|
|
3735
|
+
* `_buildNestedAttributesPayload`. Returns `null` when the child has no
|
|
3736
|
+
* dirty state and no dirty descendants, so the parent can omit it.
|
|
3737
|
+
* @returns {Record<string, ?> | null} - Nested-attribute entry or null if clean.
|
|
3738
|
+
*/
|
|
3739
|
+
_nestedAttributesEntryForParentSave() {
|
|
3740
|
+
if (this.markedForDestruction()) {
|
|
3741
|
+
if (this.isNewRecord()) return null
|
|
3742
|
+
return {id: this.primaryKeyValue(), _destroy: true}
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
const nestedAttributes = this._buildNestedAttributesPayload()
|
|
3746
|
+
const hasNestedDirty = Object.keys(nestedAttributes).length > 0
|
|
3747
|
+
|
|
3748
|
+
if (this.isNewRecord()) {
|
|
3749
|
+
/**
|
|
3750
|
+
* Entry.
|
|
3751
|
+
@type {Record<string, ?>} */
|
|
3752
|
+
const entry = {attributes: this.attributes()}
|
|
3753
|
+
|
|
3754
|
+
if (hasNestedDirty) entry.nestedAttributes = nestedAttributes
|
|
3755
|
+
|
|
3756
|
+
return entry
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
if (!this.isChanged() && !hasNestedDirty) return null
|
|
3760
|
+
|
|
3761
|
+
/**
|
|
3762
|
+
* Entry.
|
|
3763
|
+
@type {Record<string, ?>} */
|
|
3764
|
+
const entry = {id: this.primaryKeyValue()}
|
|
3765
|
+
|
|
3766
|
+
if (this.isChanged()) entry.attributes = this.attributes()
|
|
3767
|
+
if (hasNestedDirty) entry.nestedAttributes = nestedAttributes
|
|
3768
|
+
|
|
3769
|
+
return entry
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
/**
|
|
3773
|
+
* After a parent save with `nestedAttributes`, the server response includes
|
|
3774
|
+
* preloaded versions of the affected relationships. This replaces the local
|
|
3775
|
+
* `_loadedValue` for each nested-writable relationship with the server's
|
|
3776
|
+
* authoritative set, so destroyed children are dropped and newly-created
|
|
3777
|
+
* children get their server-assigned ids + persisted state.
|
|
3778
|
+
* @param {Record<string, ?>} response - Command response payload.
|
|
3779
|
+
* @returns {void}
|
|
3780
|
+
*/
|
|
3781
|
+
_reconcileNestedAttributesFromResponse(response) {
|
|
3782
|
+
const ModelClass = /**
|
|
3783
|
+
* Narrows the runtime value to the documented type.
|
|
3784
|
+
@type {typeof FrontendModelBase} */ (this.constructor)
|
|
3785
|
+
const resourceConfig = typeof ModelClass.resourceConfig === "function" ? ModelClass.resourceConfig() : null
|
|
3786
|
+
const nestedAttributesConfig = resourceConfig?.nestedAttributes
|
|
3787
|
+
|
|
3788
|
+
if (!nestedAttributesConfig) return
|
|
3789
|
+
|
|
3790
|
+
const modelData = ModelClass.modelDataFromResponse(response)
|
|
3791
|
+
const preloadedRelationships = modelData.preloadedRelationships
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Relevant preloads.
|
|
3795
|
+
@type {Record<string, ?>} */
|
|
3796
|
+
const relevantPreloads = {}
|
|
3797
|
+
|
|
3798
|
+
for (const relationshipName of Object.keys(nestedAttributesConfig)) {
|
|
3799
|
+
if (relationshipName in preloadedRelationships) {
|
|
3800
|
+
relevantPreloads[relationshipName] = preloadedRelationships[relationshipName]
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
if (Object.keys(relevantPreloads).length > 0) {
|
|
3805
|
+
ModelClass.applyPreloadedRelationships(this, relevantPreloads)
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
/**
|
|
3810
|
+
* Runs execute command.
|
|
3811
|
+
* @this {typeof FrontendModelBase}
|
|
3812
|
+
* @param {FrontendModelCommandType} commandType - Command type.
|
|
3813
|
+
* @param {Record<string, ?>} payload - Command payload.
|
|
3814
|
+
* @returns {Promise<Record<string, ?>>} - Parsed JSON response.
|
|
3815
|
+
*/
|
|
3816
|
+
static async executeCommand(commandType, payload) {
|
|
3817
|
+
const commandName = this.commandName(commandType)
|
|
3818
|
+
const serializedPayload = /**
|
|
3819
|
+
* Narrows the runtime value to the documented type.
|
|
3820
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue(payload))
|
|
3821
|
+
const resourcePath = this.resourcePath()
|
|
3822
|
+
const containsAttachmentUpload = frontendModelPayloadContainsAttachmentUpload(serializedPayload)
|
|
3823
|
+
const useSharedTransport = !containsAttachmentUpload
|
|
3824
|
+
const url = useSharedTransport ? frontendModelApiUrl() : frontendModelCommandUrl(resourcePath || "", commandName)
|
|
3825
|
+
|
|
3826
|
+
if (useSharedTransport) {
|
|
3827
|
+
const batchResponse = await new Promise((resolve, reject) => {
|
|
3828
|
+
pendingSharedFrontendModelRequests.push({
|
|
3829
|
+
commandName,
|
|
3830
|
+
commandType,
|
|
3831
|
+
modelClass: this,
|
|
3832
|
+
payload: serializedPayload,
|
|
3833
|
+
reject,
|
|
3834
|
+
requestId: `${++sharedFrontendModelRequestId}`,
|
|
3835
|
+
resolve,
|
|
3836
|
+
resourcePath
|
|
3837
|
+
})
|
|
3838
|
+
|
|
3839
|
+
scheduleSharedFrontendModelRequestFlush()
|
|
3840
|
+
})
|
|
3841
|
+
|
|
3842
|
+
const decodedBatchResponse = /**
|
|
3843
|
+
* Narrows the runtime value to the documented type.
|
|
3844
|
+
@type {Record<string, ?>} */ (batchResponse)
|
|
3845
|
+
|
|
3846
|
+
this.throwOnErrorFrontendModelResponse({
|
|
3847
|
+
commandType,
|
|
3848
|
+
response: decodedBatchResponse
|
|
3849
|
+
})
|
|
3850
|
+
|
|
3851
|
+
return decodedBatchResponse
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
return await trackFrontendModelTransportRequest(async () => {
|
|
3855
|
+
const directResponse = await fetch(url, {
|
|
3856
|
+
body: JSON.stringify(serializedPayload),
|
|
3857
|
+
credentials: "include",
|
|
3858
|
+
headers: frontendModelRequestHeaders(),
|
|
3859
|
+
method: "POST"
|
|
3860
|
+
})
|
|
3861
|
+
|
|
3862
|
+
const directResponseText = await directResponse.text()
|
|
3863
|
+
|
|
3864
|
+
if (!directResponse.ok) {
|
|
3865
|
+
throwFrontendModelHttpError({
|
|
3866
|
+
commandLabel: `${this.name}#${commandType}`,
|
|
3867
|
+
response: directResponse,
|
|
3868
|
+
responseText: directResponseText
|
|
3869
|
+
})
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
const directJson = directResponseText.length > 0 ? JSON.parse(directResponseText) : {}
|
|
3873
|
+
const decodedDirectResponse = /**
|
|
3874
|
+
* Narrows the runtime value to the documented type.
|
|
3875
|
+
@type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(directJson))
|
|
3876
|
+
|
|
3877
|
+
this.throwOnErrorFrontendModelResponse({
|
|
3878
|
+
commandType,
|
|
3879
|
+
response: decodedDirectResponse
|
|
3880
|
+
})
|
|
3881
|
+
|
|
3882
|
+
return decodedDirectResponse
|
|
3883
|
+
})
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
/**
|
|
3887
|
+
* Runs execute custom command.
|
|
3888
|
+
* @this {typeof FrontendModelBase}
|
|
3889
|
+
* @param {object} args - Command arguments.
|
|
3890
|
+
* @param {string} args.commandName - Raw command path segment.
|
|
3891
|
+
* @param {FrontendModelRequestCommandType} args.commandType - Logical command type for error handling.
|
|
3892
|
+
* @param {string | number | null} [args.memberId] - Optional member id for member-scoped commands.
|
|
3893
|
+
* @param {Record<string, ?>} args.payload - Request payload.
|
|
3894
|
+
* @param {string} args.resourcePath - Direct resource path.
|
|
3895
|
+
* @returns {Promise<Record<string, ?>>} - Decoded response payload.
|
|
3896
|
+
*/
|
|
3897
|
+
static async executeCustomCommand({commandName, commandType, memberId = null, payload, resourcePath}) {
|
|
3898
|
+
const serializedPayload = /**
|
|
3899
|
+
* Narrows the runtime value to the documented type.
|
|
3900
|
+
@type {Record<string, ?>} */ (serializeFrontendModelTransportValue(payload))
|
|
3901
|
+
const customPath = frontendModelCustomCommandPath({
|
|
3902
|
+
commandName,
|
|
3903
|
+
memberId,
|
|
3904
|
+
modelName: this.getModelName(),
|
|
3905
|
+
resourcePath
|
|
3906
|
+
})
|
|
3907
|
+
|
|
3908
|
+
const batchResponse = await new Promise((resolve, reject) => {
|
|
3909
|
+
pendingSharedFrontendModelRequests.push({
|
|
3910
|
+
commandType,
|
|
3911
|
+
customPath,
|
|
3912
|
+
modelClass: this,
|
|
3913
|
+
payload: serializedPayload,
|
|
3914
|
+
reject,
|
|
3915
|
+
requestId: `${++sharedFrontendModelRequestId}`,
|
|
3916
|
+
resolve
|
|
3917
|
+
})
|
|
3918
|
+
|
|
3919
|
+
scheduleSharedFrontendModelRequestFlush()
|
|
3920
|
+
})
|
|
3921
|
+
|
|
3922
|
+
const decodedBatchResponse = /**
|
|
3923
|
+
* Narrows the runtime value to the documented type.
|
|
3924
|
+
@type {Record<string, ?>} */ (batchResponse)
|
|
3925
|
+
|
|
3926
|
+
this.throwOnErrorFrontendModelResponse({
|
|
3927
|
+
commandType,
|
|
3928
|
+
response: decodedBatchResponse
|
|
3929
|
+
})
|
|
3930
|
+
|
|
3931
|
+
return decodedBatchResponse
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
/**
|
|
3935
|
+
* Runs throw on error frontend model response.
|
|
3936
|
+
* @this {typeof FrontendModelBase}
|
|
3937
|
+
* @param {object} args - Arguments.
|
|
3938
|
+
* @param {FrontendModelRequestCommandType} args.commandType - Command type.
|
|
3939
|
+
* @param {Record<string, ?>} args.response - Decoded response.
|
|
3940
|
+
* @returns {void}
|
|
3941
|
+
*/
|
|
3942
|
+
static throwOnErrorFrontendModelResponse({commandType, response}) {
|
|
3943
|
+
if (response?.status !== "error") return
|
|
3944
|
+
|
|
3945
|
+
const responseKeys = Object.keys(response)
|
|
3946
|
+
const hasOnlyStatus = responseKeys.length === 1 && responseKeys[0] === "status"
|
|
3947
|
+
const hasErrorMessage = typeof response.errorMessage === "string" && response.errorMessage.length > 0
|
|
3948
|
+
const hasErrorEnvelopeKeys = Boolean(
|
|
3949
|
+
response.code !== undefined
|
|
3950
|
+
|| response.error !== undefined
|
|
3951
|
+
|| response.errors !== undefined
|
|
3952
|
+
|| response.message !== undefined
|
|
3953
|
+
)
|
|
3954
|
+
const nonStatusKeys = responseKeys.filter((key) => key !== "status")
|
|
3955
|
+
const configuredAttributeNames = this.configuredFrontendModelAttributeNames()
|
|
3956
|
+
const looksLikeRawModelPayload = nonStatusKeys.length > 0
|
|
3957
|
+
&& nonStatusKeys.every((key) => configuredAttributeNames.has(key))
|
|
3958
|
+
|
|
3959
|
+
if (!hasErrorMessage && !hasOnlyStatus && !hasErrorEnvelopeKeys && looksLikeRawModelPayload) return
|
|
3960
|
+
|
|
3961
|
+
const debugErrorMessage = typeof response.debugErrorMessage === "string" && response.debugErrorMessage.length > 0
|
|
3962
|
+
? response.debugErrorMessage
|
|
3963
|
+
: null
|
|
3964
|
+
const errorMessage = debugErrorMessage || (hasErrorMessage
|
|
3965
|
+
? response.errorMessage
|
|
3966
|
+
: `Request failed for ${this.name}#${commandType}`)
|
|
3967
|
+
|
|
3968
|
+
const error = /**
|
|
3969
|
+
* Narrows the runtime value to the documented type.
|
|
3970
|
+
@type {Error & {velocious?: Record<string, ?>, errorType?: string, validationErrors?: Record<string, ?>}} */ (new Error(errorMessage))
|
|
3971
|
+
if (response.velocious && typeof response.velocious === "object") {
|
|
3972
|
+
error.velocious = response.velocious
|
|
3973
|
+
}
|
|
3974
|
+
if (typeof response.errorType === "string") {
|
|
3975
|
+
error.errorType = response.errorType
|
|
3976
|
+
}
|
|
3977
|
+
if (response.validationErrors && typeof response.validationErrors === "object") {
|
|
3978
|
+
error.validationErrors = response.validationErrors
|
|
3979
|
+
}
|
|
3980
|
+
throw error
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
/**
|
|
3984
|
+
* Runs configured frontend model attribute names.
|
|
3985
|
+
* @this {typeof FrontendModelBase}
|
|
3986
|
+
* @returns {Set<string>} - Configured frontend model attribute names.
|
|
3987
|
+
*/
|
|
3988
|
+
static configuredFrontendModelAttributeNames() {
|
|
3989
|
+
const resourceConfig = /**
|
|
3990
|
+
* Narrows the runtime value to the documented type.
|
|
3991
|
+
@type {Record<string, ?>} */ (this.resourceConfig())
|
|
3992
|
+
const attributes = resourceConfig.attributes
|
|
3993
|
+
|
|
3994
|
+
if (Array.isArray(attributes)) {
|
|
3995
|
+
return new Set(attributes.filter((attributeName) => typeof attributeName === "string"))
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
if (attributes && typeof attributes === "object") {
|
|
3999
|
+
return new Set(Object.keys(attributes))
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
return new Set()
|
|
4003
|
+
}
|
|
4004
|
+
}
|