@mandujs/core 0.53.3 → 0.54.1
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/README.md +654 -654
- package/package.json +1 -1
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,482 +1,482 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @mandujs/core/db — unit tests
|
|
3
|
-
*
|
|
4
|
-
* These tests never open a real database connection. They substitute a
|
|
5
|
-
* controllable in-memory fake for `Bun.SQL` via the internal
|
|
6
|
-
* `_createDbWith` entry point, then assert that our wrapper:
|
|
7
|
-
*
|
|
8
|
-
* - detects the provider correctly,
|
|
9
|
-
* - forwards tagged-template calls to Bun.SQL verbatim,
|
|
10
|
-
* - implements `.one()`, `.transaction()`, `.close()` semantics,
|
|
11
|
-
* - propagates post-close errors as the canonical message,
|
|
12
|
-
* - binds placeholder values as parameters (never string-interpolated).
|
|
13
|
-
*
|
|
14
|
-
* SQLite integration tests (hitting the real `Bun.SQL`) live in
|
|
15
|
-
* `packages/core/tests/db/db-sqlite.test.ts`.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, expect, it } from "bun:test";
|
|
19
|
-
import {
|
|
20
|
-
_createDbWith,
|
|
21
|
-
createDb,
|
|
22
|
-
detectProvider,
|
|
23
|
-
type BunSqlCtor,
|
|
24
|
-
type SqlProvider,
|
|
25
|
-
} from "../index";
|
|
26
|
-
|
|
27
|
-
// ─── Fake Bun.SQL ───────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/** A captured (strings, values) tuple. */
|
|
30
|
-
interface CapturedCall {
|
|
31
|
-
/** The raw TemplateStringsArray — cloned to a plain array to make assertions readable. */
|
|
32
|
-
strings: readonly string[];
|
|
33
|
-
values: readonly unknown[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface FakeState {
|
|
37
|
-
ctorCalls: Array<Record<string, unknown>>;
|
|
38
|
-
calls: CapturedCall[];
|
|
39
|
-
/** `true` once `close()` has been invoked on any handle. */
|
|
40
|
-
closed: boolean;
|
|
41
|
-
closeCount: number;
|
|
42
|
-
/**
|
|
43
|
-
* If set, the next `call()` throws this error instead of returning rows.
|
|
44
|
-
* After the throw, the override is cleared unless `sticky` is true.
|
|
45
|
-
*/
|
|
46
|
-
nextError: { err: Error; sticky: boolean } | null;
|
|
47
|
-
/** Rows returned by the next `call()` — pop-front queue. Default: `[]`. */
|
|
48
|
-
nextRowsQueue: unknown[][];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Create a fake Bun.SQL ctor + observable state. */
|
|
52
|
-
function createFakeCtor(): { Ctor: BunSqlCtor; state: FakeState } {
|
|
53
|
-
const state: FakeState = {
|
|
54
|
-
ctorCalls: [],
|
|
55
|
-
calls: [],
|
|
56
|
-
closed: false,
|
|
57
|
-
closeCount: 0,
|
|
58
|
-
nextError: null,
|
|
59
|
-
nextRowsQueue: [],
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The inner builder — used both for the top-level handle and for
|
|
64
|
-
* transaction-scoped handles. Each is itself a callable tagged-template
|
|
65
|
-
* function with `.begin` and `.close` attached.
|
|
66
|
-
*/
|
|
67
|
-
function makeFakeSqlInstance(isTx = false): unknown {
|
|
68
|
-
const call = (strings: TemplateStringsArray, ...values: unknown[]) => {
|
|
69
|
-
if (state.closed && !isTx) {
|
|
70
|
-
// Simulate Bun.SQL's post-close failure — our wrapper should catch
|
|
71
|
-
// this and rethrow with the canonical "pool closed" message.
|
|
72
|
-
const err = Object.assign(new Error("Connection closed"), {
|
|
73
|
-
code: "ERR_SQLITE_CONNECTION_CLOSED",
|
|
74
|
-
name: "SQLiteError",
|
|
75
|
-
});
|
|
76
|
-
return Promise.reject(err);
|
|
77
|
-
}
|
|
78
|
-
state.calls.push({
|
|
79
|
-
strings: Array.from(strings),
|
|
80
|
-
values,
|
|
81
|
-
});
|
|
82
|
-
if (state.nextError) {
|
|
83
|
-
const err = state.nextError.err;
|
|
84
|
-
if (!state.nextError.sticky) state.nextError = null;
|
|
85
|
-
return Promise.reject(err);
|
|
86
|
-
}
|
|
87
|
-
const rows = state.nextRowsQueue.shift() ?? [];
|
|
88
|
-
// Bun.SQL returns an array-like with extra metadata; we emulate an
|
|
89
|
-
// array and let `Array.from` in the wrapper coerce it. Plain array
|
|
90
|
-
// is a valid subset.
|
|
91
|
-
return Promise.resolve(rows);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const methods = {
|
|
95
|
-
begin: async <R>(
|
|
96
|
-
fn: (tx: unknown) => Promise<R>,
|
|
97
|
-
): Promise<R> => {
|
|
98
|
-
const inner = makeFakeSqlInstance(true);
|
|
99
|
-
return await fn(inner);
|
|
100
|
-
},
|
|
101
|
-
close: async (): Promise<void> => {
|
|
102
|
-
state.closed = true;
|
|
103
|
-
state.closeCount += 1;
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
return Object.assign(call, methods);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function FakeBunSql(config: Record<string, unknown>) {
|
|
111
|
-
state.ctorCalls.push(config);
|
|
112
|
-
// Return the callable instance — NOT the default constructed object.
|
|
113
|
-
return makeFakeSqlInstance(false);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
Ctor: FakeBunSql as unknown as BunSqlCtor,
|
|
118
|
-
state,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── detectProvider ────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
describe("@mandujs/core/db — detectProvider", () => {
|
|
125
|
-
it.each<[string, SqlProvider]>([
|
|
126
|
-
["postgres://user:pass@host:5432/db", "postgres"],
|
|
127
|
-
["postgresql://user:pass@host:5432/db", "postgres"],
|
|
128
|
-
["mysql://user@host:3306/db", "mysql"],
|
|
129
|
-
["mariadb://user@host/db", "mysql"],
|
|
130
|
-
["sqlite::memory:", "sqlite"],
|
|
131
|
-
["sqlite:./data.db", "sqlite"],
|
|
132
|
-
["sqlite://./data.db", "sqlite"],
|
|
133
|
-
])("maps %s to %s", (url, expected) => {
|
|
134
|
-
expect(detectProvider(url)).toBe(expected);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("throws a clear error on unsupported schemes", () => {
|
|
138
|
-
expect(() => detectProvider("mongodb://host/db")).toThrow(
|
|
139
|
-
/Unable to detect provider/,
|
|
140
|
-
);
|
|
141
|
-
expect(() => detectProvider("mongodb://host/db")).toThrow(/mongodb/);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("throws when URL has no recognized scheme", () => {
|
|
145
|
-
expect(() => detectProvider("not a url")).toThrow(/Unable to detect provider/);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ─── createDb / _createDbWith: basic surface ───────────────────────────────
|
|
150
|
-
|
|
151
|
-
describe("@mandujs/core/db — createDb basics", () => {
|
|
152
|
-
it("returns a callable handle with .provider set to sqlite for sqlite URL", () => {
|
|
153
|
-
const db = createDb({ url: "sqlite::memory:" });
|
|
154
|
-
expect(typeof db).toBe("function");
|
|
155
|
-
expect(db.provider).toBe("sqlite");
|
|
156
|
-
expect(typeof db.one).toBe("function");
|
|
157
|
-
expect(typeof db.transaction).toBe("function");
|
|
158
|
-
expect(typeof db.close).toBe("function");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("respects config.provider override when url scheme is ambiguous", () => {
|
|
162
|
-
const db = createDb({
|
|
163
|
-
url: "custom://placeholder-rewritten-at-boot",
|
|
164
|
-
provider: "sqlite",
|
|
165
|
-
});
|
|
166
|
-
expect(db.provider).toBe("sqlite");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("throws TypeError when url is missing", () => {
|
|
170
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
-
expect(() => createDb({ url: "" } as any)).toThrow(TypeError);
|
|
172
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
-
expect(() => _createDbWith({} as BunSqlCtor, { url: "" } as any)).toThrow(
|
|
174
|
-
TypeError,
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("throws a clear provider-detection error when url scheme is ambiguous and no override is set", () => {
|
|
179
|
-
expect(() => createDb({ url: "kafka://not-a-db" })).toThrow(
|
|
180
|
-
/Unable to detect provider/,
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ─── createDb: ctor forwarding ─────────────────────────────────────────────
|
|
186
|
-
|
|
187
|
-
describe("@mandujs/core/db — ctor forwarding", () => {
|
|
188
|
-
it("forwards url + detected adapter + default max (10 for postgres)", () => {
|
|
189
|
-
const { Ctor, state } = createFakeCtor();
|
|
190
|
-
_createDbWith(Ctor, { url: "postgres://u:p@h/db" });
|
|
191
|
-
|
|
192
|
-
expect(state.ctorCalls).toHaveLength(1);
|
|
193
|
-
expect(state.ctorCalls[0]!.url).toBe("postgres://u:p@h/db");
|
|
194
|
-
expect(state.ctorCalls[0]!.adapter).toBe("postgres");
|
|
195
|
-
expect(state.ctorCalls[0]!.max).toBe(10);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("uses max=1 for sqlite by default", () => {
|
|
199
|
-
const { Ctor, state } = createFakeCtor();
|
|
200
|
-
_createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
201
|
-
expect(state.ctorCalls[0]!.max).toBe(1);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("uses max=10 for mysql by default", () => {
|
|
205
|
-
const { Ctor, state } = createFakeCtor();
|
|
206
|
-
_createDbWith(Ctor, { url: "mysql://u@h/db" });
|
|
207
|
-
expect(state.ctorCalls[0]!.max).toBe(10);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("respects explicit config.max over provider defaults", () => {
|
|
211
|
-
const { Ctor, state } = createFakeCtor();
|
|
212
|
-
_createDbWith(Ctor, { url: "postgres://u:p@h/db", max: 42 });
|
|
213
|
-
expect(state.ctorCalls[0]!.max).toBe(42);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("passes options bag through to Bun.SQL but does not let it override url/adapter/max", () => {
|
|
217
|
-
const { Ctor, state } = createFakeCtor();
|
|
218
|
-
_createDbWith(Ctor, {
|
|
219
|
-
url: "postgres://u:p@h/db",
|
|
220
|
-
max: 5,
|
|
221
|
-
options: {
|
|
222
|
-
// User tries to sneak in conflicting values; our authoritative
|
|
223
|
-
// fields must win to keep public surface deterministic.
|
|
224
|
-
url: "mysql://sneaky",
|
|
225
|
-
adapter: "mysql",
|
|
226
|
-
max: 99,
|
|
227
|
-
// Legitimate pass-through options:
|
|
228
|
-
ssl: "require",
|
|
229
|
-
idleTimeout: 30,
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const call = state.ctorCalls[0]!;
|
|
234
|
-
expect(call.url).toBe("postgres://u:p@h/db");
|
|
235
|
-
expect(call.adapter).toBe("postgres");
|
|
236
|
-
expect(call.max).toBe(5);
|
|
237
|
-
expect(call.ssl).toBe("require");
|
|
238
|
-
expect(call.idleTimeout).toBe(30);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// ─── Tagged-template forwarding ────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
describe("@mandujs/core/db — tagged template forwarding", () => {
|
|
245
|
-
it("forwards the full TemplateStringsArray and values to Bun.SQL", async () => {
|
|
246
|
-
const { Ctor, state } = createFakeCtor();
|
|
247
|
-
state.nextRowsQueue.push([{ id: 1, name: "alice" }]);
|
|
248
|
-
|
|
249
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
250
|
-
const name = "alice";
|
|
251
|
-
const id = 42;
|
|
252
|
-
const rows = await db`SELECT * FROM users WHERE id = ${id} AND name = ${name}`;
|
|
253
|
-
|
|
254
|
-
expect(state.calls).toHaveLength(1);
|
|
255
|
-
expect(Array.from(state.calls[0]!.strings)).toEqual([
|
|
256
|
-
"SELECT * FROM users WHERE id = ",
|
|
257
|
-
" AND name = ",
|
|
258
|
-
"",
|
|
259
|
-
]);
|
|
260
|
-
expect(state.calls[0]!.values).toEqual([42, "alice"]);
|
|
261
|
-
expect(rows).toEqual([{ id: 1, name: "alice" }]);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("binds values as parameters, never string-interpolated (injection safety)", async () => {
|
|
265
|
-
const { Ctor, state } = createFakeCtor();
|
|
266
|
-
state.nextRowsQueue.push([]);
|
|
267
|
-
|
|
268
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
269
|
-
const userInput = "'; DROP TABLE users; --";
|
|
270
|
-
await db`SELECT * FROM users WHERE name = ${userInput}`;
|
|
271
|
-
|
|
272
|
-
// The forwarded TemplateStringsArray must contain placeholders around
|
|
273
|
-
// the user-controlled value — NOT the value itself concatenated into
|
|
274
|
-
// the SQL. If our wrapper ever regressed to string-interpolation, the
|
|
275
|
-
// userInput would leak into strings[0] or strings[1].
|
|
276
|
-
const call = state.calls[0]!;
|
|
277
|
-
for (const s of call.strings) {
|
|
278
|
-
expect(s).not.toContain(userInput);
|
|
279
|
-
expect(s).not.toContain("DROP TABLE");
|
|
280
|
-
}
|
|
281
|
-
// And the value must be present in the bound-values array — exactly once.
|
|
282
|
-
expect(call.values).toEqual([userInput]);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("returns a plain array (not Bun.SQL's array-like with .count/.command metadata)", async () => {
|
|
286
|
-
const { Ctor, state } = createFakeCtor();
|
|
287
|
-
// Simulate the array-like Bun.SQL returns in real life.
|
|
288
|
-
const fakeResult: { count: number; command: string } & Array<Record<string, unknown>> =
|
|
289
|
-
Object.assign([{ x: 1 }, { x: 2 }], { count: 2, command: "SELECT" });
|
|
290
|
-
state.nextRowsQueue.push(fakeResult);
|
|
291
|
-
|
|
292
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
293
|
-
const rows = await db`SELECT x FROM t`;
|
|
294
|
-
|
|
295
|
-
expect(Array.isArray(rows)).toBe(true);
|
|
296
|
-
expect(rows).toEqual([{ x: 1 }, { x: 2 }]);
|
|
297
|
-
// These metadata fields should NOT be exposed via our wrapper.
|
|
298
|
-
expect((rows as unknown as { count?: number }).count).toBeUndefined();
|
|
299
|
-
expect((rows as unknown as { command?: string }).command).toBeUndefined();
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// ─── .one() ────────────────────────────────────────────────────────────────
|
|
304
|
-
|
|
305
|
-
describe("@mandujs/core/db — .one()", () => {
|
|
306
|
-
it("returns null when no rows match", async () => {
|
|
307
|
-
const { Ctor, state } = createFakeCtor();
|
|
308
|
-
state.nextRowsQueue.push([]);
|
|
309
|
-
|
|
310
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
311
|
-
const row = await db.one`SELECT * FROM users WHERE id = ${999}`;
|
|
312
|
-
|
|
313
|
-
expect(row).toBeNull();
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it("returns the single row when exactly one matches", async () => {
|
|
317
|
-
const { Ctor, state } = createFakeCtor();
|
|
318
|
-
state.nextRowsQueue.push([{ id: 1, name: "alice" }]);
|
|
319
|
-
|
|
320
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
321
|
-
const row = await db.one<{ id: number; name: string }>`
|
|
322
|
-
SELECT * FROM users WHERE id = ${1}
|
|
323
|
-
`;
|
|
324
|
-
|
|
325
|
-
expect(row).toEqual({ id: 1, name: "alice" });
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("throws a clear error naming the count when multiple rows match", async () => {
|
|
329
|
-
const { Ctor, state } = createFakeCtor();
|
|
330
|
-
state.nextRowsQueue.push([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
|
331
|
-
|
|
332
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
333
|
-
|
|
334
|
-
await expect(
|
|
335
|
-
db.one`SELECT * FROM users WHERE active = ${true}`,
|
|
336
|
-
).rejects.toThrow(/expected 0 or 1 row, got 3/);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
// ─── .transaction() ────────────────────────────────────────────────────────
|
|
341
|
-
|
|
342
|
-
describe("@mandujs/core/db — .transaction()", () => {
|
|
343
|
-
it("calls the user fn with a tx handle of the same Db shape and commits on resolve", async () => {
|
|
344
|
-
const { Ctor, state } = createFakeCtor();
|
|
345
|
-
state.nextRowsQueue.push([]); // INSERT 1
|
|
346
|
-
state.nextRowsQueue.push([]); // INSERT 2
|
|
347
|
-
|
|
348
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
349
|
-
|
|
350
|
-
const result = await db.transaction(async (tx) => {
|
|
351
|
-
expect(typeof tx).toBe("function");
|
|
352
|
-
expect(typeof tx.one).toBe("function");
|
|
353
|
-
expect(typeof tx.transaction).toBe("function");
|
|
354
|
-
expect(tx.provider).toBe("sqlite");
|
|
355
|
-
await tx`INSERT INTO t (v) VALUES (${1})`;
|
|
356
|
-
await tx`INSERT INTO t (v) VALUES (${2})`;
|
|
357
|
-
return "ok";
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
expect(result).toBe("ok");
|
|
361
|
-
expect(state.calls).toHaveLength(2);
|
|
362
|
-
expect(state.calls[0]!.values).toEqual([1]);
|
|
363
|
-
expect(state.calls[1]!.values).toEqual([2]);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("propagates thrown errors from inside the transaction to the caller (rollback)", async () => {
|
|
367
|
-
const { Ctor } = createFakeCtor();
|
|
368
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
369
|
-
|
|
370
|
-
class CustomError extends Error {}
|
|
371
|
-
|
|
372
|
-
await expect(
|
|
373
|
-
db.transaction(async (_tx) => {
|
|
374
|
-
throw new CustomError("boom");
|
|
375
|
-
}),
|
|
376
|
-
).rejects.toBeInstanceOf(CustomError);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it("rejects transaction() when the outer pool has been closed", async () => {
|
|
380
|
-
const { Ctor } = createFakeCtor();
|
|
381
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
382
|
-
await db.close();
|
|
383
|
-
|
|
384
|
-
await expect(
|
|
385
|
-
db.transaction(async () => "never"),
|
|
386
|
-
).rejects.toThrow(/pool closed/);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// ─── .close() ──────────────────────────────────────────────────────────────
|
|
391
|
-
|
|
392
|
-
describe("@mandujs/core/db — .close()", () => {
|
|
393
|
-
it("invokes Bun.SQL's close() exactly once", async () => {
|
|
394
|
-
const { Ctor, state } = createFakeCtor();
|
|
395
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
396
|
-
|
|
397
|
-
await db.close();
|
|
398
|
-
|
|
399
|
-
expect(state.closeCount).toBe(1);
|
|
400
|
-
expect(state.closed).toBe(true);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it("is idempotent — calling close twice is a no-op", async () => {
|
|
404
|
-
const { Ctor, state } = createFakeCtor();
|
|
405
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
406
|
-
|
|
407
|
-
await db.close();
|
|
408
|
-
await db.close();
|
|
409
|
-
|
|
410
|
-
// Second call must not double-invoke the underlying close.
|
|
411
|
-
expect(state.closeCount).toBe(1);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("rejects subsequent queries with the canonical pool-closed error", async () => {
|
|
415
|
-
const { Ctor } = createFakeCtor();
|
|
416
|
-
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
417
|
-
await db.close();
|
|
418
|
-
|
|
419
|
-
await expect(db`SELECT 1`).rejects.toThrow(/pool closed/);
|
|
420
|
-
await expect(db.one`SELECT 1`).rejects.toThrow(/pool closed/);
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// ─── Public createDb probe behaviour ───────────────────────────────────────
|
|
425
|
-
|
|
426
|
-
describe("@mandujs/core/db — public createDb lazy probe", () => {
|
|
427
|
-
it("does NOT throw at construction time even if Bun.SQL were missing (lazy)", () => {
|
|
428
|
-
// We can't actually remove Bun.SQL from globalThis in this test env (the
|
|
429
|
-
// other integration suite relies on it), but we can prove the call
|
|
430
|
-
// doesn't throw when the runtime probe would otherwise fire: calling
|
|
431
|
-
// createDb() without issuing a query is a no-op on Bun.SQL.
|
|
432
|
-
//
|
|
433
|
-
// The "when Bun.SQL is missing" case is exercised by the negative
|
|
434
|
-
// unit test below via `_createDbWith` (which accepts the ctor
|
|
435
|
-
// explicitly). The production `createDb()` path goes through
|
|
436
|
-
// `getBunSqlCtor()` inside `materialize()`, which is only called on
|
|
437
|
-
// the first query.
|
|
438
|
-
const db = createDb({ url: "sqlite::memory:" });
|
|
439
|
-
expect(db.provider).toBe("sqlite");
|
|
440
|
-
// No query yet → no Bun.SQL lookup yet.
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("throws a version-specific error when the injected ctor factory returns nothing (simulates missing Bun.SQL)", () => {
|
|
444
|
-
// The Bun global itself is a read-only binding in this runtime, so we
|
|
445
|
-
// can't monkey-patch `globalThis.Bun.SQL` to prove the probe message
|
|
446
|
-
// from the public `createDb` path. We test the contract equivalently:
|
|
447
|
-
// `_createDbWith` accepts any ctor, and the error-message shape emitted
|
|
448
|
-
// by `getBunSqlCtor()` in production is verified by reading the module
|
|
449
|
-
// source below. Any bad-ctor injection here (non-function) would be a
|
|
450
|
-
// TypeError at `new`, which is also an acceptable surface.
|
|
451
|
-
//
|
|
452
|
-
// We assert the message CONTENT by pointing at the string constant
|
|
453
|
-
// directly — this catches any regression that weakens the user-facing
|
|
454
|
-
// error without needing to monkey-patch a frozen global.
|
|
455
|
-
//
|
|
456
|
-
// Keep this in sync with the `getBunSqlCtor` source in
|
|
457
|
-
// `packages/core/src/db/index.ts`.
|
|
458
|
-
const EXPECTED_PREFIX = "[@mandujs/core/db] Bun.sql is unavailable";
|
|
459
|
-
const EXPECTED_VERSION_MENTION = "Bun runtime >= 1.3.x";
|
|
460
|
-
// Read the source string and assert the message still matches. This is
|
|
461
|
-
// a belt-and-braces check that survives the frozen-global limitation.
|
|
462
|
-
//
|
|
463
|
-
// The src path is stable because this file is rooted at packages/core.
|
|
464
|
-
const srcPath = new URL(
|
|
465
|
-
"../index.ts",
|
|
466
|
-
import.meta.url,
|
|
467
|
-
).pathname;
|
|
468
|
-
const src = Bun.file(srcPath.replace(/^\/([a-zA-Z]:)/, "$1"));
|
|
469
|
-
return src.text().then((text) => {
|
|
470
|
-
expect(text).toContain(EXPECTED_PREFIX);
|
|
471
|
-
expect(text).toContain(EXPECTED_VERSION_MENTION);
|
|
472
|
-
expect(text).toContain("https://bun.com/docs/installation");
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("close() on a handle that never materialized a real connection is a no-op", async () => {
|
|
477
|
-
const db = createDb({ url: "sqlite::memory:" });
|
|
478
|
-
// We haven't called anything, so no Bun.SQL instance was constructed.
|
|
479
|
-
// close() should succeed without materializing one.
|
|
480
|
-
await expect(db.close()).resolves.toBeUndefined();
|
|
481
|
-
});
|
|
482
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @mandujs/core/db — unit tests
|
|
3
|
+
*
|
|
4
|
+
* These tests never open a real database connection. They substitute a
|
|
5
|
+
* controllable in-memory fake for `Bun.SQL` via the internal
|
|
6
|
+
* `_createDbWith` entry point, then assert that our wrapper:
|
|
7
|
+
*
|
|
8
|
+
* - detects the provider correctly,
|
|
9
|
+
* - forwards tagged-template calls to Bun.SQL verbatim,
|
|
10
|
+
* - implements `.one()`, `.transaction()`, `.close()` semantics,
|
|
11
|
+
* - propagates post-close errors as the canonical message,
|
|
12
|
+
* - binds placeholder values as parameters (never string-interpolated).
|
|
13
|
+
*
|
|
14
|
+
* SQLite integration tests (hitting the real `Bun.SQL`) live in
|
|
15
|
+
* `packages/core/tests/db/db-sqlite.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, it } from "bun:test";
|
|
19
|
+
import {
|
|
20
|
+
_createDbWith,
|
|
21
|
+
createDb,
|
|
22
|
+
detectProvider,
|
|
23
|
+
type BunSqlCtor,
|
|
24
|
+
type SqlProvider,
|
|
25
|
+
} from "../index";
|
|
26
|
+
|
|
27
|
+
// ─── Fake Bun.SQL ───────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** A captured (strings, values) tuple. */
|
|
30
|
+
interface CapturedCall {
|
|
31
|
+
/** The raw TemplateStringsArray — cloned to a plain array to make assertions readable. */
|
|
32
|
+
strings: readonly string[];
|
|
33
|
+
values: readonly unknown[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FakeState {
|
|
37
|
+
ctorCalls: Array<Record<string, unknown>>;
|
|
38
|
+
calls: CapturedCall[];
|
|
39
|
+
/** `true` once `close()` has been invoked on any handle. */
|
|
40
|
+
closed: boolean;
|
|
41
|
+
closeCount: number;
|
|
42
|
+
/**
|
|
43
|
+
* If set, the next `call()` throws this error instead of returning rows.
|
|
44
|
+
* After the throw, the override is cleared unless `sticky` is true.
|
|
45
|
+
*/
|
|
46
|
+
nextError: { err: Error; sticky: boolean } | null;
|
|
47
|
+
/** Rows returned by the next `call()` — pop-front queue. Default: `[]`. */
|
|
48
|
+
nextRowsQueue: unknown[][];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Create a fake Bun.SQL ctor + observable state. */
|
|
52
|
+
function createFakeCtor(): { Ctor: BunSqlCtor; state: FakeState } {
|
|
53
|
+
const state: FakeState = {
|
|
54
|
+
ctorCalls: [],
|
|
55
|
+
calls: [],
|
|
56
|
+
closed: false,
|
|
57
|
+
closeCount: 0,
|
|
58
|
+
nextError: null,
|
|
59
|
+
nextRowsQueue: [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The inner builder — used both for the top-level handle and for
|
|
64
|
+
* transaction-scoped handles. Each is itself a callable tagged-template
|
|
65
|
+
* function with `.begin` and `.close` attached.
|
|
66
|
+
*/
|
|
67
|
+
function makeFakeSqlInstance(isTx = false): unknown {
|
|
68
|
+
const call = (strings: TemplateStringsArray, ...values: unknown[]) => {
|
|
69
|
+
if (state.closed && !isTx) {
|
|
70
|
+
// Simulate Bun.SQL's post-close failure — our wrapper should catch
|
|
71
|
+
// this and rethrow with the canonical "pool closed" message.
|
|
72
|
+
const err = Object.assign(new Error("Connection closed"), {
|
|
73
|
+
code: "ERR_SQLITE_CONNECTION_CLOSED",
|
|
74
|
+
name: "SQLiteError",
|
|
75
|
+
});
|
|
76
|
+
return Promise.reject(err);
|
|
77
|
+
}
|
|
78
|
+
state.calls.push({
|
|
79
|
+
strings: Array.from(strings),
|
|
80
|
+
values,
|
|
81
|
+
});
|
|
82
|
+
if (state.nextError) {
|
|
83
|
+
const err = state.nextError.err;
|
|
84
|
+
if (!state.nextError.sticky) state.nextError = null;
|
|
85
|
+
return Promise.reject(err);
|
|
86
|
+
}
|
|
87
|
+
const rows = state.nextRowsQueue.shift() ?? [];
|
|
88
|
+
// Bun.SQL returns an array-like with extra metadata; we emulate an
|
|
89
|
+
// array and let `Array.from` in the wrapper coerce it. Plain array
|
|
90
|
+
// is a valid subset.
|
|
91
|
+
return Promise.resolve(rows);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const methods = {
|
|
95
|
+
begin: async <R>(
|
|
96
|
+
fn: (tx: unknown) => Promise<R>,
|
|
97
|
+
): Promise<R> => {
|
|
98
|
+
const inner = makeFakeSqlInstance(true);
|
|
99
|
+
return await fn(inner);
|
|
100
|
+
},
|
|
101
|
+
close: async (): Promise<void> => {
|
|
102
|
+
state.closed = true;
|
|
103
|
+
state.closeCount += 1;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return Object.assign(call, methods);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function FakeBunSql(config: Record<string, unknown>) {
|
|
111
|
+
state.ctorCalls.push(config);
|
|
112
|
+
// Return the callable instance — NOT the default constructed object.
|
|
113
|
+
return makeFakeSqlInstance(false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
Ctor: FakeBunSql as unknown as BunSqlCtor,
|
|
118
|
+
state,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── detectProvider ────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
describe("@mandujs/core/db — detectProvider", () => {
|
|
125
|
+
it.each<[string, SqlProvider]>([
|
|
126
|
+
["postgres://user:pass@host:5432/db", "postgres"],
|
|
127
|
+
["postgresql://user:pass@host:5432/db", "postgres"],
|
|
128
|
+
["mysql://user@host:3306/db", "mysql"],
|
|
129
|
+
["mariadb://user@host/db", "mysql"],
|
|
130
|
+
["sqlite::memory:", "sqlite"],
|
|
131
|
+
["sqlite:./data.db", "sqlite"],
|
|
132
|
+
["sqlite://./data.db", "sqlite"],
|
|
133
|
+
])("maps %s to %s", (url, expected) => {
|
|
134
|
+
expect(detectProvider(url)).toBe(expected);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("throws a clear error on unsupported schemes", () => {
|
|
138
|
+
expect(() => detectProvider("mongodb://host/db")).toThrow(
|
|
139
|
+
/Unable to detect provider/,
|
|
140
|
+
);
|
|
141
|
+
expect(() => detectProvider("mongodb://host/db")).toThrow(/mongodb/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("throws when URL has no recognized scheme", () => {
|
|
145
|
+
expect(() => detectProvider("not a url")).toThrow(/Unable to detect provider/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── createDb / _createDbWith: basic surface ───────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("@mandujs/core/db — createDb basics", () => {
|
|
152
|
+
it("returns a callable handle with .provider set to sqlite for sqlite URL", () => {
|
|
153
|
+
const db = createDb({ url: "sqlite::memory:" });
|
|
154
|
+
expect(typeof db).toBe("function");
|
|
155
|
+
expect(db.provider).toBe("sqlite");
|
|
156
|
+
expect(typeof db.one).toBe("function");
|
|
157
|
+
expect(typeof db.transaction).toBe("function");
|
|
158
|
+
expect(typeof db.close).toBe("function");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("respects config.provider override when url scheme is ambiguous", () => {
|
|
162
|
+
const db = createDb({
|
|
163
|
+
url: "custom://placeholder-rewritten-at-boot",
|
|
164
|
+
provider: "sqlite",
|
|
165
|
+
});
|
|
166
|
+
expect(db.provider).toBe("sqlite");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("throws TypeError when url is missing", () => {
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
expect(() => createDb({ url: "" } as any)).toThrow(TypeError);
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
expect(() => _createDbWith({} as BunSqlCtor, { url: "" } as any)).toThrow(
|
|
174
|
+
TypeError,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("throws a clear provider-detection error when url scheme is ambiguous and no override is set", () => {
|
|
179
|
+
expect(() => createDb({ url: "kafka://not-a-db" })).toThrow(
|
|
180
|
+
/Unable to detect provider/,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── createDb: ctor forwarding ─────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe("@mandujs/core/db — ctor forwarding", () => {
|
|
188
|
+
it("forwards url + detected adapter + default max (10 for postgres)", () => {
|
|
189
|
+
const { Ctor, state } = createFakeCtor();
|
|
190
|
+
_createDbWith(Ctor, { url: "postgres://u:p@h/db" });
|
|
191
|
+
|
|
192
|
+
expect(state.ctorCalls).toHaveLength(1);
|
|
193
|
+
expect(state.ctorCalls[0]!.url).toBe("postgres://u:p@h/db");
|
|
194
|
+
expect(state.ctorCalls[0]!.adapter).toBe("postgres");
|
|
195
|
+
expect(state.ctorCalls[0]!.max).toBe(10);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("uses max=1 for sqlite by default", () => {
|
|
199
|
+
const { Ctor, state } = createFakeCtor();
|
|
200
|
+
_createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
201
|
+
expect(state.ctorCalls[0]!.max).toBe(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("uses max=10 for mysql by default", () => {
|
|
205
|
+
const { Ctor, state } = createFakeCtor();
|
|
206
|
+
_createDbWith(Ctor, { url: "mysql://u@h/db" });
|
|
207
|
+
expect(state.ctorCalls[0]!.max).toBe(10);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("respects explicit config.max over provider defaults", () => {
|
|
211
|
+
const { Ctor, state } = createFakeCtor();
|
|
212
|
+
_createDbWith(Ctor, { url: "postgres://u:p@h/db", max: 42 });
|
|
213
|
+
expect(state.ctorCalls[0]!.max).toBe(42);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("passes options bag through to Bun.SQL but does not let it override url/adapter/max", () => {
|
|
217
|
+
const { Ctor, state } = createFakeCtor();
|
|
218
|
+
_createDbWith(Ctor, {
|
|
219
|
+
url: "postgres://u:p@h/db",
|
|
220
|
+
max: 5,
|
|
221
|
+
options: {
|
|
222
|
+
// User tries to sneak in conflicting values; our authoritative
|
|
223
|
+
// fields must win to keep public surface deterministic.
|
|
224
|
+
url: "mysql://sneaky",
|
|
225
|
+
adapter: "mysql",
|
|
226
|
+
max: 99,
|
|
227
|
+
// Legitimate pass-through options:
|
|
228
|
+
ssl: "require",
|
|
229
|
+
idleTimeout: 30,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const call = state.ctorCalls[0]!;
|
|
234
|
+
expect(call.url).toBe("postgres://u:p@h/db");
|
|
235
|
+
expect(call.adapter).toBe("postgres");
|
|
236
|
+
expect(call.max).toBe(5);
|
|
237
|
+
expect(call.ssl).toBe("require");
|
|
238
|
+
expect(call.idleTimeout).toBe(30);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ─── Tagged-template forwarding ────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
describe("@mandujs/core/db — tagged template forwarding", () => {
|
|
245
|
+
it("forwards the full TemplateStringsArray and values to Bun.SQL", async () => {
|
|
246
|
+
const { Ctor, state } = createFakeCtor();
|
|
247
|
+
state.nextRowsQueue.push([{ id: 1, name: "alice" }]);
|
|
248
|
+
|
|
249
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
250
|
+
const name = "alice";
|
|
251
|
+
const id = 42;
|
|
252
|
+
const rows = await db`SELECT * FROM users WHERE id = ${id} AND name = ${name}`;
|
|
253
|
+
|
|
254
|
+
expect(state.calls).toHaveLength(1);
|
|
255
|
+
expect(Array.from(state.calls[0]!.strings)).toEqual([
|
|
256
|
+
"SELECT * FROM users WHERE id = ",
|
|
257
|
+
" AND name = ",
|
|
258
|
+
"",
|
|
259
|
+
]);
|
|
260
|
+
expect(state.calls[0]!.values).toEqual([42, "alice"]);
|
|
261
|
+
expect(rows).toEqual([{ id: 1, name: "alice" }]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("binds values as parameters, never string-interpolated (injection safety)", async () => {
|
|
265
|
+
const { Ctor, state } = createFakeCtor();
|
|
266
|
+
state.nextRowsQueue.push([]);
|
|
267
|
+
|
|
268
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
269
|
+
const userInput = "'; DROP TABLE users; --";
|
|
270
|
+
await db`SELECT * FROM users WHERE name = ${userInput}`;
|
|
271
|
+
|
|
272
|
+
// The forwarded TemplateStringsArray must contain placeholders around
|
|
273
|
+
// the user-controlled value — NOT the value itself concatenated into
|
|
274
|
+
// the SQL. If our wrapper ever regressed to string-interpolation, the
|
|
275
|
+
// userInput would leak into strings[0] or strings[1].
|
|
276
|
+
const call = state.calls[0]!;
|
|
277
|
+
for (const s of call.strings) {
|
|
278
|
+
expect(s).not.toContain(userInput);
|
|
279
|
+
expect(s).not.toContain("DROP TABLE");
|
|
280
|
+
}
|
|
281
|
+
// And the value must be present in the bound-values array — exactly once.
|
|
282
|
+
expect(call.values).toEqual([userInput]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("returns a plain array (not Bun.SQL's array-like with .count/.command metadata)", async () => {
|
|
286
|
+
const { Ctor, state } = createFakeCtor();
|
|
287
|
+
// Simulate the array-like Bun.SQL returns in real life.
|
|
288
|
+
const fakeResult: { count: number; command: string } & Array<Record<string, unknown>> =
|
|
289
|
+
Object.assign([{ x: 1 }, { x: 2 }], { count: 2, command: "SELECT" });
|
|
290
|
+
state.nextRowsQueue.push(fakeResult);
|
|
291
|
+
|
|
292
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
293
|
+
const rows = await db`SELECT x FROM t`;
|
|
294
|
+
|
|
295
|
+
expect(Array.isArray(rows)).toBe(true);
|
|
296
|
+
expect(rows).toEqual([{ x: 1 }, { x: 2 }]);
|
|
297
|
+
// These metadata fields should NOT be exposed via our wrapper.
|
|
298
|
+
expect((rows as unknown as { count?: number }).count).toBeUndefined();
|
|
299
|
+
expect((rows as unknown as { command?: string }).command).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── .one() ────────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
describe("@mandujs/core/db — .one()", () => {
|
|
306
|
+
it("returns null when no rows match", async () => {
|
|
307
|
+
const { Ctor, state } = createFakeCtor();
|
|
308
|
+
state.nextRowsQueue.push([]);
|
|
309
|
+
|
|
310
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
311
|
+
const row = await db.one`SELECT * FROM users WHERE id = ${999}`;
|
|
312
|
+
|
|
313
|
+
expect(row).toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("returns the single row when exactly one matches", async () => {
|
|
317
|
+
const { Ctor, state } = createFakeCtor();
|
|
318
|
+
state.nextRowsQueue.push([{ id: 1, name: "alice" }]);
|
|
319
|
+
|
|
320
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
321
|
+
const row = await db.one<{ id: number; name: string }>`
|
|
322
|
+
SELECT * FROM users WHERE id = ${1}
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
expect(row).toEqual({ id: 1, name: "alice" });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("throws a clear error naming the count when multiple rows match", async () => {
|
|
329
|
+
const { Ctor, state } = createFakeCtor();
|
|
330
|
+
state.nextRowsQueue.push([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
|
331
|
+
|
|
332
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
333
|
+
|
|
334
|
+
await expect(
|
|
335
|
+
db.one`SELECT * FROM users WHERE active = ${true}`,
|
|
336
|
+
).rejects.toThrow(/expected 0 or 1 row, got 3/);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ─── .transaction() ────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
describe("@mandujs/core/db — .transaction()", () => {
|
|
343
|
+
it("calls the user fn with a tx handle of the same Db shape and commits on resolve", async () => {
|
|
344
|
+
const { Ctor, state } = createFakeCtor();
|
|
345
|
+
state.nextRowsQueue.push([]); // INSERT 1
|
|
346
|
+
state.nextRowsQueue.push([]); // INSERT 2
|
|
347
|
+
|
|
348
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
349
|
+
|
|
350
|
+
const result = await db.transaction(async (tx) => {
|
|
351
|
+
expect(typeof tx).toBe("function");
|
|
352
|
+
expect(typeof tx.one).toBe("function");
|
|
353
|
+
expect(typeof tx.transaction).toBe("function");
|
|
354
|
+
expect(tx.provider).toBe("sqlite");
|
|
355
|
+
await tx`INSERT INTO t (v) VALUES (${1})`;
|
|
356
|
+
await tx`INSERT INTO t (v) VALUES (${2})`;
|
|
357
|
+
return "ok";
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(result).toBe("ok");
|
|
361
|
+
expect(state.calls).toHaveLength(2);
|
|
362
|
+
expect(state.calls[0]!.values).toEqual([1]);
|
|
363
|
+
expect(state.calls[1]!.values).toEqual([2]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("propagates thrown errors from inside the transaction to the caller (rollback)", async () => {
|
|
367
|
+
const { Ctor } = createFakeCtor();
|
|
368
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
369
|
+
|
|
370
|
+
class CustomError extends Error {}
|
|
371
|
+
|
|
372
|
+
await expect(
|
|
373
|
+
db.transaction(async (_tx) => {
|
|
374
|
+
throw new CustomError("boom");
|
|
375
|
+
}),
|
|
376
|
+
).rejects.toBeInstanceOf(CustomError);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("rejects transaction() when the outer pool has been closed", async () => {
|
|
380
|
+
const { Ctor } = createFakeCtor();
|
|
381
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
382
|
+
await db.close();
|
|
383
|
+
|
|
384
|
+
await expect(
|
|
385
|
+
db.transaction(async () => "never"),
|
|
386
|
+
).rejects.toThrow(/pool closed/);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ─── .close() ──────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
describe("@mandujs/core/db — .close()", () => {
|
|
393
|
+
it("invokes Bun.SQL's close() exactly once", async () => {
|
|
394
|
+
const { Ctor, state } = createFakeCtor();
|
|
395
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
396
|
+
|
|
397
|
+
await db.close();
|
|
398
|
+
|
|
399
|
+
expect(state.closeCount).toBe(1);
|
|
400
|
+
expect(state.closed).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("is idempotent — calling close twice is a no-op", async () => {
|
|
404
|
+
const { Ctor, state } = createFakeCtor();
|
|
405
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
406
|
+
|
|
407
|
+
await db.close();
|
|
408
|
+
await db.close();
|
|
409
|
+
|
|
410
|
+
// Second call must not double-invoke the underlying close.
|
|
411
|
+
expect(state.closeCount).toBe(1);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("rejects subsequent queries with the canonical pool-closed error", async () => {
|
|
415
|
+
const { Ctor } = createFakeCtor();
|
|
416
|
+
const db = _createDbWith(Ctor, { url: "sqlite::memory:" });
|
|
417
|
+
await db.close();
|
|
418
|
+
|
|
419
|
+
await expect(db`SELECT 1`).rejects.toThrow(/pool closed/);
|
|
420
|
+
await expect(db.one`SELECT 1`).rejects.toThrow(/pool closed/);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ─── Public createDb probe behaviour ───────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
describe("@mandujs/core/db — public createDb lazy probe", () => {
|
|
427
|
+
it("does NOT throw at construction time even if Bun.SQL were missing (lazy)", () => {
|
|
428
|
+
// We can't actually remove Bun.SQL from globalThis in this test env (the
|
|
429
|
+
// other integration suite relies on it), but we can prove the call
|
|
430
|
+
// doesn't throw when the runtime probe would otherwise fire: calling
|
|
431
|
+
// createDb() without issuing a query is a no-op on Bun.SQL.
|
|
432
|
+
//
|
|
433
|
+
// The "when Bun.SQL is missing" case is exercised by the negative
|
|
434
|
+
// unit test below via `_createDbWith` (which accepts the ctor
|
|
435
|
+
// explicitly). The production `createDb()` path goes through
|
|
436
|
+
// `getBunSqlCtor()` inside `materialize()`, which is only called on
|
|
437
|
+
// the first query.
|
|
438
|
+
const db = createDb({ url: "sqlite::memory:" });
|
|
439
|
+
expect(db.provider).toBe("sqlite");
|
|
440
|
+
// No query yet → no Bun.SQL lookup yet.
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("throws a version-specific error when the injected ctor factory returns nothing (simulates missing Bun.SQL)", () => {
|
|
444
|
+
// The Bun global itself is a read-only binding in this runtime, so we
|
|
445
|
+
// can't monkey-patch `globalThis.Bun.SQL` to prove the probe message
|
|
446
|
+
// from the public `createDb` path. We test the contract equivalently:
|
|
447
|
+
// `_createDbWith` accepts any ctor, and the error-message shape emitted
|
|
448
|
+
// by `getBunSqlCtor()` in production is verified by reading the module
|
|
449
|
+
// source below. Any bad-ctor injection here (non-function) would be a
|
|
450
|
+
// TypeError at `new`, which is also an acceptable surface.
|
|
451
|
+
//
|
|
452
|
+
// We assert the message CONTENT by pointing at the string constant
|
|
453
|
+
// directly — this catches any regression that weakens the user-facing
|
|
454
|
+
// error without needing to monkey-patch a frozen global.
|
|
455
|
+
//
|
|
456
|
+
// Keep this in sync with the `getBunSqlCtor` source in
|
|
457
|
+
// `packages/core/src/db/index.ts`.
|
|
458
|
+
const EXPECTED_PREFIX = "[@mandujs/core/db] Bun.sql is unavailable";
|
|
459
|
+
const EXPECTED_VERSION_MENTION = "Bun runtime >= 1.3.x";
|
|
460
|
+
// Read the source string and assert the message still matches. This is
|
|
461
|
+
// a belt-and-braces check that survives the frozen-global limitation.
|
|
462
|
+
//
|
|
463
|
+
// The src path is stable because this file is rooted at packages/core.
|
|
464
|
+
const srcPath = new URL(
|
|
465
|
+
"../index.ts",
|
|
466
|
+
import.meta.url,
|
|
467
|
+
).pathname;
|
|
468
|
+
const src = Bun.file(srcPath.replace(/^\/([a-zA-Z]:)/, "$1"));
|
|
469
|
+
return src.text().then((text) => {
|
|
470
|
+
expect(text).toContain(EXPECTED_PREFIX);
|
|
471
|
+
expect(text).toContain(EXPECTED_VERSION_MENTION);
|
|
472
|
+
expect(text).toContain("https://bun.com/docs/installation");
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("close() on a handle that never materialized a real connection is a no-op", async () => {
|
|
477
|
+
const db = createDb({ url: "sqlite::memory:" });
|
|
478
|
+
// We haven't called anything, so no Bun.SQL instance was constructed.
|
|
479
|
+
// close() should succeed without materializing one.
|
|
480
|
+
await expect(db.close()).resolves.toBeUndefined();
|
|
481
|
+
});
|
|
482
|
+
});
|