@primate/core 0.4.6 → 0.6.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 +771 -6
- package/lib/private/App.js +49 -16
- package/lib/private/Binder.d.ts +2 -2
- package/lib/private/Flags.d.ts +8 -4
- package/lib/private/Module.d.ts +4 -16
- package/lib/private/Module.js +1 -17
- package/lib/private/app/EnvSchema.d.ts +5 -0
- package/lib/private/app/EnvSchema.js +2 -0
- package/lib/private/app/Facade.browser.d.ts +11 -0
- package/lib/private/app/Facade.browser.js +19 -0
- package/lib/private/app/Facade.d.ts +811 -0
- package/lib/private/app/Facade.js +56 -0
- package/lib/private/asset/Asset.d.ts +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 +19 -10
- package/lib/private/build/client/plugin/app-request.d.ts +4 -0
- package/lib/private/build/client/plugin/app-request.js +19 -0
- package/lib/private/build/client/plugin/view.d.ts +4 -0
- package/lib/private/build/client/plugin/view.js +13 -0
- package/lib/private/build/hook.d.ts +1 -2
- package/lib/private/build/hook.js +25 -19
- package/lib/private/build/index.d.ts +2 -1
- package/lib/private/build/index.js +27 -27
- package/lib/private/build/presets.d.ts +10 -0
- package/lib/private/build/presets.js +39 -0
- package/lib/private/build/server/index.js +10 -14
- package/lib/private/build/server/plugin/app-request.d.ts +4 -0
- package/lib/private/build/server/plugin/app-request.js +19 -0
- 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/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} +12 -12
- package/lib/private/build/server/plugin/native-addons.js +10 -11
- package/lib/private/build/server/plugin/node-imports.js +2 -2
- package/lib/private/build/server/plugin/route.js +4 -4
- package/lib/private/build/server/plugin/store.js +11 -33
- package/lib/private/build/server/plugin/stores.js +12 -9
- package/lib/private/build/server/plugin/view.js +5 -5
- 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/build/shared/plugin/app-request.d.ts +4 -0
- package/lib/private/build/shared/plugin/app-request.js +19 -0
- package/lib/private/bye.js +2 -3
- package/lib/private/client/Data.d.ts +3 -2
- package/lib/private/client/Render.d.ts +8 -0
- package/lib/private/{frontend → client}/ServerData.d.ts +1 -1
- package/lib/private/client/ServerView.d.ts +4 -0
- package/lib/private/client/ValidateInit.d.ts +1 -3
- package/lib/private/client/ValidationError.d.ts +1 -1
- package/lib/private/{frontend → client}/View.d.ts +1 -1
- package/lib/private/{frontend → client}/ViewOptions.d.ts +1 -1
- package/lib/private/{frontend → client}/ViewResponse.d.ts +2 -2
- package/lib/private/client/app.js +3 -3
- package/lib/private/client/boot.d.ts +5 -0
- package/lib/private/client/boot.js +64 -0
- package/lib/private/client/create-form.d.ts +32 -0
- package/lib/private/client/create-form.js +123 -0
- package/lib/private/client/extract-issues.d.ts +4 -0
- package/lib/private/client/extract-issues.js +21 -0
- package/lib/private/client/http.d.ts +13 -0
- package/lib/private/client/http.js +57 -0
- package/lib/private/client/index.d.ts +23 -0
- package/lib/private/client/index.js +16 -0
- package/lib/private/client/navigate.d.ts +13 -0
- package/lib/private/client/navigate.js +67 -0
- package/lib/private/client/root.d.ts +9 -0
- package/lib/private/client/root.js +11 -0
- package/lib/private/client/submit.d.ts +2 -0
- package/lib/private/client/submit.js +41 -0
- 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 +7 -21
- package/lib/private/config/index.js +4 -1
- package/lib/private/config/schema.d.ts +82 -22
- package/lib/private/config/schema.js +25 -7
- package/lib/private/cookie.d.ts +12 -6
- package/lib/private/cookie.js +10 -14
- package/lib/private/db/As.d.ts +10 -0
- package/lib/private/db/DB.d.ts +49 -0
- package/lib/private/db/DB.js +3 -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 +33 -0
- package/lib/private/db/MemoryDB.js +335 -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 +25 -0
- package/lib/private/db/common.js +67 -0
- package/lib/private/db/errors.d.ts +104 -0
- package/lib/private/db/errors.js +237 -0
- package/lib/private/db/migrate/apply.d.ts +2 -0
- package/lib/private/db/migrate/apply.js +32 -0
- package/lib/private/db/migrate/bundle.d.ts +3 -0
- package/lib/private/db/migrate/bundle.js +22 -0
- package/lib/private/db/migrate/create.d.ts +2 -0
- package/lib/private/db/migrate/create.js +154 -0
- package/lib/private/db/migrate/index.d.ts +10 -0
- package/lib/private/db/migrate/index.js +6 -0
- package/lib/private/db/migrate/status.d.ts +2 -0
- package/lib/private/db/migrate/status.js +38 -0
- package/lib/private/db/migrate/store.d.ts +5 -0
- package/lib/private/db/migrate/store.js +33 -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.js +2 -0
- package/lib/private/db/test.d.ts +4 -0
- package/lib/private/db/test.js +1750 -0
- package/lib/private/errors.d.ts +88 -0
- package/lib/private/errors.js +211 -0
- package/lib/private/frontend.d.ts +72 -0
- package/lib/private/frontend.js +245 -0
- package/lib/private/hash.js +0 -1
- package/lib/private/i18n/Catalog.d.ts +5 -2
- package/lib/private/i18n/Formatter.js +21 -9
- package/lib/private/i18n/errors.d.ts +16 -0
- package/lib/private/i18n/errors.js +27 -0
- 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/module.d.ts +3 -0
- package/lib/private/i18n/module.js +115 -0
- 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 +10 -5
- 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 +19 -0
- package/lib/private/index.d.ts +14 -0
- package/lib/private/index.js +2 -0
- package/lib/private/log.d.ts +1 -0
- package/lib/private/log.js +15 -14
- package/lib/private/module/Hook.d.ts +1 -1
- package/lib/private/module/Next.d.ts +1 -1
- package/lib/private/module/Setup.d.ts +21 -0
- package/lib/private/module/Setup.js +2 -0
- package/lib/private/module/create.d.ts +16 -0
- package/lib/private/module/create.js +28 -0
- package/lib/private/orm/ExtractSchema.d.ts +9 -0
- package/lib/private/orm/ExtractSchema.js +2 -0
- package/lib/private/orm/ForeignKey.d.ts +14 -0
- package/lib/private/orm/ForeignKey.js +25 -0
- package/lib/private/orm/PrimaryKey.d.ts +17 -0
- package/lib/private/orm/PrimaryKey.js +38 -0
- package/lib/private/orm/StoreInput.d.ts +10 -0
- package/lib/private/orm/StoreInput.js +2 -0
- package/lib/private/orm/key.d.ts +8 -0
- package/lib/private/orm/key.js +8 -0
- package/lib/private/orm/parse.d.ts +13 -0
- package/lib/private/orm/parse.js +29 -0
- package/lib/private/orm/relation.d.ts +43 -0
- package/lib/private/orm/relation.js +26 -0
- package/lib/private/orm/store.d.ts +187 -0
- package/lib/private/orm/store.js +542 -0
- package/lib/private/paths.d.ts +2 -2
- package/lib/private/paths.js +3 -3
- package/lib/private/request/RequestBag.d.ts +5 -1
- package/lib/private/request/RequestBag.js +16 -9
- package/lib/private/request/RequestBody.d.ts +6 -11
- package/lib/private/request/RequestBody.js +51 -59
- package/lib/private/request/RequestContext.d.ts +12 -0
- package/lib/private/request/RequestContext.js +31 -0
- package/lib/private/request/RequestFacade.d.ts +11 -6
- package/lib/private/request/RequestPublic.d.ts +9 -0
- package/lib/private/request/RequestPublic.js +2 -0
- package/lib/private/request/RequestView.d.ts +11 -0
- package/lib/private/request/RequestView.js +3 -0
- package/lib/private/request/handle.d.ts +4 -0
- package/lib/private/request/handle.js +18 -0
- package/lib/private/request/parse.d.ts +2 -2
- package/lib/private/request/parse.js +55 -16
- package/lib/private/request/route.js +37 -22
- package/lib/private/request/router.d.ts +1 -1
- package/lib/private/request/router.js +22 -50
- package/lib/private/{database/symbol/wrap.d.ts → request/sContext.d.ts} +1 -1
- package/lib/private/request/sContext.js +2 -0
- package/lib/private/request/storage.d.ts +4 -0
- package/lib/private/request/storage.js +5 -0
- package/lib/private/response/ResponseFunction.d.ts +2 -3
- 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/error.d.ts +1 -1
- package/lib/private/response/error.js +1 -1
- package/lib/private/response/json.d.ts +1 -1
- package/lib/private/response/json.js +2 -2
- package/lib/private/response/redirect.d.ts +5 -5
- package/lib/private/response/redirect.js +9 -10
- package/lib/private/response/respond.js +16 -17
- 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 +3 -3
- package/lib/private/response/view.js +7 -14
- package/lib/private/response/ws.d.ts +1 -1
- 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 +6 -2
- package/lib/private/route/router.js +32 -18
- package/lib/private/route/wrap.js +2 -2
- package/lib/private/serve/App.d.ts +11 -25
- package/lib/private/serve/App.js +138 -120
- package/lib/private/serve/Init.d.ts +5 -5
- package/lib/private/serve/dev-module.d.ts +2 -0
- package/lib/private/serve/dev-module.js +34 -0
- package/lib/private/serve/hook.d.ts +1 -2
- package/lib/private/serve/hook.js +2 -9
- package/lib/private/serve/index.d.ts +1 -1
- package/lib/private/serve/index.js +36 -2
- package/lib/private/server/TAG.d.ts +3 -0
- package/lib/private/server/TAG.js +2 -0
- package/lib/private/server/index.d.ts +5 -0
- package/lib/private/server/index.js +6 -0
- package/lib/private/session/Data.d.ts +1 -2
- package/lib/private/session/SessionHandle.d.ts +1 -1
- package/lib/private/session/SessionHandle.js +11 -9
- package/lib/private/session/index.d.ts +3 -4
- package/lib/private/session/module.d.ts +3 -0
- package/lib/private/session/module.js +114 -0
- package/lib/private/session/schema.d.ts +17 -9
- package/lib/private/session/schema.js +11 -6
- 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/Manager.js +6 -12
- 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 +3 -0
- package/lib/public/client.js +2 -0
- package/lib/public/db/MemoryDB.d.ts +2 -0
- package/lib/public/db/MemoryDB.js +2 -0
- package/lib/public/db/errors.d.ts +2 -0
- package/lib/public/db/errors.js +2 -0
- package/lib/public/db/migrate.d.ts +2 -0
- package/lib/public/db/migrate.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/frontend.d.ts +3 -0
- package/lib/public/frontend.js +2 -0
- package/lib/public/index.d.ts +2 -0
- package/lib/public/index.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/store.d.ts +2 -0
- package/lib/public/orm/store.js +2 -0
- package/lib/public/request/server.d.ts +5 -0
- package/lib/public/request/server.js +7 -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/lib/public/server.d.ts +3 -0
- package/lib/public/server.js +2 -0
- package/package.json +40 -30
- package/lib/private/AppError.d.ts +0 -4
- package/lib/private/AppError.js +0 -8
- package/lib/private/backend/Module.d.ts +0 -18
- package/lib/private/backend/Module.js +0 -22
- package/lib/private/backend/TAG.d.ts +0 -3
- package/lib/private/backend/TAG.js +0 -2
- 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/build/server/plugin/store-wrap.d.ts +0 -4
- package/lib/private/build/server/plugin/store-wrap.js +0 -33
- package/lib/private/client/spa/index.d.ts +0 -6
- package/lib/private/client/spa/index.js +0 -201
- 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/fail.d.ts +0 -3
- package/lib/private/fail.js +0 -5
- package/lib/private/frontend/Module.d.ts +0 -62
- package/lib/private/frontend/Module.js +0 -250
- package/lib/private/frontend/Render.d.ts +0 -9
- package/lib/private/frontend/ServerView.d.ts +0 -5
- package/lib/private/i18n/Module.d.ts +0 -16
- package/lib/private/i18n/Module.js +0 -122
- package/lib/private/i18n/index.d.ts +0 -28
- package/lib/private/i18n/index.js +0 -236
- package/lib/private/module/BuildHook.d.ts +0 -5
- package/lib/private/module/BuildHook.js +0 -2
- package/lib/private/module/NextBuild.d.ts +0 -5
- package/lib/private/module/NextBuild.js +0 -2
- package/lib/private/module/NextServe.d.ts +0 -5
- package/lib/private/module/NextServe.js +0 -2
- package/lib/private/reducer.d.ts +0 -24
- package/lib/private/reducer.js +0 -10
- package/lib/private/route/guard.d.ts +0 -4
- package/lib/private/route/guard.js +0 -22
- package/lib/private/serve/module/Dev.d.ts +0 -11
- package/lib/private/serve/module/Dev.js +0 -28
- package/lib/private/serve/module/Handle.d.ts +0 -10
- package/lib/private/serve/module/Handle.js +0 -15
- package/lib/private/session/SessionModule.d.ts +0 -14
- package/lib/private/session/SessionModule.js +0 -122
- 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/App.d.ts +0 -2
- package/lib/public/App.js +0 -2
- package/lib/public/AppError.d.ts +0 -2
- package/lib/public/AppError.js +0 -2
- package/lib/public/BuildApp.d.ts +0 -2
- package/lib/public/BuildApp.js +0 -2
- package/lib/public/BuildHook.d.ts +0 -2
- package/lib/public/BuildHook.js +0 -2
- package/lib/public/Database.d.ts +0 -2
- package/lib/public/Database.js +0 -2
- package/lib/public/Mode.d.ts +0 -2
- package/lib/public/Mode.js +0 -2
- package/lib/public/Module.d.ts +0 -2
- package/lib/public/Module.js +0 -2
- package/lib/public/Next.d.ts +0 -2
- package/lib/public/Next.js +0 -2
- package/lib/public/NextBuild.d.ts +0 -2
- package/lib/public/NextBuild.js +0 -2
- package/lib/public/NextHandle.d.ts +0 -2
- package/lib/public/NextHandle.js +0 -2
- package/lib/public/NextRoute.d.ts +0 -3
- package/lib/public/NextRoute.js +0 -2
- package/lib/public/NextServe.d.ts +0 -2
- package/lib/public/NextServe.js +0 -2
- package/lib/public/ServeApp.d.ts +0 -2
- package/lib/public/ServeApp.js +0 -2
- package/lib/public/Target.d.ts +0 -2
- package/lib/public/Target.js +0 -2
- package/lib/public/backend/Module.d.ts +0 -2
- package/lib/public/backend/Module.js +0 -2
- package/lib/public/backend/TAG.d.ts +0 -2
- package/lib/public/backend/TAG.js +0 -2
- package/lib/public/client/Data.d.ts +0 -2
- package/lib/public/client/Data.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/spa.d.ts +0 -2
- package/lib/public/client/spa.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/fail.d.ts +0 -2
- package/lib/public/fail.js +0 -2
- package/lib/public/frontend/Module.d.ts +0 -2
- package/lib/public/frontend/Module.js +0 -2
- package/lib/public/frontend/Publish.d.ts +0 -2
- package/lib/public/frontend/Publish.js +0 -2
- package/lib/public/frontend/Render.d.ts +0 -2
- package/lib/public/frontend/Render.js +0 -2
- package/lib/public/frontend/ViewResponse.d.ts +0 -2
- package/lib/public/frontend/ViewResponse.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/{frontend → client}/Publish.d.ts +0 -0
- /package/lib/private/{frontend → client}/Publish.js +0 -0
- /package/lib/private/{frontend → client}/Render.js +0 -0
- /package/lib/private/{frontend → client}/ServerData.js +0 -0
- /package/lib/private/{frontend → client}/ServerView.js +0 -0
- /package/lib/private/{frontend → client}/View.js +0 -0
- /package/lib/private/{frontend → client}/ViewOptions.js +0 -0
- /package/lib/private/{frontend → client}/ViewResponse.js +0 -0
- /package/lib/private/client/{spa/storage.d.ts → storage.d.ts} +0 -0
- /package/lib/private/client/{spa/storage.js → storage.js} +0 -0
- /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.d.ts +0 -0
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
import { Code } from "#db/errors";
|
|
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 = store({
|
|
110
|
+
name: "post",
|
|
111
|
+
db,
|
|
112
|
+
schema: {
|
|
113
|
+
id: key.primary(p.uuid),
|
|
114
|
+
title: p.string,
|
|
115
|
+
user_id: p.uint,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
Post.update;
|
|
119
|
+
const User = store({
|
|
120
|
+
name: "user",
|
|
121
|
+
db,
|
|
122
|
+
schema: {
|
|
123
|
+
id: key.primary(p.uuid),
|
|
124
|
+
age: p.u8.optional(),
|
|
125
|
+
lastname: p.string.optional(),
|
|
126
|
+
name: p.string.default("Donald"),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const UserN = store({
|
|
130
|
+
name: "user_n",
|
|
131
|
+
db,
|
|
132
|
+
schema: {
|
|
133
|
+
id: key.primary(p.u32),
|
|
134
|
+
age: p.u8.optional(),
|
|
135
|
+
lastname: p.string.optional(),
|
|
136
|
+
name: p.string.default("Donald"),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const UserB = store({
|
|
140
|
+
name: "user_b",
|
|
141
|
+
db,
|
|
142
|
+
schema: {
|
|
143
|
+
id: key.primary(p.u128),
|
|
144
|
+
age: p.u8.optional(),
|
|
145
|
+
lastname: p.string.optional(),
|
|
146
|
+
name: p.string.default("Donald"),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const USER_STORES = [User, UserN, UserB];
|
|
150
|
+
const Type = store({
|
|
151
|
+
name: "type",
|
|
152
|
+
db,
|
|
153
|
+
schema: {
|
|
154
|
+
id: key.primary(p.uuid),
|
|
155
|
+
boolean: p.boolean.optional(),
|
|
156
|
+
date: p.date.optional(),
|
|
157
|
+
f32: p.f32.optional(),
|
|
158
|
+
f64: p.f64.optional(),
|
|
159
|
+
i128: p.i128.optional(),
|
|
160
|
+
i16: p.i16.optional(),
|
|
161
|
+
i32: p.i32.optional(),
|
|
162
|
+
i64: p.i64.optional(),
|
|
163
|
+
i8: p.i8.optional(),
|
|
164
|
+
string: p.string.optional(),
|
|
165
|
+
u128: p.u128.optional(),
|
|
166
|
+
u16: p.u16.optional(),
|
|
167
|
+
u32: p.u32.optional(),
|
|
168
|
+
u64: p.u64.optional(),
|
|
169
|
+
u8: p.u8.optional(),
|
|
170
|
+
json: p.json().optional(),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
// this stresses identifier quoting in CREATE/INSERT/SELECT/UPDATE/DELETE
|
|
174
|
+
const Reserved = store({
|
|
175
|
+
// deliberately reserved-like table name
|
|
176
|
+
name: "select",
|
|
177
|
+
db,
|
|
178
|
+
schema: {
|
|
179
|
+
id: key.primary(p.uuid),
|
|
180
|
+
// deliberately reserved-looking column name
|
|
181
|
+
order: p.u8.optional(),
|
|
182
|
+
name: p.string,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const AuthorSchema = {
|
|
186
|
+
id: key.primary(p.uuid),
|
|
187
|
+
name: p.string,
|
|
188
|
+
};
|
|
189
|
+
const ArticleSchema = {
|
|
190
|
+
id: key.primary(p.uuid),
|
|
191
|
+
title: p.string,
|
|
192
|
+
author_id: key.foreign(p.uuid),
|
|
193
|
+
};
|
|
194
|
+
const ProfileSchema = {
|
|
195
|
+
id: key.primary(p.uuid),
|
|
196
|
+
bio: p.string,
|
|
197
|
+
url: p.url.optional(),
|
|
198
|
+
author_id: key.foreign(p.uuid),
|
|
199
|
+
};
|
|
200
|
+
const Author = store({
|
|
201
|
+
db,
|
|
202
|
+
name: "author",
|
|
203
|
+
schema: AuthorSchema,
|
|
204
|
+
relations: {
|
|
205
|
+
articles: relation.many(ArticleSchema, "author_id"),
|
|
206
|
+
profile: relation.one(ProfileSchema, "author_id"),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
/*const AuthorWithJSON = store({
|
|
210
|
+
name: "author_json",
|
|
211
|
+
db,
|
|
212
|
+
schema: {
|
|
213
|
+
id: key.primary(p.uuid),
|
|
214
|
+
name: p.string,
|
|
215
|
+
meta: p.json(p({ views: p.u32, tags: p.array(p.string) })),
|
|
216
|
+
notes: p.json(), // untyped
|
|
217
|
+
},
|
|
218
|
+
});*/
|
|
219
|
+
const Article = store({
|
|
220
|
+
db,
|
|
221
|
+
name: "article",
|
|
222
|
+
schema: ArticleSchema,
|
|
223
|
+
relations: {
|
|
224
|
+
author: relation.one(AuthorSchema, "author_id", { reverse: true }),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const Profile = store({
|
|
228
|
+
db,
|
|
229
|
+
name: "profile",
|
|
230
|
+
schema: ProfileSchema,
|
|
231
|
+
relations: {
|
|
232
|
+
author: relation.one(AuthorSchema, "author_id", { reverse: true }),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
function $store(label, s, body) {
|
|
236
|
+
test.case(label, async (assert) => {
|
|
237
|
+
await s.table.create();
|
|
238
|
+
try {
|
|
239
|
+
await body(assert);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
await s.table.delete();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function $user(label, body) {
|
|
247
|
+
test.case(label, async (assert) => {
|
|
248
|
+
for (const S of USER_STORES)
|
|
249
|
+
await S.table.create();
|
|
250
|
+
try {
|
|
251
|
+
for (const u of Object.values(USERS)) {
|
|
252
|
+
for (const S of USER_STORES)
|
|
253
|
+
await S.insert(u);
|
|
254
|
+
}
|
|
255
|
+
await body(assert);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
for (const S of USER_STORES)
|
|
259
|
+
await S.table.delete();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function $user$(label, body) {
|
|
264
|
+
$user(`security: ${label}`, body);
|
|
265
|
+
}
|
|
266
|
+
function $type(label, body) {
|
|
267
|
+
$store(`type: ${label}`, Type, body);
|
|
268
|
+
}
|
|
269
|
+
function $rel(label, body) {
|
|
270
|
+
test.case(`relation: ${label}`, async (assert) => {
|
|
271
|
+
await Author.table.create();
|
|
272
|
+
await Article.table.create();
|
|
273
|
+
await Profile.table.create();
|
|
274
|
+
try {
|
|
275
|
+
const john = await Author.insert({ name: "John" });
|
|
276
|
+
const bob = await Author.insert({ name: "Bob" });
|
|
277
|
+
const ned = await Author.insert({ name: "Ned" });
|
|
278
|
+
const jid = john.id;
|
|
279
|
+
const bid = bob.id;
|
|
280
|
+
const nid = ned.id;
|
|
281
|
+
await Article.insert({ title: "John First Post", author_id: jid });
|
|
282
|
+
await Article.insert({ title: "John Second Post", author_id: jid });
|
|
283
|
+
await Article.insert({ title: "Bob Only Post", author_id: bid });
|
|
284
|
+
await Profile.insert({
|
|
285
|
+
bio: "John is a writer",
|
|
286
|
+
url: new URL("https://example.com/john"),
|
|
287
|
+
author_id: jid,
|
|
288
|
+
});
|
|
289
|
+
const ts = ["foo a", "foo c", "foo d", "foo e", "foo f", "foo g"];
|
|
290
|
+
for (const title of ts)
|
|
291
|
+
await Article.insert({ title, author_id: jid });
|
|
292
|
+
await Article.insert({ title: "bar x", author_id: jid });
|
|
293
|
+
await Article.insert({ title: "foo b", author_id: bid });
|
|
294
|
+
await Article.insert({ title: "bar y", author_id: bid });
|
|
295
|
+
await Article.insert({ title: "bar z", author_id: nid });
|
|
296
|
+
await body(assert);
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
await Profile.table.delete();
|
|
300
|
+
await Article.table.delete();
|
|
301
|
+
await Author.table.delete();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function $rel$(label, body) {
|
|
306
|
+
$rel(`security: ${label}`, body);
|
|
307
|
+
}
|
|
308
|
+
function security_pair(base_prefix, label, mk, run_base, run_with, expected) {
|
|
309
|
+
const run = throws;
|
|
310
|
+
$user$(`${base_prefix}: ${label}`, async (assert) => {
|
|
311
|
+
const { base } = mk();
|
|
312
|
+
await run(assert, expected, () => run_base(base));
|
|
313
|
+
});
|
|
314
|
+
$rel$(`with: ${base_prefix}: ${label}`, async (assert) => {
|
|
315
|
+
const { with: w } = mk();
|
|
316
|
+
await run(assert, expected, () => run_with(w));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function bad_where(label, mk, expected) {
|
|
320
|
+
return security_pair("where", label, mk, base => User.find({ where: base }), w => Author.find({ with: { articles: { where: w } } }), expected);
|
|
321
|
+
}
|
|
322
|
+
function bad_select(label, mk, expected) {
|
|
323
|
+
return security_pair("find", label, mk, base => User.find({ select: base }), w => Author.find({ with: { articles: { select: w } } }), expected);
|
|
324
|
+
}
|
|
325
|
+
function bad_sort(label, mk, expected) {
|
|
326
|
+
return security_pair("find", label, mk, base => User.find({ sort: base }), w => Author.find({ with: { articles: { sort: w } } }), expected);
|
|
327
|
+
}
|
|
328
|
+
$store("insert", User, async (assert) => {
|
|
329
|
+
const donald = await User.insert({ age: 30, name: "Donald" });
|
|
330
|
+
assert(donald).type();
|
|
331
|
+
assert(await User.has(donald.id)).true();
|
|
332
|
+
const ryan = await User.insert({ age: 40, name: "Ryan" });
|
|
333
|
+
assert(await User.has(donald.id)).true();
|
|
334
|
+
assert(await User.has(ryan.id)).true();
|
|
335
|
+
});
|
|
336
|
+
$store("insert: primary key is optional (string)", User, async (assert) => {
|
|
337
|
+
const user = await User.insert({ name: "Test" });
|
|
338
|
+
assert(user.id).type();
|
|
339
|
+
assert(await User.has(user.id)).true();
|
|
340
|
+
});
|
|
341
|
+
$store("insert: primary key is optional (number)", UserN, async (assert) => {
|
|
342
|
+
const user = await UserN.insert({ name: "Test" });
|
|
343
|
+
assert(user.id).type();
|
|
344
|
+
assert(await UserN.has(user.id)).true();
|
|
345
|
+
});
|
|
346
|
+
$store("insert: primary key is optional (bigint)", UserB, async (assert) => {
|
|
347
|
+
const user = await UserB.insert({ name: "Test" });
|
|
348
|
+
assert(user.id).type();
|
|
349
|
+
assert(await UserB.has(user.id)).true();
|
|
350
|
+
});
|
|
351
|
+
const ManualUser = store({
|
|
352
|
+
name: "manual_user",
|
|
353
|
+
db,
|
|
354
|
+
schema: {
|
|
355
|
+
id: key.primary(p.uuid, { generate: false }),
|
|
356
|
+
name: p.string,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
$store("insert: generate=false requires PK", ManualUser, async (assert) => {
|
|
360
|
+
await throws(assert, Code.pk_required, () => {
|
|
361
|
+
return ManualUser.insert({ name: "Test" });
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
$store("insert: generate=false accepts provided PK", ManualUser, async (assert) => {
|
|
365
|
+
const id = "4d0996db-bda9-4f95-ad7c-7075b10d4ba6";
|
|
366
|
+
const user = await ManualUser.insert({ id, name: "Test" });
|
|
367
|
+
assert(user.id).equals(id);
|
|
368
|
+
assert(await ManualUser.has(id)).true();
|
|
369
|
+
});
|
|
370
|
+
$store("insert: defaults apply", User, async (assert) => {
|
|
371
|
+
const u = await User.insert({});
|
|
372
|
+
assert(u.name).equals("Donald");
|
|
373
|
+
});
|
|
374
|
+
$store("insert: reject null values", User, async (assert) => {
|
|
375
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
376
|
+
// lastname is optional, but null is forbidden on insert
|
|
377
|
+
return User.insert({ name: "Nullman", lastname: null });
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
$user("find: empty object equals no options", async (assert) => {
|
|
381
|
+
const a = await User.find();
|
|
382
|
+
const b = await User.find({});
|
|
383
|
+
assert(a.length).equals(b.length);
|
|
384
|
+
});
|
|
385
|
+
$user("find: $like: handles regex metacharacters", async (assert) => {
|
|
386
|
+
await User.insert({ name: "A[1]" });
|
|
387
|
+
const got = await User.find({
|
|
388
|
+
where: { name: { $like: "A[1]" } },
|
|
389
|
+
select: ["name"],
|
|
390
|
+
});
|
|
391
|
+
assert(got.length).equals(1);
|
|
392
|
+
assert(got[0].name).equals("A[1]");
|
|
393
|
+
});
|
|
394
|
+
$user("find: types", async (assert) => {
|
|
395
|
+
const where = { name: "Ryan" };
|
|
396
|
+
assert(await User.find({ where })).type();
|
|
397
|
+
assert(await UserN.find({ where })).type();
|
|
398
|
+
assert(await UserB.find({ where })).type();
|
|
399
|
+
});
|
|
400
|
+
$user("find: select narrows type", async (assert) => {
|
|
401
|
+
const select = ["id", "name"];
|
|
402
|
+
const users = await User.find({ select });
|
|
403
|
+
assert(users).type();
|
|
404
|
+
const users_n = await UserN.find({ select });
|
|
405
|
+
assert(users_n).type();
|
|
406
|
+
const users_b = await UserB.find({ select });
|
|
407
|
+
assert(users_b).type();
|
|
408
|
+
});
|
|
409
|
+
$user("find: basic query", async (assert) => {
|
|
410
|
+
const result = await User.find({ where: { name: "Ryan" } });
|
|
411
|
+
assert(result.length).equals(1);
|
|
412
|
+
});
|
|
413
|
+
$user("find: sorting by multiple fields", async (assert) => {
|
|
414
|
+
// sorting by multiple fields: age descending, then Lastname ascending
|
|
415
|
+
const sorted = await User.find({
|
|
416
|
+
select: ["age", "name"],
|
|
417
|
+
sort: { age: "desc", lastname: "asc" },
|
|
418
|
+
});
|
|
419
|
+
assert(sorted.length).equals(5);
|
|
420
|
+
["ben", "paul", "ryan", "donald", "jeremy"].forEach((user, i) => {
|
|
421
|
+
assert(sorted[i]).equals(pick(USERS[user], "name", "age"));
|
|
422
|
+
});
|
|
423
|
+
const descending = await User.find({
|
|
424
|
+
select: ["age", "name"],
|
|
425
|
+
sort: { age: "desc", lastname: "desc" },
|
|
426
|
+
});
|
|
427
|
+
const descended = ["ben", "ryan", "paul", "donald", "jeremy"];
|
|
428
|
+
descended.forEach((user, i) => {
|
|
429
|
+
assert(descending[i]).equals(pick(USERS[user], "name", "age"));
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
$user("find: sorting ascending and descending", async (assert) => {
|
|
433
|
+
const ascending = await User.find({
|
|
434
|
+
select: ["age", "name"],
|
|
435
|
+
sort: { age: "asc" },
|
|
436
|
+
limit: 2,
|
|
437
|
+
});
|
|
438
|
+
const ascended = ["jeremy", "donald"];
|
|
439
|
+
ascended.forEach((user, i) => {
|
|
440
|
+
assert(ascending[i]).equals(pick(USERS[user], "name", "age"));
|
|
441
|
+
});
|
|
442
|
+
const descending = await User.find({
|
|
443
|
+
select: ["age", "name"],
|
|
444
|
+
sort: { age: "desc" },
|
|
445
|
+
limit: 1,
|
|
446
|
+
});
|
|
447
|
+
assert(descending[0]).equals(pick(USERS.ben, "name", "age"));
|
|
448
|
+
});
|
|
449
|
+
$user("find: null criteria uses IS NULL semantics", async (assert) => {
|
|
450
|
+
// inserted fixtures include Jeremy without a lastname (NULL in DB)
|
|
451
|
+
// querying with { lastname: null } should find him
|
|
452
|
+
const rows = await User.find({
|
|
453
|
+
select: ["name", "lastname"],
|
|
454
|
+
where: { lastname: null },
|
|
455
|
+
sort: { name: "asc" },
|
|
456
|
+
});
|
|
457
|
+
assert(rows.length).equals(1);
|
|
458
|
+
if (rows.length > 0) {
|
|
459
|
+
assert(rows[0].name).equals("Just Jeremy");
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
$user("find: $like operator for strings", async (assert) => {
|
|
463
|
+
const prefix = await User.find({ where: { name: { $like: "J%" } } });
|
|
464
|
+
assert(prefix.length).equals(1);
|
|
465
|
+
if (prefix.length > 0)
|
|
466
|
+
assert(prefix[0].name).equals("Just Jeremy");
|
|
467
|
+
const suffix = await User.find({ where: { lastname: { $like: "%er" } } });
|
|
468
|
+
assert(suffix.length).equals(2);
|
|
469
|
+
const lastnames = suffix.map(u => u.lastname).sort();
|
|
470
|
+
assert(lastnames).equals(["Miller", "Miller"]);
|
|
471
|
+
const contains = await User.find({ where: { name: { $like: "%on%" } } });
|
|
472
|
+
assert(contains.length).equals(1);
|
|
473
|
+
if (contains.length > 0)
|
|
474
|
+
assert(contains[0].name).equals("Donald");
|
|
475
|
+
const exact = await User.find({ where: { name: { $like: "Ryan" } } });
|
|
476
|
+
assert(exact.length).equals(1);
|
|
477
|
+
if (exact.length > 0)
|
|
478
|
+
assert(exact[0].name).equals("Ryan");
|
|
479
|
+
const none = await User.find({ where: { name: { $like: "xyz%" } } });
|
|
480
|
+
assert(none.length).equals(0);
|
|
481
|
+
});
|
|
482
|
+
$user("find: $like with null/undefined fields", async (assert) => {
|
|
483
|
+
// Jeremy has no lastname (null), should not match ANY $like patterns
|
|
484
|
+
const results = await User.find({
|
|
485
|
+
where: { lastname: { $like: "%ll%" } },
|
|
486
|
+
});
|
|
487
|
+
assert(results.length).equals(2); // Ben, Paul
|
|
488
|
+
const names = results.map(u => u.name).sort();
|
|
489
|
+
assert(names).equals(["Ben", "Paul"]);
|
|
490
|
+
});
|
|
491
|
+
$user("find: $like: reject non-string types", async (assert) => {
|
|
492
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
493
|
+
// age is u8, should not accept $like
|
|
494
|
+
return User.find({ where: { age: { $like: "30%" } } });
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
$user("update: single record", async (assert) => {
|
|
498
|
+
const [donald] = (await User.find({ where: { name: "Donald" } }));
|
|
499
|
+
await User.update(donald.id, { set: { age: 35 } });
|
|
500
|
+
const [updated] = (await User.find({ where: { name: "Donald" } }));
|
|
501
|
+
assert(updated.age).equals(35);
|
|
502
|
+
assert(updated).equals({ ...USERS.donald, age: 35, id: donald.id });
|
|
503
|
+
});
|
|
504
|
+
$user("update: unset fields", async (assert) => {
|
|
505
|
+
const [donald] = (await User.find({ where: { name: "Donald" } }));
|
|
506
|
+
assert(donald.age).equals(30);
|
|
507
|
+
await User.update(donald.id, { set: { age: null } });
|
|
508
|
+
const [updated] = (await User.find({ where: { name: "Donald" } }));
|
|
509
|
+
assert(updated.age).undefined();
|
|
510
|
+
const [paul] = (await User.find({ where: { name: "Paul" } }));
|
|
511
|
+
await User.update(paul.id, { set: { age: null, lastname: null } });
|
|
512
|
+
const [updated_paul] = (await User.find({ where: { name: "Paul" } }));
|
|
513
|
+
assert(updated_paul).equals({ id: updated_paul.id, name: "Paul" });
|
|
514
|
+
});
|
|
515
|
+
$user("update: cannot unset required fields", async (assert) => {
|
|
516
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
517
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
518
|
+
// name is required (non-nullable) -> cannot be unset
|
|
519
|
+
return User.update(donald.id, { set: { name: null } });
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
$user("update: cannot unset required fields (multi-update)", async (assert) => {
|
|
523
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
524
|
+
return User.update({
|
|
525
|
+
where: { name: { $like: "D%" } },
|
|
526
|
+
set: { name: null },
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
$user("update: multiple records", async (assert) => {
|
|
531
|
+
const n_updated = await User.update({
|
|
532
|
+
where: { age: 40 },
|
|
533
|
+
set: { age: 45 },
|
|
534
|
+
});
|
|
535
|
+
assert(n_updated).equals(2);
|
|
536
|
+
const updated = await User.find({ where: { age: 45 } });
|
|
537
|
+
assert(updated.length).equals(2);
|
|
538
|
+
});
|
|
539
|
+
$user("update: where and changeset share a column", async (assert) => {
|
|
540
|
+
// Donald has age 30; update age using criteria on the same column
|
|
541
|
+
const n = await User.update({ where: { age: 30 }, set: { age: 31 } });
|
|
542
|
+
assert(n).equals(1);
|
|
543
|
+
assert(await User.count({ where: { age: 30 } })).equals(0);
|
|
544
|
+
assert(await User.count({ where: { age: 31 } })).equals(1);
|
|
545
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
546
|
+
assert(donald.age).equals(31);
|
|
547
|
+
});
|
|
548
|
+
$rel("count: no with", async (assert) => {
|
|
549
|
+
await throws(assert, Code.count_with_invalid, async () => {
|
|
550
|
+
await Author.count({ with: { articles: true } });
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
$user("update: update all", async (assert) => {
|
|
554
|
+
await User.update({ set: { age: 99 } });
|
|
555
|
+
const users = await User.find();
|
|
556
|
+
for (const user of users)
|
|
557
|
+
assert(user.age).equals(99);
|
|
558
|
+
});
|
|
559
|
+
$user("update: $like criteria", async (assert) => {
|
|
560
|
+
// update all users whose names start with "J"
|
|
561
|
+
const updated = await User.update({
|
|
562
|
+
where: { name: { $like: "J%" } },
|
|
563
|
+
set: { age: 25 },
|
|
564
|
+
});
|
|
565
|
+
assert(updated).equals(1);
|
|
566
|
+
const jeremy = await User.find({ where: { name: "Just Jeremy" } });
|
|
567
|
+
assert(jeremy[0].age).equals(25);
|
|
568
|
+
});
|
|
569
|
+
$user("delete: single record", async (assert) => {
|
|
570
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
571
|
+
await User.delete(donald.id);
|
|
572
|
+
const deleted = await User.find({ where: { name: "Donald" } });
|
|
573
|
+
assert(deleted.length).equals(0);
|
|
574
|
+
});
|
|
575
|
+
$user("delete: multiple records", async (assert) => {
|
|
576
|
+
const n = await User.delete({ where: { age: 40 } });
|
|
577
|
+
assert(n).equals(2);
|
|
578
|
+
const remaining = await User.find({ sort: { age: "asc" } });
|
|
579
|
+
assert(remaining.length).equals(3);
|
|
580
|
+
assert(remaining[0].name).equals("Just Jeremy");
|
|
581
|
+
});
|
|
582
|
+
$user("delete: $like criteria", async (assert) => {
|
|
583
|
+
// delete all users with "Miller" lastname
|
|
584
|
+
const deleted = await User.delete({
|
|
585
|
+
where: { lastname: { $like: "%Miller%" } },
|
|
586
|
+
});
|
|
587
|
+
assert(deleted).equals(2);
|
|
588
|
+
const remaining = await User.find();
|
|
589
|
+
assert(remaining.length).equals(3);
|
|
590
|
+
const remainingNames = remaining.map(u => u.name).sort();
|
|
591
|
+
assert(remainingNames).equals(["Donald", "Just Jeremy", "Ryan"]);
|
|
592
|
+
});
|
|
593
|
+
$user("count", async (assert) => {
|
|
594
|
+
assert(await User.count()).equals(5);
|
|
595
|
+
assert(await User.count({ where: { name: "Ryan" } })).equals(1);
|
|
596
|
+
assert(await User.count({ where: { age: 40 } })).equals(2);
|
|
597
|
+
assert(await User.count({ where: { age: 30 } })).equals(1);
|
|
598
|
+
assert(await User.count({ where: { age: 35 } })).equals(0);
|
|
599
|
+
});
|
|
600
|
+
$user("count: $like operator", async (assert) => {
|
|
601
|
+
assert(await User.count({ where: { name: { $like: "J%" } } })).equals(1);
|
|
602
|
+
assert(await User.count({ where: { lastname: { $like: "%er" } } }))
|
|
603
|
+
.equals(2);
|
|
604
|
+
assert(await User.count({ where: { name: { $like: "%xyz%" } } }))
|
|
605
|
+
.equals(0);
|
|
606
|
+
});
|
|
607
|
+
$user("has", async (assert) => {
|
|
608
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
609
|
+
assert(await User.has(donald.id)).true();
|
|
610
|
+
await User.delete(donald.id);
|
|
611
|
+
assert(await User.has(donald.id)).false();
|
|
612
|
+
});
|
|
613
|
+
$type("boolean", async (assert) => {
|
|
614
|
+
const t = await Type.insert({ boolean: true });
|
|
615
|
+
assert(t.boolean).equals(true);
|
|
616
|
+
assert((await Type.get(t.id)).boolean).equals(true);
|
|
617
|
+
await Type.update(t.id, { set: { boolean: false } });
|
|
618
|
+
assert((await Type.get(t.id)).boolean).equals(false);
|
|
619
|
+
});
|
|
620
|
+
$type("string", async (assert) => {
|
|
621
|
+
const t = await Type.insert({ string: "foo" });
|
|
622
|
+
assert(t.string).equals("foo");
|
|
623
|
+
assert((await Type.get(t.id)).string).equals("foo");
|
|
624
|
+
await Type.update(t.id, { set: { string: "bar" } });
|
|
625
|
+
assert((await Type.get(t.id)).string).equals("bar");
|
|
626
|
+
});
|
|
627
|
+
$type("date", async (assert) => {
|
|
628
|
+
const now = new Date();
|
|
629
|
+
const t = await Type.insert({ date: now });
|
|
630
|
+
assert(t.date?.getTime()).equals(now.getTime());
|
|
631
|
+
assert((await Type.get(t.id)).date?.getTime()).equals(now.getTime());
|
|
632
|
+
const next = new Date();
|
|
633
|
+
await Type.update(t.id, { set: { date: next } });
|
|
634
|
+
assert((await Type.get(t.id)).date).equals(next);
|
|
635
|
+
});
|
|
636
|
+
$type("f32", async (assert) => {
|
|
637
|
+
const t = await Type.insert({ f32: 1.5 });
|
|
638
|
+
assert(t.f32).equals(1.5);
|
|
639
|
+
assert((await Type.get(t.id)).f32).equals(1.5);
|
|
640
|
+
await Type.update(t.id, { set: { f32: 123456.75 } });
|
|
641
|
+
assert((await Type.get(t.id)).f32).equals(123456.75);
|
|
642
|
+
});
|
|
643
|
+
$type("f64", async (assert) => {
|
|
644
|
+
const f1 = 123456.78901;
|
|
645
|
+
const t = await Type.insert({ f64: f1 });
|
|
646
|
+
assert(t.f64).equals(f1);
|
|
647
|
+
assert((await Type.get(t.id)).f64).equals(f1);
|
|
648
|
+
await Type.update(t.id, { set: { f32: 1.5 } });
|
|
649
|
+
assert((await Type.get(t.id)).f32).equals(1.5);
|
|
650
|
+
});
|
|
651
|
+
[8, 16, 32].forEach(n => {
|
|
652
|
+
$type(`i${n}`, async (assert) => {
|
|
653
|
+
const k = `i${n}`;
|
|
654
|
+
// lower bound
|
|
655
|
+
const lb = -(2 ** (n - 1));
|
|
656
|
+
const t = await Type.insert({ [k]: lb });
|
|
657
|
+
assert(t[any(k)]).equals(lb);
|
|
658
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
659
|
+
// upper bound
|
|
660
|
+
const ub = 2 ** (n - 1) - 1;
|
|
661
|
+
await Type.update(t.id, { set: { [k]: ub } });
|
|
662
|
+
assert((await Type.get(t.id))[any(k)]).equals(ub);
|
|
663
|
+
});
|
|
664
|
+
$type(`u${n}`, async (assert) => {
|
|
665
|
+
const k = `u${n}`;
|
|
666
|
+
// lower bound
|
|
667
|
+
const lb = 0;
|
|
668
|
+
const t = await Type.insert({ [k]: lb });
|
|
669
|
+
assert(t[any(k)]).equals(lb);
|
|
670
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
671
|
+
// upper bound
|
|
672
|
+
const ub = 2 ** n - 1;
|
|
673
|
+
await Type.update(t.id, { set: { [k]: ub } });
|
|
674
|
+
assert((await Type.get(t.id))[any(k)]).equals(ub);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
[64n, 128n].forEach(i => {
|
|
678
|
+
$type(`i${i}`, async (assert) => {
|
|
679
|
+
const k = `i${i}`;
|
|
680
|
+
// lower bound
|
|
681
|
+
const lb = -(2n ** (i - 1n));
|
|
682
|
+
const t = await Type.insert({ [k]: lb });
|
|
683
|
+
assert(t[any(k)]).equals(lb);
|
|
684
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
685
|
+
// upper bound
|
|
686
|
+
const ub = 2n ** (i - 1n) - 1n;
|
|
687
|
+
const tu = await Type.insert({ [k]: ub });
|
|
688
|
+
assert(tu[any(k)]).equals(ub);
|
|
689
|
+
assert((await Type.get(tu.id))[any(k)]).equals(ub);
|
|
690
|
+
});
|
|
691
|
+
$type(`u${i}`, async (assert) => {
|
|
692
|
+
const k = `u${i}`;
|
|
693
|
+
// lower bound
|
|
694
|
+
const lb = 0n;
|
|
695
|
+
const t = await Type.insert({ [k]: lb });
|
|
696
|
+
assert(t[any(k)]).equals(lb);
|
|
697
|
+
assert((await Type.get(t.id))[any(k)]).equals(lb);
|
|
698
|
+
// upper bound
|
|
699
|
+
const ub = 2n ** i - 1n;
|
|
700
|
+
const tu = await Type.insert({ [k]: ub });
|
|
701
|
+
assert(tu[any(k)]).equals(ub);
|
|
702
|
+
assert((await Type.get(tu.id))[any(k)]).equals(ub);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
$user$("find: reject non-array select", async (assert) => {
|
|
706
|
+
await throws(assert, Code.select_invalid, () => {
|
|
707
|
+
return User.find({ select: {} });
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
$user$("find: projection limits fields", async (assert) => {
|
|
711
|
+
const records = await User.find({ select: ["id", "name"] });
|
|
712
|
+
assert(records.length).equals(5);
|
|
713
|
+
for (const r of records) {
|
|
714
|
+
// only id + name must be present
|
|
715
|
+
assert(Object.keys(r).toSorted()).equals(["id", "name"].toSorted());
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
$user$("update: reject unknown field on set", async (assert) => {
|
|
719
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
720
|
+
await throws(assert, Code.field_unknown, () => {
|
|
721
|
+
return User.update(donald.id, { set: { nope: 1 } });
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
$user$("update: reject empty set object", async (assert) => {
|
|
725
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
726
|
+
await throws(assert, Code.set_empty, () => {
|
|
727
|
+
return User.update(donald.id, { set: {} });
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
$user$("update: reject updating primary key", async (assert) => {
|
|
731
|
+
const [donald] = await User.find({ where: { name: "Donald" } });
|
|
732
|
+
await throws(assert, Code.pk_immutable, () => {
|
|
733
|
+
return User.update(donald.id, { set: { id: "nope" } });
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
$user$("update: reject missing set", async (assert) => {
|
|
737
|
+
await throws(assert, Code.set_empty, () => {
|
|
738
|
+
return User.update({ where: { name: "Donald" } });
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
$user$("delete: reject missing where", async (assert) => {
|
|
742
|
+
await throws(assert, Code.where_required, () => {
|
|
743
|
+
return User.delete({});
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
$user$("inject invalid identifier (where)", async (assert) => {
|
|
747
|
+
// attempted injection via bogus key
|
|
748
|
+
await throws(assert, Code.identifier_invalid, () => {
|
|
749
|
+
return User.find({ where: { "name; DROP TABLE user;": "x" } });
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
$user$("inject invalid identifier (select)", async (assert) => {
|
|
753
|
+
await throws(assert, Code.identifier_invalid, () => {
|
|
754
|
+
return User.find({ select: ["name; DROP TABLE user;"] });
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
$user$("value safety: does not interpolate values", async (assert) => {
|
|
758
|
+
const evil = "x' ; DROP TABLE user; --";
|
|
759
|
+
// should just behave like a normal string compare (0 matches), not explode
|
|
760
|
+
const rows = await User.find({ where: { name: evil } });
|
|
761
|
+
assert(rows.length).equals(0);
|
|
762
|
+
// table should still exist / be queryable
|
|
763
|
+
assert(await User.count()).equals(5);
|
|
764
|
+
});
|
|
765
|
+
$user$("update respects unset / binding map", async (assert) => {
|
|
766
|
+
const [paul] = await User.find({ where: { name: "Paul" } });
|
|
767
|
+
// allowed, lastname is optional
|
|
768
|
+
await User.update(paul.id, { set: { lastname: null } });
|
|
769
|
+
const [after] = await User.find({
|
|
770
|
+
select: ["id", "name", "lastname"],
|
|
771
|
+
where: { id: paul.id },
|
|
772
|
+
});
|
|
773
|
+
assert(after.lastname).undefined();
|
|
774
|
+
});
|
|
775
|
+
$user$("count: reject unknown where column", async (assert) => {
|
|
776
|
+
await throws(assert, Code.field_unknown, () => {
|
|
777
|
+
return User.count({ where: { nope: 1 } });
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
$user$("$like: reject unknown field", async (assert) => {
|
|
781
|
+
await throws(assert, Code.field_unknown, () => {
|
|
782
|
+
return User.find({ where: { unknown: { $like: "test%" } } });
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
$user$("insert: reject unknown column", async (assert) => {
|
|
786
|
+
await throws(assert, Code.field_unknown, () => {
|
|
787
|
+
return User.insert({ name: "X", nope: 1 });
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
$user$("update: reject unknown where column", async (assert) => {
|
|
791
|
+
await throws(assert, Code.field_unknown, () => User.update({
|
|
792
|
+
where: { nope: 1 },
|
|
793
|
+
set: { age: 1 },
|
|
794
|
+
}));
|
|
795
|
+
});
|
|
796
|
+
$user$("delete: reject unknown where column", async (assert) => {
|
|
797
|
+
await throws(assert, Code.field_unknown, () => User.delete({ where: { nope: 1 } }));
|
|
798
|
+
});
|
|
799
|
+
$user$("number operators: reject on non-number fields", async (assert) => {
|
|
800
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
801
|
+
// name is string, should not accept $gte
|
|
802
|
+
return User.find({ where: { name: { $gte: 10 } } });
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
$user$("mixed operators: reject invalid combinations", async (assert) => {
|
|
806
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
807
|
+
// can't mix string and number operators
|
|
808
|
+
return User.find({ where: { name: { $like: "test%", $gte: 5 } } });
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
$store("insert: reject null on required fields", User, async (assert) => {
|
|
812
|
+
await throws(assert, Code.null_not_allowed, () => {
|
|
813
|
+
// name is required, but passing null explicitly is still forbidden
|
|
814
|
+
return User.insert({ name: null });
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
$store("insert: omission stores NULL", User, async (assert) => {
|
|
818
|
+
const u = await User.insert({ name: "NoLast" });
|
|
819
|
+
const [got] = await User.find({
|
|
820
|
+
where: { id: u.id },
|
|
821
|
+
select: ["id", "name", "lastname"],
|
|
822
|
+
});
|
|
823
|
+
assert(got.id).equals(u.id);
|
|
824
|
+
assert(got.name).equals("NoLast");
|
|
825
|
+
// returned shape must not contain null
|
|
826
|
+
assert(got.lastname).undefined();
|
|
827
|
+
});
|
|
828
|
+
$store("insert: default fields can be omitted", User, async (assert) => {
|
|
829
|
+
// User has name: p.string.default("Donald")
|
|
830
|
+
// should be insertable without providing name
|
|
831
|
+
const user = await User.insert({ age: 30 });
|
|
832
|
+
assert(user.name).equals("Donald");
|
|
833
|
+
});
|
|
834
|
+
$store("reserved table / column names", Reserved, async (assert) => {
|
|
835
|
+
const a = await Reserved.insert({ name: "alpha", order: 1 });
|
|
836
|
+
const b = await Reserved.insert({ name: "beta", order: 2 });
|
|
837
|
+
const got = await Reserved.find({ where: { name: "alpha" } });
|
|
838
|
+
assert(got.length).equals(1);
|
|
839
|
+
assert(got[0]).equals({ id: a.id, name: "alpha", order: 1 });
|
|
840
|
+
// update using the reserved column
|
|
841
|
+
const n = await Reserved.update({
|
|
842
|
+
where: { name: "beta" },
|
|
843
|
+
set: { order: 9 },
|
|
844
|
+
});
|
|
845
|
+
assert(n).equals(1);
|
|
846
|
+
const [after] = await Reserved.find({ where: { id: b.id } });
|
|
847
|
+
assert(after.order).equals(9);
|
|
848
|
+
// and delete to complete the cycle
|
|
849
|
+
await Reserved.delete(a.id);
|
|
850
|
+
assert(await Reserved.has(a.id)).false();
|
|
851
|
+
});
|
|
852
|
+
$rel("get with one (reverse)", async (assert) => {
|
|
853
|
+
const articles = await Article.find();
|
|
854
|
+
const article = await Article.get(articles[0].id, {
|
|
855
|
+
with: {
|
|
856
|
+
author: true,
|
|
857
|
+
},
|
|
858
|
+
});
|
|
859
|
+
assert(article).type();
|
|
860
|
+
assert(article.author).not.null();
|
|
861
|
+
assert(article.author?.name).defined();
|
|
862
|
+
});
|
|
863
|
+
$rel("get with many", async (assert) => {
|
|
864
|
+
const [first] = await Author.find({ where: { name: "John" } });
|
|
865
|
+
const john = await Author.get(first.id, { with: { articles: true } });
|
|
866
|
+
assert(john.articles).type();
|
|
867
|
+
const titles = john.articles.map(a => a.title);
|
|
868
|
+
assert(titles.includes("John First Post")).true();
|
|
869
|
+
assert(titles.includes("John Second Post")).true();
|
|
870
|
+
});
|
|
871
|
+
$rel("get by id", async (assert) => {
|
|
872
|
+
const [first] = await Author.find({ where: { name: "John" } });
|
|
873
|
+
const john = await Author.get(first.id, { with: { profile: true } });
|
|
874
|
+
assert(john.profile).not.null();
|
|
875
|
+
assert(john.profile?.bio).equals("John is a writer");
|
|
876
|
+
});
|
|
877
|
+
$rel("get by id returns null when missing", async (assert) => {
|
|
878
|
+
const [first] = await Author.find({ where: { name: "Bob" } });
|
|
879
|
+
const bob = await Author.get(first.id, { with: { profile: true } });
|
|
880
|
+
assert(bob.profile).null();
|
|
881
|
+
});
|
|
882
|
+
$rel("find", async (assert) => {
|
|
883
|
+
const articles = await Article.find({
|
|
884
|
+
where: { title: { $like: "% Post" } },
|
|
885
|
+
with: { author: true },
|
|
886
|
+
sort: { title: "asc" },
|
|
887
|
+
});
|
|
888
|
+
assert(articles.length).equals(3);
|
|
889
|
+
for (const article of articles) {
|
|
890
|
+
assert(article.author).not.null();
|
|
891
|
+
assert(article.author?.name).defined();
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
$rel("try", async (assert) => {
|
|
895
|
+
const [first] = await Article.find();
|
|
896
|
+
const article = await Article.try(first.id, { with: { author: true } });
|
|
897
|
+
assert(article).defined();
|
|
898
|
+
assert(article?.author).not.null();
|
|
899
|
+
});
|
|
900
|
+
$rel("many returns empty array when no matches", async (assert) => {
|
|
901
|
+
// insert author with no articles
|
|
902
|
+
const lonely = await Author.insert({ name: "Lonely" });
|
|
903
|
+
const author = await Author.get(lonely.id, { with: { articles: true } });
|
|
904
|
+
assert(author.articles).type();
|
|
905
|
+
assert(author.articles.length).equals(0);
|
|
906
|
+
});
|
|
907
|
+
$rel("multiple relations in one query", async (assert) => {
|
|
908
|
+
const authors = await Author.find({ where: { name: "John" } });
|
|
909
|
+
const john = await Author.get(authors[0].id, {
|
|
910
|
+
with: {
|
|
911
|
+
articles: true,
|
|
912
|
+
profile: true,
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
const titles = john.articles.map(a => a.title);
|
|
916
|
+
assert(titles.includes("John First Post")).true();
|
|
917
|
+
assert(titles.includes("John Second Post")).true();
|
|
918
|
+
assert(john.profile).not.null();
|
|
919
|
+
assert(john.profile?.bio).equals("John is a writer");
|
|
920
|
+
});
|
|
921
|
+
$rel("no fields without 'with'", async (assert) => {
|
|
922
|
+
const articles = await Article.find();
|
|
923
|
+
const article = await Article.get(articles[0].id);
|
|
924
|
+
assert("author" in article).false();
|
|
925
|
+
});
|
|
926
|
+
$rel("find: complex relation subqueries", async (assert) => {
|
|
927
|
+
const SUBLIMIT = 5;
|
|
928
|
+
const authors = await Author.find({
|
|
929
|
+
select: ["id", "name"],
|
|
930
|
+
sort: { name: "asc" },
|
|
931
|
+
limit: 20,
|
|
932
|
+
with: {
|
|
933
|
+
articles: {
|
|
934
|
+
where: { title: { $like: "%foo%" } },
|
|
935
|
+
select: ["id", "title"],
|
|
936
|
+
sort: { title: "asc" },
|
|
937
|
+
limit: SUBLIMIT,
|
|
938
|
+
},
|
|
939
|
+
profile: true,
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
assert(authors).type();
|
|
943
|
+
// parents must *not* be filtered by relation where
|
|
944
|
+
const base = await Author.find({
|
|
945
|
+
select: ["id", "name"],
|
|
946
|
+
sort: { name: "asc" },
|
|
947
|
+
limit: 20,
|
|
948
|
+
});
|
|
949
|
+
assert(authors.map(r => r.name)).equals(base.map(r => r.name));
|
|
950
|
+
// base projection is respected (only id/name + relation keys)
|
|
951
|
+
for (const author of authors) {
|
|
952
|
+
assert(Object.keys(author).toSorted()).equals(["articles", "id", "name", "profile"].toSorted());
|
|
953
|
+
}
|
|
954
|
+
// per-parent relation correctness (filter + sort + limit)
|
|
955
|
+
for (const author of authors) {
|
|
956
|
+
// many => always array
|
|
957
|
+
assert(Array.isArray(author.articles)).true();
|
|
958
|
+
// projection + filter enforcement on returned relation rows
|
|
959
|
+
for (const article of author.articles) {
|
|
960
|
+
assert(Object.keys(article).toSorted()).equals(["id", "title"]
|
|
961
|
+
.toSorted());
|
|
962
|
+
assert(article.title.includes("foo")).true();
|
|
963
|
+
}
|
|
964
|
+
// relation sort asc by title
|
|
965
|
+
const titles = author.articles.map(a => a.title);
|
|
966
|
+
assert([...titles].toSorted()).equals(titles);
|
|
967
|
+
// relation limit is per-parent
|
|
968
|
+
assert(author.articles.length <= SUBLIMIT).true();
|
|
969
|
+
// compare against base query (same semantics, explicit per-parent)
|
|
970
|
+
const expected = await Article.find({
|
|
971
|
+
where: { author_id: author.id, title: { $like: "%foo%" } },
|
|
972
|
+
select: ["id", "title"],
|
|
973
|
+
sort: { title: "asc" },
|
|
974
|
+
});
|
|
975
|
+
const expected_titles = expected.slice(0, SUBLIMIT).map(a => a.title);
|
|
976
|
+
assert(titles).equals(expected_titles);
|
|
977
|
+
// profile: one => null or full record, FK must point back
|
|
978
|
+
if (author.profile !== null) {
|
|
979
|
+
assert(Object.keys(author.profile).toSorted()).equals(["author_id", "bio", "id", "url"].toSorted());
|
|
980
|
+
assert(author.profile.author_id).equals(author.id);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// ensure the inner limit was actually used (needs > SUBLIMIT matches)
|
|
984
|
+
let used = false;
|
|
985
|
+
for (const author of authors) {
|
|
986
|
+
const n_foo = await Article.count({
|
|
987
|
+
where: { author_id: author.id, title: { $like: "%foo%" } },
|
|
988
|
+
});
|
|
989
|
+
if (n_foo > SUBLIMIT)
|
|
990
|
+
used = true;
|
|
991
|
+
}
|
|
992
|
+
assert(used).true();
|
|
993
|
+
const john = authors.find(r => r.name === "John");
|
|
994
|
+
if (john !== undefined) {
|
|
995
|
+
assert(john.profile).not.null();
|
|
996
|
+
assert(john.profile?.bio).equals("John is a writer");
|
|
997
|
+
}
|
|
998
|
+
// parent with *zero* matching related rows must still be present,
|
|
999
|
+
// and the many-relation must be [] (not missing / not null)
|
|
1000
|
+
const ned = authors.find(a => a.name === "Ned");
|
|
1001
|
+
assert(ned).defined();
|
|
1002
|
+
assert(ned.articles).type();
|
|
1003
|
+
assert(ned.articles.length).equals(0);
|
|
1004
|
+
// and one-relation should be null when missing
|
|
1005
|
+
assert(ned.profile).null();
|
|
1006
|
+
});
|
|
1007
|
+
$rel("type: select narrows nested types", async (assert) => {
|
|
1008
|
+
const rows = await Author.find({
|
|
1009
|
+
select: ["id"],
|
|
1010
|
+
with: {
|
|
1011
|
+
profile: { select: ["bio"] },
|
|
1012
|
+
articles: { select: ["title"] },
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
assert(rows).type();
|
|
1016
|
+
// runtime: selected base only has id (plus relation keys)
|
|
1017
|
+
for (const r of rows)
|
|
1018
|
+
assert("name" in r).false();
|
|
1019
|
+
});
|
|
1020
|
+
$rel$("with: reject unknown relation name", async (assert) => {
|
|
1021
|
+
await throws(assert, Code.relation_unknown, () => {
|
|
1022
|
+
return Author.find({ with: { nope: true } });
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
$user("get/try: missing id", async (assert) => {
|
|
1026
|
+
const missing = "00000000-0000-4000-8000-000000000000";
|
|
1027
|
+
// const missing = `missing-${Date.now()}-${Math.random()}`;
|
|
1028
|
+
await throws(assert, Code.record_not_found, () => User.get(missing));
|
|
1029
|
+
assert(await User.try(missing)).undefined();
|
|
1030
|
+
});
|
|
1031
|
+
$rel("get/try: missing id (with relations)", async (assert) => {
|
|
1032
|
+
//const missing = `missing-${Date.now()}-${Math.random()}`;
|
|
1033
|
+
const missing = "00000000-0000-4000-8000-000000000000";
|
|
1034
|
+
await throws(assert, Code.record_not_found, () => Article.get(missing, { with: { author: true } }));
|
|
1035
|
+
assert(await Article.try(missing, { with: { author: true } })).undefined();
|
|
1036
|
+
});
|
|
1037
|
+
$rel("get/try: missing id (+ parent)", async (assert) => {
|
|
1038
|
+
const missing = "00000000-0000-4000-8000-000000000000";
|
|
1039
|
+
//const missing = `missing-${Date.now()}-${Math.random()}`;
|
|
1040
|
+
await throws(assert, Code.record_not_found, () => Author.get(missing, { with: { articles: true, profile: true } }));
|
|
1041
|
+
assert(await Author.try(missing, {
|
|
1042
|
+
with: { articles: true, profile: true },
|
|
1043
|
+
})).undefined();
|
|
1044
|
+
});
|
|
1045
|
+
$user("try: does not swallow invalid options", async (assert) => {
|
|
1046
|
+
const [u] = await User.find({ select: ["id"] });
|
|
1047
|
+
await throws(assert, Code.select_empty, () => {
|
|
1048
|
+
return User.try(u.id, { select: [] });
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
$type("where: treat date as literal value (not operator)", async (assert) => {
|
|
1052
|
+
const d = new Date();
|
|
1053
|
+
const inserted = await Type.insert({ date: d });
|
|
1054
|
+
// to be treated as equality, not operator parsing.
|
|
1055
|
+
const got = await Type.find({ where: { date: new Date(d.getTime()) } });
|
|
1056
|
+
assert(got.length).equals(1);
|
|
1057
|
+
assert(got[0].id).equals(inserted.id);
|
|
1058
|
+
});
|
|
1059
|
+
$type("where: number operators ($gt/$gte/$lt/$lte/$ne)", async (assert) => {
|
|
1060
|
+
// use a marker to avoid cross-test interference if the DB isn't reset
|
|
1061
|
+
await Type.insert({ string: "ops-num", u8: 201 });
|
|
1062
|
+
await Type.insert({ string: "ops-num", u8: 202 });
|
|
1063
|
+
await Type.insert({ string: "ops-num", u8: 203 });
|
|
1064
|
+
const gt = await Type.find({
|
|
1065
|
+
where: { string: "ops-num", u8: { $gt: 201 } },
|
|
1066
|
+
sort: { u8: "asc" },
|
|
1067
|
+
});
|
|
1068
|
+
assert(gt.map(r => r.u8)).equals([202, 203]);
|
|
1069
|
+
const between = await Type.find({
|
|
1070
|
+
where: { string: "ops-num", u8: { $gte: 202, $lt: 203 } },
|
|
1071
|
+
});
|
|
1072
|
+
assert(between.length).equals(1);
|
|
1073
|
+
assert(between[0].u8).equals(202);
|
|
1074
|
+
const ne = await Type.find({
|
|
1075
|
+
where: { string: "ops-num", u8: { $ne: 202 } },
|
|
1076
|
+
sort: { u8: "asc" },
|
|
1077
|
+
});
|
|
1078
|
+
assert(ne.map(r => r.u8)).equals([201, 203]);
|
|
1079
|
+
const lte = await Type.find({
|
|
1080
|
+
where: { string: "ops-num", u8: { $lte: 202 } },
|
|
1081
|
+
sort: { u8: "asc" },
|
|
1082
|
+
});
|
|
1083
|
+
assert(lte.map(r => r.u8)).equals([201, 202]);
|
|
1084
|
+
});
|
|
1085
|
+
$type("where: bigint operators ($gt/$gte/$lt/$lte/$ne)", async (assert) => {
|
|
1086
|
+
await Type.insert({ string: "ops-big", u64: 201n });
|
|
1087
|
+
await Type.insert({ string: "ops-big", u64: 202n });
|
|
1088
|
+
await Type.insert({ string: "ops-big", u64: 203n });
|
|
1089
|
+
const gt = await Type.find({
|
|
1090
|
+
where: { string: "ops-big", u64: { $gt: 201n } },
|
|
1091
|
+
sort: { u64: "asc" },
|
|
1092
|
+
});
|
|
1093
|
+
assert(gt.map(r => r.u64)).equals([202n, 203n]);
|
|
1094
|
+
const between = await Type.find({
|
|
1095
|
+
where: { string: "ops-big", u64: { $gte: 202n, $lt: 203n } },
|
|
1096
|
+
});
|
|
1097
|
+
assert(between.length).equals(1);
|
|
1098
|
+
assert(between[0].u64).equals(202n);
|
|
1099
|
+
const ne = await Type.find({
|
|
1100
|
+
where: { string: "ops-big", u64: { $ne: 202n } },
|
|
1101
|
+
sort: { u64: "asc" },
|
|
1102
|
+
});
|
|
1103
|
+
assert(ne.map(r => r.u64)).equals([201n, 203n]);
|
|
1104
|
+
});
|
|
1105
|
+
$type("where: datetime operators ($before/$after/$ne)", async (assert) => {
|
|
1106
|
+
const d1 = new Date("2020-01-01T00:00:00.000Z");
|
|
1107
|
+
const d2 = new Date("2020-01-02T00:00:00.000Z");
|
|
1108
|
+
const d3 = new Date("2020-01-03T00:00:00.000Z");
|
|
1109
|
+
await Type.insert({ string: "ops-date", date: d1 });
|
|
1110
|
+
await Type.insert({ string: "ops-date", date: d2 });
|
|
1111
|
+
await Type.insert({ string: "ops-date", date: d3 });
|
|
1112
|
+
const before = await Type.find({
|
|
1113
|
+
where: { string: "ops-date", date: { $before: d2 } },
|
|
1114
|
+
sort: { date: "asc" },
|
|
1115
|
+
});
|
|
1116
|
+
assert(before.map(r => r.date.getTime())).equals([d1.getTime()]);
|
|
1117
|
+
const after = await Type.find({
|
|
1118
|
+
where: { string: "ops-date", date: { $after: d2 } },
|
|
1119
|
+
sort: { date: "asc" },
|
|
1120
|
+
});
|
|
1121
|
+
assert(after.map(r => r.date.getTime())).equals([d3.getTime()]);
|
|
1122
|
+
const ne = await Type.find({
|
|
1123
|
+
where: { string: "ops-date", date: { $ne: d2 } },
|
|
1124
|
+
sort: { date: "asc" },
|
|
1125
|
+
});
|
|
1126
|
+
assert(ne.map(r => r.date.getTime()))
|
|
1127
|
+
.equals([d1.getTime(), d3.getTime()]);
|
|
1128
|
+
});
|
|
1129
|
+
$type("where: operator validation errors", async (assert) => {
|
|
1130
|
+
await throws(assert, Code.operator_empty, () => {
|
|
1131
|
+
return Type.find({ where: { u8: {} } });
|
|
1132
|
+
});
|
|
1133
|
+
// string/time: only $like
|
|
1134
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
1135
|
+
return Type.find({ where: { string: { $gt: 1 } } });
|
|
1136
|
+
});
|
|
1137
|
+
// number: no $like
|
|
1138
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
1139
|
+
return Type.find({ where: { u8: { $like: "x" } } });
|
|
1140
|
+
});
|
|
1141
|
+
// datetime: no $gt
|
|
1142
|
+
await throws(assert, Code.operator_unknown, () => {
|
|
1143
|
+
return Type.find({ where: { date: { $gt: new Date() } } });
|
|
1144
|
+
});
|
|
1145
|
+
await throws(assert, Code.wrong_type, () => {
|
|
1146
|
+
return Type.find({ where: { u8: { $gt: "nope" } } });
|
|
1147
|
+
});
|
|
1148
|
+
await throws(assert, Code.wrong_type, () => {
|
|
1149
|
+
return Type.find({ where: { date: { $before: "nope" } } });
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
$type("where: combined operators on same field", async (assert) => {
|
|
1153
|
+
await Type.insert({ string: "combo", u8: 100 });
|
|
1154
|
+
await Type.insert({ string: "combo", u8: 150 });
|
|
1155
|
+
await Type.insert({ string: "combo", u8: 200 });
|
|
1156
|
+
const results = await Type.find({
|
|
1157
|
+
where: {
|
|
1158
|
+
string: "combo",
|
|
1159
|
+
u8: { $gte: 150, $ne: 200 },
|
|
1160
|
+
},
|
|
1161
|
+
sort: { u8: "asc" },
|
|
1162
|
+
});
|
|
1163
|
+
assert(results.map(r => r.u8)).equals([150]);
|
|
1164
|
+
});
|
|
1165
|
+
$type("json: opaque roundtrip", async (assert) => {
|
|
1166
|
+
const json = { a: 1, b: [true, "x"], c: null };
|
|
1167
|
+
const t = await Type.insert({ json });
|
|
1168
|
+
assert(t.json).equals(json);
|
|
1169
|
+
assert((await Type.get(t.id)).json).equals(json);
|
|
1170
|
+
});
|
|
1171
|
+
$type("json: update", async (assert) => {
|
|
1172
|
+
const t = await Type.insert({ json: { x: 1 } });
|
|
1173
|
+
await Type.update(t.id, { set: { json: { x: 2, y: "hello" } } });
|
|
1174
|
+
assert((await Type.get(t.id)).json).equals({ x: 2, y: "hello" });
|
|
1175
|
+
});
|
|
1176
|
+
$type("json: nested object", async (assert) => {
|
|
1177
|
+
const value = { outer: { inner: { deep: true } } };
|
|
1178
|
+
const t = await Type.insert({ json: value });
|
|
1179
|
+
assert((await Type.get(t.id)).json).equals(value);
|
|
1180
|
+
});
|
|
1181
|
+
$type("json: array at root", async (assert) => {
|
|
1182
|
+
const value = [1, "two", false, null];
|
|
1183
|
+
const t = await Type.insert({ json: value });
|
|
1184
|
+
assert((await Type.get(t.id)).json).equals(value);
|
|
1185
|
+
});
|
|
1186
|
+
$type("json: null value unsets field", async (assert) => {
|
|
1187
|
+
const t = await Type.insert({ json: { x: 1 } });
|
|
1188
|
+
await Type.update(t.id, { set: { json: null } });
|
|
1189
|
+
assert((await Type.get(t.id)).json).undefined();
|
|
1190
|
+
});
|
|
1191
|
+
$type("json: empty object", async (assert) => {
|
|
1192
|
+
const t = await Type.insert({ json: {} });
|
|
1193
|
+
assert((await Type.get(t.id)).json).equals({});
|
|
1194
|
+
});
|
|
1195
|
+
$type("json: empty array", async (assert) => {
|
|
1196
|
+
const t = await Type.insert({ json: [] });
|
|
1197
|
+
assert((await Type.get(t.id)).json).equals([]);
|
|
1198
|
+
});
|
|
1199
|
+
$type("json: primitive string", async (assert) => {
|
|
1200
|
+
const t = await Type.insert({ json: "hello" });
|
|
1201
|
+
assert((await Type.get(t.id)).json).equals("hello");
|
|
1202
|
+
});
|
|
1203
|
+
$type("json: primitive number", async (assert) => {
|
|
1204
|
+
const t = await Type.insert({ json: 42 });
|
|
1205
|
+
assert((await Type.get(t.id)).json).equals(42);
|
|
1206
|
+
});
|
|
1207
|
+
$type("json: primitive float", async (assert) => {
|
|
1208
|
+
const t = await Type.insert({ json: 3.14 });
|
|
1209
|
+
assert((await Type.get(t.id)).json).equals(3.14);
|
|
1210
|
+
});
|
|
1211
|
+
$type("json: primitive boolean true", async (assert) => {
|
|
1212
|
+
const t = await Type.insert({ json: true });
|
|
1213
|
+
assert((await Type.get(t.id)).json).equals(true);
|
|
1214
|
+
});
|
|
1215
|
+
$type("json: primitive boolean false", async (assert) => {
|
|
1216
|
+
const t = await Type.insert({ json: false });
|
|
1217
|
+
assert((await Type.get(t.id)).json).equals(false);
|
|
1218
|
+
});
|
|
1219
|
+
$type("json: null inside object survives roundtrip", async (assert) => {
|
|
1220
|
+
const value = { a: 1, b: null };
|
|
1221
|
+
const t = await Type.insert({ json: value });
|
|
1222
|
+
assert((await Type.get(t.id)).json).equals({ a: 1, b: null });
|
|
1223
|
+
});
|
|
1224
|
+
$type("json: null inside array survives roundtrip", async (assert) => {
|
|
1225
|
+
const value = [1, null, "three"];
|
|
1226
|
+
const t = await Type.insert({ json: value });
|
|
1227
|
+
assert((await Type.get(t.id)).json).equals([1, null, "three"]);
|
|
1228
|
+
});
|
|
1229
|
+
$store("where: null matches omitted optional field", User, async (assert) => {
|
|
1230
|
+
const u = await User.insert({ name: "NoLast" });
|
|
1231
|
+
const rows = await User.find({
|
|
1232
|
+
where: { id: u.id, lastname: null },
|
|
1233
|
+
select: ["id"],
|
|
1234
|
+
});
|
|
1235
|
+
assert(rows.length).equals(1);
|
|
1236
|
+
assert(rows[0].id).equals(u.id);
|
|
1237
|
+
});
|
|
1238
|
+
/*$store("json column: inferred type on insert", AuthorWithJSON, async assert => {
|
|
1239
|
+
const author = await AuthorWithJSON.insert({
|
|
1240
|
+
name: "John",
|
|
1241
|
+
articles: [{ title: "First", published: new Date() }],
|
|
1242
|
+
meta: { views: 0, tags: ["typescript"] },
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
assert(author).type<{
|
|
1246
|
+
id: string;
|
|
1247
|
+
name: string;
|
|
1248
|
+
articles?: { title: string; published: Date }[];
|
|
1249
|
+
meta?: { views: number; tags: string[] };
|
|
1250
|
+
notes?: JSONValue;
|
|
1251
|
+
}>();
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
$store("json column: inferred type on find", AuthorWithJSON, async assert => {
|
|
1255
|
+
const authors = await AuthorWithJSON.find();
|
|
1256
|
+
assert(authors).type<{
|
|
1257
|
+
id: string;
|
|
1258
|
+
name: string;
|
|
1259
|
+
articles?: { title: string; published: Date }[];
|
|
1260
|
+
meta?: { views: number; tags: string[] };
|
|
1261
|
+
notes?: JSONValue;
|
|
1262
|
+
}[]>();
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
$store("json column: inferred type on get", AuthorWithJSON, async assert => {
|
|
1266
|
+
const a = await AuthorWithJSON.insert({
|
|
1267
|
+
name: "John",
|
|
1268
|
+
articles: [{ title: "First", published: new Date() }],
|
|
1269
|
+
meta: { views: 0, tags: ["typescript"] },
|
|
1270
|
+
});
|
|
1271
|
+
const author = await AuthorWithJSON.get(a.id);
|
|
1272
|
+
|
|
1273
|
+
assert(author.articles).type<{ title: string; published: Date }[] | undefined>();
|
|
1274
|
+
assert(author.meta).type<{ views: number; tags: string[] } | undefined>();
|
|
1275
|
+
assert(author.notes).type<JSONValue | undefined>();
|
|
1276
|
+
|
|
1277
|
+
// inner field access fully typed
|
|
1278
|
+
assert(author.articles?.[0].title).type<string | undefined>();
|
|
1279
|
+
assert(author.articles?.[0].published).type<Date | undefined>();
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
$store("json column: roundtrip preserves types", AuthorWithJSON, async assert => {
|
|
1283
|
+
const now = new Date();
|
|
1284
|
+
const a = await AuthorWithJSON.insert({
|
|
1285
|
+
name: "John",
|
|
1286
|
+
articles: [{ title: "First", published: now }],
|
|
1287
|
+
meta: { views: 42, tags: ["ts", "primate"] },
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const author = await AuthorWithJSON.get(a.id);
|
|
1291
|
+
|
|
1292
|
+
assert(author.articles?.[0].title).equals("First");
|
|
1293
|
+
assert(author.articles?.[0].published?.getTime()).equals(now.getTime());
|
|
1294
|
+
assert(author.meta?.views).equals(42);
|
|
1295
|
+
assert(author.meta?.tags).equals(["ts", "primate"]);
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
$store("json column: nested where is typed", AuthorWithJSON, async assert => {
|
|
1299
|
+
// these should compile
|
|
1300
|
+
await AuthorWithJSON.find({ where: { articles: { title: "First" } } });
|
|
1301
|
+
await AuthorWithJSON.find({ where: { meta: { views: { $gt: 10 } } } });
|
|
1302
|
+
await AuthorWithJSON.find({ where: { meta: { tags: { $like: "%ts%" } } } });
|
|
1303
|
+
|
|
1304
|
+
// $contains always available, untyped
|
|
1305
|
+
await AuthorWithJSON.find({ where: { articles: { $contains: { title: "First" } } } });
|
|
1306
|
+
});*/
|
|
1307
|
+
$user("where: null matches unset via update", async (assert) => {
|
|
1308
|
+
const [paul] = await User.find({ where: { name: "Paul" } });
|
|
1309
|
+
await User.update(paul.id, { set: { lastname: null } });
|
|
1310
|
+
const rows = await User.find({
|
|
1311
|
+
where: { id: paul.id, lastname: null },
|
|
1312
|
+
select: ["id"],
|
|
1313
|
+
});
|
|
1314
|
+
assert(rows.length).equals(1);
|
|
1315
|
+
assert(rows[0].id).equals(paul.id);
|
|
1316
|
+
});
|
|
1317
|
+
$user("find: $like: underscore is single-character *", async (assert) => {
|
|
1318
|
+
await User.insert({ name: "A1" });
|
|
1319
|
+
await User.insert({ name: "A12" });
|
|
1320
|
+
const one = await User.find({
|
|
1321
|
+
where: { name: { $like: "A_" } },
|
|
1322
|
+
select: ["name"],
|
|
1323
|
+
sort: { name: "asc" },
|
|
1324
|
+
});
|
|
1325
|
+
assert(one.map(r => r.name)).equals(["A1"]);
|
|
1326
|
+
const two = await User.find({
|
|
1327
|
+
where: { name: { $like: "A__" } },
|
|
1328
|
+
select: ["name"],
|
|
1329
|
+
sort: { name: "asc" },
|
|
1330
|
+
});
|
|
1331
|
+
assert(two.map(r => r.name)).equals(["A12"]);
|
|
1332
|
+
});
|
|
1333
|
+
$user("find: $like: question mark is literal", async (assert) => {
|
|
1334
|
+
await User.insert({ name: "A?" });
|
|
1335
|
+
await User.insert({ name: "A1" });
|
|
1336
|
+
const got = await User.find({
|
|
1337
|
+
where: { name: { $like: "A?" } },
|
|
1338
|
+
select: ["name"],
|
|
1339
|
+
sort: { name: "asc" },
|
|
1340
|
+
});
|
|
1341
|
+
// '?' means only one character
|
|
1342
|
+
assert(got.map(r => r.name)).equals(["A?"]);
|
|
1343
|
+
});
|
|
1344
|
+
$rel("try: does not swallow invalid relation name", async (assert) => {
|
|
1345
|
+
const [row] = await Article.find({ select: ["id"] });
|
|
1346
|
+
await throws(assert, Code.relation_unknown, () => {
|
|
1347
|
+
// programmer error: should throw, not return undefined
|
|
1348
|
+
return Article.try(row.id, { with: { nope: true } });
|
|
1349
|
+
});
|
|
1350
|
+
});
|
|
1351
|
+
$rel$("with: reject non-array relation select", async (assert) => {
|
|
1352
|
+
await throws(assert, Code.select_invalid, () => {
|
|
1353
|
+
return Author.find({
|
|
1354
|
+
with: {
|
|
1355
|
+
articles: {
|
|
1356
|
+
select: {}, // must be array
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1360
|
+
});
|
|
1361
|
+
});
|
|
1362
|
+
$rel$("with: reject non-uint relation limit", async (assert) => {
|
|
1363
|
+
await throws(assert, Code.limit_invalid, () => {
|
|
1364
|
+
return Author.find({
|
|
1365
|
+
with: {
|
|
1366
|
+
articles: {
|
|
1367
|
+
limit: -1, // must be uint
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
});
|
|
1373
|
+
$rel("one relation returns null when FK missing", async (assert) => {
|
|
1374
|
+
const author_id = "4d0996db-bda9-4f95-ad7c-7075b10d4ba6";
|
|
1375
|
+
const orphan = await Article.insert({
|
|
1376
|
+
title: "Orphan",
|
|
1377
|
+
author_id,
|
|
1378
|
+
});
|
|
1379
|
+
const got = await Article.get(orphan.id, { with: { author: true } });
|
|
1380
|
+
assert(got.author).null();
|
|
1381
|
+
});
|
|
1382
|
+
$user("get/try: happy path", async (assert) => {
|
|
1383
|
+
const [u] = await User.find({ where: { name: "Donald" } });
|
|
1384
|
+
const got = await User.get(u.id);
|
|
1385
|
+
assert(got.id).equals(u.id);
|
|
1386
|
+
const tried = await User.try(u.id);
|
|
1387
|
+
assert(tried?.id).equals(u.id);
|
|
1388
|
+
});
|
|
1389
|
+
$user$("find: unknown option keys throw", async (assert) => {
|
|
1390
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1391
|
+
// wrong call-shape (looks like where but isn't nested)
|
|
1392
|
+
return User.find({ name: "John" });
|
|
1393
|
+
});
|
|
1394
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1395
|
+
// totally unknown option
|
|
1396
|
+
return User.find({ banana: true });
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
$user("find: $like is case-sensitive", async (assert) => {
|
|
1400
|
+
await User.insert({ name: "MiXeDCase" });
|
|
1401
|
+
const no = await User.find({ where: { name: { $like: "mixed%" } } });
|
|
1402
|
+
assert(no.length).equals(0);
|
|
1403
|
+
const yes = await User.find({ where: { name: { $like: "MiXeD%" } } });
|
|
1404
|
+
assert(yes.length).equals(1);
|
|
1405
|
+
assert(yes[0].name).equals("MiXeDCase");
|
|
1406
|
+
});
|
|
1407
|
+
$user("find: $ilike is case-insensitive", async (assert) => {
|
|
1408
|
+
await User.insert({ name: "MiXeDCase2" });
|
|
1409
|
+
const rows = await User.find({ where: { name: { $ilike: "mixed%" } } });
|
|
1410
|
+
assert(rows.length).equals(1);
|
|
1411
|
+
assert(rows[0].name).equals("MiXeDCase2");
|
|
1412
|
+
});
|
|
1413
|
+
$user$("get: unknown option keys throw", async (assert) => {
|
|
1414
|
+
const u = await User.insert({ name: "Guard", age: 1 });
|
|
1415
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1416
|
+
// get only accepts { select?, with? }
|
|
1417
|
+
return User.get(u.id, { where: { name: "Guard" } });
|
|
1418
|
+
});
|
|
1419
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1420
|
+
return User.get(u.id, { sort: { name: "asc" } });
|
|
1421
|
+
});
|
|
1422
|
+
await throws(assert, Code.option_unknown, () => {
|
|
1423
|
+
return User.get(u.id, { banana: true });
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
$user$("inject invalid identifier (table name)", async (assert) => {
|
|
1427
|
+
await throws(assert, Code.identifier_invalid, () => {
|
|
1428
|
+
store({
|
|
1429
|
+
name: "users; DROP TABLE users",
|
|
1430
|
+
db,
|
|
1431
|
+
schema: {
|
|
1432
|
+
id: key.primary(p.uuid),
|
|
1433
|
+
},
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
$user("find: $like: literal percent sign", async (assert) => {
|
|
1438
|
+
await User.insert({ name: "100% complete" });
|
|
1439
|
+
await User.insert({ name: "100 complete" });
|
|
1440
|
+
const got = await User.find({
|
|
1441
|
+
where: { name: { $like: "100\\% complete" } },
|
|
1442
|
+
select: ["name"],
|
|
1443
|
+
});
|
|
1444
|
+
assert(got.length).equals(1);
|
|
1445
|
+
assert(got[0].name).equals("100% complete");
|
|
1446
|
+
});
|
|
1447
|
+
$user("find: $like: literal underscore", async (assert) => {
|
|
1448
|
+
await User.insert({ name: "file_name" });
|
|
1449
|
+
await User.insert({ name: "file1name" });
|
|
1450
|
+
const got = await User.find({
|
|
1451
|
+
where: { name: { $like: "file\\_name" } },
|
|
1452
|
+
select: ["name"],
|
|
1453
|
+
});
|
|
1454
|
+
assert(got.length).equals(1);
|
|
1455
|
+
assert(got[0].name).equals("file_name");
|
|
1456
|
+
});
|
|
1457
|
+
$rel("with + select (no id)", async (assert) => {
|
|
1458
|
+
const rows = await Author.find({
|
|
1459
|
+
select: ["name"],
|
|
1460
|
+
with: { articles: { select: ["title"], sort: { title: "asc" } } },
|
|
1461
|
+
sort: { name: "asc" },
|
|
1462
|
+
});
|
|
1463
|
+
// no id leaked
|
|
1464
|
+
for (const r of rows)
|
|
1465
|
+
assert("id" in r).false();
|
|
1466
|
+
// but relations are loaded
|
|
1467
|
+
const john = rows.find(r => r.name === "John");
|
|
1468
|
+
assert(Array.isArray(john.articles)).true();
|
|
1469
|
+
assert(john.articles.length).nequals(0);
|
|
1470
|
+
});
|
|
1471
|
+
$rel("with: relation fields are decoded (URL)", async (assert) => {
|
|
1472
|
+
const [first] = await Author.find({ where: { name: "John" } });
|
|
1473
|
+
const john = await Author.get(first.id, {
|
|
1474
|
+
with: { profile: true },
|
|
1475
|
+
});
|
|
1476
|
+
assert(john.profile).not.null();
|
|
1477
|
+
// this is the whole point: driver must unbind it
|
|
1478
|
+
assert(john.profile.url).type();
|
|
1479
|
+
assert(john.profile.url instanceof URL).true();
|
|
1480
|
+
assert(john.profile.url?.href).equals("https://example.com/john");
|
|
1481
|
+
});
|
|
1482
|
+
$rel("reverse one: with + select (no author_id) still loads relation", async (assert) => {
|
|
1483
|
+
const [row] = await Article.find({ select: ["id"] });
|
|
1484
|
+
const got = await Article.get(row.id, {
|
|
1485
|
+
select: ["title"], // intentionally omit author_id
|
|
1486
|
+
with: { author: true },
|
|
1487
|
+
});
|
|
1488
|
+
assert("author_id" in got).false(); // stripped
|
|
1489
|
+
assert(got.author).not.null();
|
|
1490
|
+
});
|
|
1491
|
+
$rel("with: joined relations decode URL fields (base + rel)", async (assert) => {
|
|
1492
|
+
const ParentSchema = {
|
|
1493
|
+
id: key.primary(p.uuid),
|
|
1494
|
+
name: p.string,
|
|
1495
|
+
url: p.url.optional(),
|
|
1496
|
+
};
|
|
1497
|
+
const ChildSchema = {
|
|
1498
|
+
id: key.primary(p.uuid),
|
|
1499
|
+
parent_id: key.foreign(p.uuid),
|
|
1500
|
+
url: p.url.optional(),
|
|
1501
|
+
};
|
|
1502
|
+
const Parent = store({
|
|
1503
|
+
name: "j_parent",
|
|
1504
|
+
db,
|
|
1505
|
+
schema: ParentSchema,
|
|
1506
|
+
relations: {
|
|
1507
|
+
children: relation.many(ChildSchema, "parent_id"),
|
|
1508
|
+
},
|
|
1509
|
+
});
|
|
1510
|
+
const Child = store({
|
|
1511
|
+
db,
|
|
1512
|
+
name: "j_child",
|
|
1513
|
+
schema: ChildSchema,
|
|
1514
|
+
relations: {
|
|
1515
|
+
parent: relation.one(ParentSchema, "parent_id", { reverse: true }),
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
await Parent.table.create();
|
|
1519
|
+
await Child.table.create();
|
|
1520
|
+
try {
|
|
1521
|
+
const p0 = await Parent.insert({
|
|
1522
|
+
name: "P0",
|
|
1523
|
+
url: new URL("https://example.com/parent"),
|
|
1524
|
+
});
|
|
1525
|
+
await Child.insert({
|
|
1526
|
+
parent_id: p0.id,
|
|
1527
|
+
url: new URL("https://example.com/joined"),
|
|
1528
|
+
});
|
|
1529
|
+
// IMPORTANT: force joined path + projection that omits pk, but still must join.
|
|
1530
|
+
const [got] = await Parent.find({
|
|
1531
|
+
where: { id: p0.id },
|
|
1532
|
+
select: ["url"],
|
|
1533
|
+
with: { children: true },
|
|
1534
|
+
});
|
|
1535
|
+
assert(got).defined();
|
|
1536
|
+
// pk must not leak
|
|
1537
|
+
assert("id" in got).false();
|
|
1538
|
+
// ---- base assertion (should fail if joined base isn't unbound/decoded)
|
|
1539
|
+
assert(got.url).type();
|
|
1540
|
+
assert(got.url instanceof URL).true();
|
|
1541
|
+
assert(got.url.href).equals("https://example.com/parent");
|
|
1542
|
+
// ---- relation assertion (should fail if joined relation isn't unbound/decoded)
|
|
1543
|
+
assert(Array.isArray(got.children)).true();
|
|
1544
|
+
assert(got.children.length).equals(1);
|
|
1545
|
+
const c0 = got.children[0];
|
|
1546
|
+
assert(c0.url).type();
|
|
1547
|
+
assert(c0.url instanceof URL).true();
|
|
1548
|
+
assert(c0.url.href).equals("https://example.com/joined");
|
|
1549
|
+
}
|
|
1550
|
+
finally {
|
|
1551
|
+
await Child.table.delete();
|
|
1552
|
+
await Parent.table.delete();
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
$rel("find: limit applies to parent rows, not joined rows", async (assert) => {
|
|
1556
|
+
const authors = await Author.find({
|
|
1557
|
+
limit: 2,
|
|
1558
|
+
sort: { name: "asc" },
|
|
1559
|
+
with: { articles: true },
|
|
1560
|
+
});
|
|
1561
|
+
assert(authors.length).equals(2);
|
|
1562
|
+
});
|
|
1563
|
+
for (const c of BAD_WHERE) {
|
|
1564
|
+
bad_where(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1565
|
+
}
|
|
1566
|
+
for (const c of BAD_SELECT) {
|
|
1567
|
+
bad_select(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1568
|
+
}
|
|
1569
|
+
for (const c of BAD_SORT) {
|
|
1570
|
+
bad_sort(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1571
|
+
}
|
|
1572
|
+
for (const c of BAD_WHERE_COLUMN) {
|
|
1573
|
+
bad_where(c.label, () => ({ base: c.base, with: c.with }), c.expected);
|
|
1574
|
+
}
|
|
1575
|
+
test.case("store: relation name conflicts with field throws", async (assert) => {
|
|
1576
|
+
await throws(assert, Code.relation_conflicts_with_field, async () => store({
|
|
1577
|
+
name: "conflict",
|
|
1578
|
+
db,
|
|
1579
|
+
schema: {
|
|
1580
|
+
id: key.primary(p.uuid),
|
|
1581
|
+
articles: p.string,
|
|
1582
|
+
},
|
|
1583
|
+
relations: {
|
|
1584
|
+
articles: relation.many(ArticleSchema, "author_id"),
|
|
1585
|
+
},
|
|
1586
|
+
}));
|
|
1587
|
+
});
|
|
1588
|
+
// introspect: returns null for non-existent table
|
|
1589
|
+
test.case("schema: introspect non-existent", async (assert) => {
|
|
1590
|
+
assert(await db.schema.introspect("no_such_table")).null();
|
|
1591
|
+
});
|
|
1592
|
+
// UUID pk (User): wrong JS type
|
|
1593
|
+
$store("pk: get rejects non-string for uuid pk", User, async (assert) => {
|
|
1594
|
+
await throws(assert, Code.pk_invalid, () => User.get(123));
|
|
1595
|
+
});
|
|
1596
|
+
$store("pk: has rejects non-string for uuid pk", User, async (assert) => {
|
|
1597
|
+
await throws(assert, Code.pk_invalid, () => User.has(123));
|
|
1598
|
+
});
|
|
1599
|
+
$store("pk: delete rejects non-string for uuid pk", User, async (assert) => {
|
|
1600
|
+
await throws(assert, Code.pk_invalid, () => User.delete(123));
|
|
1601
|
+
});
|
|
1602
|
+
$store("pk: update rejects non-string for uuid pk", User, async (assert) => {
|
|
1603
|
+
await throws(assert, Code.pk_invalid, () => User.update(123, { set: { name: "x" } }));
|
|
1604
|
+
});
|
|
1605
|
+
// UUID pk (User): valid JS type but malformed UUID
|
|
1606
|
+
$store("pk: get rejects malformed uuid", User, async (assert) => {
|
|
1607
|
+
await throws(assert, Code.pk_invalid, () => User.get("not-a-uuid"));
|
|
1608
|
+
});
|
|
1609
|
+
$store("pk: has rejects malformed uuid", User, async (assert) => {
|
|
1610
|
+
await throws(assert, Code.pk_invalid, () => User.has("not-a-uuid"));
|
|
1611
|
+
});
|
|
1612
|
+
$store("pk: delete rejects malformed uuid", User, async (assert) => {
|
|
1613
|
+
await throws(assert, Code.pk_invalid, () => User.delete("not-a-uuid"));
|
|
1614
|
+
});
|
|
1615
|
+
$store("pk: update rejects malformed uuid", User, async (assert) => {
|
|
1616
|
+
await throws(assert, Code.pk_invalid, () => User.update("not-a-uuid", { set: { name: "x" } }));
|
|
1617
|
+
});
|
|
1618
|
+
// Numeric pk (UserN u32): wrong JS type
|
|
1619
|
+
$store("pk: get rejects string for numeric pk", UserN, async (assert) => {
|
|
1620
|
+
await throws(assert, Code.pk_invalid, () => UserN.get("abc"));
|
|
1621
|
+
});
|
|
1622
|
+
$store("pk: has rejects string for numeric pk", UserN, async (assert) => {
|
|
1623
|
+
await throws(assert, Code.pk_invalid, () => UserN.has("abc"));
|
|
1624
|
+
});
|
|
1625
|
+
$store("pk: delete rejects string for numeric pk", UserN, async (assert) => {
|
|
1626
|
+
await throws(assert, Code.pk_invalid, () => UserN.delete("abc"));
|
|
1627
|
+
});
|
|
1628
|
+
$store("pk: update rejects string for numeric pk", UserN, async (assert) => {
|
|
1629
|
+
await throws(assert, Code.pk_invalid, () => UserN.update("abc", { set: { name: "x" } }));
|
|
1630
|
+
});
|
|
1631
|
+
// Numeric pk (UserN u32): negative value
|
|
1632
|
+
$store("pk: get rejects negative for unsigned pk", UserN, async (assert) => {
|
|
1633
|
+
await throws(assert, Code.pk_invalid, () => UserN.get(-1));
|
|
1634
|
+
});
|
|
1635
|
+
// Bigint pk (UserB u128): wrong JS type
|
|
1636
|
+
$store("pk: get rejects string for bigint pk", UserB, async (assert) => {
|
|
1637
|
+
await throws(assert, Code.pk_invalid, () => UserB.get("abc"));
|
|
1638
|
+
});
|
|
1639
|
+
$store("pk: get rejects negative for unsigned bigint pk", UserB, async (assert) => {
|
|
1640
|
+
await throws(assert, Code.pk_invalid, () => UserB.get(-1n));
|
|
1641
|
+
});
|
|
1642
|
+
// introspect: returns correct types after create
|
|
1643
|
+
$store("schema: introspect after create", User, async (assert) => {
|
|
1644
|
+
// insert a record so MongoDB has something to sample
|
|
1645
|
+
await User.insert({ name: "test", age: 1, lastname: "test" });
|
|
1646
|
+
const types = await db.schema.introspect(User.name, User.pk);
|
|
1647
|
+
assert(types).not.null();
|
|
1648
|
+
if (types !== null) {
|
|
1649
|
+
assert(types.id).includes("uuid");
|
|
1650
|
+
assert(types.name).includes("string");
|
|
1651
|
+
assert(types.age).includes("u8");
|
|
1652
|
+
assert(types.lastname).includes("string");
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
// alter: add a field
|
|
1656
|
+
$store("schema: alter add", User, async (assert) => {
|
|
1657
|
+
await db.schema.alter("user", {
|
|
1658
|
+
add: { email: "string" }, drop: [], rename: [],
|
|
1659
|
+
});
|
|
1660
|
+
const MigratedUser = store({
|
|
1661
|
+
name: "user",
|
|
1662
|
+
db,
|
|
1663
|
+
schema: {
|
|
1664
|
+
id: key.primary(p.uuid),
|
|
1665
|
+
age: p.u8.optional(),
|
|
1666
|
+
lastname: p.string.optional(),
|
|
1667
|
+
name: p.string.default("Donald"),
|
|
1668
|
+
email: p.string.email(),
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
await MigratedUser.insert({ name: "test", age: 1, email: "test@test.com" });
|
|
1672
|
+
const types = await db.schema.introspect("user");
|
|
1673
|
+
assert(types.email).includes("string");
|
|
1674
|
+
});
|
|
1675
|
+
// alter: drop a field
|
|
1676
|
+
$store("schema: alter drop", User, async (assert) => {
|
|
1677
|
+
await db.schema.alter("user", { add: {}, drop: ["lastname"], rename: [] });
|
|
1678
|
+
const types = await db.schema.introspect("user");
|
|
1679
|
+
assert(types.lastname).undefined();
|
|
1680
|
+
});
|
|
1681
|
+
// alter: rename a field
|
|
1682
|
+
$store("schema: alter rename", User, async (assert) => {
|
|
1683
|
+
await db.schema.alter("user", {
|
|
1684
|
+
add: {}, drop: [], rename: [["lastname", "surname"]],
|
|
1685
|
+
});
|
|
1686
|
+
const MigratedUser = store({
|
|
1687
|
+
name: "user",
|
|
1688
|
+
db,
|
|
1689
|
+
schema: {
|
|
1690
|
+
id: key.primary(p.uuid),
|
|
1691
|
+
age: p.u8.optional(),
|
|
1692
|
+
surname: p.string.optional(),
|
|
1693
|
+
name: p.string.default("Donald"),
|
|
1694
|
+
},
|
|
1695
|
+
});
|
|
1696
|
+
await MigratedUser.insert({ name: "Donald", surname: "Adams" });
|
|
1697
|
+
const types = await db.schema.introspect("user");
|
|
1698
|
+
assert(types.lastname).undefined();
|
|
1699
|
+
assert(types.surname).includes("string");
|
|
1700
|
+
});
|
|
1701
|
+
// alter: combined add + drop
|
|
1702
|
+
$store("schema: alter add and drop", User, async (assert) => {
|
|
1703
|
+
await db.schema.alter("user", {
|
|
1704
|
+
add: { email: "string" }, drop: ["lastname"], rename: [],
|
|
1705
|
+
});
|
|
1706
|
+
const MigratedUser = store({
|
|
1707
|
+
name: "user",
|
|
1708
|
+
db,
|
|
1709
|
+
schema: {
|
|
1710
|
+
id: key.primary(p.uuid),
|
|
1711
|
+
age: p.u8.optional(),
|
|
1712
|
+
name: p.string.default("Donald"),
|
|
1713
|
+
email: p.string.email(),
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
await MigratedUser.insert({ name: "Donald", email: "test@test.com" });
|
|
1717
|
+
const types = await db.schema.introspect("user");
|
|
1718
|
+
assert(types.email).includes("string");
|
|
1719
|
+
assert(types.lastname).undefined();
|
|
1720
|
+
});
|
|
1721
|
+
const UUIDTest = store({
|
|
1722
|
+
name: "uuid_test",
|
|
1723
|
+
db,
|
|
1724
|
+
schema: {
|
|
1725
|
+
id: key.primary(p.uuid),
|
|
1726
|
+
uuid: p.uuid,
|
|
1727
|
+
uuid_v4: p.uuid.v4(),
|
|
1728
|
+
uuid_v7: p.uuid.v7(),
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
$store("uuid: round-trips uuid column", UUIDTest, async (assert) => {
|
|
1732
|
+
const uuid = "4d0996db-bda9-4f95-ad7c-7075b10d4ba6"; // any valid uuid
|
|
1733
|
+
const uuid_v4 = "7c9e6679-7425-40de-944b-e07fc1f90ae7"; // distinct v4
|
|
1734
|
+
const uuid_v7 = "01932b6e-5a2f-7e4f-9a3b-4f6d2c8b1a0e"; // v7
|
|
1735
|
+
const row = await UUIDTest.insert({ uuid, uuid_v4, uuid_v7 });
|
|
1736
|
+
assert(row.uuid).equals(uuid).type();
|
|
1737
|
+
assert(row.uuid_v4).equals(uuid_v4).type();
|
|
1738
|
+
assert(row.uuid_v7).equals(uuid_v7).type();
|
|
1739
|
+
const fetched = await UUIDTest.get(row.id);
|
|
1740
|
+
assert(fetched.uuid).equals(uuid);
|
|
1741
|
+
assert(fetched.uuid_v4).equals(uuid_v4);
|
|
1742
|
+
assert(fetched.uuid_v7).equals(uuid_v7);
|
|
1743
|
+
});
|
|
1744
|
+
test.case("schema: alter non-existent throws", async (assert) => {
|
|
1745
|
+
await throws(assert, Code.table_not_found, () => db.schema.alter("no_such_table", {
|
|
1746
|
+
add: { email: "string" }, drop: [], rename: [],
|
|
1747
|
+
}));
|
|
1748
|
+
});
|
|
1749
|
+
};
|
|
1750
|
+
//# sourceMappingURL=test.js.map
|