@primate/core 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/private/App.d.ts +531 -3
- package/lib/private/App.js +8 -3
- package/lib/private/Binder.d.ts +2 -2
- package/lib/private/Flags.d.ts +3 -3
- package/lib/private/Module.d.ts +5 -5
- package/lib/private/app/Facade.d.ts +566 -0
- package/lib/private/app/Facade.js +33 -0
- package/lib/private/asset/Asset.d.ts +1 -1
- package/lib/private/backend/Module.d.ts +1 -1
- package/lib/private/backend/Module.js +2 -5
- package/lib/private/backend/TAG.d.ts +1 -1
- package/lib/private/backend/TAG.js +1 -1
- package/lib/private/build/App.d.ts +2 -2
- package/lib/private/build/App.js +0 -2
- package/lib/private/build/client/index.js +11 -7
- package/lib/private/build/hook.js +4 -3
- package/lib/private/build/index.js +14 -13
- package/lib/private/build/presets.d.ts +10 -0
- package/lib/private/build/presets.js +39 -0
- package/lib/private/build/server/index.js +5 -7
- package/lib/private/build/server/plugin/assets.js +9 -7
- package/lib/private/build/server/plugin/config.js +11 -4
- package/lib/private/build/server/plugin/db-default.d.ts +4 -0
- package/lib/private/build/server/plugin/db-default.js +45 -0
- package/lib/private/build/server/plugin/frontend.js +4 -2
- package/lib/private/build/server/plugin/live-reload.d.ts +4 -0
- package/lib/private/build/server/plugin/{hot-reload.js → live-reload.js} +4 -4
- package/lib/private/build/server/plugin/native-addons.js +6 -4
- package/lib/private/build/server/plugin/node-imports.js +2 -2
- package/lib/private/build/server/plugin/route.js +2 -2
- package/lib/private/build/server/plugin/store-wrap.js +2 -2
- package/lib/private/build/server/plugin/store.js +6 -6
- package/lib/private/build/server/plugin/stores.js +4 -5
- package/lib/private/build/server/plugin/view.js +4 -4
- package/lib/private/build/server/plugin/views.js +4 -4
- package/lib/private/build/server/plugin/virtual-pages.js +8 -8
- package/lib/private/build/server/plugin/virtual-routes.js +8 -13
- package/lib/private/build/server/plugin/wasm.js +2 -3
- package/lib/private/bye.js +2 -3
- package/lib/private/client/Data.d.ts +1 -1
- package/lib/private/client/ValidateInit.d.ts +1 -3
- package/lib/private/client/ValidationError.d.ts +1 -1
- package/lib/private/client/app.js +2 -1
- package/lib/private/client/create-form.d.ts +31 -0
- package/lib/private/client/create-form.js +124 -0
- package/lib/private/client/extract-issues.d.ts +4 -0
- package/lib/private/client/extract-issues.js +20 -0
- package/lib/private/client/spa/index.d.ts +1 -1
- package/lib/private/client/spa/index.js +4 -5
- package/lib/private/client/{toValidated.d.ts → to-validated.d.ts} +2 -4
- package/lib/private/client/{toValidated.js → to-validated.js} +1 -1
- package/lib/private/client/validate-field.d.ts +3 -0
- package/lib/private/client/validate-field.js +41 -0
- package/lib/private/config/index.d.ts +2 -21
- package/lib/private/config/index.js +3 -1
- package/lib/private/config/schema.d.ts +13 -9
- package/lib/private/config/schema.js +10 -5
- package/lib/private/cookie.d.ts +5 -5
- package/lib/private/cookie.js +10 -14
- package/lib/private/db/As.d.ts +10 -0
- package/lib/private/db/DB.d.ts +33 -0
- package/lib/private/db/DB.js +2 -0
- package/lib/private/db/DataDict.d.ts +5 -0
- package/lib/private/db/DataKey.d.ts +4 -0
- package/lib/private/db/DataValue.d.ts +17 -0
- package/lib/private/db/MemoryDB.d.ts +35 -0
- package/lib/private/db/MemoryDB.js +317 -0
- package/lib/private/db/PK.d.ts +3 -0
- package/lib/private/db/PK.js +2 -0
- package/lib/private/{database → db}/Query.d.ts +2 -2
- package/lib/private/{database → db}/QueryBuilder.d.ts +2 -3
- package/lib/private/db/ReadArgs.d.ts +9 -0
- package/lib/private/db/ReadArgs.js +2 -0
- package/lib/private/db/ReadRelationsArgs.d.ts +6 -0
- package/lib/private/db/ReadRelationsArgs.js +2 -0
- package/lib/private/{database → db}/Schema.d.ts +1 -2
- package/lib/private/db/Sort.d.ts +4 -0
- package/lib/private/{database → db}/TypeMap.d.ts +2 -3
- package/lib/private/{database → db}/Types.d.ts +1 -1
- package/lib/private/db/With.d.ts +16 -0
- package/lib/private/db/With.js +2 -0
- package/lib/private/db/common.d.ts +18 -0
- package/lib/private/db/common.js +36 -0
- package/lib/private/db/error.d.ts +81 -0
- package/lib/private/db/error.js +199 -0
- package/lib/private/db/sql.d.ts +28 -0
- package/lib/private/db/sql.js +177 -0
- package/lib/private/db/storage.d.ts +3 -0
- package/lib/private/db/storage.js +3 -0
- package/lib/private/db/symbol/wrap.js +2 -0
- package/lib/private/db/symbol.js +2 -0
- package/lib/private/db/test.d.ts +4 -0
- package/lib/private/db/test.js +1384 -0
- package/lib/private/frontend/Module.d.ts +6 -6
- package/lib/private/frontend/Module.js +28 -23
- package/lib/private/frontend/Render.d.ts +1 -2
- package/lib/private/frontend/ServerData.d.ts +1 -1
- package/lib/private/frontend/ServerView.d.ts +1 -2
- package/lib/private/frontend/View.d.ts +1 -1
- package/lib/private/frontend/ViewOptions.d.ts +1 -1
- package/lib/private/frontend/ViewResponse.d.ts +1 -1
- package/lib/private/hash.js +0 -1
- package/lib/private/i18n/Catalog.d.ts +5 -2
- package/lib/private/i18n/Formatter.js +20 -8
- package/lib/private/i18n/Module.d.ts +3 -3
- package/lib/private/i18n/Module.js +28 -17
- package/lib/private/i18n/format.d.ts +4 -0
- package/lib/private/i18n/format.js +95 -0
- package/lib/private/i18n/index/client.d.ts +9 -0
- package/lib/private/i18n/index/client.js +152 -0
- package/lib/private/i18n/index/server.d.ts +9 -0
- package/lib/private/i18n/index/server.js +57 -0
- package/lib/private/i18n/index/types.d.ts +33 -0
- package/lib/private/i18n/index/types.js +2 -0
- package/lib/private/i18n/locale.d.ts +9 -3
- package/lib/private/i18n/ordinals.d.ts +1 -1
- package/lib/private/i18n/resolve.d.ts +7 -0
- package/lib/private/i18n/resolve.js +30 -0
- package/lib/private/i18n/schema.d.ts +4 -4
- package/lib/private/i18n/schema.js +6 -10
- package/lib/private/i18n/storage.d.ts +3 -0
- package/lib/private/i18n/storage.js +5 -0
- package/lib/private/i18n/validate.d.ts +2 -0
- package/lib/private/i18n/validate.js +21 -0
- package/lib/private/log.d.ts +1 -0
- package/lib/private/log.js +10 -10
- package/lib/private/module/Hook.d.ts +1 -1
- package/lib/private/module/Next.d.ts +1 -1
- package/lib/private/orm/ForeignKey.d.ts +11 -0
- package/lib/private/orm/ForeignKey.js +22 -0
- package/lib/private/orm/PrimaryKey.d.ts +15 -0
- package/lib/private/orm/PrimaryKey.js +35 -0
- package/lib/private/orm/Set.d.ts +11 -0
- package/lib/private/orm/Set.js +2 -0
- package/lib/private/orm/Store.d.ts +171 -0
- package/lib/private/orm/Store.js +518 -0
- package/lib/private/orm/foreign.d.ts +4 -0
- package/lib/private/orm/foreign.js +5 -0
- package/lib/private/orm/key.d.ts +8 -0
- package/lib/private/orm/key.js +4 -0
- package/lib/private/orm/parse.d.ts +12 -0
- package/lib/private/orm/parse.js +29 -0
- package/lib/private/orm/primary.d.ts +5 -0
- package/lib/private/orm/primary.js +5 -0
- package/lib/private/orm/relation.d.ts +43 -0
- package/lib/private/orm/relation.js +26 -0
- package/lib/private/orm/types.d.ts +18 -0
- package/lib/private/orm/types.js +2 -0
- package/lib/private/orm/wrap.d.ts +5 -0
- package/lib/private/orm/wrap.js +5 -0
- package/lib/private/paths.d.ts +2 -2
- package/lib/private/request/RequestBag.d.ts +1 -1
- package/lib/private/request/RequestBag.js +11 -7
- package/lib/private/request/RequestBody.d.ts +2 -4
- package/lib/private/request/RequestBody.js +8 -11
- package/lib/private/request/RequestContext.d.ts +12 -0
- package/lib/private/request/RequestContext.js +31 -0
- package/lib/private/request/RequestFacade.d.ts +16 -6
- package/lib/private/request/parse.d.ts +2 -2
- package/lib/private/request/parse.js +54 -16
- package/lib/private/request/route.js +37 -21
- package/lib/private/request/router.d.ts +1 -1
- package/lib/private/request/router.js +2 -2
- package/lib/private/request/sContext.d.ts +3 -0
- package/lib/private/request/sContext.js +2 -0
- package/lib/private/response/ResponseFunction.d.ts +1 -2
- package/lib/private/response/ResponseLike.d.ts +1 -1
- package/lib/private/response/binary.d.ts +1 -1
- package/lib/private/response/binary.js +4 -3
- package/lib/private/response/json.d.ts +1 -1
- package/lib/private/response/json.js +2 -2
- package/lib/private/response/redirect.js +1 -1
- package/lib/private/response/respond.js +7 -10
- package/lib/private/response/sse.d.ts +1 -1
- package/lib/private/response/sse.js +2 -2
- package/lib/private/response/text.d.ts +1 -1
- package/lib/private/response/text.js +3 -3
- package/lib/private/response/view.d.ts +2 -2
- package/lib/private/response/view.js +3 -3
- package/lib/private/response.d.ts +1 -1
- package/lib/private/route/Handler.d.ts +1 -1
- package/lib/private/route/hook.d.ts +6 -0
- package/lib/private/route/hook.js +5 -0
- package/lib/private/route/router.d.ts +9 -5
- package/lib/private/route/router.js +34 -17
- package/lib/private/route/wrap.js +2 -2
- package/lib/private/serve/App.d.ts +5 -18
- package/lib/private/serve/App.js +91 -82
- package/lib/private/serve/Init.d.ts +3 -3
- package/lib/private/serve/hook.js +1 -7
- package/lib/private/serve/index.js +6 -2
- package/lib/private/serve/module/Dev.d.ts +1 -1
- package/lib/private/serve/module/Dev.js +9 -5
- package/lib/private/serve/module/Handle.js +2 -2
- package/lib/private/session/Data.d.ts +1 -2
- package/lib/private/session/SessionHandle.d.ts +1 -1
- package/lib/private/session/SessionHandle.js +10 -9
- package/lib/private/session/SessionModule.js +7 -7
- package/lib/private/session/index.d.ts +3 -4
- package/lib/private/session/schema.d.ts +6 -6
- package/lib/private/session/schema.js +5 -4
- package/lib/private/session/storage.d.ts +1 -2
- package/lib/private/session/storage.js +2 -2
- package/lib/private/tags.js +2 -2
- package/lib/private/target/Target.d.ts +1 -1
- package/lib/public/AppFacade.d.ts +2 -0
- package/lib/public/AppFacade.js +2 -0
- package/lib/public/build/presets.d.ts +2 -0
- package/lib/public/build/presets.js +2 -0
- package/lib/public/build/transform.d.ts +2 -0
- package/lib/public/build/transform.js +2 -0
- package/lib/public/client.d.ts +14 -0
- package/lib/public/client.js +10 -0
- package/lib/public/db/MemoryDB.d.ts +2 -0
- package/lib/public/db/MemoryDB.js +2 -0
- package/lib/public/db/error.d.ts +2 -0
- package/lib/public/db/error.js +2 -0
- package/lib/public/db/sql.d.ts +2 -0
- package/lib/public/db/sql.js +2 -0
- package/lib/public/db/test.d.ts +2 -0
- package/lib/public/db/test.js +2 -0
- package/lib/public/db.d.ts +12 -0
- package/lib/public/db.js +2 -0
- package/lib/public/orm/Store.d.ts +2 -0
- package/lib/public/orm/Store.js +2 -0
- package/lib/public/orm/key.d.ts +2 -0
- package/lib/public/orm/key.js +2 -0
- package/lib/public/orm/relation.d.ts +2 -0
- package/lib/public/orm/relation.js +2 -0
- package/lib/public/orm/wrap.d.ts +2 -0
- package/lib/public/orm/wrap.js +2 -0
- package/lib/public/request.d.ts +4 -0
- package/lib/public/request.js +2 -0
- package/lib/public/response.d.ts +22 -0
- package/lib/public/response.js +19 -0
- package/lib/public/route/hook.d.ts +2 -0
- package/lib/public/route/hook.js +2 -0
- package/package.json +28 -23
- package/lib/private/build/client/reload.d.ts +0 -7
- package/lib/private/build/client/reload.js +0 -6
- package/lib/private/build/server/plugin/database-default.d.ts +0 -4
- package/lib/private/build/server/plugin/database-default.js +0 -48
- package/lib/private/build/server/plugin/hot-reload.d.ts +0 -4
- package/lib/private/client/validate.d.ts +0 -3
- package/lib/private/client/validate.js +0 -54
- package/lib/private/database/As.d.ts +0 -7
- package/lib/private/database/Binds.d.ts +0 -4
- package/lib/private/database/Binds.js +0 -2
- package/lib/private/database/Changes.d.ts +0 -11
- package/lib/private/database/Changes.js +0 -2
- package/lib/private/database/ColumnTypes.d.ts +0 -11
- package/lib/private/database/ColumnTypes.js +0 -2
- package/lib/private/database/DataDict.d.ts +0 -5
- package/lib/private/database/DataKey.d.ts +0 -4
- package/lib/private/database/DataValue.d.ts +0 -5
- package/lib/private/database/Database.d.ts +0 -56
- package/lib/private/database/Database.js +0 -153
- package/lib/private/database/InMemoryDatabase.d.ts +0 -37
- package/lib/private/database/InMemoryDatabase.js +0 -181
- package/lib/private/database/Sort.d.ts +0 -4
- package/lib/private/database/Store.d.ts +0 -172
- package/lib/private/database/Store.js +0 -261
- package/lib/private/database/storage.d.ts +0 -4
- package/lib/private/database/storage.js +0 -3
- package/lib/private/database/symbol/wrap.js +0 -2
- package/lib/private/database/symbol.js +0 -2
- package/lib/private/database/test.d.ts +0 -4
- package/lib/private/database/test.js +0 -678
- package/lib/private/database/wrap.d.ts +0 -5
- package/lib/private/database/wrap.js +0 -5
- package/lib/private/i18n/index.d.ts +0 -28
- package/lib/private/i18n/index.js +0 -236
- package/lib/private/route/guard.d.ts +0 -4
- package/lib/private/route/guard.js +0 -22
- package/lib/private/wasm/API.d.ts +0 -7
- package/lib/private/wasm/API.js +0 -2
- package/lib/private/wasm/BufferViewSource.d.ts +0 -4
- package/lib/private/wasm/BufferViewSource.js +0 -2
- package/lib/private/wasm/Exports.d.ts +0 -23
- package/lib/private/wasm/Exports.js +0 -2
- package/lib/private/wasm/I32.d.ts +0 -5
- package/lib/private/wasm/I32.js +0 -2
- package/lib/private/wasm/I32_SIZE.d.ts +0 -3
- package/lib/private/wasm/I32_SIZE.js +0 -2
- package/lib/private/wasm/Instantiation.d.ts +0 -12
- package/lib/private/wasm/Instantiation.js +0 -2
- package/lib/private/wasm/Tagged.d.ts +0 -7
- package/lib/private/wasm/Tagged.js +0 -2
- package/lib/private/wasm/buffersize.d.ts +0 -2
- package/lib/private/wasm/buffersize.js +0 -5
- package/lib/private/wasm/decode-bytes.d.ts +0 -3
- package/lib/private/wasm/decode-bytes.js +0 -5
- package/lib/private/wasm/decode-json.d.ts +0 -7
- package/lib/private/wasm/decode-json.js +0 -11
- package/lib/private/wasm/decode-option.d.ts +0 -5
- package/lib/private/wasm/decode-option.js +0 -10
- package/lib/private/wasm/decode-response.d.ts +0 -19
- package/lib/private/wasm/decode-response.js +0 -90
- package/lib/private/wasm/decode-string.d.ts +0 -3
- package/lib/private/wasm/decode-string.js +0 -5
- package/lib/private/wasm/decode-websocket-close.d.ts +0 -5
- package/lib/private/wasm/decode-websocket-close.js +0 -6
- package/lib/private/wasm/decode-websocket-send.d.ts +0 -6
- package/lib/private/wasm/decode-websocket-send.js +0 -19
- package/lib/private/wasm/encode-buffer.d.ts +0 -3
- package/lib/private/wasm/encode-buffer.js +0 -6
- package/lib/private/wasm/encode-request.d.ts +0 -9
- package/lib/private/wasm/encode-request.js +0 -195
- package/lib/private/wasm/encode-session.d.ts +0 -3
- package/lib/private/wasm/encode-session.js +0 -25
- package/lib/private/wasm/encode-string-map.d.ts +0 -5
- package/lib/private/wasm/encode-string-map.js +0 -14
- package/lib/private/wasm/encode-string.d.ts +0 -13
- package/lib/private/wasm/encode-string.js +0 -17
- package/lib/private/wasm/encode-url.d.ts +0 -11
- package/lib/private/wasm/encode-url.js +0 -14
- package/lib/private/wasm/encode-websocket-close.d.ts +0 -2
- package/lib/private/wasm/encode-websocket-close.js +0 -7
- package/lib/private/wasm/encode-websocket-message.d.ts +0 -4
- package/lib/private/wasm/encode-websocket-message.js +0 -30
- package/lib/private/wasm/encode-websocket-open.d.ts +0 -2
- package/lib/private/wasm/encode-websocket-open.js +0 -7
- package/lib/private/wasm/filesize.d.ts +0 -2
- package/lib/private/wasm/filesize.js +0 -8
- package/lib/private/wasm/instantiate.d.ts +0 -37
- package/lib/private/wasm/instantiate.js +0 -408
- package/lib/private/wasm/open-websocket.d.ts +0 -4
- package/lib/private/wasm/open-websocket.js +0 -26
- package/lib/private/wasm/stringsize.d.ts +0 -3
- package/lib/private/wasm/stringsize.js +0 -5
- package/lib/private/wasm/urlsize.d.ts +0 -2
- package/lib/private/wasm/urlsize.js +0 -5
- package/lib/public/Database.d.ts +0 -2
- package/lib/public/Database.js +0 -2
- package/lib/public/client/ValidateInit.d.ts +0 -2
- package/lib/public/client/ValidateInit.js +0 -2
- package/lib/public/client/ValidateUpdater.d.ts +0 -2
- package/lib/public/client/ValidateUpdater.js +0 -2
- package/lib/public/client/ValidationError.d.ts +0 -2
- package/lib/public/client/ValidationError.js +0 -2
- package/lib/public/client/toValidated.d.ts +0 -2
- package/lib/public/client/toValidated.js +0 -2
- package/lib/public/client/validate.d.ts +0 -2
- package/lib/public/client/validate.js +0 -2
- package/lib/public/database/As.d.ts +0 -2
- package/lib/public/database/As.js +0 -2
- package/lib/public/database/DataDict.d.ts +0 -2
- package/lib/public/database/DataDict.js +0 -2
- package/lib/public/database/InMemoryDatabase.d.ts +0 -2
- package/lib/public/database/InMemoryDatabase.js +0 -2
- package/lib/public/database/Sort.d.ts +0 -2
- package/lib/public/database/Sort.js +0 -2
- package/lib/public/database/Store.d.ts +0 -2
- package/lib/public/database/Store.js +0 -2
- package/lib/public/database/TypeMap.d.ts +0 -2
- package/lib/public/database/TypeMap.js +0 -2
- package/lib/public/database/Types.d.ts +0 -2
- package/lib/public/database/Types.js +0 -2
- package/lib/public/database/test.d.ts +0 -2
- package/lib/public/database/test.js +0 -2
- package/lib/public/database/wrap.d.ts +0 -2
- package/lib/public/database/wrap.js +0 -2
- package/lib/public/request/RequestBody.d.ts +0 -2
- package/lib/public/request/RequestBody.js +0 -2
- package/lib/public/request/RequestFacade.d.ts +0 -2
- package/lib/public/request/RequestFacade.js +0 -2
- package/lib/public/request/Verb.d.ts +0 -2
- package/lib/public/request/Verb.js +0 -2
- package/lib/public/response/ResponseFunction.d.ts +0 -2
- package/lib/public/response/ResponseFunction.js +0 -2
- package/lib/public/response/ResponseLike.d.ts +0 -2
- package/lib/public/response/ResponseLike.js +0 -2
- package/lib/public/response/binary.d.ts +0 -2
- package/lib/public/response/binary.js +0 -2
- package/lib/public/response/error.d.ts +0 -2
- package/lib/public/response/error.js +0 -2
- package/lib/public/response/json.d.ts +0 -2
- package/lib/public/response/json.js +0 -2
- package/lib/public/response/redirect.d.ts +0 -2
- package/lib/public/response/redirect.js +0 -2
- package/lib/public/response/sse.d.ts +0 -2
- package/lib/public/response/sse.js +0 -2
- package/lib/public/response/text.d.ts +0 -2
- package/lib/public/response/text.js +0 -2
- package/lib/public/response/view.d.ts +0 -2
- package/lib/public/response/view.js +0 -2
- package/lib/public/response/ws.d.ts +0 -2
- package/lib/public/response/ws.js +0 -2
- package/lib/public/wasm/decode-json.d.ts +0 -5
- package/lib/public/wasm/decode-json.js +0 -3
- package/lib/public/wasm/decode-response.d.ts +0 -3
- package/lib/public/wasm/decode-response.js +0 -3
- package/lib/public/wasm/encode-request.d.ts +0 -3
- package/lib/public/wasm/encode-request.js +0 -3
- package/lib/public/wasm/encode-session.d.ts +0 -3
- package/lib/public/wasm/encode-session.js +0 -3
- package/lib/public/wasm/instantiate.d.ts +0 -4
- package/lib/public/wasm/instantiate.js +0 -3
- /package/lib/private/{database → db}/As.js +0 -0
- /package/lib/private/{database → db}/DataDict.js +0 -0
- /package/lib/private/{database → db}/DataKey.js +0 -0
- /package/lib/private/{database → db}/DataValue.js +0 -0
- /package/lib/private/{database → db}/Query.js +0 -0
- /package/lib/private/{database → db}/QueryBuilder.js +0 -0
- /package/lib/private/{database → db}/Schema.js +0 -0
- /package/lib/private/{database → db}/Sort.js +0 -0
- /package/lib/private/{database → db}/TypeMap.js +0 -0
- /package/lib/private/{database → db}/Types.js +0 -0
- /package/lib/private/{database → db}/primary.d.ts +0 -0
- /package/lib/private/{database → db}/primary.js +0 -0
- /package/lib/private/{database → db}/symbol/wrap.d.ts +0 -0
- /package/lib/private/{database → db}/symbol.d.ts +0 -0
|
@@ -0,0 +1,1384 @@
|
|
|
1
|
+
import { Code } from "#db/error";
|
|
2
|
+
import key from "#orm/key";
|
|
3
|
+
import relation from "#orm/relation";
|
|
4
|
+
import Store from "#orm/Store";
|
|
5
|
+
import test from "@rcompat/test";
|
|
6
|
+
import any from "@rcompat/test/any";
|
|
7
|
+
import p from "pema";
|
|
8
|
+
const BAD_WHERE = [
|
|
9
|
+
{
|
|
10
|
+
label: "reject array where value",
|
|
11
|
+
base: { name: ["Donald"] },
|
|
12
|
+
with: { title: ["foo a"] },
|
|
13
|
+
expected: Code.where_invalid_value,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "reject empty operator object",
|
|
17
|
+
base: { name: {} },
|
|
18
|
+
with: { title: {} },
|
|
19
|
+
expected: Code.operator_empty,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: "reject undefined where value",
|
|
23
|
+
base: { age: undefined },
|
|
24
|
+
with: { title: undefined },
|
|
25
|
+
expected: Code.field_undefined,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "reject unknown operator",
|
|
29
|
+
base: { name: { $nope: "x" } },
|
|
30
|
+
with: { title: { $nope: "x" } },
|
|
31
|
+
expected: Code.operator_unknown,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
const BAD_SELECT = [
|
|
35
|
+
{
|
|
36
|
+
label: "reject empty select",
|
|
37
|
+
base: [],
|
|
38
|
+
with: [],
|
|
39
|
+
expected: Code.select_empty,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "reject unknown select column",
|
|
43
|
+
base: ["nope"],
|
|
44
|
+
with: ["nope"],
|
|
45
|
+
expected: Code.field_unknown,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "reject duplicate select fields",
|
|
49
|
+
base: ["id", "id"],
|
|
50
|
+
with: ["id", "id"],
|
|
51
|
+
expected: Code.field_duplicate,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
const BAD_SORT = [
|
|
55
|
+
{
|
|
56
|
+
label: "reject empty sort",
|
|
57
|
+
base: {},
|
|
58
|
+
with: {},
|
|
59
|
+
expected: Code.sort_empty,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "reject unknown sort column",
|
|
63
|
+
base: { nope: "asc" },
|
|
64
|
+
with: { nope: "asc" },
|
|
65
|
+
expected: Code.field_unknown,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: "reject invalid direction",
|
|
69
|
+
base: { age: "ascending" },
|
|
70
|
+
with: { title: "ascending" },
|
|
71
|
+
expected: Code.sort_invalid_value,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: "reject undefined direction",
|
|
75
|
+
base: { age: undefined },
|
|
76
|
+
with: { title: undefined },
|
|
77
|
+
expected: Code.sort_invalid_value,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const BAD_WHERE_COLUMN = [
|
|
81
|
+
{
|
|
82
|
+
label: "reject unknown criteria column",
|
|
83
|
+
base: { nope: "x" },
|
|
84
|
+
with: { nope: "x" },
|
|
85
|
+
expected: Code.field_unknown,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
const USERS = {
|
|
89
|
+
ben: { age: 60, lastname: "Miller", name: "Ben" },
|
|
90
|
+
donald: { age: 30, lastname: "Duck", name: "Donald" },
|
|
91
|
+
jeremy: { age: 20, name: "Just Jeremy" },
|
|
92
|
+
paul: { age: 40, lastname: "Miller", name: "Paul" },
|
|
93
|
+
ryan: { age: 40, lastname: "Wilson", name: "Ryan" },
|
|
94
|
+
};
|
|
95
|
+
function pick(record, ...projection) {
|
|
96
|
+
return Object.fromEntries(Object.entries(record).filter(([k]) => projection.includes(k)));
|
|
97
|
+
}
|
|
98
|
+
async function throws(assert, code, fn) {
|
|
99
|
+
try {
|
|
100
|
+
await fn();
|
|
101
|
+
assert(false).true();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
assert(error.code).equals(code);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export default (db) => {
|
|
108
|
+
test.ended(() => db.close());
|
|
109
|
+
const Post = new Store({
|
|
110
|
+
id: key.primary(p.string),
|
|
111
|
+
title: p.string,
|
|
112
|
+
user_id: p.uint,
|
|
113
|
+
}, { db, name: "post" });
|
|
114
|
+
Post.update;
|
|
115
|
+
const User = new Store({
|
|
116
|
+
id: key.primary(p.string),
|
|
117
|
+
age: p.u8.optional(),
|
|
118
|
+
lastname: p.string.optional(),
|
|
119
|
+
name: p.string.default("Donald"),
|
|
120
|
+
}, { db: db, name: "user" });
|
|
121
|
+
const UserN = new Store({
|
|
122
|
+
id: key.primary(p.u32),
|
|
123
|
+
age: p.u8.optional(),
|
|
124
|
+
lastname: p.string.optional(),
|
|
125
|
+
name: p.string.default("Donald"),
|
|
126
|
+
}, { db: db, name: "user_n" });
|
|
127
|
+
const UserB = new Store({
|
|
128
|
+
id: key.primary(p.u128),
|
|
129
|
+
age: p.u8.optional(),
|
|
130
|
+
lastname: p.string.optional(),
|
|
131
|
+
name: p.string.default("Donald"),
|
|
132
|
+
}, { db: db, name: "user_b" });
|
|
133
|
+
const USER_STORES = [User, UserN, UserB];
|
|
134
|
+
const Type = new Store({
|
|
135
|
+
id: key.primary(p.string),
|
|
136
|
+
boolean: p.boolean.optional(),
|
|
137
|
+
date: p.date.optional(),
|
|
138
|
+
f32: p.f32.optional(),
|
|
139
|
+
f64: p.f64.optional(),
|
|
140
|
+
i128: p.i128.optional(),
|
|
141
|
+
i16: p.i16.optional(),
|
|
142
|
+
i32: p.i32.optional(),
|
|
143
|
+
i64: p.i64.optional(),
|
|
144
|
+
i8: p.i8.optional(),
|
|
145
|
+
string: p.string.optional(),
|
|
146
|
+
u128: p.u128.optional(),
|
|
147
|
+
u16: p.u16.optional(),
|
|
148
|
+
u32: p.u32.optional(),
|
|
149
|
+
u64: p.u64.optional(),
|
|
150
|
+
u8: p.u8.optional(),
|
|
151
|
+
}, { db: db, name: "type" });
|
|
152
|
+
// this stresses identifier quoting in CREATE/INSERT/SELECT/UPDATE/DELETE
|
|
153
|
+
const Reserved = new Store({
|
|
154
|
+
id: key.primary(p.string),
|
|
155
|
+
// deliberately reserved-looking column name
|
|
156
|
+
order: p.u8.optional(),
|
|
157
|
+
name: p.string,
|
|
158
|
+
}, { db: db, name: "select" }); // deliberately reserved-like table name
|
|
159
|
+
const AuthorSchema = {
|
|
160
|
+
id: key.primary(p.string),
|
|
161
|
+
name: p.string,
|
|
162
|
+
};
|
|
163
|
+
const ArticleSchema = {
|
|
164
|
+
id: key.primary(p.string),
|
|
165
|
+
title: p.string,
|
|
166
|
+
author_id: key.foreign(p.string),
|
|
167
|
+
};
|
|
168
|
+
const ProfileSchema = {
|
|
169
|
+
id: key.primary(p.string),
|
|
170
|
+
bio: p.string,
|
|
171
|
+
url: p.url.optional(),
|
|
172
|
+
author_id: key.foreign(p.string),
|
|
173
|
+
};
|
|
174
|
+
const Author = new Store(AuthorSchema, {
|
|
175
|
+
db,
|
|
176
|
+
name: "author",
|
|
177
|
+
relations: {
|
|
178
|
+
articles: relation.many(ArticleSchema, "author_id"),
|
|
179
|
+
profile: relation.one(ProfileSchema, "author_id"),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const Article = new Store(ArticleSchema, {
|
|
183
|
+
db,
|
|
184
|
+
name: "article",
|
|
185
|
+
relations: {
|
|
186
|
+
author: relation.one(AuthorSchema, "author_id", { reverse: true }),
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const Profile = new Store(ProfileSchema, {
|
|
190
|
+
db,
|
|
191
|
+
name: "profile",
|
|
192
|
+
relations: {
|
|
193
|
+
author: relation.one(AuthorSchema, "author_id", { reverse: true }),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
function $store(label, store, body) {
|
|
197
|
+
test.case(label, async (assert) => {
|
|
198
|
+
await store.collection.create();
|
|
199
|
+
try {
|
|
200
|
+
await body(assert);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await store.collection.delete();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function $user(label, body) {
|
|
208
|
+
test.case(label, async (assert) => {
|
|
209
|
+
for (const S of USER_STORES)
|
|
210
|
+
await S.collection.create();
|
|
211
|
+
try {
|
|
212
|
+
for (const u of Object.values(USERS)) {
|
|
213
|
+
for (const S of USER_STORES)
|
|
214
|
+
await S.insert(u);
|
|
215
|
+
}
|
|
216
|
+
await body(assert);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
for (const S of USER_STORES)
|
|
220
|
+
await S.collection.delete();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function $user$(label, body) {
|
|
225
|
+
$user(`security: ${label}`, body);
|
|
226
|
+
}
|
|
227
|
+
function $type(label, body) {
|
|
228
|
+
$store(`type: ${label}`, Type, body);
|
|
229
|
+
}
|
|
230
|
+
function $rel(label, body) {
|
|
231
|
+
test.case(`relation: ${label}`, async (assert) => {
|
|
232
|
+
await Author.collection.create();
|
|
233
|
+
await Article.collection.create();
|
|
234
|
+
await Profile.collection.create();
|
|
235
|
+
try {
|
|
236
|
+
const john = await Author.insert({ name: "John" });
|
|
237
|
+
const bob = await Author.insert({ name: "Bob" });
|
|
238
|
+
const ned = await Author.insert({ name: "Ned" });
|
|
239
|
+
const jid = john.id;
|
|
240
|
+
const bid = bob.id;
|
|
241
|
+
const nid = ned.id;
|
|
242
|
+
await Article.insert({ title: "John First Post", author_id: jid });
|
|
243
|
+
await Article.insert({ title: "John Second Post", author_id: jid });
|
|
244
|
+
await Article.insert({ title: "Bob Only Post", author_id: bid });
|
|
245
|
+
await Profile.insert({
|
|
246
|
+
bio: "John is a writer",
|
|
247
|
+
url: new URL("https://example.com/john"),
|
|
248
|
+
author_id: jid,
|
|
249
|
+
});
|
|
250
|
+
const ts = ["foo a", "foo c", "foo d", "foo e", "foo f", "foo g"];
|
|
251
|
+
for (const title of ts)
|
|
252
|
+
await Article.insert({ title, author_id: jid });
|
|
253
|
+
await Article.insert({ title: "bar x", author_id: jid });
|
|
254
|
+
await Article.insert({ title: "foo b", author_id: bid });
|
|
255
|
+
await Article.insert({ title: "bar y", author_id: bid });
|
|
256
|
+
await Article.insert({ title: "bar z", author_id: nid });
|
|
257
|
+
await body(assert);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
await Profile.collection.delete();
|
|
261
|
+
await Article.collection.delete();
|
|
262
|
+
await Author.collection.delete();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function $rel$(label, body) {
|
|
267
|
+
$rel(`security: ${label}`, body);
|
|
268
|
+
}
|
|
269
|
+
function security_pair(base_prefix, label, mk, run_base, run_with, expected) {
|
|
270
|
+
const run = throws;
|
|
271
|
+
$user$(`${base_prefix}: ${label}`, async (assert) => {
|
|
272
|
+
const { base } = mk();
|
|
273
|
+
await run(assert, expected, () => run_base(base));
|
|
274
|
+
});
|
|
275
|
+
$rel$(`with: ${base_prefix}: ${label}`, async (assert) => {
|
|
276
|
+
const { with: w } = mk();
|
|
277
|
+
await run(assert, expected, () => run_with(w));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function bad_where(label, mk, expected) {
|
|
281
|
+
return security_pair("where", label, mk, base => User.find({ where: base }), w => Author.find({ with: { articles: { where: w } } }), expected);
|
|
282
|
+
}
|
|
283
|
+
function bad_select(label, mk, expected) {
|
|
284
|
+
return security_pair("find", label, mk, base => User.find({ select: base }), w => Author.find({ with: { articles: { select: w } } }), expected);
|
|
285
|
+
}
|
|
286
|
+
function bad_sort(label, mk, expected) {
|
|
287
|
+
return security_pair("find", label, mk, base => User.find({ sort: base }), w => Author.find({ with: { articles: { sort: w } } }), expected);
|
|
288
|
+
}
|
|
289
|
+
$store("insert", User, async (assert) => {
|
|
290
|
+
const donald = await User.insert({ age: 30, name: "Donald" });
|
|
291
|
+
assert(donald).type();
|
|
292
|
+
assert(await User.has(donald.id)).true();
|
|
293
|
+
const ryan = await User.insert({ age: 40, name: "Ryan" });
|
|
294
|
+
assert(await User.has(donald.id)).true();
|
|
295
|
+
assert(await User.has(ryan.id)).true();
|
|
296
|
+
});
|
|
297
|
+
$store("insert: primary key is optional (string)", User, async (assert) => {
|
|
298
|
+
const user = await User.insert({ name: "Test" });
|
|
299
|
+
assert(user.id).type();
|
|
300
|
+
assert(await User.has(user.id)).true();
|
|
301
|
+
});
|
|
302
|
+
$store("insert: primary key is optional (number)", UserN, async (assert) => {
|
|
303
|
+
const user = await UserN.insert({ name: "Test" });
|
|
304
|
+
assert(user.id).type();
|
|
305
|
+
assert(await UserN.has(user.id)).true();
|
|
306
|
+
});
|
|
307
|
+
$store("insert: primary key is optional (bigint)", UserB, async (assert) => {
|
|
308
|
+
const user = await UserB.insert({ name: "Test" });
|
|
309
|
+
assert(user.id).type();
|
|
310
|
+
assert(await UserB.has(user.id)).true();
|
|
311
|
+
});
|
|
312
|
+
const ManualUser = new Store({
|
|
313
|
+
id: key.primary(p.string, { generate: false }),
|
|
314
|
+
name: p.string,
|
|
315
|
+
}, { db, name: "manual_user" });
|
|
316
|
+
$store("insert: generate=false requires PK", ManualUser, async (assert) => {
|
|
317
|
+
await throws(assert, Code.pk_required, () => {
|
|
318
|
+
return ManualUser.insert({ name: "Test" });
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
$store("insert: generate=false accepts provided PK", ManualUser, async (assert) => {
|
|
322
|
+
const user = await ManualUser.insert({ id: "manual-id", name: "Test" });
|
|
323
|
+
assert(user.id).equals("manual-id");
|
|
324
|
+
assert(await ManualUser.has("manual-id")).true();
|
|
325
|
+
});
|
|
326
|
+
$store("insert: defaults apply", User, async (assert) => {
|
|
327
|
+
const u = await User.insert({});
|
|
328
|
+
assert(u.name).equals("Donald");
|
|
329
|
+
});
|
|
330
|
+
$store("insert: reject null values", User, async (assert) => {
|
|
331
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
332
|
+
// lastname is optional, but null is forbidden on insert
|
|
333
|
+
return User.insert({ name: "Nullman", lastname: null });
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
$user("find: empty object equals no options", async (assert) => {
|
|
337
|
+
const a = await User.find();
|
|
338
|
+
const b = await User.find({});
|
|
339
|
+
assert(a.length).equals(b.length);
|
|
340
|
+
});
|
|
341
|
+
$user("find: $like: handles regex metacharacters", async (assert) => {
|
|
342
|
+
await User.insert({ name: "A[1]" });
|
|
343
|
+
const got = await User.find({
|
|
344
|
+
where: { name: { $like: "A[1]" } },
|
|
345
|
+
select: ["name"],
|
|
346
|
+
});
|
|
347
|
+
assert(got.length).equals(1);
|
|
348
|
+
assert(got[0].name).equals("A[1]");
|
|
349
|
+
});
|
|
350
|
+
$user("find: types", async (assert) => {
|
|
351
|
+
const where = { name: "Ryan" };
|
|
352
|
+
assert(await User.find({ where })).type();
|
|
353
|
+
assert(await UserN.find({ where })).type();
|
|
354
|
+
assert(await UserB.find({ where })).type();
|
|
355
|
+
});
|
|
356
|
+
$user("find: select narrows type", async (assert) => {
|
|
357
|
+
const select = ["id", "name"];
|
|
358
|
+
const users = await User.find({ select });
|
|
359
|
+
assert(users).type();
|
|
360
|
+
const users_n = await UserN.find({ select });
|
|
361
|
+
assert(users_n).type();
|
|
362
|
+
const users_b = await UserB.find({ select });
|
|
363
|
+
assert(users_b).type();
|
|
364
|
+
});
|
|
365
|
+
$user("find: basic query", async (assert) => {
|
|
366
|
+
const result = await User.find({ where: { name: "Ryan" } });
|
|
367
|
+
assert(result.length).equals(1);
|
|
368
|
+
});
|
|
369
|
+
$user("find: sorting by multiple fields", async (assert) => {
|
|
370
|
+
// sorting by multiple fields: age descending, then Lastname ascending
|
|
371
|
+
const sorted = await User.find({
|
|
372
|
+
select: ["age", "name"],
|
|
373
|
+
sort: { age: "desc", lastname: "asc" },
|
|
374
|
+
});
|
|
375
|
+
assert(sorted.length).equals(5);
|
|
376
|
+
["ben", "paul", "ryan", "donald", "jeremy"].forEach((user, i) => {
|
|
377
|
+
assert(sorted[i]).equals(pick(USERS[user], "name", "age"));
|
|
378
|
+
});
|
|
379
|
+
const descending = await User.find({
|
|
380
|
+
select: ["age", "name"],
|
|
381
|
+
sort: { age: "desc", lastname: "desc" },
|
|
382
|
+
});
|
|
383
|
+
const descended = ["ben", "ryan", "paul", "donald", "jeremy"];
|
|
384
|
+
descended.forEach((user, i) => {
|
|
385
|
+
assert(descending[i]).equals(pick(USERS[user], "name", "age"));
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
$user("find: sorting ascending and descending", async (assert) => {
|
|
389
|
+
const ascending = await User.find({
|
|
390
|
+
select: ["age", "name"],
|
|
391
|
+
sort: { age: "asc" },
|
|
392
|
+
limit: 2,
|
|
393
|
+
});
|
|
394
|
+
const ascended = ["jeremy", "donald"];
|
|
395
|
+
ascended.forEach((user, i) => {
|
|
396
|
+
assert(ascending[i]).equals(pick(USERS[user], "name", "age"));
|
|
397
|
+
});
|
|
398
|
+
const descending = await User.find({
|
|
399
|
+
select: ["age", "name"],
|
|
400
|
+
sort: { age: "desc" },
|
|
401
|
+
limit: 1,
|
|
402
|
+
});
|
|
403
|
+
assert(descending[0]).equals(pick(USERS.ben, "name", "age"));
|
|
404
|
+
});
|
|
405
|
+
$user("find: null criteria uses IS NULL semantics", async (assert) => {
|
|
406
|
+
// inserted fixtures include Jeremy without a lastname (NULL in DB)
|
|
407
|
+
// querying with { lastname: null } should find him
|
|
408
|
+
const rows = await User.find({
|
|
409
|
+
select: ["name", "lastname"],
|
|
410
|
+
where: { lastname: null },
|
|
411
|
+
sort: { name: "asc" },
|
|
412
|
+
});
|
|
413
|
+
assert(rows.length).equals(1);
|
|
414
|
+
if (rows.length > 0) {
|
|
415
|
+
assert(rows[0].name).equals("Just Jeremy");
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
$user("find: $like operator for strings", async (assert) => {
|
|
419
|
+
const prefix = await User.find({ where: { name: { $like: "J%" } } });
|
|
420
|
+
assert(prefix.length).equals(1);
|
|
421
|
+
if (prefix.length > 0)
|
|
422
|
+
assert(prefix[0].name).equals("Just Jeremy");
|
|
423
|
+
const suffix = await User.find({ where: { lastname: { $like: "%er" } } });
|
|
424
|
+
assert(suffix.length).equals(2);
|
|
425
|
+
const lastnames = suffix.map(u => u.lastname).sort();
|
|
426
|
+
assert(lastnames).equals(["Miller", "Miller"]);
|
|
427
|
+
const contains = await User.find({ where: { name: { $like: "%on%" } } });
|
|
428
|
+
assert(contains.length).equals(1);
|
|
429
|
+
if (contains.length > 0)
|
|
430
|
+
assert(contains[0].name).equals("Donald");
|
|
431
|
+
const exact = await User.find({ where: { name: { $like: "Ryan" } } });
|
|
432
|
+
assert(exact.length).equals(1);
|
|
433
|
+
if (exact.length > 0)
|
|
434
|
+
assert(exact[0].name).equals("Ryan");
|
|
435
|
+
const none = await User.find({ where: { name: { $like: "xyz%" } } });
|
|
436
|
+
assert(none.length).equals(0);
|
|
437
|
+
});
|
|
438
|
+
$user("find: $like with null/undefined fields", async (assert) => {
|
|
439
|
+
// Jeremy has no lastname (null), should not match ANY $like patterns
|
|
440
|
+
const results = await User.find({
|
|
441
|
+
where: { lastname: { $like: "%ll%" } },
|
|
442
|
+
});
|
|
443
|
+
assert(results.length).equals(2); // Ben, Paul
|
|
444
|
+
const names = results.map(u => u.name).sort();
|
|
445
|
+
assert(names).equals(["Ben", "Paul"]);
|
|
446
|
+
});
|
|
447
|
+
$user("find: $like: reject non-string types", async (assert) => {
|
|
448
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
449
|
+
// age is u8, should not accept $like
|
|
450
|
+
return User.find({ where: { age: { $like: "30%" } } });
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
$user("update: single record", async (assert) => {
|
|
454
|
+
const [donald] = (await User.find({ where: { name: "Donald" } }));
|
|
455
|
+
await User.update(donald.id, { set: { age: 35 } });
|
|
456
|
+
const [updated] = (await User.find({ where: { name: "Donald" } }));
|
|
457
|
+
assert(updated.age).equals(35);
|
|
458
|
+
assert(updated).equals({ ...USERS.donald, age: 35, id: donald.id });
|
|
459
|
+
});
|
|
460
|
+
$user("update: unset fields", async (assert) => {
|
|
461
|
+
const [donald] = (await User.find({ where: { name: "Donald" } }));
|
|
462
|
+
assert(donald.age).equals(30);
|
|
463
|
+
await User.update(donald.id, { set: { age: null } });
|
|
464
|
+
const [updated] = (await User.find({ where: { name: "Donald" } }));
|
|
465
|
+
assert(updated.age).undefined();
|
|
466
|
+
const [paul] = (await User.find({ where: { name: "Paul" } }));
|
|
467
|
+
await User.update(paul.id, { set: { age: null, lastname: null } });
|
|
468
|
+
const [updated_paul] = (await User.find({ where: { name: "Paul" } }));
|
|
469
|
+
assert(updated_paul).equals({ id: updated_paul.id, name: "Paul" });
|
|
470
|
+
});
|
|
471
|
+
$user("update: cannot unset required fields", async (assert) => {
|
|
472
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
473
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
474
|
+
// name is required (non-nullable) -> cannot be unset
|
|
475
|
+
return User.update(donald.id, { set: { name: null } });
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
$user("update: cannot unset required fields (multi-update)", async (assert) => {
|
|
479
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
480
|
+
return User.update({
|
|
481
|
+
where: { name: { $like: "D%" } },
|
|
482
|
+
set: { name: null },
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
$user("update: multiple records", async (assert) => {
|
|
487
|
+
const n_updated = await User.update({
|
|
488
|
+
where: { age: 40 },
|
|
489
|
+
set: { age: 45 },
|
|
490
|
+
});
|
|
491
|
+
assert(n_updated).equals(2);
|
|
492
|
+
const updated = await User.find({ where: { age: 45 } });
|
|
493
|
+
assert(updated.length).equals(2);
|
|
494
|
+
});
|
|
495
|
+
$user("update: where and changeset share a column", async (assert) => {
|
|
496
|
+
// Donald has age 30; update age using criteria on the same column
|
|
497
|
+
const n = await User.update({ where: { age: 30 }, set: { age: 31 } });
|
|
498
|
+
assert(n).equals(1);
|
|
499
|
+
assert(await User.count({ where: { age: 30 } })).equals(0);
|
|
500
|
+
assert(await User.count({ where: { age: 31 } })).equals(1);
|
|
501
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
502
|
+
assert(donald.age).equals(31);
|
|
503
|
+
});
|
|
504
|
+
$rel("count: no with", async (assert) => {
|
|
505
|
+
await throws(assert, Code.count_with_invalid, async () => {
|
|
506
|
+
await Author.count({ with: { articles: true } });
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
$user("update: update all", async (assert) => {
|
|
510
|
+
await User.update({ set: { age: 99 } });
|
|
511
|
+
const users = await User.find();
|
|
512
|
+
for (const user of users)
|
|
513
|
+
assert(user.age).equals(99);
|
|
514
|
+
});
|
|
515
|
+
$user("update: $like criteria", async (assert) => {
|
|
516
|
+
// update all users whose names start with "J"
|
|
517
|
+
const updated = await User.update({
|
|
518
|
+
where: { name: { $like: "J%" } },
|
|
519
|
+
set: { age: 25 },
|
|
520
|
+
});
|
|
521
|
+
assert(updated).equals(1);
|
|
522
|
+
const jeremy = await User.find({ where: { name: "Just Jeremy" } });
|
|
523
|
+
assert(jeremy[0].age).equals(25);
|
|
524
|
+
});
|
|
525
|
+
$user("delete: single record", async (assert) => {
|
|
526
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
527
|
+
await User.delete(donald.id);
|
|
528
|
+
const deleted = await User.find({ where: { name: "Donald" } });
|
|
529
|
+
assert(deleted.length).equals(0);
|
|
530
|
+
});
|
|
531
|
+
$user("delete: multiple records", async (assert) => {
|
|
532
|
+
const n = await User.delete({ where: { age: 40 } });
|
|
533
|
+
assert(n).equals(2);
|
|
534
|
+
const remaining = await User.find({ sort: { age: "asc" } });
|
|
535
|
+
assert(remaining.length).equals(3);
|
|
536
|
+
assert(remaining[0].name).equals("Just Jeremy");
|
|
537
|
+
});
|
|
538
|
+
$user("delete: $like criteria", async (assert) => {
|
|
539
|
+
// delete all users with "Miller" lastname
|
|
540
|
+
const deleted = await User.delete({
|
|
541
|
+
where: { lastname: { $like: "%Miller%" } },
|
|
542
|
+
});
|
|
543
|
+
assert(deleted).equals(2);
|
|
544
|
+
const remaining = await User.find();
|
|
545
|
+
assert(remaining.length).equals(3);
|
|
546
|
+
const remainingNames = remaining.map(u => u.name).sort();
|
|
547
|
+
assert(remainingNames).equals(["Donald", "Just Jeremy", "Ryan"]);
|
|
548
|
+
});
|
|
549
|
+
$user("count", async (assert) => {
|
|
550
|
+
assert(await User.count()).equals(5);
|
|
551
|
+
assert(await User.count({ where: { name: "Ryan" } })).equals(1);
|
|
552
|
+
assert(await User.count({ where: { age: 40 } })).equals(2);
|
|
553
|
+
assert(await User.count({ where: { age: 30 } })).equals(1);
|
|
554
|
+
assert(await User.count({ where: { age: 35 } })).equals(0);
|
|
555
|
+
});
|
|
556
|
+
$user("count: $like operator", async (assert) => {
|
|
557
|
+
assert(await User.count({ where: { name: { $like: "J%" } } })).equals(1);
|
|
558
|
+
assert(await User.count({ where: { lastname: { $like: "%er" } } }))
|
|
559
|
+
.equals(2);
|
|
560
|
+
assert(await User.count({ where: { name: { $like: "%xyz%" } } }))
|
|
561
|
+
.equals(0);
|
|
562
|
+
});
|
|
563
|
+
$user("has", async (assert) => {
|
|
564
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
565
|
+
assert(await User.has(donald.id)).true();
|
|
566
|
+
await User.delete(donald.id);
|
|
567
|
+
assert(await User.has(donald.id)).false();
|
|
568
|
+
});
|
|
569
|
+
$type("boolean", async (assert) => {
|
|
570
|
+
const t = await Type.insert({ boolean: true });
|
|
571
|
+
assert(t.boolean).equals(true);
|
|
572
|
+
assert((await Type.get(t.id)).boolean).equals(true);
|
|
573
|
+
await Type.update(t.id, { set: { boolean: false } });
|
|
574
|
+
assert((await Type.get(t.id)).boolean).equals(false);
|
|
575
|
+
});
|
|
576
|
+
$type("string", async (assert) => {
|
|
577
|
+
const t = await Type.insert({ string: "foo" });
|
|
578
|
+
assert(t.string).equals("foo");
|
|
579
|
+
assert((await Type.get(t.id)).string).equals("foo");
|
|
580
|
+
await Type.update(t.id, { set: { string: "bar" } });
|
|
581
|
+
assert((await Type.get(t.id)).string).equals("bar");
|
|
582
|
+
});
|
|
583
|
+
$type("date", async (assert) => {
|
|
584
|
+
const now = new Date();
|
|
585
|
+
const t = await Type.insert({ date: now });
|
|
586
|
+
assert(t.date?.getTime()).equals(now.getTime());
|
|
587
|
+
assert((await Type.get(t.id)).date?.getTime()).equals(now.getTime());
|
|
588
|
+
const next = new Date();
|
|
589
|
+
await Type.update(t.id, { set: { date: next } });
|
|
590
|
+
assert((await Type.get(t.id)).date).equals(next);
|
|
591
|
+
});
|
|
592
|
+
$type("f32", async (assert) => {
|
|
593
|
+
const t = await Type.insert({ f32: 1.5 });
|
|
594
|
+
assert(t.f32).equals(1.5);
|
|
595
|
+
assert((await Type.get(t.id)).f32).equals(1.5);
|
|
596
|
+
await Type.update(t.id, { set: { f32: 123456.75 } });
|
|
597
|
+
assert((await Type.get(t.id)).f32).equals(123456.75);
|
|
598
|
+
});
|
|
599
|
+
$type("f64", async (assert) => {
|
|
600
|
+
const f1 = 123456.78901;
|
|
601
|
+
const t = await Type.insert({ f64: f1 });
|
|
602
|
+
assert(t.f64).equals(f1);
|
|
603
|
+
assert((await Type.get(t.id)).f64).equals(f1);
|
|
604
|
+
await Type.update(t.id, { set: { f32: 1.5 } });
|
|
605
|
+
assert((await Type.get(t.id)).f32).equals(1.5);
|
|
606
|
+
});
|
|
607
|
+
[8, 16, 32].forEach(n => {
|
|
608
|
+
$type(`i${n}`, async (assert) => {
|
|
609
|
+
const k = `i${n}`;
|
|
610
|
+
// lower bound
|
|
611
|
+
const lb = -(2 ** (n - 1));
|
|
612
|
+
const t = await Type.insert({ [k]: lb });
|
|
613
|
+
assert(t[any(k)]).equals(lb);
|
|
614
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
615
|
+
// upper bound
|
|
616
|
+
const ub = 2 ** (n - 1) - 1;
|
|
617
|
+
await Type.update(t.id, { set: { [k]: ub } });
|
|
618
|
+
assert((await Type.get(t.id))[any(k)]).equals(ub);
|
|
619
|
+
});
|
|
620
|
+
$type(`u${n}`, async (assert) => {
|
|
621
|
+
const k = `u${n}`;
|
|
622
|
+
// lower bound
|
|
623
|
+
const lb = 0;
|
|
624
|
+
const t = await Type.insert({ [k]: lb });
|
|
625
|
+
assert(t[any(k)]).equals(lb);
|
|
626
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
627
|
+
// upper bound
|
|
628
|
+
const ub = 2 ** n - 1;
|
|
629
|
+
await Type.update(t.id, { set: { [k]: ub } });
|
|
630
|
+
assert((await Type.get(t.id))[any(k)]).equals(ub);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
[64n, 128n].forEach(i => {
|
|
634
|
+
$type(`i${i}`, async (assert) => {
|
|
635
|
+
const k = `i${i}`;
|
|
636
|
+
// lower bound
|
|
637
|
+
const lb = -(2n ** (i - 1n));
|
|
638
|
+
const t = await Type.insert({ [k]: lb });
|
|
639
|
+
assert(t[any(k)]).equals(lb);
|
|
640
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
641
|
+
// upper bound
|
|
642
|
+
const ub = 2n ** (i - 1n) - 1n;
|
|
643
|
+
const tu = await Type.insert({ [k]: ub });
|
|
644
|
+
assert(tu[any(k)]).equals(ub);
|
|
645
|
+
assert((await Type.get(tu.id))[any(k)]).equals(ub);
|
|
646
|
+
});
|
|
647
|
+
$type(`u${i}`, async (assert) => {
|
|
648
|
+
const k = `u${i}`;
|
|
649
|
+
// lower bound
|
|
650
|
+
const lb = 0n;
|
|
651
|
+
const t = await Type.insert({ [k]: lb });
|
|
652
|
+
assert(t[any(k)]).equals(lb);
|
|
653
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
654
|
+
// upper bound
|
|
655
|
+
const ub = 2n ** i - 1n;
|
|
656
|
+
const tu = await Type.insert({ [k]: ub });
|
|
657
|
+
assert(tu[any(k)]).equals(ub);
|
|
658
|
+
assert((await Type.get(tu.id))[any(k)]).equals(ub);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
$user$("find: reject non-array select", async (assert) => {
|
|
662
|
+
await throws(assert, Code.select_invalid, () => {
|
|
663
|
+
return User.find({ select: {} });
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
$user$("find: projection limits fields", async (assert) => {
|
|
667
|
+
const records = await User.find({ select: ["id", "name"] });
|
|
668
|
+
assert(records.length).equals(5);
|
|
669
|
+
for (const r of records) {
|
|
670
|
+
// only id + name must be present
|
|
671
|
+
assert(Object.keys(r).toSorted()).equals(["id", "name"].toSorted());
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
$user$("update: reject unknown field on set", async (assert) => {
|
|
675
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
676
|
+
await throws(assert, Code.field_unknown, () => {
|
|
677
|
+
return User.update(donald.id, { set: { nope: 1 } });
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
$user$("update: reject empty set object", async (assert) => {
|
|
681
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
682
|
+
await throws(assert, Code.set_empty, () => {
|
|
683
|
+
return User.update(donald.id, { set: {} });
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
$user$("update: reject updating primary key", async (assert) => {
|
|
687
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
688
|
+
await throws(assert, Code.pk_immutable, () => {
|
|
689
|
+
return User.update(donald.id, { set: { id: "nope" } });
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
$user$("update: reject missing set", async (assert) => {
|
|
693
|
+
await throws(assert, Code.set_empty, () => {
|
|
694
|
+
return User.update({ where: { name: "Donald" } });
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
$user$("delete: reject missing where", async (assert) => {
|
|
698
|
+
await throws(assert, Code.where_required, () => {
|
|
699
|
+
return User.delete({});
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
$user$("inject invalid identifier (where)", async (assert) => {
|
|
703
|
+
// attempted injection via bogus key
|
|
704
|
+
await throws(assert, Code.identifier_invalid, () => {
|
|
705
|
+
return User.find({ where: { "name; DROP TABLE user;": "x" } });
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
$user$("inject invalid identifier (select)", async (assert) => {
|
|
709
|
+
await throws(assert, Code.identifier_invalid, () => {
|
|
710
|
+
return User.find({ select: ["name; DROP TABLE user;"] });
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
$user$("value safety: does not interpolate values", async (assert) => {
|
|
714
|
+
const evil = "x' ; DROP TABLE user; --";
|
|
715
|
+
// should just behave like a normal string compare (0 matches), not explode
|
|
716
|
+
const rows = await User.find({ where: { name: evil } });
|
|
717
|
+
assert(rows.length).equals(0);
|
|
718
|
+
// table should still exist / be queryable
|
|
719
|
+
assert(await User.count()).equals(5);
|
|
720
|
+
});
|
|
721
|
+
$user$("update respects unset / binding map", async (assert) => {
|
|
722
|
+
const [paul] = await User.find({ where: { name: "Paul" } });
|
|
723
|
+
// allowed, lastname is optional
|
|
724
|
+
await User.update(paul.id, { set: { lastname: null } });
|
|
725
|
+
const [after] = await User.find({
|
|
726
|
+
select: ["id", "name", "lastname"],
|
|
727
|
+
where: { id: paul.id },
|
|
728
|
+
});
|
|
729
|
+
assert(after.lastname).undefined();
|
|
730
|
+
});
|
|
731
|
+
$user$("count: reject unknown where column", async (assert) => {
|
|
732
|
+
await throws(assert, Code.field_unknown, () => {
|
|
733
|
+
return User.count({ where: { nope: 1 } });
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
$user$("$like: reject unknown field", async (assert) => {
|
|
737
|
+
await throws(assert, Code.field_unknown, () => {
|
|
738
|
+
return User.find({ where: { unknown: { $like: "test%" } } });
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
$user$("insert: reject unknown column", async (assert) => {
|
|
742
|
+
await throws(assert, Code.field_unknown, () => {
|
|
743
|
+
return User.insert({ name: "X", nope: 1 });
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
$user$("update: reject unknown where column", async (assert) => {
|
|
747
|
+
await throws(assert, Code.field_unknown, () => User.update({
|
|
748
|
+
where: { nope: 1 },
|
|
749
|
+
set: { age: 1 },
|
|
750
|
+
}));
|
|
751
|
+
});
|
|
752
|
+
$user$("delete: reject unknown where column", async (assert) => {
|
|
753
|
+
await throws(assert, Code.field_unknown, () => User.delete({ where: { nope: 1 } }));
|
|
754
|
+
});
|
|
755
|
+
$user$("number operators: reject on non-number fields", async (assert) => {
|
|
756
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
757
|
+
// name is string, should not accept $gte
|
|
758
|
+
return User.find({ where: { name: { $gte: 10 } } });
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
$user$("mixed operators: reject invalid combinations", async (assert) => {
|
|
762
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
763
|
+
// can't mix string and number operators
|
|
764
|
+
return User.find({ where: { name: { $like: "test%", $gte: 5 } } });
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
$store("insert: reject null on required fields", User, async (assert) => {
|
|
768
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
769
|
+
// name is required, but passing null explicitly is still forbidden
|
|
770
|
+
return User.insert({ name: null });
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
$store("insert: omission stores NULL", User, async (assert) => {
|
|
774
|
+
const u = await User.insert({ name: "NoLast" });
|
|
775
|
+
const [got] = await User.find({
|
|
776
|
+
where: { id: u.id },
|
|
777
|
+
select: ["id", "name", "lastname"],
|
|
778
|
+
});
|
|
779
|
+
assert(got.id).equals(u.id);
|
|
780
|
+
assert(got.name).equals("NoLast");
|
|
781
|
+
// returned shape must not contain null
|
|
782
|
+
assert(got.lastname).undefined();
|
|
783
|
+
});
|
|
784
|
+
$store("reserved table / column names", Reserved, async (assert) => {
|
|
785
|
+
const a = await Reserved.insert({ name: "alpha", order: 1 });
|
|
786
|
+
const b = await Reserved.insert({ name: "beta", order: 2 });
|
|
787
|
+
const got = await Reserved.find({ where: { name: "alpha" } });
|
|
788
|
+
assert(got.length).equals(1);
|
|
789
|
+
assert(got[0]).equals({ id: a.id, name: "alpha", order: 1 });
|
|
790
|
+
// update using the reserved column
|
|
791
|
+
const n = await Reserved.update({
|
|
792
|
+
where: { name: "beta" },
|
|
793
|
+
set: { order: 9 },
|
|
794
|
+
});
|
|
795
|
+
assert(n).equals(1);
|
|
796
|
+
const [after] = await Reserved.find({ where: { id: b.id } });
|
|
797
|
+
assert(after.order).equals(9);
|
|
798
|
+
// and delete to complete the cycle
|
|
799
|
+
await Reserved.delete(a.id);
|
|
800
|
+
assert(await Reserved.has(a.id)).false();
|
|
801
|
+
});
|
|
802
|
+
$rel("get with one (reverse)", async (assert) => {
|
|
803
|
+
const articles = await Article.find();
|
|
804
|
+
const article = await Article.get(articles[0].id, {
|
|
805
|
+
with: {
|
|
806
|
+
author: true,
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
assert(article).type();
|
|
810
|
+
assert(article.author).not.null();
|
|
811
|
+
assert(article.author?.name).defined();
|
|
812
|
+
});
|
|
813
|
+
$rel("get with many", async (assert) => {
|
|
814
|
+
const [first] = await Author.find({ where: { name: "John" } });
|
|
815
|
+
const john = await Author.get(first.id, { with: { articles: true } });
|
|
816
|
+
assert(john.articles).type();
|
|
817
|
+
const titles = john.articles.map(a => a.title);
|
|
818
|
+
assert(titles.includes("John First Post")).true();
|
|
819
|
+
assert(titles.includes("John Second Post")).true();
|
|
820
|
+
});
|
|
821
|
+
$rel("get by id", async (assert) => {
|
|
822
|
+
const [first] = await Author.find({ where: { name: "John" } });
|
|
823
|
+
const john = await Author.get(first.id, { with: { profile: true } });
|
|
824
|
+
assert(john.profile).not.null();
|
|
825
|
+
assert(john.profile?.bio).equals("John is a writer");
|
|
826
|
+
});
|
|
827
|
+
$rel("get by id returns null when missing", async (assert) => {
|
|
828
|
+
const [first] = await Author.find({ where: { name: "Bob" } });
|
|
829
|
+
const bob = await Author.get(first.id, { with: { profile: true } });
|
|
830
|
+
assert(bob.profile).null();
|
|
831
|
+
});
|
|
832
|
+
$rel("find", async (assert) => {
|
|
833
|
+
const articles = await Article.find({
|
|
834
|
+
where: { title: { $like: "% Post" } },
|
|
835
|
+
with: { author: true },
|
|
836
|
+
sort: { title: "asc" },
|
|
837
|
+
});
|
|
838
|
+
assert(articles.length).equals(3);
|
|
839
|
+
for (const article of articles) {
|
|
840
|
+
assert(article.author).not.null();
|
|
841
|
+
assert(article.author?.name).defined();
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
$rel("try", async (assert) => {
|
|
845
|
+
const [first] = await Article.find();
|
|
846
|
+
const article = await Article.try(first.id, { with: { author: true } });
|
|
847
|
+
assert(article).defined();
|
|
848
|
+
assert(article?.author).not.null();
|
|
849
|
+
});
|
|
850
|
+
$rel("many returns empty array when no matches", async (assert) => {
|
|
851
|
+
// insert author with no articles
|
|
852
|
+
const lonely = await Author.insert({ name: "Lonely" });
|
|
853
|
+
const author = await Author.get(lonely.id, { with: { articles: true } });
|
|
854
|
+
assert(author.articles).type();
|
|
855
|
+
assert(author.articles.length).equals(0);
|
|
856
|
+
});
|
|
857
|
+
$rel("multiple relations in one query", async (assert) => {
|
|
858
|
+
const authors = await Author.find({ where: { name: "John" } });
|
|
859
|
+
const john = await Author.get(authors[0].id, {
|
|
860
|
+
with: {
|
|
861
|
+
articles: true,
|
|
862
|
+
profile: true,
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
const titles = john.articles.map(a => a.title);
|
|
866
|
+
assert(titles.includes("John First Post")).true();
|
|
867
|
+
assert(titles.includes("John Second Post")).true();
|
|
868
|
+
assert(john.profile).not.null();
|
|
869
|
+
assert(john.profile?.bio).equals("John is a writer");
|
|
870
|
+
});
|
|
871
|
+
$rel("no fields without 'with'", async (assert) => {
|
|
872
|
+
const articles = await Article.find();
|
|
873
|
+
const article = await Article.get(articles[0].id);
|
|
874
|
+
assert("author" in article).false();
|
|
875
|
+
});
|
|
876
|
+
$rel("find: complex relation subqueries", async (assert) => {
|
|
877
|
+
const SUBLIMIT = 5;
|
|
878
|
+
const authors = await Author.find({
|
|
879
|
+
select: ["id", "name"],
|
|
880
|
+
sort: { name: "asc" },
|
|
881
|
+
limit: 20,
|
|
882
|
+
with: {
|
|
883
|
+
articles: {
|
|
884
|
+
where: { title: { $like: "%foo%" } },
|
|
885
|
+
select: ["id", "title"],
|
|
886
|
+
sort: { title: "asc" },
|
|
887
|
+
limit: SUBLIMIT,
|
|
888
|
+
},
|
|
889
|
+
profile: true,
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
assert(authors).type();
|
|
893
|
+
// parents must *not* be filtered by relation where
|
|
894
|
+
const base = await Author.find({
|
|
895
|
+
select: ["id", "name"],
|
|
896
|
+
sort: { name: "asc" },
|
|
897
|
+
limit: 20,
|
|
898
|
+
});
|
|
899
|
+
assert(authors.map(r => r.name)).equals(base.map(r => r.name));
|
|
900
|
+
// base projection is respected (only id/name + relation keys)
|
|
901
|
+
for (const author of authors) {
|
|
902
|
+
assert(Object.keys(author).toSorted()).equals(["articles", "id", "name", "profile"].toSorted());
|
|
903
|
+
}
|
|
904
|
+
// per-parent relation correctness (filter + sort + limit)
|
|
905
|
+
for (const author of authors) {
|
|
906
|
+
// many => always array
|
|
907
|
+
assert(Array.isArray(author.articles)).true();
|
|
908
|
+
// projection + filter enforcement on returned relation rows
|
|
909
|
+
for (const article of author.articles) {
|
|
910
|
+
assert(Object.keys(article).toSorted()).equals(["id", "title"]
|
|
911
|
+
.toSorted());
|
|
912
|
+
assert(article.title.includes("foo")).true();
|
|
913
|
+
}
|
|
914
|
+
// relation sort asc by title
|
|
915
|
+
const titles = author.articles.map(a => a.title);
|
|
916
|
+
assert([...titles].toSorted()).equals(titles);
|
|
917
|
+
// relation limit is per-parent
|
|
918
|
+
assert(author.articles.length <= SUBLIMIT).true();
|
|
919
|
+
// compare against base query (same semantics, explicit per-parent)
|
|
920
|
+
const expected = await Article.find({
|
|
921
|
+
where: { author_id: author.id, title: { $like: "%foo%" } },
|
|
922
|
+
select: ["id", "title"],
|
|
923
|
+
sort: { title: "asc" },
|
|
924
|
+
});
|
|
925
|
+
const expected_titles = expected.slice(0, SUBLIMIT).map(a => a.title);
|
|
926
|
+
assert(titles).equals(expected_titles);
|
|
927
|
+
// profile: one => null or full record, FK must point back
|
|
928
|
+
if (author.profile !== null) {
|
|
929
|
+
assert(Object.keys(author.profile).toSorted()).equals(["author_id", "bio", "id", "url"].toSorted());
|
|
930
|
+
assert(author.profile.author_id).equals(author.id);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// ensure the inner limit was actually used (needs > SUBLIMIT matches)
|
|
934
|
+
let used = false;
|
|
935
|
+
for (const author of authors) {
|
|
936
|
+
const n_foo = await Article.count({
|
|
937
|
+
where: { author_id: author.id, title: { $like: "%foo%" } },
|
|
938
|
+
});
|
|
939
|
+
if (n_foo > SUBLIMIT)
|
|
940
|
+
used = true;
|
|
941
|
+
}
|
|
942
|
+
assert(used).true();
|
|
943
|
+
const john = authors.find(r => r.name === "John");
|
|
944
|
+
if (john !== undefined) {
|
|
945
|
+
assert(john.profile).not.null();
|
|
946
|
+
assert(john.profile?.bio).equals("John is a writer");
|
|
947
|
+
}
|
|
948
|
+
// parent with *zero* matching related rows must still be present,
|
|
949
|
+
// and the many-relation must be [] (not missing / not null)
|
|
950
|
+
const ned = authors.find(a => a.name === "Ned");
|
|
951
|
+
assert(ned).defined();
|
|
952
|
+
assert(ned.articles).type();
|
|
953
|
+
assert(ned.articles.length).equals(0);
|
|
954
|
+
// and one-relation should be null when missing
|
|
955
|
+
assert(ned.profile).null();
|
|
956
|
+
});
|
|
957
|
+
$rel("type: select narrows nested types", async (assert) => {
|
|
958
|
+
const rows = await Author.find({
|
|
959
|
+
select: ["id"],
|
|
960
|
+
with: {
|
|
961
|
+
profile: { select: ["bio"] },
|
|
962
|
+
articles: { select: ["title"] },
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
assert(rows).type();
|
|
966
|
+
// runtime: selected base only has id (plus relation keys)
|
|
967
|
+
for (const r of rows)
|
|
968
|
+
assert("name" in r).false();
|
|
969
|
+
});
|
|
970
|
+
$rel$("with: reject unknown relation name", async (assert) => {
|
|
971
|
+
await throws(assert, Code.relation_unknown, () => {
|
|
972
|
+
return Author.find({ with: { nope: true } });
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
$user("get/try: missing id", async (assert) => {
|
|
976
|
+
const missing = `missing-${Date.now()}-${Math.random()}`;
|
|
977
|
+
await throws(assert, Code.record_not_found, () => User.get(missing));
|
|
978
|
+
assert(await User.try(missing)).undefined();
|
|
979
|
+
});
|
|
980
|
+
$rel("get/try: missing id (with relations)", async (assert) => {
|
|
981
|
+
const missing = `missing-${Date.now()}-${Math.random()}`;
|
|
982
|
+
await throws(assert, Code.record_not_found, () => Article.get(missing, { with: { author: true } }));
|
|
983
|
+
assert(await Article.try(missing, { with: { author: true } })).undefined();
|
|
984
|
+
});
|
|
985
|
+
$rel("get/try: missing id (+ parent)", async (assert) => {
|
|
986
|
+
const missing = `missing-${Date.now()}-${Math.random()}`;
|
|
987
|
+
await throws(assert, Code.record_not_found, () => Author.get(missing, { with: { articles: true, profile: true } }));
|
|
988
|
+
assert(await Author.try(missing, {
|
|
989
|
+
with: { articles: true, profile: true },
|
|
990
|
+
})).undefined();
|
|
991
|
+
});
|
|
992
|
+
$user("try: does not swallow invalid options", async (assert) => {
|
|
993
|
+
const [u] = await User.find({ select: ["id"] });
|
|
994
|
+
await throws(assert, Code.select_empty, () => {
|
|
995
|
+
return User.try(u.id, { select: [] });
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
$type("where: treat date as literal value (not operator)", async (assert) => {
|
|
999
|
+
const d = new Date();
|
|
1000
|
+
const inserted = await Type.insert({ date: d });
|
|
1001
|
+
// to be treated as equality, not operator parsing.
|
|
1002
|
+
const got = await Type.find({ where: { date: new Date(d.getTime()) } });
|
|
1003
|
+
assert(got.length).equals(1);
|
|
1004
|
+
assert(got[0].id).equals(inserted.id);
|
|
1005
|
+
});
|
|
1006
|
+
$type("where: number operators ($gt/$gte/$lt/$lte/$ne)", async (assert) => {
|
|
1007
|
+
// use a marker to avoid cross-test interference if the DB isn't reset
|
|
1008
|
+
await Type.insert({ string: "ops-num", u8: 201 });
|
|
1009
|
+
await Type.insert({ string: "ops-num", u8: 202 });
|
|
1010
|
+
await Type.insert({ string: "ops-num", u8: 203 });
|
|
1011
|
+
const gt = await Type.find({
|
|
1012
|
+
where: { string: "ops-num", u8: { $gt: 201 } },
|
|
1013
|
+
sort: { u8: "asc" },
|
|
1014
|
+
});
|
|
1015
|
+
assert(gt.map(r => r.u8)).equals([202, 203]);
|
|
1016
|
+
const between = await Type.find({
|
|
1017
|
+
where: { string: "ops-num", u8: { $gte: 202, $lt: 203 } },
|
|
1018
|
+
});
|
|
1019
|
+
assert(between.length).equals(1);
|
|
1020
|
+
assert(between[0].u8).equals(202);
|
|
1021
|
+
const ne = await Type.find({
|
|
1022
|
+
where: { string: "ops-num", u8: { $ne: 202 } },
|
|
1023
|
+
sort: { u8: "asc" },
|
|
1024
|
+
});
|
|
1025
|
+
assert(ne.map(r => r.u8)).equals([201, 203]);
|
|
1026
|
+
const lte = await Type.find({
|
|
1027
|
+
where: { string: "ops-num", u8: { $lte: 202 } },
|
|
1028
|
+
sort: { u8: "asc" },
|
|
1029
|
+
});
|
|
1030
|
+
assert(lte.map(r => r.u8)).equals([201, 202]);
|
|
1031
|
+
});
|
|
1032
|
+
$type("where: bigint operators ($gt/$gte/$lt/$lte/$ne)", async (assert) => {
|
|
1033
|
+
await Type.insert({ string: "ops-big", u64: 201n });
|
|
1034
|
+
await Type.insert({ string: "ops-big", u64: 202n });
|
|
1035
|
+
await Type.insert({ string: "ops-big", u64: 203n });
|
|
1036
|
+
const gt = await Type.find({
|
|
1037
|
+
where: { string: "ops-big", u64: { $gt: 201n } },
|
|
1038
|
+
sort: { u64: "asc" },
|
|
1039
|
+
});
|
|
1040
|
+
assert(gt.map(r => r.u64)).equals([202n, 203n]);
|
|
1041
|
+
const between = await Type.find({
|
|
1042
|
+
where: { string: "ops-big", u64: { $gte: 202n, $lt: 203n } },
|
|
1043
|
+
});
|
|
1044
|
+
assert(between.length).equals(1);
|
|
1045
|
+
assert(between[0].u64).equals(202n);
|
|
1046
|
+
const ne = await Type.find({
|
|
1047
|
+
where: { string: "ops-big", u64: { $ne: 202n } },
|
|
1048
|
+
sort: { u64: "asc" },
|
|
1049
|
+
});
|
|
1050
|
+
assert(ne.map(r => r.u64)).equals([201n, 203n]);
|
|
1051
|
+
});
|
|
1052
|
+
$type("where: datetime operators ($before/$after/$ne)", async (assert) => {
|
|
1053
|
+
const d1 = new Date("2020-01-01T00:00:00.000Z");
|
|
1054
|
+
const d2 = new Date("2020-01-02T00:00:00.000Z");
|
|
1055
|
+
const d3 = new Date("2020-01-03T00:00:00.000Z");
|
|
1056
|
+
await Type.insert({ string: "ops-date", date: d1 });
|
|
1057
|
+
await Type.insert({ string: "ops-date", date: d2 });
|
|
1058
|
+
await Type.insert({ string: "ops-date", date: d3 });
|
|
1059
|
+
const before = await Type.find({
|
|
1060
|
+
where: { string: "ops-date", date: { $before: d2 } },
|
|
1061
|
+
sort: { date: "asc" },
|
|
1062
|
+
});
|
|
1063
|
+
assert(before.map(r => r.date.getTime())).equals([d1.getTime()]);
|
|
1064
|
+
const after = await Type.find({
|
|
1065
|
+
where: { string: "ops-date", date: { $after: d2 } },
|
|
1066
|
+
sort: { date: "asc" },
|
|
1067
|
+
});
|
|
1068
|
+
assert(after.map(r => r.date.getTime())).equals([d3.getTime()]);
|
|
1069
|
+
const ne = await Type.find({
|
|
1070
|
+
where: { string: "ops-date", date: { $ne: d2 } },
|
|
1071
|
+
sort: { date: "asc" },
|
|
1072
|
+
});
|
|
1073
|
+
assert(ne.map(r => r.date.getTime()))
|
|
1074
|
+
.equals([d1.getTime(), d3.getTime()]);
|
|
1075
|
+
});
|
|
1076
|
+
$type("where: operator validation errors", async (assert) => {
|
|
1077
|
+
await throws(assert, Code.operator_empty, () => {
|
|
1078
|
+
return Type.find({ where: { u8: {} } });
|
|
1079
|
+
});
|
|
1080
|
+
// string/time: only $like
|
|
1081
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
1082
|
+
return Type.find({ where: { string: { $gt: 1 } } });
|
|
1083
|
+
});
|
|
1084
|
+
// number: no $like
|
|
1085
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
1086
|
+
return Type.find({ where: { u8: { $like: "x" } } });
|
|
1087
|
+
});
|
|
1088
|
+
// datetime: no $gt
|
|
1089
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
1090
|
+
return Type.find({ where: { date: { $gt: new Date() } } });
|
|
1091
|
+
});
|
|
1092
|
+
await throws(assert, Code.wrong_type, () => {
|
|
1093
|
+
return Type.find({ where: { u8: { $gt: "nope" } } });
|
|
1094
|
+
});
|
|
1095
|
+
await throws(assert, Code.wrong_type, () => {
|
|
1096
|
+
return Type.find({ where: { date: { $before: "nope" } } });
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
$type("where: combined operators on same field", async (assert) => {
|
|
1100
|
+
await Type.insert({ string: "combo", u8: 100 });
|
|
1101
|
+
await Type.insert({ string: "combo", u8: 150 });
|
|
1102
|
+
await Type.insert({ string: "combo", u8: 200 });
|
|
1103
|
+
const results = await Type.find({
|
|
1104
|
+
where: {
|
|
1105
|
+
string: "combo",
|
|
1106
|
+
u8: { $gte: 150, $ne: 200 }
|
|
1107
|
+
},
|
|
1108
|
+
sort: { u8: "asc" },
|
|
1109
|
+
});
|
|
1110
|
+
assert(results.map(r => r.u8)).equals([150]);
|
|
1111
|
+
});
|
|
1112
|
+
$store("where: null matches omitted optional field", User, async (assert) => {
|
|
1113
|
+
const u = await User.insert({ name: "NoLast" });
|
|
1114
|
+
const rows = await User.find({
|
|
1115
|
+
where: { id: u.id, lastname: null },
|
|
1116
|
+
select: ["id"],
|
|
1117
|
+
});
|
|
1118
|
+
assert(rows.length).equals(1);
|
|
1119
|
+
assert(rows[0].id).equals(u.id);
|
|
1120
|
+
});
|
|
1121
|
+
$user("where: null matches unset via update", async (assert) => {
|
|
1122
|
+
const [paul] = await User.find({ where: { name: "Paul" } });
|
|
1123
|
+
await User.update(paul.id, { set: { lastname: null } });
|
|
1124
|
+
const rows = await User.find({
|
|
1125
|
+
where: { id: paul.id, lastname: null },
|
|
1126
|
+
select: ["id"],
|
|
1127
|
+
});
|
|
1128
|
+
assert(rows.length).equals(1);
|
|
1129
|
+
assert(rows[0].id).equals(paul.id);
|
|
1130
|
+
});
|
|
1131
|
+
$user("find: $like: underscore is single-character *", async (assert) => {
|
|
1132
|
+
await User.insert({ name: "A1" });
|
|
1133
|
+
await User.insert({ name: "A12" });
|
|
1134
|
+
const one = await User.find({
|
|
1135
|
+
where: { name: { $like: "A_" } },
|
|
1136
|
+
select: ["name"],
|
|
1137
|
+
sort: { name: "asc" },
|
|
1138
|
+
});
|
|
1139
|
+
assert(one.map(r => r.name)).equals(["A1"]);
|
|
1140
|
+
const two = await User.find({
|
|
1141
|
+
where: { name: { $like: "A__" } },
|
|
1142
|
+
select: ["name"],
|
|
1143
|
+
sort: { name: "asc" },
|
|
1144
|
+
});
|
|
1145
|
+
assert(two.map(r => r.name)).equals(["A12"]);
|
|
1146
|
+
});
|
|
1147
|
+
$user("find: $like: question mark is literal", async (assert) => {
|
|
1148
|
+
await User.insert({ name: "A?" });
|
|
1149
|
+
await User.insert({ name: "A1" });
|
|
1150
|
+
const got = await User.find({
|
|
1151
|
+
where: { name: { $like: "A?" } },
|
|
1152
|
+
select: ["name"],
|
|
1153
|
+
sort: { name: "asc" },
|
|
1154
|
+
});
|
|
1155
|
+
// '?' means only one character
|
|
1156
|
+
assert(got.map(r => r.name)).equals(["A?"]);
|
|
1157
|
+
});
|
|
1158
|
+
$rel("try: does not swallow invalid relation name", async (assert) => {
|
|
1159
|
+
const [row] = await Article.find({ select: ["id"] });
|
|
1160
|
+
await throws(assert, Code.relation_unknown, () => {
|
|
1161
|
+
// programmer error: should throw, not return undefined
|
|
1162
|
+
return Article.try(row.id, { with: { nope: true } });
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
$rel$("with: reject non-array relation select", async (assert) => {
|
|
1166
|
+
await throws(assert, Code.select_invalid, () => {
|
|
1167
|
+
return Author.find({
|
|
1168
|
+
with: {
|
|
1169
|
+
articles: {
|
|
1170
|
+
select: {}, // must be array
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
$rel$("with: reject non-uint relation limit", async (assert) => {
|
|
1177
|
+
await throws(assert, Code.limit_invalid, () => {
|
|
1178
|
+
return Author.find({
|
|
1179
|
+
with: {
|
|
1180
|
+
articles: {
|
|
1181
|
+
limit: -1, // must be uint
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
$rel("one relation returns null when FK missing", async (assert) => {
|
|
1188
|
+
const orphan = await Article.insert({
|
|
1189
|
+
title: "Orphan",
|
|
1190
|
+
author_id: "missing-author",
|
|
1191
|
+
});
|
|
1192
|
+
const got = await Article.get(orphan.id, { with: { author: true } });
|
|
1193
|
+
assert(got.author).null();
|
|
1194
|
+
});
|
|
1195
|
+
$user("get/try: happy path", async (assert) => {
|
|
1196
|
+
const [u] = await User.find({ where: { name: "Donald" } });
|
|
1197
|
+
const got = await User.get(u.id);
|
|
1198
|
+
assert(got.id).equals(u.id);
|
|
1199
|
+
const tried = await User.try(u.id);
|
|
1200
|
+
assert(tried?.id).equals(u.id);
|
|
1201
|
+
});
|
|
1202
|
+
$user$("find: unknown option keys throw", async (assert) => {
|
|
1203
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1204
|
+
// wrong call-shape (looks like where but isn't nested)
|
|
1205
|
+
return User.find({ name: "John" });
|
|
1206
|
+
});
|
|
1207
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1208
|
+
// totally unknown option
|
|
1209
|
+
return User.find({ banana: true });
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
$user("find: $like is case-sensitive", async (assert) => {
|
|
1213
|
+
await User.insert({ name: "MiXeDCase" });
|
|
1214
|
+
const no = await User.find({ where: { name: { $like: "mixed%" } } });
|
|
1215
|
+
assert(no.length).equals(0);
|
|
1216
|
+
const yes = await User.find({ where: { name: { $like: "MiXeD%" } } });
|
|
1217
|
+
assert(yes.length).equals(1);
|
|
1218
|
+
assert(yes[0].name).equals("MiXeDCase");
|
|
1219
|
+
});
|
|
1220
|
+
$user("find: $ilike is case-insensitive", async (assert) => {
|
|
1221
|
+
await User.insert({ name: "MiXeDCase2" });
|
|
1222
|
+
const rows = await User.find({ where: { name: { $ilike: "mixed%" } } });
|
|
1223
|
+
assert(rows.length).equals(1);
|
|
1224
|
+
assert(rows[0].name).equals("MiXeDCase2");
|
|
1225
|
+
});
|
|
1226
|
+
$user$("get: unknown option keys throw", async (assert) => {
|
|
1227
|
+
const u = await User.insert({ name: "Guard", age: 1 });
|
|
1228
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1229
|
+
// get only accepts { select?, with? }
|
|
1230
|
+
return User.get(u.id, { where: { name: "Guard" } });
|
|
1231
|
+
});
|
|
1232
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1233
|
+
return User.get(u.id, { sort: { name: "asc" } });
|
|
1234
|
+
});
|
|
1235
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1236
|
+
return User.get(u.id, { banana: true });
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
$user$("inject invalid identifier (table name)", async (assert) => {
|
|
1240
|
+
const BadStore = new Store({
|
|
1241
|
+
id: key.primary(p.string),
|
|
1242
|
+
}, { db, name: "users; DROP TABLE users" });
|
|
1243
|
+
await throws(assert, Code.identifier_invalid, async () => {
|
|
1244
|
+
await BadStore.collection.create();
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
$user("find: $like: literal percent sign", async (assert) => {
|
|
1248
|
+
await User.insert({ name: "100% complete" });
|
|
1249
|
+
await User.insert({ name: "100 complete" });
|
|
1250
|
+
const got = await User.find({
|
|
1251
|
+
where: { name: { $like: "100\\% complete" } },
|
|
1252
|
+
select: ["name"],
|
|
1253
|
+
});
|
|
1254
|
+
assert(got.length).equals(1);
|
|
1255
|
+
assert(got[0].name).equals("100% complete");
|
|
1256
|
+
});
|
|
1257
|
+
$user("find: $like: literal underscore", async (assert) => {
|
|
1258
|
+
await User.insert({ name: "file_name" });
|
|
1259
|
+
await User.insert({ name: "file1name" });
|
|
1260
|
+
const got = await User.find({
|
|
1261
|
+
where: { name: { $like: "file\\_name" } },
|
|
1262
|
+
select: ["name"],
|
|
1263
|
+
});
|
|
1264
|
+
assert(got.length).equals(1);
|
|
1265
|
+
assert(got[0].name).equals("file_name");
|
|
1266
|
+
});
|
|
1267
|
+
$rel("with + select (no id)", async (assert) => {
|
|
1268
|
+
const rows = await Author.find({
|
|
1269
|
+
select: ["name"],
|
|
1270
|
+
with: { articles: { select: ["title"], sort: { title: "asc" } } },
|
|
1271
|
+
sort: { name: "asc" },
|
|
1272
|
+
});
|
|
1273
|
+
// no id leaked
|
|
1274
|
+
for (const r of rows)
|
|
1275
|
+
assert("id" in r).false();
|
|
1276
|
+
// but relations are loaded
|
|
1277
|
+
const john = rows.find(r => r.name === "John");
|
|
1278
|
+
assert(Array.isArray(john.articles)).true();
|
|
1279
|
+
assert(john.articles.length).nequals(0);
|
|
1280
|
+
});
|
|
1281
|
+
$rel("with: relation fields are decoded (URL)", async (assert) => {
|
|
1282
|
+
const [first] = await Author.find({ where: { name: "John" } });
|
|
1283
|
+
const john = await Author.get(first.id, {
|
|
1284
|
+
with: { profile: true },
|
|
1285
|
+
});
|
|
1286
|
+
assert(john.profile).not.null();
|
|
1287
|
+
// this is the whole point: driver must unbind it
|
|
1288
|
+
assert(john.profile.url).type();
|
|
1289
|
+
assert(john.profile.url instanceof URL).true();
|
|
1290
|
+
assert(john.profile.url?.href).equals("https://example.com/john");
|
|
1291
|
+
});
|
|
1292
|
+
$rel("reverse one: with + select (no author_id) still loads relation", async (assert) => {
|
|
1293
|
+
const [row] = await Article.find({ select: ["id"] });
|
|
1294
|
+
const got = await Article.get(row.id, {
|
|
1295
|
+
select: ["title"], // intentionally omit author_id
|
|
1296
|
+
with: { author: true },
|
|
1297
|
+
});
|
|
1298
|
+
assert("author_id" in got).false(); // stripped
|
|
1299
|
+
assert(got.author).not.null();
|
|
1300
|
+
});
|
|
1301
|
+
$rel("with: joined relations decode URL fields (base + rel)", async (assert) => {
|
|
1302
|
+
const ParentSchema = {
|
|
1303
|
+
id: key.primary(p.string),
|
|
1304
|
+
name: p.string,
|
|
1305
|
+
url: p.url.optional(),
|
|
1306
|
+
};
|
|
1307
|
+
const ChildSchema = {
|
|
1308
|
+
id: key.primary(p.string),
|
|
1309
|
+
parent_id: key.foreign(p.string),
|
|
1310
|
+
url: p.url.optional(),
|
|
1311
|
+
};
|
|
1312
|
+
const Parent = new Store(ParentSchema, {
|
|
1313
|
+
db,
|
|
1314
|
+
name: "j_parent",
|
|
1315
|
+
relations: {
|
|
1316
|
+
children: relation.many(ChildSchema, "parent_id"),
|
|
1317
|
+
},
|
|
1318
|
+
});
|
|
1319
|
+
const Child = new Store(ChildSchema, {
|
|
1320
|
+
db,
|
|
1321
|
+
name: "j_child",
|
|
1322
|
+
relations: {
|
|
1323
|
+
parent: relation.one(ParentSchema, "parent_id", { reverse: true }),
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
await Parent.collection.create();
|
|
1327
|
+
await Child.collection.create();
|
|
1328
|
+
try {
|
|
1329
|
+
const p0 = await Parent.insert({
|
|
1330
|
+
name: "P0",
|
|
1331
|
+
url: new URL("https://example.com/parent"),
|
|
1332
|
+
});
|
|
1333
|
+
await Child.insert({
|
|
1334
|
+
parent_id: p0.id,
|
|
1335
|
+
url: new URL("https://example.com/joined"),
|
|
1336
|
+
});
|
|
1337
|
+
// IMPORTANT: force joined path + projection that omits pk, but still must join.
|
|
1338
|
+
const [got] = await Parent.find({
|
|
1339
|
+
where: { id: p0.id },
|
|
1340
|
+
select: ["url"],
|
|
1341
|
+
with: { children: true },
|
|
1342
|
+
});
|
|
1343
|
+
assert(got).defined();
|
|
1344
|
+
// pk must not leak
|
|
1345
|
+
assert("id" in got).false();
|
|
1346
|
+
// ---- base assertion (should fail if joined base isn't unbound/decoded)
|
|
1347
|
+
assert(got.url).type();
|
|
1348
|
+
assert(got.url instanceof URL).true();
|
|
1349
|
+
assert(got.url.href).equals("https://example.com/parent");
|
|
1350
|
+
// ---- relation assertion (should fail if joined relation isn't unbound/decoded)
|
|
1351
|
+
assert(Array.isArray(got.children)).true();
|
|
1352
|
+
assert(got.children.length).equals(1);
|
|
1353
|
+
const c0 = got.children[0];
|
|
1354
|
+
assert(c0.url).type();
|
|
1355
|
+
assert(c0.url instanceof URL).true();
|
|
1356
|
+
assert(c0.url.href).equals("https://example.com/joined");
|
|
1357
|
+
}
|
|
1358
|
+
finally {
|
|
1359
|
+
await Child.collection.delete();
|
|
1360
|
+
await Parent.collection.delete();
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
$rel("find: limit applies to parent rows, not joined rows", async (assert) => {
|
|
1364
|
+
const authors = await Author.find({
|
|
1365
|
+
limit: 2,
|
|
1366
|
+
sort: { name: "asc" },
|
|
1367
|
+
with: { articles: true },
|
|
1368
|
+
});
|
|
1369
|
+
assert(authors.length).equals(2);
|
|
1370
|
+
});
|
|
1371
|
+
for (const c of BAD_WHERE) {
|
|
1372
|
+
bad_where(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1373
|
+
}
|
|
1374
|
+
for (const c of BAD_SELECT) {
|
|
1375
|
+
bad_select(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1376
|
+
}
|
|
1377
|
+
for (const c of BAD_SORT) {
|
|
1378
|
+
bad_sort(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1379
|
+
}
|
|
1380
|
+
for (const c of BAD_WHERE_COLUMN) {
|
|
1381
|
+
bad_where(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
//# sourceMappingURL=test.js.map
|