@buenojs/bueno 0.8.3 → 0.8.5
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 +136 -16
- package/dist/cli/{index.js → bin.js} +3036 -1421
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/package.json +121 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +392 -438
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +61 -0
- package/src/cli/templates/database/mysql.ts +14 -0
- package/src/cli/templates/database/none.ts +16 -0
- package/src/cli/templates/database/postgresql.ts +14 -0
- package/src/cli/templates/database/sqlite.ts +14 -0
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +63 -0
- package/src/cli/templates/frontend/none.ts +17 -0
- package/src/cli/templates/frontend/react.ts +140 -0
- package/src/cli/templates/frontend/solid.ts +134 -0
- package/src/cli/templates/frontend/svelte.ts +131 -0
- package/src/cli/templates/frontend/vue.ts +130 -0
- package/src/cli/templates/generators/index.ts +339 -0
- package/src/cli/templates/generators/types.ts +56 -0
- package/src/cli/templates/index.ts +35 -2
- package/src/cli/templates/project/api.ts +81 -0
- package/src/cli/templates/project/default.ts +140 -0
- package/src/cli/templates/project/fullstack.ts +111 -0
- package/src/cli/templates/project/index.ts +95 -0
- package/src/cli/templates/project/minimal.ts +45 -0
- package/src/cli/templates/project/types.ts +94 -0
- package/src/cli/templates/project/website.ts +263 -0
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -2
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +47 -0
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +545 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +179 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +409 -298
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
13
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
15
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
16
|
+
else
|
|
17
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
18
|
+
if (d = decorators[i])
|
|
19
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
};
|
|
22
|
+
var __legacyMetadataTS = (k, v) => {
|
|
23
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
|
|
24
|
+
return Reflect.metadata(k, v);
|
|
25
|
+
};
|
|
26
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
+
var __require = import.meta.require;
|
|
28
|
+
|
|
29
|
+
// src/context/index.ts
|
|
30
|
+
var exports_context = {};
|
|
31
|
+
__export(exports_context, {
|
|
32
|
+
createContext: () => createContext,
|
|
33
|
+
Context: () => Context
|
|
34
|
+
});
|
|
35
|
+
function parseCookies(cookieHeader) {
|
|
36
|
+
const cookies = {};
|
|
37
|
+
for (const part of cookieHeader.split(";")) {
|
|
38
|
+
const trimmed = part.trim();
|
|
39
|
+
if (trimmed) {
|
|
40
|
+
const [name, ...rest] = trimmed.split("=");
|
|
41
|
+
if (name && rest.length > 0) {
|
|
42
|
+
cookies[name.trim()] = rest.join("=").trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return cookies;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class Context {
|
|
50
|
+
req;
|
|
51
|
+
params;
|
|
52
|
+
query;
|
|
53
|
+
_cookies;
|
|
54
|
+
_bodyCache;
|
|
55
|
+
_response;
|
|
56
|
+
_variables = {};
|
|
57
|
+
constructor(request, params = {}) {
|
|
58
|
+
this.req = request;
|
|
59
|
+
this.params = params;
|
|
60
|
+
this.query = this.parseQuery(request.url);
|
|
61
|
+
this._response = {
|
|
62
|
+
status: 200,
|
|
63
|
+
headers: new Headers
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
get method() {
|
|
67
|
+
return this.req.method;
|
|
68
|
+
}
|
|
69
|
+
get url() {
|
|
70
|
+
return new URL(this.req.url);
|
|
71
|
+
}
|
|
72
|
+
get path() {
|
|
73
|
+
return this.url.pathname;
|
|
74
|
+
}
|
|
75
|
+
getHeader(name) {
|
|
76
|
+
return this.req.headers.get(name) ?? undefined;
|
|
77
|
+
}
|
|
78
|
+
getCookie(name) {
|
|
79
|
+
if (!this._cookies) {
|
|
80
|
+
const cookieHeader = this.req.headers.get("Cookie");
|
|
81
|
+
this._cookies = cookieHeader ? parseCookies(cookieHeader) : {};
|
|
82
|
+
}
|
|
83
|
+
return this._cookies[name];
|
|
84
|
+
}
|
|
85
|
+
get cookies() {
|
|
86
|
+
if (!this._cookies) {
|
|
87
|
+
const cookieHeader = this.req.headers.get("Cookie");
|
|
88
|
+
this._cookies = cookieHeader ? parseCookies(cookieHeader) : {};
|
|
89
|
+
}
|
|
90
|
+
return this._cookies;
|
|
91
|
+
}
|
|
92
|
+
async body() {
|
|
93
|
+
if (this._bodyCache !== undefined) {
|
|
94
|
+
return this._bodyCache;
|
|
95
|
+
}
|
|
96
|
+
const text = await this.req.text();
|
|
97
|
+
this._bodyCache = text ? JSON.parse(text) : null;
|
|
98
|
+
return this._bodyCache;
|
|
99
|
+
}
|
|
100
|
+
async parseBody() {
|
|
101
|
+
return this.body();
|
|
102
|
+
}
|
|
103
|
+
async bodyText() {
|
|
104
|
+
return this.req.text();
|
|
105
|
+
}
|
|
106
|
+
async bodyFormData() {
|
|
107
|
+
return this.req.formData();
|
|
108
|
+
}
|
|
109
|
+
async bodyArrayBuffer() {
|
|
110
|
+
return this.req.arrayBuffer();
|
|
111
|
+
}
|
|
112
|
+
async bodyBlob() {
|
|
113
|
+
return this.req.blob();
|
|
114
|
+
}
|
|
115
|
+
set(key, value) {
|
|
116
|
+
this._variables[key] = value;
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
get(key) {
|
|
120
|
+
return this._variables[key];
|
|
121
|
+
}
|
|
122
|
+
has(key) {
|
|
123
|
+
return key in this._variables;
|
|
124
|
+
}
|
|
125
|
+
status(code) {
|
|
126
|
+
this._response.status = code;
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
setHeader(name, value) {
|
|
130
|
+
this._response.headers.set(name, value);
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
appendHeader(name, value) {
|
|
134
|
+
this._response.headers.append(name, value);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
json(data) {
|
|
138
|
+
this._response.headers.set("Content-Type", "application/json");
|
|
139
|
+
return new Response(JSON.stringify(data), {
|
|
140
|
+
status: this._response.status,
|
|
141
|
+
headers: this._response.headers
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
text(data) {
|
|
145
|
+
if (!this._response.headers.has("Content-Type")) {
|
|
146
|
+
this._response.headers.set("Content-Type", "text/plain");
|
|
147
|
+
}
|
|
148
|
+
return new Response(data, {
|
|
149
|
+
status: this._response.status,
|
|
150
|
+
headers: this._response.headers
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
html(data) {
|
|
154
|
+
this._response.headers.set("Content-Type", "text/html; charset=utf-8");
|
|
155
|
+
return new Response(data, {
|
|
156
|
+
status: this._response.status,
|
|
157
|
+
headers: this._response.headers
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
redirect(url, status = 302) {
|
|
161
|
+
return new Response(null, {
|
|
162
|
+
status,
|
|
163
|
+
headers: {
|
|
164
|
+
Location: url
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
notFound(message = "Not Found") {
|
|
169
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
170
|
+
status: 404,
|
|
171
|
+
headers: { "Content-Type": "application/json" }
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
error(message, status = 500) {
|
|
175
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
176
|
+
status,
|
|
177
|
+
headers: { "Content-Type": "application/json" }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
newResponse(body, options) {
|
|
181
|
+
return new Response(body, {
|
|
182
|
+
status: this._response.status,
|
|
183
|
+
headers: this._response.headers,
|
|
184
|
+
...options
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
parseQuery(url) {
|
|
188
|
+
const query = {};
|
|
189
|
+
const searchParams = new URL(url).searchParams;
|
|
190
|
+
for (const [key, value] of searchParams.entries()) {
|
|
191
|
+
query[key] = value;
|
|
192
|
+
}
|
|
193
|
+
return query;
|
|
194
|
+
}
|
|
195
|
+
accepts(...types) {
|
|
196
|
+
const acceptHeader = this.req.headers.get("Accept");
|
|
197
|
+
if (!acceptHeader)
|
|
198
|
+
return types[0];
|
|
199
|
+
for (const type of types) {
|
|
200
|
+
if (acceptHeader.includes(type) || acceptHeader.includes("*/*")) {
|
|
201
|
+
return type;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
get isXHR() {
|
|
207
|
+
return this.req.headers.get("X-Requested-With") === "XMLHttpRequest";
|
|
208
|
+
}
|
|
209
|
+
get ip() {
|
|
210
|
+
return this.req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? this.req.headers.get("x-real-ip") ?? undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function createContext(request, params = {}) {
|
|
214
|
+
return new Context(request, params);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/middleware/index.ts
|
|
218
|
+
var exports_middleware = {};
|
|
219
|
+
__export(exports_middleware, {
|
|
220
|
+
createPipeline: () => createPipeline,
|
|
221
|
+
compose: () => compose,
|
|
222
|
+
Pipeline: () => Pipeline
|
|
223
|
+
});
|
|
224
|
+
function compose(middleware) {
|
|
225
|
+
return async (context, handler) => {
|
|
226
|
+
let index = -1;
|
|
227
|
+
const dispatch = async (i) => {
|
|
228
|
+
if (i <= index) {
|
|
229
|
+
throw new Error("next() called multiple times");
|
|
230
|
+
}
|
|
231
|
+
index = i;
|
|
232
|
+
if (i >= middleware.length) {
|
|
233
|
+
return handler(context);
|
|
234
|
+
}
|
|
235
|
+
const fn = middleware[i];
|
|
236
|
+
return fn(context, async () => {
|
|
237
|
+
return dispatch(i + 1);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
return dispatch(0);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
class Pipeline {
|
|
245
|
+
middleware = [];
|
|
246
|
+
use(middleware) {
|
|
247
|
+
this.middleware.push(middleware);
|
|
248
|
+
return this;
|
|
249
|
+
}
|
|
250
|
+
async execute(context, handler) {
|
|
251
|
+
const fn = compose(this.middleware);
|
|
252
|
+
return fn(context, handler);
|
|
253
|
+
}
|
|
254
|
+
get length() {
|
|
255
|
+
return this.middleware.length;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function createPipeline() {
|
|
259
|
+
return new Pipeline;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/testing/index.ts
|
|
263
|
+
import {
|
|
264
|
+
copyFile,
|
|
265
|
+
stat as fsStat,
|
|
266
|
+
mkdir,
|
|
267
|
+
readdir,
|
|
268
|
+
rename,
|
|
269
|
+
rm,
|
|
270
|
+
unlink
|
|
271
|
+
} from "fs/promises";
|
|
272
|
+
import { join, relative, resolve } from "path";
|
|
273
|
+
function createTestRequest(path, options = {}) {
|
|
274
|
+
const {
|
|
275
|
+
method = "GET",
|
|
276
|
+
headers = {},
|
|
277
|
+
query = {},
|
|
278
|
+
body,
|
|
279
|
+
cookies = {}
|
|
280
|
+
} = options;
|
|
281
|
+
const url = new URL(`http://localhost${path}`);
|
|
282
|
+
for (const [key, value] of Object.entries(query)) {
|
|
283
|
+
url.searchParams.set(key, value);
|
|
284
|
+
}
|
|
285
|
+
const requestHeaders = new Headers(headers);
|
|
286
|
+
if (Object.keys(cookies).length > 0) {
|
|
287
|
+
const cookieString = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
288
|
+
requestHeaders.set("Cookie", cookieString);
|
|
289
|
+
}
|
|
290
|
+
let requestBody;
|
|
291
|
+
if (body !== undefined) {
|
|
292
|
+
if (typeof body === "string") {
|
|
293
|
+
requestBody = body;
|
|
294
|
+
if (!requestHeaders.has("Content-Type")) {
|
|
295
|
+
requestHeaders.set("Content-Type", "text/plain");
|
|
296
|
+
}
|
|
297
|
+
} else if (body instanceof FormData) {
|
|
298
|
+
requestBody = body;
|
|
299
|
+
} else if (body instanceof URLSearchParams) {
|
|
300
|
+
requestBody = body;
|
|
301
|
+
if (!requestHeaders.has("Content-Type")) {
|
|
302
|
+
requestHeaders.set("Content-Type", "application/x-www-form-urlencoded");
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
requestBody = JSON.stringify(body);
|
|
306
|
+
if (!requestHeaders.has("Content-Type")) {
|
|
307
|
+
requestHeaders.set("Content-Type", "application/json");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return new Request(url.toString(), {
|
|
312
|
+
method,
|
|
313
|
+
headers: requestHeaders,
|
|
314
|
+
body: requestBody
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async function createTestResponse(response) {
|
|
318
|
+
const clone = response.clone();
|
|
319
|
+
let body = null;
|
|
320
|
+
try {
|
|
321
|
+
const contentType = response.headers.get("Content-Type") || "";
|
|
322
|
+
if (contentType.includes("application/json")) {
|
|
323
|
+
body = await response.json();
|
|
324
|
+
} else {
|
|
325
|
+
body = await response.text();
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
body = null;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
status: response.status,
|
|
332
|
+
headers: response.headers,
|
|
333
|
+
body,
|
|
334
|
+
text: await clone.text(),
|
|
335
|
+
json: async () => response.json()
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
class AppTester {
|
|
340
|
+
router;
|
|
341
|
+
constructor(router) {
|
|
342
|
+
this.router = router;
|
|
343
|
+
}
|
|
344
|
+
async request(path, options) {
|
|
345
|
+
const request = createTestRequest(path, options);
|
|
346
|
+
const url = new URL(request.url);
|
|
347
|
+
const match = this.router.match(request.method, url.pathname);
|
|
348
|
+
if (!match) {
|
|
349
|
+
return createTestResponse(new Response("Not Found", { status: 404 }));
|
|
350
|
+
}
|
|
351
|
+
const context = new Context(request, match.params);
|
|
352
|
+
if (match.middleware && match.middleware.length > 0) {
|
|
353
|
+
const { compose: compose2 } = await Promise.resolve().then(() => exports_middleware);
|
|
354
|
+
const pipeline = compose2(match.middleware);
|
|
355
|
+
const response2 = await pipeline(context, async () => match.handler(context));
|
|
356
|
+
return createTestResponse(response2);
|
|
357
|
+
}
|
|
358
|
+
const response = await match.handler(context);
|
|
359
|
+
return createTestResponse(response);
|
|
360
|
+
}
|
|
361
|
+
async get(path, options) {
|
|
362
|
+
return this.request(path, { ...options, method: "GET" });
|
|
363
|
+
}
|
|
364
|
+
async post(path, body, options) {
|
|
365
|
+
return this.request(path, { ...options, method: "POST", body });
|
|
366
|
+
}
|
|
367
|
+
async put(path, body, options) {
|
|
368
|
+
return this.request(path, { ...options, method: "PUT", body });
|
|
369
|
+
}
|
|
370
|
+
async patch(path, body, options) {
|
|
371
|
+
return this.request(path, { ...options, method: "PATCH", body });
|
|
372
|
+
}
|
|
373
|
+
async delete(path, options) {
|
|
374
|
+
return this.request(path, { ...options, method: "DELETE" });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function createTester(router) {
|
|
378
|
+
return new AppTester(router);
|
|
379
|
+
}
|
|
380
|
+
function createMockContext(path, options = {}) {
|
|
381
|
+
const request = createTestRequest(path, options);
|
|
382
|
+
return new Context(request, {});
|
|
383
|
+
}
|
|
384
|
+
function createMockContextWithParams(path, params, options = {}) {
|
|
385
|
+
const request = createTestRequest(path, options);
|
|
386
|
+
return new Context(request, params);
|
|
387
|
+
}
|
|
388
|
+
function assertStatus(response, expected) {
|
|
389
|
+
if (response.status !== expected) {
|
|
390
|
+
throw new Error(`Expected status ${expected}, got ${response.status}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function assertOK(response) {
|
|
394
|
+
if (response.status < 200 || response.status >= 300) {
|
|
395
|
+
throw new Error(`Expected OK status, got ${response.status}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function assertJSON(response) {
|
|
399
|
+
const contentType = response.headers.get("Content-Type");
|
|
400
|
+
if (!contentType?.includes("application/json")) {
|
|
401
|
+
throw new Error(`Expected JSON response, got ${contentType}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function assertBody(response, expected) {
|
|
405
|
+
if (JSON.stringify(response.body) !== JSON.stringify(expected)) {
|
|
406
|
+
throw new Error(`Expected body ${JSON.stringify(expected)}, got ${JSON.stringify(response.body)}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function assertHeader(response, name, value) {
|
|
410
|
+
const headerValue = response.headers.get(name);
|
|
411
|
+
if (!headerValue) {
|
|
412
|
+
throw new Error(`Expected header ${name} to be present`);
|
|
413
|
+
}
|
|
414
|
+
if (value && headerValue !== value) {
|
|
415
|
+
throw new Error(`Expected header ${name} to be ${value}, got ${headerValue}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function assertRedirect(response, location) {
|
|
419
|
+
if (response.status < 300 || response.status >= 400) {
|
|
420
|
+
throw new Error(`Expected redirect status, got ${response.status}`);
|
|
421
|
+
}
|
|
422
|
+
if (location) {
|
|
423
|
+
assertHeader(response, "Location", location);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function snapshotResponse(response) {
|
|
427
|
+
return {
|
|
428
|
+
status: response.status,
|
|
429
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
430
|
+
body: response.body
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
class FixtureFactory {
|
|
435
|
+
sequences = new Map;
|
|
436
|
+
id(prefix = "test") {
|
|
437
|
+
const seq = (this.sequences.get(prefix) ?? 0) + 1;
|
|
438
|
+
this.sequences.set(prefix, seq);
|
|
439
|
+
return `${prefix}_${seq}`;
|
|
440
|
+
}
|
|
441
|
+
email(domain = "test.com") {
|
|
442
|
+
return `${this.id("email")}@${domain}`;
|
|
443
|
+
}
|
|
444
|
+
uuid() {
|
|
445
|
+
return crypto.randomUUID();
|
|
446
|
+
}
|
|
447
|
+
reset() {
|
|
448
|
+
this.sequences.clear();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function createFixtureFactory() {
|
|
452
|
+
return new FixtureFactory;
|
|
453
|
+
}
|
|
454
|
+
async function waitFor(condition, timeout = 5000, interval = 50) {
|
|
455
|
+
const start = Date.now();
|
|
456
|
+
while (Date.now() - start < timeout) {
|
|
457
|
+
if (await condition()) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
461
|
+
}
|
|
462
|
+
throw new Error("Timeout waiting for condition");
|
|
463
|
+
}
|
|
464
|
+
function sleep(ms) {
|
|
465
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
class TestCache {
|
|
469
|
+
store = new Map;
|
|
470
|
+
_operations = [];
|
|
471
|
+
_stats = {
|
|
472
|
+
hits: 0,
|
|
473
|
+
misses: 0,
|
|
474
|
+
sets: 0,
|
|
475
|
+
deletes: 0
|
|
476
|
+
};
|
|
477
|
+
get operations() {
|
|
478
|
+
return this._operations;
|
|
479
|
+
}
|
|
480
|
+
async get(key) {
|
|
481
|
+
const value = this.store.get(key);
|
|
482
|
+
this._operations.push({
|
|
483
|
+
type: "get",
|
|
484
|
+
key,
|
|
485
|
+
value: value ?? null,
|
|
486
|
+
timestamp: Date.now()
|
|
487
|
+
});
|
|
488
|
+
if (value !== undefined) {
|
|
489
|
+
this._stats.hits++;
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
this._stats.misses++;
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
async set(key, value) {
|
|
496
|
+
this.store.set(key, value);
|
|
497
|
+
this._stats.sets++;
|
|
498
|
+
this._operations.push({
|
|
499
|
+
type: "set",
|
|
500
|
+
key,
|
|
501
|
+
value,
|
|
502
|
+
timestamp: Date.now()
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async delete(key) {
|
|
506
|
+
const existed = this.store.delete(key);
|
|
507
|
+
this._stats.deletes++;
|
|
508
|
+
this._operations.push({
|
|
509
|
+
type: "delete",
|
|
510
|
+
key,
|
|
511
|
+
timestamp: Date.now()
|
|
512
|
+
});
|
|
513
|
+
return existed;
|
|
514
|
+
}
|
|
515
|
+
async has(key) {
|
|
516
|
+
const exists = this.store.has(key);
|
|
517
|
+
this._operations.push({
|
|
518
|
+
type: "has",
|
|
519
|
+
key,
|
|
520
|
+
value: exists,
|
|
521
|
+
timestamp: Date.now()
|
|
522
|
+
});
|
|
523
|
+
return exists;
|
|
524
|
+
}
|
|
525
|
+
async clearAll() {
|
|
526
|
+
this.store.clear();
|
|
527
|
+
this._operations.push({
|
|
528
|
+
type: "clear",
|
|
529
|
+
timestamp: Date.now()
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
getStats() {
|
|
533
|
+
return {
|
|
534
|
+
...this._stats,
|
|
535
|
+
keyCount: this.store.size
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
getKeys() {
|
|
539
|
+
return Array.from(this.store.keys());
|
|
540
|
+
}
|
|
541
|
+
getEntries() {
|
|
542
|
+
return Array.from(this.store.entries());
|
|
543
|
+
}
|
|
544
|
+
async setMany(entries) {
|
|
545
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
546
|
+
await this.set(key, value);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
peek(key) {
|
|
550
|
+
const value = this.store.get(key);
|
|
551
|
+
return value !== undefined ? value : null;
|
|
552
|
+
}
|
|
553
|
+
reset() {
|
|
554
|
+
this.store.clear();
|
|
555
|
+
this._operations = [];
|
|
556
|
+
this._stats = {
|
|
557
|
+
hits: 0,
|
|
558
|
+
misses: 0,
|
|
559
|
+
sets: 0,
|
|
560
|
+
deletes: 0
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function createTestCache(initialData) {
|
|
565
|
+
const cache = new TestCache;
|
|
566
|
+
if (initialData) {
|
|
567
|
+
await cache.setMany(initialData);
|
|
568
|
+
}
|
|
569
|
+
return cache;
|
|
570
|
+
}
|
|
571
|
+
function assertCacheHas(cache, key) {
|
|
572
|
+
const keys = cache.getKeys();
|
|
573
|
+
if (!keys.includes(key)) {
|
|
574
|
+
throw new Error(`Expected cache to have key "${key}". Available keys: [${keys.join(", ")}]`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function assertCacheNotHas(cache, key) {
|
|
578
|
+
const keys = cache.getKeys();
|
|
579
|
+
if (keys.includes(key)) {
|
|
580
|
+
throw new Error(`Expected cache to NOT have key "${key}"`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function assertCacheValue(cache, key, expected) {
|
|
584
|
+
const value = cache.peek(key);
|
|
585
|
+
if (value === null) {
|
|
586
|
+
throw new Error(`Expected cache to have key "${key}"`);
|
|
587
|
+
}
|
|
588
|
+
if (JSON.stringify(value) !== JSON.stringify(expected)) {
|
|
589
|
+
throw new Error(`Expected cache value for "${key}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function assertCacheStats(cache, expected) {
|
|
593
|
+
const stats = cache.getStats();
|
|
594
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
595
|
+
const actualValue = stats[key];
|
|
596
|
+
if (actualValue !== value) {
|
|
597
|
+
throw new Error(`Expected cache stat "${key}" to be ${value}, got ${actualValue}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
class TestDatabase {
|
|
603
|
+
sql = null;
|
|
604
|
+
_operations = [];
|
|
605
|
+
_isConnected = false;
|
|
606
|
+
_schema = {};
|
|
607
|
+
get operations() {
|
|
608
|
+
return this._operations;
|
|
609
|
+
}
|
|
610
|
+
async connect() {
|
|
611
|
+
if (this._isConnected)
|
|
612
|
+
return;
|
|
613
|
+
try {
|
|
614
|
+
const { SQL } = await Promise.resolve(globalThis.Bun);
|
|
615
|
+
this.sql = new SQL(":memory:", { adapter: "sqlite" });
|
|
616
|
+
this._isConnected = true;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
throw new Error(`Failed to connect to test database: ${error instanceof Error ? error.message : String(error)}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
get isConnected() {
|
|
622
|
+
return this._isConnected;
|
|
623
|
+
}
|
|
624
|
+
async query(sqlString, params = []) {
|
|
625
|
+
this.ensureConnection();
|
|
626
|
+
this._operations.push({
|
|
627
|
+
type: "query",
|
|
628
|
+
sql: sqlString,
|
|
629
|
+
params,
|
|
630
|
+
timestamp: Date.now()
|
|
631
|
+
});
|
|
632
|
+
const sql = this.sql;
|
|
633
|
+
return sql.unsafe(sqlString, params);
|
|
634
|
+
}
|
|
635
|
+
async queryOne(sqlString, params = []) {
|
|
636
|
+
const results = await this.query(sqlString, params);
|
|
637
|
+
return results.length > 0 ? results[0] : null;
|
|
638
|
+
}
|
|
639
|
+
async execute(sqlString, params = []) {
|
|
640
|
+
this.ensureConnection();
|
|
641
|
+
this._operations.push({
|
|
642
|
+
type: "execute",
|
|
643
|
+
sql: sqlString,
|
|
644
|
+
params,
|
|
645
|
+
timestamp: Date.now()
|
|
646
|
+
});
|
|
647
|
+
const sql = this.sql;
|
|
648
|
+
const results = await sql.unsafe(sqlString, params);
|
|
649
|
+
const lastIdResult = await sql.unsafe("SELECT last_insert_rowid() as id");
|
|
650
|
+
const insertId = lastIdResult[0];
|
|
651
|
+
return {
|
|
652
|
+
rows: results,
|
|
653
|
+
rowCount: results.length,
|
|
654
|
+
insertId: insertId?.id
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
async transaction(callback) {
|
|
658
|
+
this.ensureConnection();
|
|
659
|
+
this._operations.push({
|
|
660
|
+
type: "transaction",
|
|
661
|
+
sql: "BEGIN TRANSACTION",
|
|
662
|
+
timestamp: Date.now()
|
|
663
|
+
});
|
|
664
|
+
const sql = this.sql;
|
|
665
|
+
try {
|
|
666
|
+
await sql.unsafe("BEGIN TRANSACTION");
|
|
667
|
+
const result = await callback(this);
|
|
668
|
+
await sql.unsafe("COMMIT");
|
|
669
|
+
return result;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
await sql.unsafe("ROLLBACK");
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async rollback() {
|
|
676
|
+
this.ensureConnection();
|
|
677
|
+
const sql = this.sql;
|
|
678
|
+
await sql.unsafe("ROLLBACK");
|
|
679
|
+
}
|
|
680
|
+
async close() {
|
|
681
|
+
if (!this._isConnected)
|
|
682
|
+
return;
|
|
683
|
+
const sql = this.sql;
|
|
684
|
+
if (sql.close) {
|
|
685
|
+
await sql.close();
|
|
686
|
+
}
|
|
687
|
+
this.sql = null;
|
|
688
|
+
this._isConnected = false;
|
|
689
|
+
}
|
|
690
|
+
async getTables() {
|
|
691
|
+
const result = await this.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
|
|
692
|
+
return result.map((row) => row.name);
|
|
693
|
+
}
|
|
694
|
+
async seed(tables) {
|
|
695
|
+
for (const [table, rows] of Object.entries(tables)) {
|
|
696
|
+
for (const row of rows) {
|
|
697
|
+
const keys = Object.keys(row);
|
|
698
|
+
const values = Object.values(row);
|
|
699
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
700
|
+
const columns = keys.join(", ");
|
|
701
|
+
await this.execute(`INSERT INTO ${table} (${columns}) VALUES (${placeholders})`, values);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async reset() {
|
|
706
|
+
const tables = await this.getTables();
|
|
707
|
+
for (const table of tables) {
|
|
708
|
+
await this.execute(`DROP TABLE IF EXISTS ${table}`);
|
|
709
|
+
}
|
|
710
|
+
this._operations = [];
|
|
711
|
+
this._schema = {};
|
|
712
|
+
}
|
|
713
|
+
async createTable(name, columns) {
|
|
714
|
+
const columnDefs = Object.entries(columns).map(([colName, def]) => `${colName} ${def}`).join(", ");
|
|
715
|
+
await this.execute(`CREATE TABLE ${name} (${columnDefs})`);
|
|
716
|
+
this._schema[name] = columns;
|
|
717
|
+
}
|
|
718
|
+
async dropTable(name) {
|
|
719
|
+
await this.execute(`DROP TABLE IF EXISTS ${name}`);
|
|
720
|
+
delete this._schema[name];
|
|
721
|
+
}
|
|
722
|
+
async truncate(name) {
|
|
723
|
+
await this.execute(`DELETE FROM ${name}`);
|
|
724
|
+
}
|
|
725
|
+
getSchema() {
|
|
726
|
+
return { ...this._schema };
|
|
727
|
+
}
|
|
728
|
+
async getTableInfo(table) {
|
|
729
|
+
return this.query(`PRAGMA table_info(${table})`);
|
|
730
|
+
}
|
|
731
|
+
async count(table, where, params = []) {
|
|
732
|
+
const sql = where ? `SELECT COUNT(*) as count FROM ${table} WHERE ${where}` : `SELECT COUNT(*) as count FROM ${table}`;
|
|
733
|
+
const result = await this.queryOne(sql, params);
|
|
734
|
+
return Number(result?.count ?? 0);
|
|
735
|
+
}
|
|
736
|
+
async exists(table, where, params = []) {
|
|
737
|
+
const count = await this.count(table, where, params);
|
|
738
|
+
return count > 0;
|
|
739
|
+
}
|
|
740
|
+
clearOperations() {
|
|
741
|
+
this._operations = [];
|
|
742
|
+
}
|
|
743
|
+
ensureConnection() {
|
|
744
|
+
if (!this._isConnected || !this.sql) {
|
|
745
|
+
throw new Error("TestDatabase not connected. Call connect() first.");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async function createTestDatabase(options = {}) {
|
|
750
|
+
const db = new TestDatabase;
|
|
751
|
+
await db.connect();
|
|
752
|
+
if (options.schema) {
|
|
753
|
+
for (const [table, columns] of Object.entries(options.schema)) {
|
|
754
|
+
await db.createTable(table, columns);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (options.seed) {
|
|
758
|
+
await db.seed(options.seed);
|
|
759
|
+
}
|
|
760
|
+
return db;
|
|
761
|
+
}
|
|
762
|
+
async function assertTableRowCount(db, table, expected) {
|
|
763
|
+
const count = await db.count(table);
|
|
764
|
+
if (count !== expected) {
|
|
765
|
+
throw new Error(`Expected table "${table}" to have ${expected} rows, but found ${count}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function assertTableHasRow(db, table, condition, params = []) {
|
|
769
|
+
const exists = await db.exists(table, condition, params);
|
|
770
|
+
if (!exists) {
|
|
771
|
+
throw new Error(`Expected table "${table}" to have a row matching: ${condition}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function assertTableNotHasRow(db, table, condition, params = []) {
|
|
775
|
+
const exists = await db.exists(table, condition, params);
|
|
776
|
+
if (exists) {
|
|
777
|
+
throw new Error(`Expected table "${table}" to NOT have a row matching: ${condition}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
async function assertTableExists(db, table) {
|
|
781
|
+
const tables = await db.getTables();
|
|
782
|
+
if (!tables.includes(table)) {
|
|
783
|
+
throw new Error(`Expected table "${table}" to exist. Available tables: [${tables.join(", ")}]`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async function assertTableNotExists(db, table) {
|
|
787
|
+
const tables = await db.getTables();
|
|
788
|
+
if (tables.includes(table)) {
|
|
789
|
+
throw new Error(`Expected table "${table}" to NOT exist`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function assertTableValue(db, table, column, condition, expected, params = []) {
|
|
793
|
+
const sql = `SELECT ${column} as value FROM ${table} WHERE ${condition} LIMIT 1`;
|
|
794
|
+
const result = await db.queryOne(sql, params);
|
|
795
|
+
if (!result) {
|
|
796
|
+
throw new Error(`Expected to find a row in "${table}" matching: ${condition}`);
|
|
797
|
+
}
|
|
798
|
+
if (JSON.stringify(result.value) !== JSON.stringify(expected)) {
|
|
799
|
+
throw new Error(`Expected ${column} to be ${JSON.stringify(expected)}, got ${JSON.stringify(result.value)}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
class TestStorage {
|
|
804
|
+
_basePath;
|
|
805
|
+
_operations = [];
|
|
806
|
+
_initialized = false;
|
|
807
|
+
get basePath() {
|
|
808
|
+
return this._basePath;
|
|
809
|
+
}
|
|
810
|
+
get operations() {
|
|
811
|
+
return this._operations;
|
|
812
|
+
}
|
|
813
|
+
constructor(basePath) {
|
|
814
|
+
this._basePath = basePath;
|
|
815
|
+
}
|
|
816
|
+
async init() {
|
|
817
|
+
if (this._initialized)
|
|
818
|
+
return;
|
|
819
|
+
await mkdir(this._basePath, { recursive: true });
|
|
820
|
+
this._initialized = true;
|
|
821
|
+
}
|
|
822
|
+
async write(path, content) {
|
|
823
|
+
await this.ensureInitialized();
|
|
824
|
+
const fullPath = this.resolvePath(path);
|
|
825
|
+
const parentDir = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
826
|
+
if (parentDir) {
|
|
827
|
+
await mkdir(parentDir, { recursive: true });
|
|
828
|
+
}
|
|
829
|
+
const file = Bun.file(fullPath);
|
|
830
|
+
const writer = file.writer();
|
|
831
|
+
if (typeof content === "string") {
|
|
832
|
+
writer.write(content);
|
|
833
|
+
} else if (content instanceof Uint8Array) {
|
|
834
|
+
writer.write(content);
|
|
835
|
+
} else if (content instanceof ArrayBuffer) {
|
|
836
|
+
writer.write(new Uint8Array(content));
|
|
837
|
+
}
|
|
838
|
+
await writer.end();
|
|
839
|
+
const size = typeof content === "string" ? new TextEncoder().encode(content).length : content.byteLength;
|
|
840
|
+
this._operations.push({
|
|
841
|
+
type: "write",
|
|
842
|
+
path,
|
|
843
|
+
size,
|
|
844
|
+
timestamp: Date.now()
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
async read(path) {
|
|
848
|
+
await this.ensureInitialized();
|
|
849
|
+
const fullPath = this.resolvePath(path);
|
|
850
|
+
const file = Bun.file(fullPath);
|
|
851
|
+
this._operations.push({
|
|
852
|
+
type: "read",
|
|
853
|
+
path,
|
|
854
|
+
timestamp: Date.now()
|
|
855
|
+
});
|
|
856
|
+
if (!await file.exists()) {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
return file.text();
|
|
860
|
+
}
|
|
861
|
+
async readBytes(path) {
|
|
862
|
+
await this.ensureInitialized();
|
|
863
|
+
const fullPath = this.resolvePath(path);
|
|
864
|
+
const file = Bun.file(fullPath);
|
|
865
|
+
this._operations.push({
|
|
866
|
+
type: "read",
|
|
867
|
+
path,
|
|
868
|
+
timestamp: Date.now()
|
|
869
|
+
});
|
|
870
|
+
if (!await file.exists()) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
return file.arrayBuffer();
|
|
874
|
+
}
|
|
875
|
+
async delete(path) {
|
|
876
|
+
await this.ensureInitialized();
|
|
877
|
+
const fullPath = this.resolvePath(path);
|
|
878
|
+
const file = Bun.file(fullPath);
|
|
879
|
+
const exists = await file.exists();
|
|
880
|
+
if (exists) {
|
|
881
|
+
await unlink(fullPath);
|
|
882
|
+
}
|
|
883
|
+
this._operations.push({
|
|
884
|
+
type: "delete",
|
|
885
|
+
path,
|
|
886
|
+
timestamp: Date.now()
|
|
887
|
+
});
|
|
888
|
+
return exists;
|
|
889
|
+
}
|
|
890
|
+
async exists(path) {
|
|
891
|
+
await this.ensureInitialized();
|
|
892
|
+
const fullPath = this.resolvePath(path);
|
|
893
|
+
const file = Bun.file(fullPath);
|
|
894
|
+
const exists = await file.exists();
|
|
895
|
+
this._operations.push({
|
|
896
|
+
type: "exists",
|
|
897
|
+
path,
|
|
898
|
+
timestamp: Date.now()
|
|
899
|
+
});
|
|
900
|
+
return exists;
|
|
901
|
+
}
|
|
902
|
+
async list(prefix) {
|
|
903
|
+
await this.ensureInitialized();
|
|
904
|
+
const files = [];
|
|
905
|
+
const scanDir = async (dir) => {
|
|
906
|
+
try {
|
|
907
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
908
|
+
for (const entry of entries) {
|
|
909
|
+
const fullPath = join(dir, entry.name);
|
|
910
|
+
if (entry.isDirectory()) {
|
|
911
|
+
await scanDir(fullPath);
|
|
912
|
+
} else if (entry.isFile()) {
|
|
913
|
+
const relativePath = relative(this._basePath, fullPath);
|
|
914
|
+
if (!prefix || relativePath.startsWith(prefix)) {
|
|
915
|
+
files.push(relativePath);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} catch {}
|
|
920
|
+
};
|
|
921
|
+
await scanDir(this._basePath);
|
|
922
|
+
this._operations.push({
|
|
923
|
+
type: "list",
|
|
924
|
+
path: prefix,
|
|
925
|
+
timestamp: Date.now()
|
|
926
|
+
});
|
|
927
|
+
return files.sort();
|
|
928
|
+
}
|
|
929
|
+
async stat(path) {
|
|
930
|
+
await this.ensureInitialized();
|
|
931
|
+
const fullPath = this.resolvePath(path);
|
|
932
|
+
const file = Bun.file(fullPath);
|
|
933
|
+
if (!await file.exists()) {
|
|
934
|
+
this._operations.push({
|
|
935
|
+
type: "stat",
|
|
936
|
+
path,
|
|
937
|
+
timestamp: Date.now()
|
|
938
|
+
});
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const stats = await fsStat(fullPath);
|
|
942
|
+
this._operations.push({
|
|
943
|
+
type: "stat",
|
|
944
|
+
path,
|
|
945
|
+
size: stats.size,
|
|
946
|
+
timestamp: Date.now()
|
|
947
|
+
});
|
|
948
|
+
return {
|
|
949
|
+
size: stats.size,
|
|
950
|
+
created: stats.birthtimeMs,
|
|
951
|
+
modified: stats.mtimeMs
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
async copy(src, dest) {
|
|
955
|
+
await this.ensureInitialized();
|
|
956
|
+
const srcPath = this.resolvePath(src);
|
|
957
|
+
const destPath = this.resolvePath(dest);
|
|
958
|
+
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
959
|
+
if (parentDir) {
|
|
960
|
+
await mkdir(parentDir, { recursive: true });
|
|
961
|
+
}
|
|
962
|
+
await copyFile(srcPath, destPath);
|
|
963
|
+
const stats = await fsStat(destPath);
|
|
964
|
+
this._operations.push({
|
|
965
|
+
type: "copy",
|
|
966
|
+
src,
|
|
967
|
+
dest,
|
|
968
|
+
size: stats.size,
|
|
969
|
+
timestamp: Date.now()
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
async move(src, dest) {
|
|
973
|
+
await this.ensureInitialized();
|
|
974
|
+
const srcPath = this.resolvePath(src);
|
|
975
|
+
const destPath = this.resolvePath(dest);
|
|
976
|
+
const parentDir = destPath.substring(0, destPath.lastIndexOf("/"));
|
|
977
|
+
if (parentDir) {
|
|
978
|
+
await mkdir(parentDir, { recursive: true });
|
|
979
|
+
}
|
|
980
|
+
await rename(srcPath, destPath);
|
|
981
|
+
this._operations.push({
|
|
982
|
+
type: "move",
|
|
983
|
+
src,
|
|
984
|
+
dest,
|
|
985
|
+
timestamp: Date.now()
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
async clear() {
|
|
989
|
+
await this.ensureInitialized();
|
|
990
|
+
try {
|
|
991
|
+
const files = await this.list();
|
|
992
|
+
for (const file of files) {
|
|
993
|
+
const fullPath = this.resolvePath(file);
|
|
994
|
+
await unlink(fullPath);
|
|
995
|
+
}
|
|
996
|
+
} catch {}
|
|
997
|
+
this._operations.push({
|
|
998
|
+
type: "clear",
|
|
999
|
+
timestamp: Date.now()
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
getBasePath() {
|
|
1003
|
+
return this._basePath;
|
|
1004
|
+
}
|
|
1005
|
+
async reset() {
|
|
1006
|
+
await this.clear();
|
|
1007
|
+
this._operations = [];
|
|
1008
|
+
}
|
|
1009
|
+
async cleanup() {
|
|
1010
|
+
try {
|
|
1011
|
+
await rm(this._basePath, { recursive: true, force: true });
|
|
1012
|
+
} catch {}
|
|
1013
|
+
this._operations = [];
|
|
1014
|
+
this._initialized = false;
|
|
1015
|
+
}
|
|
1016
|
+
resolvePath(path) {
|
|
1017
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
1018
|
+
return resolve(this._basePath, normalizedPath);
|
|
1019
|
+
}
|
|
1020
|
+
async ensureInitialized() {
|
|
1021
|
+
if (!this._initialized) {
|
|
1022
|
+
await this.init();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
async function createTestStorage(options = {}) {
|
|
1027
|
+
const basePath = options.basePath ?? await createTempDir("bueno-test-storage-");
|
|
1028
|
+
const storage = new TestStorage(basePath);
|
|
1029
|
+
await storage.init();
|
|
1030
|
+
return storage;
|
|
1031
|
+
}
|
|
1032
|
+
async function createTempDir(prefix) {
|
|
1033
|
+
const { mkdtemp } = await import("fs/promises");
|
|
1034
|
+
const { tmpdir } = await import("os");
|
|
1035
|
+
const { join: join2 } = await import("path");
|
|
1036
|
+
return mkdtemp(join2(tmpdir(), prefix));
|
|
1037
|
+
}
|
|
1038
|
+
async function assertFileExists(storage, path) {
|
|
1039
|
+
const exists = await storage.exists(path);
|
|
1040
|
+
if (!exists) {
|
|
1041
|
+
const files = await storage.list();
|
|
1042
|
+
throw new Error(`Expected file "${path}" to exist. Available files: [${files.join(", ")}]`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function assertFileNotExists(storage, path) {
|
|
1046
|
+
const exists = await storage.exists(path);
|
|
1047
|
+
if (exists) {
|
|
1048
|
+
throw new Error(`Expected file "${path}" to NOT exist`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async function assertFileContent(storage, path, expected) {
|
|
1052
|
+
const content = await storage.read(path);
|
|
1053
|
+
if (content === null) {
|
|
1054
|
+
throw new Error(`Expected file "${path}" to exist`);
|
|
1055
|
+
}
|
|
1056
|
+
if (content !== expected) {
|
|
1057
|
+
throw new Error(`Expected file content for "${path}" to be:
|
|
1058
|
+
${expected}
|
|
1059
|
+
|
|
1060
|
+
Got:
|
|
1061
|
+
${content}`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function assertFileSize(storage, path, expectedSize) {
|
|
1065
|
+
const stats = await storage.stat(path);
|
|
1066
|
+
if (stats === null) {
|
|
1067
|
+
throw new Error(`Expected file "${path}" to exist`);
|
|
1068
|
+
}
|
|
1069
|
+
if (stats.size !== expectedSize) {
|
|
1070
|
+
throw new Error(`Expected file "${path}" to have size ${expectedSize}, got ${stats.size}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
export {
|
|
1074
|
+
waitFor,
|
|
1075
|
+
snapshotResponse,
|
|
1076
|
+
sleep,
|
|
1077
|
+
createTester,
|
|
1078
|
+
createTestStorage,
|
|
1079
|
+
createTestResponse,
|
|
1080
|
+
createTestRequest,
|
|
1081
|
+
createTestDatabase,
|
|
1082
|
+
createTestCache,
|
|
1083
|
+
createMockContextWithParams,
|
|
1084
|
+
createMockContext,
|
|
1085
|
+
createFixtureFactory,
|
|
1086
|
+
assertTableValue,
|
|
1087
|
+
assertTableRowCount,
|
|
1088
|
+
assertTableNotHasRow,
|
|
1089
|
+
assertTableNotExists,
|
|
1090
|
+
assertTableHasRow,
|
|
1091
|
+
assertTableExists,
|
|
1092
|
+
assertStatus,
|
|
1093
|
+
assertRedirect,
|
|
1094
|
+
assertOK,
|
|
1095
|
+
assertJSON,
|
|
1096
|
+
assertHeader,
|
|
1097
|
+
assertFileSize,
|
|
1098
|
+
assertFileNotExists,
|
|
1099
|
+
assertFileExists,
|
|
1100
|
+
assertFileContent,
|
|
1101
|
+
assertCacheValue,
|
|
1102
|
+
assertCacheStats,
|
|
1103
|
+
assertCacheNotHas,
|
|
1104
|
+
assertCacheHas,
|
|
1105
|
+
assertBody,
|
|
1106
|
+
TestStorage,
|
|
1107
|
+
TestDatabase,
|
|
1108
|
+
TestCache,
|
|
1109
|
+
FixtureFactory,
|
|
1110
|
+
AppTester
|
|
1111
|
+
};
|