@getstrata/core 0.3.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/bootstrap/httpKernel.d.ts +2 -0
  2. package/dist/config/queue.d.ts +8 -0
  3. package/dist/config/rateLimit.d.ts +2 -1
  4. package/dist/core/database/baseRepository.d.ts +16 -4
  5. package/dist/core/database/boundConnection.d.ts +5 -0
  6. package/dist/core/database/index.d.ts +10 -4
  7. package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
  8. package/dist/core/database/migrations/runner.d.ts +8 -2
  9. package/dist/core/database/migrations/types.d.ts +1 -0
  10. package/dist/core/database/model.d.ts +46 -17
  11. package/dist/core/database/query.d.ts +26 -14
  12. package/dist/core/database/relationships.d.ts +32 -2
  13. package/dist/core/database/repositoryConnection.d.ts +4 -0
  14. package/dist/core/database/repositoryQuery.d.ts +16 -1
  15. package/dist/core/database/schema/blueprint.d.ts +47 -0
  16. package/dist/core/database/schema/columnDefinition.d.ts +36 -0
  17. package/dist/core/database/schema/driver.d.ts +8 -0
  18. package/dist/core/database/schema/errors.d.ts +4 -0
  19. package/dist/core/database/schema/grammars/compileStatements.d.ts +8 -0
  20. package/dist/core/database/schema/grammars/createGrammar.d.ts +4 -0
  21. package/dist/core/database/schema/grammars/grammar.d.ts +10 -0
  22. package/dist/core/database/schema/grammars/index.d.ts +7 -0
  23. package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +2 -0
  24. package/dist/core/database/schema/grammars/postgresGrammar.d.ts +2 -0
  25. package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +2 -0
  26. package/dist/core/database/schema/index.d.ts +12 -0
  27. package/dist/core/database/schema/schema.d.ts +21 -0
  28. package/dist/core/database/seeders/runner.d.ts +6 -0
  29. package/dist/core/database/seeders/types.d.ts +8 -0
  30. package/dist/core/database/types.d.ts +39 -2
  31. package/dist/core/database/whereBuilder.d.ts +17 -0
  32. package/dist/core/http/index.d.ts +1 -1
  33. package/dist/core/http/securedRouteModelBinding.d.ts +2 -1
  34. package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
  35. package/dist/core/pagination/index.d.ts +11 -1
  36. package/dist/core/queue/failedJobRepository.d.ts +6 -0
  37. package/dist/core/queue/failedJobService.d.ts +15 -0
  38. package/dist/core/queue/failedJobTable.d.ts +3 -0
  39. package/dist/core/queue/jobRegistry.d.ts +14 -0
  40. package/dist/core/queue/jobRunner.d.ts +9 -0
  41. package/dist/core/queue/publicQueue.d.ts +15 -0
  42. package/dist/core/queue/redisQueue.d.ts +29 -0
  43. package/dist/core/queue/resilientQueue.d.ts +9 -0
  44. package/dist/core/queue/types.d.ts +8 -0
  45. package/dist/core/scheduler/schedule.d.ts +15 -0
  46. package/dist/entries/auth/accessControl.js +113 -0
  47. package/dist/entries/auth/authContext.js +15 -0
  48. package/dist/entries/auth/guard.js +2870 -0
  49. package/dist/entries/auth/membershipContext.js +276 -0
  50. package/dist/entries/auth/membershipScope.js +390 -0
  51. package/dist/entries/auth/membershipService.js +477 -0
  52. package/dist/entries/auth/oauth/oidcProvider.js +49 -0
  53. package/dist/entries/auth/oauth/providers.js +71 -0
  54. package/dist/entries/auth/oauth/samlProvider.js +26 -0
  55. package/dist/entries/auth/oauth/types.js +1 -0
  56. package/dist/entries/auth/password.js +15 -0
  57. package/dist/entries/auth/policy.js +134 -0
  58. package/dist/entries/auth/sessionCookie.js +75 -0
  59. package/dist/entries/auth/tokenHash.js +17 -0
  60. package/dist/entries/cache/tags.js +13 -0
  61. package/dist/entries/crypto/fieldEncryption.js +93 -0
  62. package/dist/entries/crypto/mfaSecret.js +105 -0
  63. package/dist/entries/database/factory.js +17 -0
  64. package/dist/entries/database/seeders.js +30 -0
  65. package/dist/entries/database/types.js +1 -0
  66. package/dist/entries/database.js +2218 -0
  67. package/dist/entries/errors/http.js +78 -0
  68. package/dist/entries/http/contentNegotiation.js +26 -0
  69. package/dist/entries/http/csrfToken.js +147 -0
  70. package/dist/entries/http/etag.js +170 -0
  71. package/dist/entries/http/flashSession.js +106 -0
  72. package/dist/entries/http/middleware.js +68 -0
  73. package/dist/entries/http/parseFormBody.js +88 -0
  74. package/dist/entries/http/requestMetaContext.js +18 -0
  75. package/dist/entries/http/resources.js +19 -0
  76. package/dist/entries/http/webErrorResponse.js +3143 -0
  77. package/dist/entries/http/webFormRequest.js +293 -0
  78. package/dist/entries/http.js +3924 -0
  79. package/dist/entries/jobs/dispatchWebhookJob.js +342 -0
  80. package/dist/entries/lifecycle/gracefulShutdown.js +50 -0
  81. package/dist/entries/metrics/prometheus.js +70 -0
  82. package/dist/entries/pagination.js +14 -0
  83. package/dist/entries/queue/createAppQueue.js +2848 -0
  84. package/dist/entries/queue/failedJobService.js +38 -0
  85. package/dist/entries/queue/jobRegistry.js +32 -0
  86. package/dist/entries/queue/jobRunner.js +75 -0
  87. package/dist/entries/queue/publicQueue.js +2476 -0
  88. package/dist/entries/queue/queueMetrics.js +2898 -0
  89. package/dist/entries/queue/types.js +1 -0
  90. package/dist/entries/security/oauthState.js +75 -0
  91. package/dist/entries/security/publicReads.js +33 -0
  92. package/dist/entries/security/safeUrl.js +143 -0
  93. package/dist/entries/security/securityEvents.js +41 -0
  94. package/dist/entries/security/stripeWebhook.js +115 -0
  95. package/dist/entries/security/tokenExpiry.js +16 -0
  96. package/dist/entries/security/totp.js +51 -0
  97. package/dist/entries/storage/storage.js +123 -0
  98. package/dist/entries/tenant/tenantContext.js +30 -0
  99. package/dist/entries/tenant/tenantMiddleware.js +312 -0
  100. package/dist/entries/tracing/traceContext.js +15 -0
  101. package/dist/entries/validation/rules.js +232 -0
  102. package/dist/entries/view.js +3072 -0
  103. package/dist/framework/public-api.d.ts +65 -38
  104. package/dist/index.js +3213 -281
  105. package/dist/modules/user/apiTokenRepository.d.ts +1 -1
  106. package/dist/modules/user/apiTokenTable.d.ts +1 -1
  107. package/dist/modules/user/authService.d.ts +1 -1
  108. package/dist/modules/user/notificationRepository.d.ts +1 -1
  109. package/dist/modules/user/notificationService.d.ts +1 -1
  110. package/dist/modules/user/notificationTable.d.ts +1 -1
  111. package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
  112. package/dist/modules/user/provider.d.ts +1 -1
  113. package/dist/modules/user/repository.d.ts +1 -1
  114. package/dist/modules/user/table.d.ts +1 -1
  115. package/dist/modules/user/tokenService.d.ts +1 -1
  116. package/package.json +289 -3
@@ -0,0 +1,3924 @@
1
+ // @bun
2
+ // ../../src/core/errors/http.ts
3
+ class HttpError extends Error {
4
+ status;
5
+ details;
6
+ constructor(status, message, details) {
7
+ super(message);
8
+ this.name = new.target.name;
9
+ this.status = status;
10
+ this.details = details;
11
+ }
12
+ }
13
+
14
+ class BadRequestError extends HttpError {
15
+ constructor(message = "Bad Request", details) {
16
+ super(400, message, details);
17
+ }
18
+ }
19
+
20
+ class NotFoundError extends HttpError {
21
+ constructor(message = "Not Found", details) {
22
+ super(404, message, details);
23
+ }
24
+ }
25
+
26
+ class ConflictError extends HttpError {
27
+ constructor(message = "Conflict", details) {
28
+ super(409, message, details);
29
+ }
30
+ }
31
+
32
+ class UnprocessableEntityError extends HttpError {
33
+ constructor(message = "Unprocessable Entity", details) {
34
+ super(422, message, details);
35
+ }
36
+ }
37
+
38
+ class ValidationError extends HttpError {
39
+ constructor(message = "Validation failed", details) {
40
+ super(422, message, details);
41
+ }
42
+ }
43
+
44
+ class ForbiddenError extends HttpError {
45
+ constructor(message = "Forbidden", details) {
46
+ super(403, message, details);
47
+ }
48
+ }
49
+
50
+ class UnauthorizedError extends HttpError {
51
+ constructor(message = "Unauthorized", details) {
52
+ super(401, message, details);
53
+ }
54
+ }
55
+
56
+ class PayloadTooLargeError extends HttpError {
57
+ constructor(message = "Payload Too Large", details) {
58
+ super(413, message, details);
59
+ }
60
+ }
61
+
62
+ class PreconditionFailedError extends HttpError {
63
+ constructor(message = "Precondition Failed", details) {
64
+ super(412, message, details);
65
+ }
66
+ }
67
+
68
+ // ../../src/core/database/errors.ts
69
+ function isPostgresError(error) {
70
+ return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
71
+ }
72
+ function getPostgresSqlState(error) {
73
+ if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
74
+ return error.errno;
75
+ }
76
+ if (typeof error.errno === "number") {
77
+ return String(error.errno).padStart(5, "0");
78
+ }
79
+ if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
80
+ return error.code;
81
+ }
82
+ return;
83
+ }
84
+ function mapDatabaseError(error) {
85
+ if (error instanceof HttpError) {
86
+ return error;
87
+ }
88
+ if (!isPostgresError(error)) {
89
+ const message = error instanceof Error ? error.message : "Database operation failed.";
90
+ return new BadRequestError(message);
91
+ }
92
+ const sqlState = getPostgresSqlState(error);
93
+ switch (sqlState) {
94
+ case "23505":
95
+ return new ConflictError(error.detail ?? "A record with these values already exists.", {
96
+ constraint: error.constraint
97
+ });
98
+ case "23503":
99
+ return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
100
+ constraint: error.constraint
101
+ });
102
+ case "23502":
103
+ return new BadRequestError(error.detail ?? "Required field is missing.", {
104
+ constraint: error.constraint
105
+ });
106
+ case "23514":
107
+ return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
108
+ constraint: error.constraint
109
+ });
110
+ default:
111
+ return new BadRequestError(error.message ?? "Database operation failed.", {
112
+ code: error.code,
113
+ sqlState
114
+ });
115
+ }
116
+ }
117
+ async function withDatabaseErrorHandling(operation) {
118
+ try {
119
+ return await operation();
120
+ } catch (error) {
121
+ throw mapDatabaseError(error);
122
+ }
123
+ }
124
+
125
+ // ../../src/config/frontend.ts
126
+ function readFrontendMode() {
127
+ const mode = (process.env.FRONTEND_MODE ?? "api").trim();
128
+ if (mode === "server-htmx") {
129
+ return "server-htmx";
130
+ }
131
+ if (mode === "spa-react") {
132
+ return "spa-react";
133
+ }
134
+ return "api";
135
+ }
136
+ function isViewsEnabled() {
137
+ return readFrontendMode() === "server-htmx";
138
+ }
139
+
140
+ // ../../src/core/view/etaViewEngine.ts
141
+ import { join } from "path";
142
+ import { Eta } from "eta";
143
+ var DEFAULT_VIEWS_DIRECTORY = join(process.cwd(), "resources/views");
144
+ var DEFAULT_LAYOUT = "layouts/app.eta";
145
+
146
+ class EtaViewEngine {
147
+ eta;
148
+ resolveLayoutData;
149
+ constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
150
+ this.eta = new Eta({
151
+ views: viewsDirectory,
152
+ autoTrim: false
153
+ });
154
+ this.resolveLayoutData = resolveLayoutData;
155
+ }
156
+ async render(name, data = {}, options = {}) {
157
+ const template = name.endsWith(".eta") ? name : `${name}.eta`;
158
+ const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
159
+ const mergedData = { ...layoutData, ...data };
160
+ const body = await this.eta.renderAsync(template, mergedData);
161
+ const layout = options.layout ?? DEFAULT_LAYOUT;
162
+ if (layout === false) {
163
+ return body;
164
+ }
165
+ const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
166
+ return await this.eta.renderAsync(layoutTemplate, {
167
+ ...mergedData,
168
+ body
169
+ });
170
+ }
171
+ }
172
+ // ../../src/core/view/htmlResponse.ts
173
+ function htmlResponse(html, init = {}) {
174
+ return new Response(html, {
175
+ status: init.status ?? 200,
176
+ statusText: init.statusText,
177
+ headers: {
178
+ "Content-Type": "text/html; charset=utf-8"
179
+ }
180
+ });
181
+ }
182
+ function isHtmxRequest(request) {
183
+ return request.headers.get("HX-Request") === "true";
184
+ }
185
+ // ../../src/bootstrap/config.ts
186
+ var CORE_QUEUE_TOKEN = "core.queue";
187
+ var CORE_POLICY_GATE_TOKEN = "core.policyGate";
188
+ var CORE_AUTH_TOKEN = "core.auth";
189
+ var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
190
+ var DEFAULT_QUEUE_DRIVER = "sync";
191
+
192
+ // ../../src/core/auth/oauth/oidcProvider.ts
193
+ class OidcProvider {
194
+ options;
195
+ name;
196
+ constructor(options) {
197
+ this.options = options;
198
+ this.name = options.name;
199
+ }
200
+ getAuthorizationUrl(state) {
201
+ const params = new URLSearchParams({
202
+ client_id: this.options.clientId,
203
+ redirect_uri: this.options.redirectUri,
204
+ response_type: "code",
205
+ scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
206
+ state
207
+ });
208
+ return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
209
+ }
210
+ async exchangeCode(code) {
211
+ const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
212
+ method: "POST",
213
+ headers: { "content-type": "application/x-www-form-urlencoded" },
214
+ body: new URLSearchParams({
215
+ grant_type: "authorization_code",
216
+ code,
217
+ redirect_uri: this.options.redirectUri,
218
+ client_id: this.options.clientId,
219
+ client_secret: this.options.clientSecret
220
+ })
221
+ });
222
+ const tokenBody = await tokenResponse.json();
223
+ if (!tokenBody.access_token) {
224
+ throw new Error("OIDC token exchange failed.");
225
+ }
226
+ const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
227
+ headers: { authorization: `Bearer ${tokenBody.access_token}` }
228
+ });
229
+ const profile = await profileResponse.json();
230
+ return {
231
+ providerUserId: profile.sub,
232
+ email: profile.email ?? `${profile.sub}@oidc.local`,
233
+ name: profile.name ?? profile.sub
234
+ };
235
+ }
236
+ }
237
+
238
+ // ../../src/core/auth/oauth/providers.ts
239
+ class GitHubOAuthProvider {
240
+ options;
241
+ name = "github";
242
+ constructor(options) {
243
+ this.options = options;
244
+ }
245
+ getAuthorizationUrl(state) {
246
+ const params = new URLSearchParams({
247
+ client_id: this.options.clientId,
248
+ redirect_uri: this.options.redirectUri,
249
+ scope: "read:user user:email",
250
+ state
251
+ });
252
+ return `https://github.com/login/oauth/authorize?${params.toString()}`;
253
+ }
254
+ async exchangeCode(code) {
255
+ const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
256
+ method: "POST",
257
+ headers: {
258
+ accept: "application/json",
259
+ "content-type": "application/json"
260
+ },
261
+ body: JSON.stringify({
262
+ client_id: this.options.clientId,
263
+ client_secret: this.options.clientSecret,
264
+ code,
265
+ redirect_uri: this.options.redirectUri
266
+ })
267
+ });
268
+ const tokenBody = await tokenResponse.json();
269
+ if (!tokenBody.access_token) {
270
+ throw new Error("GitHub OAuth token exchange failed.");
271
+ }
272
+ const profileResponse = await fetch("https://api.github.com/user", {
273
+ headers: {
274
+ authorization: `Bearer ${tokenBody.access_token}`,
275
+ accept: "application/json",
276
+ "user-agent": "workhub"
277
+ }
278
+ });
279
+ const profile = await profileResponse.json();
280
+ return {
281
+ providerUserId: String(profile.id),
282
+ email: profile.email ?? `${profile.login}@users.noreply.github.com`,
283
+ name: profile.name ?? profile.login
284
+ };
285
+ }
286
+ }
287
+
288
+ class MockOAuthProvider {
289
+ profile;
290
+ name = "mock";
291
+ constructor(profile) {
292
+ this.profile = profile;
293
+ }
294
+ getAuthorizationUrl(state) {
295
+ return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
296
+ }
297
+ async exchangeCode(code) {
298
+ if (code !== "valid-code") {
299
+ throw new Error("Invalid OAuth code.");
300
+ }
301
+ return this.profile;
302
+ }
303
+ }
304
+
305
+ // ../../src/core/auth/oauth/samlProvider.ts
306
+ class SamlProvider {
307
+ loginUrl;
308
+ name = "saml";
309
+ constructor(loginUrl) {
310
+ this.loginUrl = loginUrl;
311
+ }
312
+ getAuthorizationUrl(state) {
313
+ return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
314
+ }
315
+ async exchangeCode(code) {
316
+ if (!code.startsWith("saml:")) {
317
+ throw new Error("Invalid SAML assertion reference.");
318
+ }
319
+ const [, email, name] = code.split(":");
320
+ return {
321
+ providerUserId: email ?? "saml-user",
322
+ email: email ?? "saml-user@workhub.test",
323
+ name: name ?? "SAML User"
324
+ };
325
+ }
326
+ }
327
+
328
+ // ../../src/config/features.ts
329
+ function readFeatureFlags() {
330
+ return {
331
+ webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
332
+ fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
333
+ auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
334
+ oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
335
+ samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
336
+ scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
337
+ billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
338
+ siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
339
+ publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
340
+ emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
341
+ mfa: (process.env.FEATURE_MFA ?? "false") === "true"
342
+ };
343
+ }
344
+ var featureFlags = readFeatureFlags();
345
+ function isFeatureEnabled(feature) {
346
+ return readFeatureFlags()[feature];
347
+ }
348
+
349
+ // ../../src/core/events/eventBus.ts
350
+ class EventBus {
351
+ constructor() {}
352
+ listeners = new Map;
353
+ listen(event, listener) {
354
+ const handlers = this.listeners.get(event) ?? new Set;
355
+ handlers.add(listener);
356
+ this.listeners.set(event, handlers);
357
+ return () => {
358
+ handlers.delete(listener);
359
+ if (handlers.size === 0) {
360
+ this.listeners.delete(event);
361
+ }
362
+ };
363
+ }
364
+ async dispatch(event, payload) {
365
+ const handlers = this.listeners.get(event);
366
+ if (!handlers || handlers.size === 0) {
367
+ return;
368
+ }
369
+ for (const handler of handlers) {
370
+ await handler(payload);
371
+ }
372
+ }
373
+ }
374
+ var eventBus = new EventBus;
375
+
376
+ // ../../src/core/events/index.ts
377
+ function modelEventName(tableName, action) {
378
+ return `${tableName}.${action}`;
379
+ }
380
+
381
+ // ../../src/core/pagination/index.ts
382
+ function buildPaginationMeta(input) {
383
+ const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
384
+ return {
385
+ page: input.page,
386
+ per_page: input.perPage,
387
+ total: input.total,
388
+ last_page: lastPage
389
+ };
390
+ }
391
+
392
+ // ../../src/core/database/query.ts
393
+ function quoteIdentifier(identifier) {
394
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
395
+ throw new Error(`Invalid SQL identifier: ${identifier}`);
396
+ }
397
+ return `"${identifier}"`;
398
+ }
399
+ function qualifyColumn(tableName, column) {
400
+ return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
401
+ }
402
+ function resolveQualifiedColumn(defaultTable, columnName) {
403
+ if (columnName.includes(".")) {
404
+ const [table, column] = columnName.split(".", 2);
405
+ if (!table || !column) {
406
+ throw new Error(`Invalid qualified column: ${columnName}`);
407
+ }
408
+ return qualifyColumn(table, column);
409
+ }
410
+ return qualifyColumn(defaultTable, columnName);
411
+ }
412
+ function parseQualifiedColumn(reference) {
413
+ const [table, column] = reference.split(".", 2);
414
+ if (!table || !column) {
415
+ throw new Error(`Join columns must be qualified as table.column: ${reference}`);
416
+ }
417
+ return { table, column };
418
+ }
419
+ function normalizeDirection(direction = "ASC") {
420
+ return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
421
+ }
422
+ function isQueryOperator(value) {
423
+ return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
424
+ }
425
+ function pushParam(values, value) {
426
+ values.push(value);
427
+ return `$${values.length}`;
428
+ }
429
+ function buildInClause(column, values, params) {
430
+ if (values.length === 0) {
431
+ return "1 = 0";
432
+ }
433
+ const placeholders = values.map((value) => pushParam(params, value)).join(", ");
434
+ return `${column} IN (${placeholders})`;
435
+ }
436
+ function buildOperatorClauses(column, operator, params) {
437
+ const clauses = [];
438
+ if (operator.isNull === true) {
439
+ clauses.push(`${column} IS NULL`);
440
+ }
441
+ if (operator.isNull === false) {
442
+ clauses.push(`${column} IS NOT NULL`);
443
+ }
444
+ if (operator.eq !== undefined) {
445
+ if (operator.eq === null) {
446
+ clauses.push(`${column} IS NULL`);
447
+ } else {
448
+ clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
449
+ }
450
+ }
451
+ if (operator.in !== undefined) {
452
+ clauses.push(buildInClause(column, operator.in, params));
453
+ }
454
+ if (operator.gt !== undefined) {
455
+ clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
456
+ }
457
+ if (operator.gte !== undefined) {
458
+ clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
459
+ }
460
+ if (operator.lt !== undefined) {
461
+ clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
462
+ }
463
+ if (operator.lte !== undefined) {
464
+ clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
465
+ }
466
+ if (operator.ilike !== undefined) {
467
+ clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
468
+ }
469
+ if (operator.tsMatch !== undefined) {
470
+ clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
471
+ }
472
+ return clauses;
473
+ }
474
+ function appendWhereParts(tableName, where, params) {
475
+ const clauses = [];
476
+ for (const [columnName, filterValue] of Object.entries(where)) {
477
+ if (filterValue === undefined) {
478
+ continue;
479
+ }
480
+ const column = resolveQualifiedColumn(tableName, columnName);
481
+ if (Array.isArray(filterValue)) {
482
+ clauses.push(buildInClause(column, filterValue, params));
483
+ continue;
484
+ }
485
+ if (isQueryOperator(filterValue)) {
486
+ clauses.push(...buildOperatorClauses(column, filterValue, params));
487
+ continue;
488
+ }
489
+ if (filterValue === null) {
490
+ clauses.push(`${column} IS NULL`);
491
+ continue;
492
+ }
493
+ clauses.push(`${column} = ${pushParam(params, filterValue)}`);
494
+ }
495
+ return clauses.join(" AND ");
496
+ }
497
+ function buildWhereClause(tableName, where = {}) {
498
+ const params = [];
499
+ const body = appendWhereParts(tableName, where, params);
500
+ return {
501
+ clause: body.length > 0 ? ` WHERE ${body}` : "",
502
+ params
503
+ };
504
+ }
505
+ function buildWhereNodeClause(tableName, node, params) {
506
+ if ("where" in node) {
507
+ return appendWhereParts(tableName, node.where, params);
508
+ }
509
+ const grouped = buildWhereGroupClause(tableName, node.group, params);
510
+ if (!grouped) {
511
+ return "";
512
+ }
513
+ return grouped.includes(" OR ") ? `(${grouped})` : grouped;
514
+ }
515
+ function buildWhereGroupClause(tableName, nodes, params) {
516
+ let result = "";
517
+ for (const node of nodes) {
518
+ const part = buildWhereNodeClause(tableName, node, params);
519
+ if (!part) {
520
+ continue;
521
+ }
522
+ if (!result) {
523
+ result = part;
524
+ continue;
525
+ }
526
+ result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
527
+ }
528
+ if (!result) {
529
+ return "";
530
+ }
531
+ return result;
532
+ }
533
+ function buildAdvancedWhereClause(tableName, where = {}, whereNodes = []) {
534
+ const params = [];
535
+ const nodes = [];
536
+ if (Object.keys(where).length > 0) {
537
+ nodes.push({ kind: "and", where });
538
+ }
539
+ nodes.push(...whereNodes);
540
+ const combined = buildWhereGroupClause(tableName, nodes, params);
541
+ return {
542
+ clause: combined ? ` WHERE ${combined}` : "",
543
+ params
544
+ };
545
+ }
546
+ function resolveSoftDeleteColumn(table) {
547
+ if (!table.softDeletes) {
548
+ return null;
549
+ }
550
+ if (table.softDeletes === true) {
551
+ return "deleted_at";
552
+ }
553
+ return table.softDeletes.column ?? "deleted_at";
554
+ }
555
+ function appendSoftDeleteScope(table, options, clauses) {
556
+ const column = resolveSoftDeleteColumn(table);
557
+ if (!column) {
558
+ return;
559
+ }
560
+ const qualifiedColumn = qualifyColumn(table.name, column);
561
+ if (options.onlyTrashed) {
562
+ clauses.push(`${qualifiedColumn} IS NOT NULL`);
563
+ return;
564
+ }
565
+ if (!options.withTrashed) {
566
+ clauses.push(`${qualifiedColumn} IS NULL`);
567
+ }
568
+ }
569
+ function buildQueryWhereClause(table, options = {}, whereNodes = []) {
570
+ const { clause, params } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes);
571
+ const softDeleteClauses = [];
572
+ appendSoftDeleteScope(table, options, softDeleteClauses);
573
+ if (softDeleteClauses.length === 0) {
574
+ return { clause, params };
575
+ }
576
+ const base = clause.replace(/^ WHERE /, "");
577
+ const scope = softDeleteClauses.join(" AND ");
578
+ return {
579
+ clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
580
+ params
581
+ };
582
+ }
583
+ function isQueryOrder(value) {
584
+ return "column" in value;
585
+ }
586
+ function normalizeOrderBy(orderBy) {
587
+ if (!orderBy) {
588
+ return [];
589
+ }
590
+ if (Array.isArray(orderBy)) {
591
+ return orderBy;
592
+ }
593
+ if (isQueryOrder(orderBy)) {
594
+ return [orderBy];
595
+ }
596
+ return Object.entries(orderBy).map(([column, direction]) => ({
597
+ column,
598
+ direction
599
+ }));
600
+ }
601
+ function buildOrderByClause(tableName, orderBy) {
602
+ const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
603
+ return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
604
+ });
605
+ return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
606
+ }
607
+ function buildGroupByClause(tableName, groupBy) {
608
+ if (!groupBy) {
609
+ return "";
610
+ }
611
+ const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
612
+ const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
613
+ return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
614
+ }
615
+ function buildHavingClause(tableName, having, params) {
616
+ if (!having) {
617
+ return "";
618
+ }
619
+ const body = appendWhereParts(tableName, having, params);
620
+ return body.length > 0 ? ` HAVING ${body}` : "";
621
+ }
622
+ function buildJoinClause(joins = []) {
623
+ return joins.map((join2) => {
624
+ const joinType = join2.type === "left" ? "LEFT JOIN" : "INNER JOIN";
625
+ const onClause = join2.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
626
+ return ` ${joinType} ${quoteIdentifier(join2.table)} ON ${onClause}`;
627
+ }).join("");
628
+ }
629
+ function buildLimitClause(limit) {
630
+ if (limit === undefined) {
631
+ return "";
632
+ }
633
+ if (!Number.isInteger(limit) || limit <= 0) {
634
+ throw new Error("Query limit must be a positive integer.");
635
+ }
636
+ return ` LIMIT ${limit}`;
637
+ }
638
+ function buildOffsetClause(offset) {
639
+ if (offset === undefined) {
640
+ return "";
641
+ }
642
+ if (!Number.isInteger(offset) || offset < 0) {
643
+ throw new Error("Query offset must be a non-negative integer.");
644
+ }
645
+ return ` OFFSET ${offset}`;
646
+ }
647
+ function buildReturningColumns(table) {
648
+ return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
649
+ }
650
+ function buildSelectList(table, select, params = []) {
651
+ if (!select || select.length === 0) {
652
+ return buildReturningColumns(table);
653
+ }
654
+ return select.map((item) => {
655
+ if (item.kind === "column") {
656
+ const column2 = qualifyColumn(item.table, item.column);
657
+ return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
658
+ }
659
+ if (item.kind === "literalText") {
660
+ return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
661
+ }
662
+ const column = qualifyColumn(item.table, item.column);
663
+ const placeholder = pushParam(params, item.query);
664
+ return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
665
+ }).join(", ");
666
+ }
667
+ function getDefinedColumnEntries(table, values, options = {}) {
668
+ const record = values;
669
+ const excluded = new Set(options.exclude ?? []);
670
+ return table.columns.flatMap((column) => {
671
+ if (excluded.has(column) || !Object.hasOwn(record, column)) {
672
+ return [];
673
+ }
674
+ const value = record[column];
675
+ if (value === undefined) {
676
+ return [];
677
+ }
678
+ return [[column, value]];
679
+ });
680
+ }
681
+ function buildSelectQuery(table, options = {}, whereNodes = []) {
682
+ const params = [];
683
+ const columns = buildSelectList(table, options.select, params);
684
+ const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
685
+ params.push(...whereParams);
686
+ const joins = buildJoinClause(options.joins);
687
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
688
+ const havingClause = buildHavingClause(table.name, options.having, params);
689
+ const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
690
+ const limit = buildLimitClause(options.limit);
691
+ const offset = buildOffsetClause(options.offset);
692
+ return {
693
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
694
+ params
695
+ };
696
+ }
697
+ function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
698
+ const params = [];
699
+ const { clause, params: whereParams } = buildQueryWhereClause(table, {
700
+ where,
701
+ withTrashed: options.withTrashed,
702
+ onlyTrashed: options.onlyTrashed
703
+ }, whereNodes);
704
+ params.push(...whereParams);
705
+ const joins = buildJoinClause(options.joins);
706
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
707
+ return {
708
+ text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
709
+ params
710
+ };
711
+ }
712
+ function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
713
+ assertSafeProjectionExpression(expression);
714
+ const params = [];
715
+ const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
716
+ params.push(...whereParams);
717
+ const joins = buildJoinClause(options.joins);
718
+ const groupBy = buildGroupByClause(table.name, options.groupBy);
719
+ const orderBy = buildOrderByClause(table.name, options.orderBy);
720
+ const limit = buildLimitClause(options.limit);
721
+ return {
722
+ text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
723
+ params
724
+ };
725
+ }
726
+ var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
727
+ function assertSafeProjectionExpression(expression) {
728
+ if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
729
+ throw new Error(`Unsafe projection expression: ${expression}`);
730
+ }
731
+ }
732
+ function buildGroupedCountQuery(table, column, where = {}, options = {}) {
733
+ const qualifiedColumn = qualifyColumn(table.name, column);
734
+ const { clause, params } = buildQueryWhereClause(table, {
735
+ where,
736
+ ...options
737
+ });
738
+ return {
739
+ text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
740
+ params
741
+ };
742
+ }
743
+ function buildInsertQuery(table, values) {
744
+ const entries = getDefinedColumnEntries(table, values);
745
+ if (entries.length === 0) {
746
+ throw new Error(`Cannot insert into ${table.name} without any column values.`);
747
+ }
748
+ const params = [];
749
+ const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
750
+ const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
751
+ const returningColumns = buildReturningColumns(table);
752
+ return {
753
+ text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
754
+ params
755
+ };
756
+ }
757
+ function buildUpdateQuery(table, id, changes) {
758
+ const entries = getDefinedColumnEntries(table, changes, {
759
+ exclude: [table.primaryKey]
760
+ });
761
+ if (entries.length === 0) {
762
+ throw new Error(`Cannot update ${table.name} without any changed column values.`);
763
+ }
764
+ const params = [];
765
+ const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
766
+ const primaryKeyPlaceholder = pushParam(params, id);
767
+ const returningColumns = buildReturningColumns(table);
768
+ const scopeClauses = [];
769
+ appendSoftDeleteScope(table, {}, scopeClauses);
770
+ const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
771
+ return {
772
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
773
+ params
774
+ };
775
+ }
776
+ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
777
+ const deletedAtColumn = resolveSoftDeleteColumn(table);
778
+ if (!deletedAtColumn) {
779
+ throw new Error(`Table ${table.name} does not support soft deletes.`);
780
+ }
781
+ const returningColumns = buildReturningColumns(table);
782
+ const scopeClauses = [];
783
+ appendSoftDeleteScope(table, {}, scopeClauses);
784
+ const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
785
+ return {
786
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
787
+ params: [deletedAt, id]
788
+ };
789
+ }
790
+ function buildRestoreByIdQuery(table, id) {
791
+ const deletedAtColumn = resolveSoftDeleteColumn(table);
792
+ if (!deletedAtColumn) {
793
+ throw new Error(`Table ${table.name} does not support soft deletes.`);
794
+ }
795
+ const returningColumns = buildReturningColumns(table);
796
+ return {
797
+ text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
798
+ params: [null, id]
799
+ };
800
+ }
801
+ function buildDeleteByIdQuery(table, id) {
802
+ return {
803
+ text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
804
+ params: [id]
805
+ };
806
+ }
807
+
808
+ // ../../src/core/database/relationships.ts
809
+ function hasMany(definition) {
810
+ return {
811
+ type: "hasMany",
812
+ ...definition
813
+ };
814
+ }
815
+ function hasOne(definition) {
816
+ return {
817
+ type: "hasOne",
818
+ ...definition
819
+ };
820
+ }
821
+ function belongsTo(definition) {
822
+ return {
823
+ type: "belongsTo",
824
+ ...definition
825
+ };
826
+ }
827
+ function belongsToMany(definition) {
828
+ return {
829
+ type: "belongsToMany",
830
+ ...definition
831
+ };
832
+ }
833
+ function indexHasManyRelation(parents, children, relation) {
834
+ const groups = new Map;
835
+ for (const parent of parents) {
836
+ groups.set(parent[relation.localKey], []);
837
+ }
838
+ for (const child of children) {
839
+ const key = child[relation.foreignKey];
840
+ const group = groups.get(key);
841
+ if (!group) {
842
+ continue;
843
+ }
844
+ group.push(child);
845
+ }
846
+ return groups;
847
+ }
848
+ function indexHasOneRelation(parents, children, relation) {
849
+ const grouped = indexHasManyRelation(parents, children, relation);
850
+ const result = new Map;
851
+ for (const parent of parents) {
852
+ const matches = grouped.get(parent[relation.localKey]) ?? [];
853
+ result.set(parent[relation.localKey], matches[0]);
854
+ }
855
+ return result;
856
+ }
857
+ function indexBelongsToRelation(children, parents, relation) {
858
+ const parentsById = new Map;
859
+ for (const parent of parents) {
860
+ parentsById.set(parent[relation.ownerKey], parent);
861
+ }
862
+ const result = new Map;
863
+ for (const child of children) {
864
+ const foreignKey = child[relation.foreignKey];
865
+ const parent = parentsById.get(foreignKey);
866
+ if (parent) {
867
+ result.set(foreignKey, parent);
868
+ }
869
+ }
870
+ return result;
871
+ }
872
+ function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
873
+ const relatedById = new Map;
874
+ for (const related of relatedRows) {
875
+ relatedById.set(related[relation.relatedKey], related);
876
+ }
877
+ const groups = new Map;
878
+ for (const parent of parents) {
879
+ groups.set(parent[relation.parentKey], []);
880
+ }
881
+ for (const pivot of pivotRows) {
882
+ const parentId = pivot[relation.foreignPivotKey];
883
+ const relatedId = pivot[relation.relatedPivotKey];
884
+ const group = groups.get(parentId);
885
+ const related = relatedById.get(relatedId);
886
+ if (!group || !related) {
887
+ continue;
888
+ }
889
+ group.push(related);
890
+ }
891
+ return groups;
892
+ }
893
+
894
+ // ../../src/config/database.ts
895
+ function readInteger(name, fallback) {
896
+ const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
897
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
898
+ }
899
+ var databaseConfig = {
900
+ url: process.env.DATABASE_URL ?? "",
901
+ poolMax: readInteger("DB_POOL_MAX", 10),
902
+ idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
903
+ maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
904
+ connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
905
+ };
906
+
907
+ // ../../src/core/database/connectionContext.ts
908
+ import { AsyncLocalStorage } from "async_hooks";
909
+ var activeConnection = new AsyncLocalStorage;
910
+ function runWithDatabaseConnection(connection, callback) {
911
+ return activeConnection.run(connection, callback);
912
+ }
913
+ function getActiveDatabaseConnection(fallback) {
914
+ return activeConnection.getStore() ?? fallback;
915
+ }
916
+
917
+ // ../../src/db/connection/createConnection.ts
918
+ var {SQL } = globalThis.Bun;
919
+ function createDatabaseConnection(config) {
920
+ if (!config.url) {
921
+ throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
922
+ }
923
+ return new SQL({
924
+ url: config.url,
925
+ max: config.poolMax,
926
+ idleTimeout: config.idleTimeoutSeconds,
927
+ maxLifetime: config.maxLifetimeSeconds,
928
+ connectionTimeout: config.connectionTimeoutSeconds
929
+ });
930
+ }
931
+
932
+ // ../../src/db/connection/index.ts
933
+ var connectionHolder = {
934
+ connection: null
935
+ };
936
+ function getDatabase() {
937
+ if (!connectionHolder.connection) {
938
+ connectionHolder.connection = createDatabaseConnection(databaseConfig);
939
+ }
940
+ return connectionHolder.connection;
941
+ }
942
+ var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
943
+ function resolveDatabase() {
944
+ return getActiveDatabaseConnection(getDatabase());
945
+ }
946
+ function resolveDatabaseForProperty(property) {
947
+ if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
948
+ return getDatabase();
949
+ }
950
+ return resolveDatabase();
951
+ }
952
+ var db = new Proxy(function database() {}, {
953
+ apply(_target, _thisArg, args) {
954
+ return resolveDatabase()(...args);
955
+ },
956
+ get(_target, property) {
957
+ const connection = resolveDatabaseForProperty(property);
958
+ const value = connection[property];
959
+ return typeof value === "function" ? value.bind(connection) : value;
960
+ }
961
+ });
962
+ var connection_default = db;
963
+
964
+ // ../../src/core/database/boundConnection.ts
965
+ var boundConnectionHolder = {
966
+ connection: null
967
+ };
968
+ function bindDatabaseConnection(connection) {
969
+ boundConnectionHolder.connection = connection;
970
+ }
971
+ function getBoundDatabaseConnection() {
972
+ return boundConnectionHolder.connection;
973
+ }
974
+
975
+ // ../../src/core/database/repositoryConnection.ts
976
+ function resolveRepositoryConnection() {
977
+ return getBoundDatabaseConnection() ?? connection_default;
978
+ }
979
+ var repositoryConnection = new Proxy({}, {
980
+ get(_target, property) {
981
+ const connection = resolveRepositoryConnection();
982
+ const value = connection[property];
983
+ return typeof value === "function" ? value.bind(connection) : value;
984
+ }
985
+ });
986
+
987
+ // ../../src/core/database/whereBuilder.ts
988
+ class WhereBuilder {
989
+ nodes = [];
990
+ where(where) {
991
+ this.nodes.push({ kind: "and", where });
992
+ return this;
993
+ }
994
+ orWhere(where) {
995
+ this.nodes.push({ kind: "or", where });
996
+ return this;
997
+ }
998
+ whereGroup(fn) {
999
+ const nested = new WhereBuilder;
1000
+ fn(nested);
1001
+ if (nested.nodes.length > 0) {
1002
+ this.nodes.push({ kind: "and", group: nested.nodes });
1003
+ }
1004
+ return this;
1005
+ }
1006
+ orWhereGroup(fn) {
1007
+ const nested = new WhereBuilder;
1008
+ fn(nested);
1009
+ if (nested.nodes.length > 0) {
1010
+ this.nodes.push({ kind: "or", group: nested.nodes });
1011
+ }
1012
+ return this;
1013
+ }
1014
+ }
1015
+
1016
+ // ../../src/core/database/repositoryQuery.ts
1017
+ class RepositoryQuery {
1018
+ repository;
1019
+ whereClause;
1020
+ queryOptions;
1021
+ eagerLoads = [];
1022
+ whereNodes = [];
1023
+ constructor(repository, whereClause = {}, queryOptions = {}) {
1024
+ this.repository = repository;
1025
+ this.whereClause = whereClause;
1026
+ this.queryOptions = queryOptions;
1027
+ }
1028
+ where(input) {
1029
+ if (typeof input === "function") {
1030
+ const builder = new WhereBuilder;
1031
+ input(builder);
1032
+ this.whereNodes.push(...builder.nodes);
1033
+ return this;
1034
+ }
1035
+ this.whereClause = { ...this.whereClause, ...input };
1036
+ return this;
1037
+ }
1038
+ orWhere(input) {
1039
+ if (typeof input === "function") {
1040
+ const builder = new WhereBuilder;
1041
+ input(builder);
1042
+ if (builder.nodes.length > 0) {
1043
+ this.whereNodes.push({ kind: "or", group: builder.nodes });
1044
+ }
1045
+ return this;
1046
+ }
1047
+ this.whereNodes.push({ kind: "or", where: input });
1048
+ return this;
1049
+ }
1050
+ orderBy(orderBy) {
1051
+ this.queryOptions = { ...this.queryOptions, orderBy };
1052
+ return this;
1053
+ }
1054
+ limit(limit) {
1055
+ this.queryOptions = { ...this.queryOptions, limit };
1056
+ return this;
1057
+ }
1058
+ offset(offset) {
1059
+ this.queryOptions = { ...this.queryOptions, offset };
1060
+ return this;
1061
+ }
1062
+ join(left, right) {
1063
+ return this.addJoin("inner", left, right);
1064
+ }
1065
+ leftJoin(left, right) {
1066
+ return this.addJoin("left", left, right);
1067
+ }
1068
+ groupBy(groupBy) {
1069
+ this.queryOptions = { ...this.queryOptions, groupBy };
1070
+ return this;
1071
+ }
1072
+ having(having) {
1073
+ this.queryOptions = { ...this.queryOptions, having };
1074
+ return this;
1075
+ }
1076
+ withHasMany(as, relation, childRepository, options = {}) {
1077
+ this.eagerLoads.push({
1078
+ kind: "hasMany",
1079
+ as,
1080
+ relation,
1081
+ repository: childRepository,
1082
+ options
1083
+ });
1084
+ return this;
1085
+ }
1086
+ withBelongsTo(as, relation, parentRepository, options = {}) {
1087
+ this.eagerLoads.push({
1088
+ kind: "belongsTo",
1089
+ as,
1090
+ relation,
1091
+ repository: parentRepository,
1092
+ options
1093
+ });
1094
+ return this;
1095
+ }
1096
+ async get() {
1097
+ const rows = await this.repository.findAll(this.buildOptions());
1098
+ return await this.attach(rows);
1099
+ }
1100
+ async first() {
1101
+ const rows = await this.get();
1102
+ return rows[0] ?? null;
1103
+ }
1104
+ async paginate(options) {
1105
+ return await this.repository.paginate({
1106
+ ...this.buildOptions(),
1107
+ page: options.page,
1108
+ perPage: options.perPage
1109
+ });
1110
+ }
1111
+ buildOptions() {
1112
+ return {
1113
+ ...this.queryOptions,
1114
+ where: this.whereClause,
1115
+ whereNodes: this.whereNodes
1116
+ };
1117
+ }
1118
+ addJoin(type, left, right) {
1119
+ const leftRef = parseQualifiedColumn(left);
1120
+ const rightRef = parseQualifiedColumn(right);
1121
+ const table = type === "inner" ? rightRef.table : rightRef.table;
1122
+ const joins = this.queryOptions.joins ?? [];
1123
+ const existing = joins.find((join2) => join2.table === table && join2.type === type);
1124
+ if (existing) {
1125
+ existing.on.push({ left: leftRef, right: rightRef });
1126
+ return this;
1127
+ }
1128
+ this.queryOptions = {
1129
+ ...this.queryOptions,
1130
+ joins: [
1131
+ ...joins,
1132
+ {
1133
+ type,
1134
+ table,
1135
+ on: [{ left: leftRef, right: rightRef }]
1136
+ }
1137
+ ]
1138
+ };
1139
+ return this;
1140
+ }
1141
+ async attach(rows) {
1142
+ if (rows.length === 0 || this.eagerLoads.length === 0) {
1143
+ return rows.map((row) => ({ ...row }));
1144
+ }
1145
+ let result = rows.map((row) => ({ ...row }));
1146
+ for (const load of this.eagerLoads) {
1147
+ if (load.kind === "hasMany") {
1148
+ const relation2 = load.relation;
1149
+ const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
1150
+ result = result.map((row) => ({
1151
+ ...row,
1152
+ [load.as]: grouped2.get(row[relation2.localKey]) ?? []
1153
+ }));
1154
+ continue;
1155
+ }
1156
+ const relation = load.relation;
1157
+ const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
1158
+ result = result.map((row) => ({
1159
+ ...row,
1160
+ [load.as]: grouped.get(row[relation.foreignKey])
1161
+ }));
1162
+ }
1163
+ return result;
1164
+ }
1165
+ }
1166
+
1167
+ // ../../src/core/database/baseRepository.ts
1168
+ class BaseRepository {
1169
+ table;
1170
+ connection;
1171
+ constructor(table, connection = repositoryConnection) {
1172
+ this.table = table;
1173
+ this.connection = connection;
1174
+ }
1175
+ async findAll(options = {}) {
1176
+ return await withDatabaseErrorHandling(async () => {
1177
+ const { whereNodes, ...queryOptions } = options;
1178
+ const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
1179
+ return await this.connection.unsafe(text, params);
1180
+ });
1181
+ }
1182
+ async paginate(options) {
1183
+ const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
1184
+ const total = await this.countWhere(where, {
1185
+ withTrashed: options.withTrashed,
1186
+ onlyTrashed: options.onlyTrashed,
1187
+ joins: options.joins,
1188
+ groupBy: options.groupBy
1189
+ }, whereNodes);
1190
+ const offset = (page - 1) * perPage;
1191
+ const data = await this.findAll({
1192
+ ...queryOptions,
1193
+ where,
1194
+ whereNodes,
1195
+ limit: perPage,
1196
+ offset
1197
+ });
1198
+ return {
1199
+ data,
1200
+ meta: buildPaginationMeta({ page, perPage, total })
1201
+ };
1202
+ }
1203
+ async chunk(count, callback, options = {}) {
1204
+ if (!Number.isInteger(count) || count <= 0) {
1205
+ throw new Error("Chunk size must be a positive integer.");
1206
+ }
1207
+ let offset = 0;
1208
+ while (true) {
1209
+ const rows = await this.findAll({
1210
+ ...options,
1211
+ limit: count,
1212
+ offset
1213
+ });
1214
+ if (rows.length === 0) {
1215
+ return;
1216
+ }
1217
+ const shouldContinue = await callback(rows);
1218
+ if (shouldContinue === false || rows.length < count) {
1219
+ return;
1220
+ }
1221
+ offset += count;
1222
+ }
1223
+ }
1224
+ async cursorPaginate(options) {
1225
+ const {
1226
+ perPage,
1227
+ cursor,
1228
+ cursorColumn = this.table.primaryKey,
1229
+ direction = "asc",
1230
+ where = {},
1231
+ whereNodes,
1232
+ ...queryOptions
1233
+ } = options;
1234
+ if (!Number.isInteger(perPage) || perPage <= 0) {
1235
+ throw new Error("Cursor page size must be a positive integer.");
1236
+ }
1237
+ const cursorWhere = { ...where };
1238
+ if (cursor !== undefined) {
1239
+ cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
1240
+ }
1241
+ const rows = await this.findAll({
1242
+ ...queryOptions,
1243
+ where: cursorWhere,
1244
+ whereNodes,
1245
+ orderBy: { [cursorColumn]: direction },
1246
+ limit: perPage + 1
1247
+ });
1248
+ const hasMore = rows.length > perPage;
1249
+ const data = hasMore ? rows.slice(0, perPage) : rows;
1250
+ const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
1251
+ const prevCursor = cursor ?? null;
1252
+ return {
1253
+ data,
1254
+ meta: {
1255
+ per_page: perPage,
1256
+ next_cursor: nextCursor,
1257
+ prev_cursor: prevCursor,
1258
+ has_more: hasMore
1259
+ }
1260
+ };
1261
+ }
1262
+ async findById(id) {
1263
+ return await this.firstOrNull({
1264
+ [this.table.primaryKey]: id
1265
+ });
1266
+ }
1267
+ async findByIdOrThrow(id, errorFactory) {
1268
+ const record = await this.findById(id);
1269
+ if (record) {
1270
+ return record;
1271
+ }
1272
+ throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
1273
+ }
1274
+ async findByIds(ids) {
1275
+ const uniqueIds = [...new Set(ids)];
1276
+ if (uniqueIds.length === 0) {
1277
+ return [];
1278
+ }
1279
+ return await this.findWhere({
1280
+ [this.table.primaryKey]: uniqueIds
1281
+ });
1282
+ }
1283
+ async firstOrNull(where, options = {}) {
1284
+ const [record] = await this.findAll({ ...options, where, limit: 1 });
1285
+ return record ?? null;
1286
+ }
1287
+ async create(values) {
1288
+ return await withDatabaseErrorHandling(async () => {
1289
+ const { text, params } = buildInsertQuery(this.table, values);
1290
+ const [record] = await this.connection.unsafe(text, params);
1291
+ if (!record) {
1292
+ throw new Error(`Insert into ${this.table.name} did not return a record.`);
1293
+ }
1294
+ const entity = record;
1295
+ await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
1296
+ return entity;
1297
+ });
1298
+ }
1299
+ async updateById(id, changes) {
1300
+ return await withDatabaseErrorHandling(async () => {
1301
+ const { text, params } = buildUpdateQuery(this.table, id, changes);
1302
+ const [record] = await this.connection.unsafe(text, params);
1303
+ const entity = record ?? null;
1304
+ if (entity) {
1305
+ await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
1306
+ }
1307
+ return entity;
1308
+ });
1309
+ }
1310
+ async updateByIdOrThrow(id, changes, errorFactory) {
1311
+ const record = await this.updateById(id, changes);
1312
+ if (record) {
1313
+ return record;
1314
+ }
1315
+ throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
1316
+ }
1317
+ async deleteById(id) {
1318
+ if (resolveSoftDeleteColumn(this.table)) {
1319
+ return await this.softDeleteById(id);
1320
+ }
1321
+ return await this.forceDeleteById(id);
1322
+ }
1323
+ async softDeleteById(id) {
1324
+ return await withDatabaseErrorHandling(async () => {
1325
+ const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
1326
+ const [record] = await this.connection.unsafe(text, params);
1327
+ if (!record) {
1328
+ return false;
1329
+ }
1330
+ await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
1331
+ return true;
1332
+ });
1333
+ }
1334
+ async forceDeleteById(id) {
1335
+ return await withDatabaseErrorHandling(async () => {
1336
+ const { text, params } = buildDeleteByIdQuery(this.table, id);
1337
+ const [row] = await this.connection.unsafe(text, params);
1338
+ if (!row) {
1339
+ return false;
1340
+ }
1341
+ await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
1342
+ id
1343
+ });
1344
+ return true;
1345
+ });
1346
+ }
1347
+ async restoreById(id) {
1348
+ return await withDatabaseErrorHandling(async () => {
1349
+ const { text, params } = buildRestoreByIdQuery(this.table, id);
1350
+ const [record] = await this.connection.unsafe(text, params);
1351
+ if (!record) {
1352
+ return null;
1353
+ }
1354
+ const entity = record;
1355
+ await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
1356
+ return entity;
1357
+ });
1358
+ }
1359
+ withConnection(connection) {
1360
+ const clone = Object.create(Object.getPrototypeOf(this));
1361
+ Object.assign(clone, this);
1362
+ clone.connection = connection;
1363
+ return clone;
1364
+ }
1365
+ getConnection() {
1366
+ return this.connection;
1367
+ }
1368
+ getTable() {
1369
+ return this.table;
1370
+ }
1371
+ query(where = {}) {
1372
+ return new RepositoryQuery(this, where);
1373
+ }
1374
+ async findWhere(where, options = {}) {
1375
+ return await this.findAll({ ...options, where });
1376
+ }
1377
+ async countWhere(where = {}, options = {}, whereNodes = []) {
1378
+ const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
1379
+ const [row] = await this.connection.unsafe(text, params);
1380
+ return Number(row?.count ?? 0);
1381
+ }
1382
+ async averageColumn(column, where = {}) {
1383
+ const qualifiedColumn = qualifyColumn(this.table.name, column);
1384
+ return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
1385
+ }
1386
+ async averageExpression(expression, alias, where = {}) {
1387
+ const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
1388
+ const [row] = await this.connection.unsafe(text, params);
1389
+ return Math.round(Number(row?.[alias] ?? 0));
1390
+ }
1391
+ async pluckNumberValues(expression, alias, options = {}) {
1392
+ const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
1393
+ const rows = await this.connection.unsafe(text, params);
1394
+ return rows.flatMap((row) => {
1395
+ const value = row[alias];
1396
+ return value === null || value === undefined ? [] : [Number(value)];
1397
+ });
1398
+ }
1399
+ async countGroupedBy(column, where = {}) {
1400
+ const { text, params } = buildGroupedCountQuery(this.table, column, where);
1401
+ const rows = await this.connection.unsafe(text, params);
1402
+ return rows.map(({ value, count }) => ({
1403
+ value,
1404
+ count: Number(count)
1405
+ }));
1406
+ }
1407
+ async findByHasManyRelation(relation, parentId, options = {}) {
1408
+ return await this.findWhere({
1409
+ [relation.foreignKey]: parentId
1410
+ }, options);
1411
+ }
1412
+ async loadHasManyForParents(parents, relation, options = {}) {
1413
+ if (parents.length === 0) {
1414
+ return indexHasManyRelation(parents, [], relation);
1415
+ }
1416
+ const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
1417
+ const children = await this.findWhere({
1418
+ [relation.foreignKey]: parentIds
1419
+ }, options);
1420
+ return indexHasManyRelation(parents, children, relation);
1421
+ }
1422
+ async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
1423
+ if (children.length === 0) {
1424
+ return new Map;
1425
+ }
1426
+ const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
1427
+ const parents = await parentRepository.withConnection(this.connection).findWhere({
1428
+ [relation.ownerKey]: ownerIds
1429
+ }, options);
1430
+ return indexBelongsToRelation(children, parents, relation);
1431
+ }
1432
+ }
1433
+ var baseRepository_default = BaseRepository;
1434
+ // ../../src/core/database/connection.ts
1435
+ function createDatabaseConnection2(source) {
1436
+ return {
1437
+ async unsafe(query, params = []) {
1438
+ return await source.unsafe(query, params);
1439
+ }
1440
+ };
1441
+ }
1442
+ // ../../src/core/database/model.ts
1443
+ var modelRepositories = new WeakMap;
1444
+ var modelGlobalScopes = new WeakMap;
1445
+ var modelBooted = new WeakSet;
1446
+ function resolveModelRepository(model) {
1447
+ const repository = modelRepositories.get(model);
1448
+ if (!repository) {
1449
+ throw new Error(`${model.name}.repository() is not implemented.`);
1450
+ }
1451
+ return repository;
1452
+ }
1453
+ function modelStatics(model) {
1454
+ return model;
1455
+ }
1456
+ function ensureBooted(model) {
1457
+ if (modelBooted.has(model)) {
1458
+ return;
1459
+ }
1460
+ modelBooted.add(model);
1461
+ const boot = model.boot;
1462
+ if (typeof boot === "function") {
1463
+ boot.call(model);
1464
+ }
1465
+ }
1466
+ function getGlobalScopes(model) {
1467
+ return modelGlobalScopes.get(model) ?? [];
1468
+ }
1469
+ function hydrateValue(value, cast) {
1470
+ if (value === null || value === undefined) {
1471
+ return value;
1472
+ }
1473
+ switch (cast) {
1474
+ case "date":
1475
+ case "datetime":
1476
+ return value instanceof Date ? value : new Date(String(value));
1477
+ case "json":
1478
+ return typeof value === "string" ? JSON.parse(value) : value;
1479
+ case "bool":
1480
+ case "boolean":
1481
+ return value === true || value === 1 || value === "1" || value === "true";
1482
+ default:
1483
+ return value;
1484
+ }
1485
+ }
1486
+ function dehydrateValue(value, cast) {
1487
+ if (value === null || value === undefined) {
1488
+ return value;
1489
+ }
1490
+ switch (cast) {
1491
+ case "date":
1492
+ case "datetime":
1493
+ return value instanceof Date ? value : new Date(String(value));
1494
+ case "json":
1495
+ return typeof value === "string" ? value : JSON.stringify(value);
1496
+ case "bool":
1497
+ case "boolean":
1498
+ return Boolean(value);
1499
+ default:
1500
+ return value;
1501
+ }
1502
+ }
1503
+ function filterMassAssignable(fillable, guarded, input) {
1504
+ const resolvedGuarded = guarded ?? true;
1505
+ if (fillable && fillable.length > 0) {
1506
+ const allowed = new Set(fillable);
1507
+ return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
1508
+ }
1509
+ if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
1510
+ return {};
1511
+ }
1512
+ const blocked = new Set(resolvedGuarded);
1513
+ return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
1514
+ }
1515
+ function applyCasts(values, casts, direction) {
1516
+ if (Object.keys(casts).length === 0) {
1517
+ return values;
1518
+ }
1519
+ const result = { ...values };
1520
+ const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
1521
+ for (const [key, cast] of Object.entries(casts)) {
1522
+ if (key in result && cast) {
1523
+ result[key] = castFn(result[key], cast);
1524
+ }
1525
+ }
1526
+ return result;
1527
+ }
1528
+ function applyTimestampsOnCreate(columns, values, enabled) {
1529
+ if (!enabled) {
1530
+ return values;
1531
+ }
1532
+ const now = new Date;
1533
+ const result = { ...values };
1534
+ if (columns.includes("created_at")) {
1535
+ result.created_at = now;
1536
+ }
1537
+ if (columns.includes("updated_at")) {
1538
+ result.updated_at = now;
1539
+ }
1540
+ return result;
1541
+ }
1542
+ function applyTimestampsOnUpdate(columns, values, enabled) {
1543
+ if (!enabled) {
1544
+ return values;
1545
+ }
1546
+ const result = { ...values };
1547
+ if (columns.includes("updated_at")) {
1548
+ result.updated_at = new Date;
1549
+ }
1550
+ return result;
1551
+ }
1552
+
1553
+ class Model {
1554
+ attributes;
1555
+ repository;
1556
+ static $fillable;
1557
+ static $guarded;
1558
+ static $casts = {};
1559
+ static $timestamps = true;
1560
+ _exists;
1561
+ constructor(attributes, repository, exists = true) {
1562
+ this.attributes = attributes;
1563
+ this.repository = repository;
1564
+ this._exists = exists;
1565
+ }
1566
+ get $exists() {
1567
+ return this._exists;
1568
+ }
1569
+ get(key) {
1570
+ return this.attributes[key];
1571
+ }
1572
+ get id() {
1573
+ return this.attributes[this.primaryKey()];
1574
+ }
1575
+ toObject() {
1576
+ return { ...this.attributes };
1577
+ }
1578
+ primaryKey() {
1579
+ throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
1580
+ }
1581
+ static primaryKeyField() {
1582
+ return resolveModelRepository(Model).getTable().primaryKey;
1583
+ }
1584
+ static hydrateAttributes(attributes) {
1585
+ const casts = Model.$casts ?? {};
1586
+ return applyCasts(attributes, casts, "hydrate");
1587
+ }
1588
+ static dehydrateAttributes(attributes) {
1589
+ const casts = Model.$casts ?? {};
1590
+ return applyCasts(attributes, casts, "dehydrate");
1591
+ }
1592
+ static fromRecord(record, repository, exists = true) {
1593
+ const hydrated = Model.hydrateAttributes(record);
1594
+ return new Model(hydrated, repository, exists);
1595
+ }
1596
+ static boot() {}
1597
+ static addGlobalScope(_name, scope) {
1598
+ ensureBooted(Model);
1599
+ const existing = modelGlobalScopes.get(Model) ?? [];
1600
+ modelGlobalScopes.set(Model, [
1601
+ ...existing,
1602
+ scope
1603
+ ]);
1604
+ }
1605
+ static repository() {
1606
+ return resolveModelRepository(this);
1607
+ }
1608
+ static query() {
1609
+ ensureBooted(this);
1610
+ const repository = resolveModelRepository(Model);
1611
+ let query = repository.query();
1612
+ for (const scope of getGlobalScopes(Model)) {
1613
+ query = scope(query);
1614
+ }
1615
+ return query;
1616
+ }
1617
+ static async create(attributes) {
1618
+ const statics = modelStatics(this);
1619
+ ensureBooted(Model);
1620
+ const repository = resolveModelRepository(Model);
1621
+ const table = repository.getTable();
1622
+ const timestamps = statics.$timestamps ?? true;
1623
+ const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
1624
+ const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1625
+ const payload = statics.dehydrateAttributes(withTimestamps);
1626
+ const record = await repository.create(payload);
1627
+ return statics.fromRecord(record, repository, true);
1628
+ }
1629
+ static async find(id) {
1630
+ const statics = modelStatics(this);
1631
+ const repository = resolveModelRepository(Model);
1632
+ const primaryKey = statics.primaryKeyField();
1633
+ const record = await Model.query.call(Model).where({ [primaryKey]: id }).first();
1634
+ return record ? statics.fromRecord(record, repository, true) : null;
1635
+ }
1636
+ static async findOrFail(id, errorFactory) {
1637
+ const model = await Model.find.call(Model, id);
1638
+ if (model) {
1639
+ return model;
1640
+ }
1641
+ throw errorFactory?.(id) ?? new NotFoundError(`${Model.name} ${String(id)} not found.`);
1642
+ }
1643
+ static async all(options = {}) {
1644
+ const statics = modelStatics(this);
1645
+ const repository = resolveModelRepository(Model);
1646
+ let query = Model.query.call(Model);
1647
+ if (options.orderBy) {
1648
+ query = query.orderBy(options.orderBy);
1649
+ }
1650
+ if (options.limit !== undefined) {
1651
+ query = query.limit(options.limit);
1652
+ }
1653
+ const rows = await query.get();
1654
+ return rows.map((row) => statics.fromRecord(row, repository, true));
1655
+ }
1656
+ static async firstWhere(where, options = {}) {
1657
+ const statics = modelStatics(this);
1658
+ const repository = resolveModelRepository(Model);
1659
+ let query = Model.query.call(Model).where(where);
1660
+ if (options.orderBy) {
1661
+ query = query.orderBy(options.orderBy);
1662
+ }
1663
+ const record = await query.first();
1664
+ return record ? statics.fromRecord(record, repository, true) : null;
1665
+ }
1666
+ async save() {
1667
+ const ModelClass = modelStatics(this.constructor);
1668
+ const timestamps = ModelClass.$timestamps ?? true;
1669
+ const casts = ModelClass.$casts ?? {};
1670
+ const table = this.repository.getTable();
1671
+ if (this.$exists) {
1672
+ const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
1673
+ const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
1674
+ this.attributes = ModelClass.hydrateAttributes(record2);
1675
+ return this;
1676
+ }
1677
+ const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
1678
+ const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
1679
+ const payload = ModelClass.dehydrateAttributes(withTimestamps);
1680
+ const record = await this.repository.create(payload);
1681
+ this.attributes = ModelClass.hydrateAttributes(record);
1682
+ this._exists = true;
1683
+ return this;
1684
+ }
1685
+ async update(changes) {
1686
+ const ModelClass = modelStatics(this.constructor);
1687
+ const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
1688
+ Object.assign(this.attributes, assignable);
1689
+ return await this.save();
1690
+ }
1691
+ async delete() {
1692
+ if (resolveSoftDeleteColumn(this.repository.getTable())) {
1693
+ return await this.repository.deleteById(this.id);
1694
+ }
1695
+ return await this.repository.forceDeleteById(this.id);
1696
+ }
1697
+ async forceDelete() {
1698
+ return await this.repository.forceDeleteById(this.id);
1699
+ }
1700
+ async restore() {
1701
+ const ModelClass = modelStatics(this.constructor);
1702
+ const record = await this.repository.restoreById(this.id);
1703
+ if (!record) {
1704
+ return null;
1705
+ }
1706
+ this.attributes = ModelClass.hydrateAttributes(record);
1707
+ return this;
1708
+ }
1709
+ async loadHasMany(as, relation, childRepository, options = {}) {
1710
+ const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
1711
+ const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
1712
+ return Object.assign(this, { [as]: loaded });
1713
+ }
1714
+ async loadHasOne(as, relation, childRepository, options = {}) {
1715
+ const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
1716
+ const value = loaded[as]?.[0];
1717
+ return Object.assign(this, { [as]: value });
1718
+ }
1719
+ async loadBelongsTo(as, relation, parentRepository, options = {}) {
1720
+ const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
1721
+ const loaded = grouped.get(this.attributes[relation.foreignKey]);
1722
+ return Object.assign(this, { [as]: loaded });
1723
+ }
1724
+ async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
1725
+ const connection = this.repository.getConnection();
1726
+ const parentId = this.attributes[relation.parentKey];
1727
+ const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
1728
+ if (pivotRows.length === 0) {
1729
+ return Object.assign(this, { [as]: [] });
1730
+ }
1731
+ const relatedIds = [
1732
+ ...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
1733
+ ];
1734
+ const relatedRows = await relatedRepository.withConnection(connection).findAll({
1735
+ ...options,
1736
+ where: {
1737
+ [relation.relatedKey]: relatedIds
1738
+ }
1739
+ });
1740
+ const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
1741
+ const loaded = grouped.get(parentId) ?? [];
1742
+ return Object.assign(this, { [as]: loaded });
1743
+ }
1744
+ mergeAttributes(patch) {
1745
+ Object.assign(this.attributes, patch);
1746
+ return this;
1747
+ }
1748
+ }
1749
+ function registerModelRepository(model, repository) {
1750
+ modelRepositories.set(model, repository);
1751
+ ensureBooted(model);
1752
+ return model;
1753
+ }
1754
+ // ../../src/core/database/schema/columnDefinition.ts
1755
+ class ColumnDefinition {
1756
+ name;
1757
+ kind;
1758
+ length;
1759
+ isNullable = false;
1760
+ isPrimary = false;
1761
+ isUnique = false;
1762
+ autoIncrement = false;
1763
+ defaultValue;
1764
+ checkExpression;
1765
+ foreignKey;
1766
+ constructor(name, kind) {
1767
+ this.name = name;
1768
+ this.kind = kind;
1769
+ }
1770
+ nullable() {
1771
+ this.isNullable = true;
1772
+ return this;
1773
+ }
1774
+ notNullable() {
1775
+ this.isNullable = false;
1776
+ return this;
1777
+ }
1778
+ default(value) {
1779
+ if (typeof value === "boolean") {
1780
+ this.defaultValue = value ? "TRUE" : "FALSE";
1781
+ return this;
1782
+ }
1783
+ if (typeof value === "number") {
1784
+ this.defaultValue = String(value);
1785
+ return this;
1786
+ }
1787
+ this.defaultValue = `'${value.replace(/'/g, "''")}'`;
1788
+ return this;
1789
+ }
1790
+ defaultRaw(expression) {
1791
+ this.defaultValue = expression;
1792
+ return this;
1793
+ }
1794
+ unique() {
1795
+ this.isUnique = true;
1796
+ return this;
1797
+ }
1798
+ primary() {
1799
+ this.isPrimary = true;
1800
+ return this;
1801
+ }
1802
+ check(expression) {
1803
+ this.checkExpression = expression;
1804
+ return this;
1805
+ }
1806
+ }
1807
+
1808
+ class ForeignIdColumnDefinition extends ColumnDefinition {
1809
+ constructor(name) {
1810
+ super(name, "foreignId");
1811
+ this.notNullable();
1812
+ }
1813
+ references(table, column = "id") {
1814
+ this.foreignKey = {
1815
+ referencesTable: table,
1816
+ referencesColumn: column
1817
+ };
1818
+ return this;
1819
+ }
1820
+ constrained(table) {
1821
+ const referencesTable = table ?? inferReferencedTable(this.name);
1822
+ return this.references(referencesTable);
1823
+ }
1824
+ cascadeOnDelete() {
1825
+ if (!this.foreignKey) {
1826
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
1827
+ }
1828
+ this.foreignKey.onDelete = "cascade";
1829
+ return this;
1830
+ }
1831
+ nullOnDelete() {
1832
+ if (!this.foreignKey) {
1833
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
1834
+ }
1835
+ this.foreignKey.onDelete = "set null";
1836
+ return this;
1837
+ }
1838
+ }
1839
+ function inferReferencedTable(columnName) {
1840
+ if (!columnName.endsWith("_id")) {
1841
+ throw new Error(`Cannot infer referenced table from column ${columnName}`);
1842
+ }
1843
+ return columnName.slice(0, -3);
1844
+ }
1845
+
1846
+ // ../../src/core/database/schema/blueprint.ts
1847
+ class Blueprint {
1848
+ table;
1849
+ action;
1850
+ columns = [];
1851
+ indexes = [];
1852
+ droppedColumns = [];
1853
+ droppedIndexes = [];
1854
+ constructor(table, action) {
1855
+ this.table = table;
1856
+ this.action = action;
1857
+ }
1858
+ id(name = "id") {
1859
+ const column = new ColumnDefinition(name, "id");
1860
+ column.primary();
1861
+ column.autoIncrement = true;
1862
+ this.columns.push(column);
1863
+ return column;
1864
+ }
1865
+ string(name, length) {
1866
+ const column = new ColumnDefinition(name, "string");
1867
+ column.length = length;
1868
+ column.notNullable();
1869
+ this.columns.push(column);
1870
+ return column;
1871
+ }
1872
+ text(name) {
1873
+ const column = new ColumnDefinition(name, "text");
1874
+ column.notNullable();
1875
+ this.columns.push(column);
1876
+ return column;
1877
+ }
1878
+ boolean(name) {
1879
+ const column = new ColumnDefinition(name, "boolean");
1880
+ column.notNullable();
1881
+ this.columns.push(column);
1882
+ return column;
1883
+ }
1884
+ integer(name) {
1885
+ const column = new ColumnDefinition(name, "integer");
1886
+ column.notNullable();
1887
+ this.columns.push(column);
1888
+ return column;
1889
+ }
1890
+ bigInteger(name) {
1891
+ const column = new ColumnDefinition(name, "bigInteger");
1892
+ column.notNullable();
1893
+ this.columns.push(column);
1894
+ return column;
1895
+ }
1896
+ timestamp(name) {
1897
+ const column = new ColumnDefinition(name, "timestamp");
1898
+ column.notNullable();
1899
+ this.columns.push(column);
1900
+ return column;
1901
+ }
1902
+ json(name) {
1903
+ const column = new ColumnDefinition(name, "json");
1904
+ column.notNullable();
1905
+ this.columns.push(column);
1906
+ return column;
1907
+ }
1908
+ jsonb(name) {
1909
+ const column = new ColumnDefinition(name, "jsonb");
1910
+ column.notNullable();
1911
+ this.columns.push(column);
1912
+ return column;
1913
+ }
1914
+ foreignId(name) {
1915
+ const column = new ForeignIdColumnDefinition(name);
1916
+ this.columns.push(column);
1917
+ return column;
1918
+ }
1919
+ timestamps() {
1920
+ this.timestamp("created_at").defaultRaw("NOW()");
1921
+ this.timestamp("updated_at").defaultRaw("NOW()");
1922
+ }
1923
+ softDeletes() {
1924
+ this.timestamp("deleted_at").nullable();
1925
+ }
1926
+ dropColumn(name) {
1927
+ this.droppedColumns.push(name);
1928
+ }
1929
+ dropSoftDeletes() {
1930
+ this.dropColumn("deleted_at");
1931
+ this.dropIndex(`idx_${this.table}_deleted_at`);
1932
+ }
1933
+ dropIndex(name) {
1934
+ this.droppedIndexes.push(name);
1935
+ }
1936
+ unique(columns, name) {
1937
+ this.indexes.push({
1938
+ name,
1939
+ columns: Array.isArray(columns) ? columns : [columns],
1940
+ kind: "unique"
1941
+ });
1942
+ }
1943
+ index(columns, options = {}) {
1944
+ this.indexes.push({
1945
+ name: options.name,
1946
+ columns: Array.isArray(columns) ? columns : [columns],
1947
+ kind: "index",
1948
+ order: options.order
1949
+ });
1950
+ }
1951
+ partialIndex(columns, where, nameOrOptions) {
1952
+ const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
1953
+ this.indexes.push({
1954
+ name: options.name,
1955
+ columns: Array.isArray(columns) ? columns : [columns],
1956
+ kind: options.unique ? "uniquePartial" : "partial",
1957
+ where
1958
+ });
1959
+ }
1960
+ fullText(columns, name) {
1961
+ this.indexes.push({
1962
+ name,
1963
+ columns: Array.isArray(columns) ? columns : [columns],
1964
+ kind: "fullText"
1965
+ });
1966
+ }
1967
+ ginIndex(column, name) {
1968
+ this.indexes.push({
1969
+ name,
1970
+ columns: [column],
1971
+ kind: "gin"
1972
+ });
1973
+ }
1974
+ }
1975
+ // ../../src/core/database/schema/driver.ts
1976
+ function normalizeConnectionName(connection) {
1977
+ const normalized = connection.trim().toLowerCase();
1978
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
1979
+ return "pgsql";
1980
+ }
1981
+ if (normalized === "mysql" || normalized === "mariadb") {
1982
+ return "mysql";
1983
+ }
1984
+ if (normalized === "sqlite") {
1985
+ return "sqlite";
1986
+ }
1987
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
1988
+ }
1989
+ function resolveDriverFromUrl(url) {
1990
+ const normalized = url.trim().toLowerCase();
1991
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
1992
+ return "pgsql";
1993
+ }
1994
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
1995
+ return "mysql";
1996
+ }
1997
+ if (normalized.startsWith("sqlite:")) {
1998
+ return "sqlite";
1999
+ }
2000
+ return null;
2001
+ }
2002
+ function resolveDatabaseDriver(options = {}) {
2003
+ const connection = options.connection ?? process.env.DB_CONNECTION;
2004
+ if (connection) {
2005
+ return normalizeConnectionName(connection);
2006
+ }
2007
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
2008
+ const fromUrl = resolveDriverFromUrl(url);
2009
+ if (fromUrl) {
2010
+ return fromUrl;
2011
+ }
2012
+ return "pgsql";
2013
+ }
2014
+ // ../../src/core/database/schema/errors.ts
2015
+ class UnsupportedSchemaFeatureError extends Error {
2016
+ constructor(feature, driver) {
2017
+ super(`${feature} is not supported for the ${driver} driver`);
2018
+ this.name = "UnsupportedSchemaFeatureError";
2019
+ }
2020
+ }
2021
+ // ../../src/core/database/schema/grammars/grammar.ts
2022
+ function compileColumnType(driver, column) {
2023
+ switch (column.kind) {
2024
+ case "id":
2025
+ return compileIdType(driver);
2026
+ case "string":
2027
+ return compileStringType(driver, column.length);
2028
+ case "text":
2029
+ return compileTextType(driver);
2030
+ case "boolean":
2031
+ return compileBooleanType(driver);
2032
+ case "integer":
2033
+ case "foreignId":
2034
+ return compileIntegerType(driver);
2035
+ case "bigInteger":
2036
+ return compileBigIntegerType(driver);
2037
+ case "timestamp":
2038
+ return compileTimestampType(driver);
2039
+ case "json":
2040
+ return compileJsonType(driver);
2041
+ case "jsonb":
2042
+ return compileJsonbType(driver);
2043
+ default:
2044
+ throw new Error(`Unsupported column kind: ${column.kind}`);
2045
+ }
2046
+ }
2047
+ function compileIdType(driver) {
2048
+ switch (driver) {
2049
+ case "pgsql":
2050
+ return "SERIAL";
2051
+ case "mysql":
2052
+ return "BIGINT UNSIGNED";
2053
+ case "sqlite":
2054
+ return "INTEGER";
2055
+ }
2056
+ }
2057
+ function compileStringType(driver, length) {
2058
+ switch (driver) {
2059
+ case "pgsql":
2060
+ return "TEXT";
2061
+ case "mysql":
2062
+ return length ? `VARCHAR(${length})` : "VARCHAR(255)";
2063
+ case "sqlite":
2064
+ return "TEXT";
2065
+ }
2066
+ }
2067
+ function compileTextType(driver) {
2068
+ switch (driver) {
2069
+ case "pgsql":
2070
+ case "sqlite":
2071
+ return "TEXT";
2072
+ case "mysql":
2073
+ return "TEXT";
2074
+ }
2075
+ }
2076
+ function compileBooleanType(driver) {
2077
+ switch (driver) {
2078
+ case "pgsql":
2079
+ return "BOOLEAN";
2080
+ case "mysql":
2081
+ return "BOOLEAN";
2082
+ case "sqlite":
2083
+ return "INTEGER";
2084
+ }
2085
+ }
2086
+ function compileIntegerType(driver) {
2087
+ switch (driver) {
2088
+ case "pgsql":
2089
+ return "INTEGER";
2090
+ case "mysql":
2091
+ return "INT";
2092
+ case "sqlite":
2093
+ return "INTEGER";
2094
+ }
2095
+ }
2096
+ function compileBigIntegerType(driver) {
2097
+ switch (driver) {
2098
+ case "pgsql":
2099
+ return "BIGINT";
2100
+ case "mysql":
2101
+ return "BIGINT";
2102
+ case "sqlite":
2103
+ return "INTEGER";
2104
+ }
2105
+ }
2106
+ function compileTimestampType(driver) {
2107
+ switch (driver) {
2108
+ case "pgsql":
2109
+ return "TIMESTAMPTZ";
2110
+ case "mysql":
2111
+ return "TIMESTAMP";
2112
+ case "sqlite":
2113
+ return "TEXT";
2114
+ }
2115
+ }
2116
+ function compileJsonType(driver) {
2117
+ switch (driver) {
2118
+ case "pgsql":
2119
+ return "JSONB";
2120
+ case "mysql":
2121
+ return "JSON";
2122
+ case "sqlite":
2123
+ return "TEXT";
2124
+ }
2125
+ }
2126
+ function compileJsonbType(driver) {
2127
+ switch (driver) {
2128
+ case "pgsql":
2129
+ return "JSONB";
2130
+ case "mysql":
2131
+ return "JSON";
2132
+ case "sqlite":
2133
+ return "TEXT";
2134
+ }
2135
+ }
2136
+
2137
+ // ../../src/core/database/schema/grammars/compileStatements.ts
2138
+ function compileCreateTable(driver, blueprint) {
2139
+ const table = quoteIdentifier(blueprint.table);
2140
+ const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
2141
+ for (const index of blueprint.indexes) {
2142
+ if (index.kind === "unique" && index.columns.length > 1) {
2143
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
2144
+ parts.push(`UNIQUE (${columns})`);
2145
+ }
2146
+ }
2147
+ const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
2148
+ ${parts.join(`,
2149
+ `)}
2150
+ )`];
2151
+ for (const index of blueprint.indexes) {
2152
+ if (index.kind === "unique" && index.columns.length === 1) {
2153
+ continue;
2154
+ }
2155
+ if (index.kind === "index") {
2156
+ statements.push(compileIndex(driver, blueprint.table, index));
2157
+ } else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
2158
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
2159
+ }
2160
+ }
2161
+ return statements;
2162
+ }
2163
+ function compileAlterTable(driver, blueprint) {
2164
+ const statements = [];
2165
+ const table = quoteIdentifier(blueprint.table);
2166
+ for (const column of blueprint.columns) {
2167
+ const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
2168
+ statements.push(`ALTER TABLE ${table}
2169
+ ${addPrefix} ${compileColumn(driver, column, "alter")}`);
2170
+ }
2171
+ for (const columnName of blueprint.droppedColumns) {
2172
+ const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
2173
+ statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
2174
+ }
2175
+ for (const indexName of blueprint.droppedIndexes) {
2176
+ statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
2177
+ }
2178
+ for (const index of blueprint.indexes) {
2179
+ if (index.kind === "index" || index.kind === "unique") {
2180
+ statements.push(compileIndex(driver, blueprint.table, index));
2181
+ } else {
2182
+ statements.push(...compileSpecialIndex(driver, blueprint.table, index));
2183
+ }
2184
+ }
2185
+ return statements;
2186
+ }
2187
+ function compileDropTable(driver, tableName) {
2188
+ const cascade = driver === "pgsql" ? " CASCADE" : "";
2189
+ return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
2190
+ }
2191
+ function compileColumn(driver, column, mode) {
2192
+ const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
2193
+ if (column.autoIncrement && driver === "mysql") {
2194
+ parts[1] = `${parts[1]} AUTO_INCREMENT`;
2195
+ }
2196
+ if (column.isPrimary && mode === "create") {
2197
+ if (driver === "sqlite") {
2198
+ parts.push("PRIMARY KEY AUTOINCREMENT");
2199
+ } else {
2200
+ parts.push("PRIMARY KEY");
2201
+ }
2202
+ } else if (!column.isNullable) {
2203
+ parts.push("NOT NULL");
2204
+ } else if (column.isNullable) {
2205
+ parts.push("NULL");
2206
+ }
2207
+ if (column.defaultValue !== undefined) {
2208
+ parts.push(`DEFAULT ${column.defaultValue}`);
2209
+ }
2210
+ if (column.isUnique) {
2211
+ parts.push("UNIQUE");
2212
+ }
2213
+ if (column.checkExpression) {
2214
+ parts.push(`CHECK (${column.checkExpression})`);
2215
+ }
2216
+ if (column.foreignKey) {
2217
+ const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
2218
+ const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
2219
+ let clause = `REFERENCES ${reference}`;
2220
+ if (onDelete === "cascade") {
2221
+ clause += " ON DELETE CASCADE";
2222
+ } else if (onDelete === "set null") {
2223
+ clause += " ON DELETE SET NULL";
2224
+ }
2225
+ parts.push(clause);
2226
+ }
2227
+ return parts.join(" ");
2228
+ }
2229
+ function compileIndex(_driver, tableName, index) {
2230
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
2231
+ const columns = index.columns.map((column) => {
2232
+ const quoted = quoteIdentifier(column);
2233
+ if (index.order === "desc") {
2234
+ return `${quoted} DESC`;
2235
+ }
2236
+ return quoted;
2237
+ }).join(", ");
2238
+ const unique = index.kind === "unique" ? "UNIQUE " : "";
2239
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
2240
+ }
2241
+ function compileSpecialIndex(driver, tableName, index) {
2242
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
2243
+ const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
2244
+ switch (index.kind) {
2245
+ case "partial":
2246
+ case "uniquePartial": {
2247
+ if (driver !== "pgsql") {
2248
+ throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
2249
+ }
2250
+ const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
2251
+ return [
2252
+ `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
2253
+ ];
2254
+ }
2255
+ case "gin": {
2256
+ if (driver !== "pgsql") {
2257
+ throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
2258
+ }
2259
+ return [
2260
+ `CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
2261
+ ];
2262
+ }
2263
+ case "fullText": {
2264
+ if (driver === "mysql") {
2265
+ return [
2266
+ `CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
2267
+ ];
2268
+ }
2269
+ if (driver === "pgsql") {
2270
+ throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
2271
+ }
2272
+ throw new UnsupportedSchemaFeatureError("fullText()", driver);
2273
+ }
2274
+ default:
2275
+ return [];
2276
+ }
2277
+ }
2278
+ function defaultIndexName(tableName, columns, kind) {
2279
+ return `idx_${tableName}_${columns.join("_")}_${kind}`;
2280
+ }
2281
+ function compileBlueprint(driver, blueprint) {
2282
+ switch (blueprint.action) {
2283
+ case "create":
2284
+ return compileCreateTable(driver, blueprint);
2285
+ case "alter":
2286
+ return compileAlterTable(driver, blueprint);
2287
+ case "drop":
2288
+ return compileDropTable(driver, blueprint.table);
2289
+ default:
2290
+ throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
2291
+ }
2292
+ }
2293
+ // ../../src/core/database/schema/grammars/createGrammar.ts
2294
+ function createGrammar(driver) {
2295
+ return {
2296
+ driver,
2297
+ compile(blueprint) {
2298
+ return compileBlueprint(driver, blueprint);
2299
+ }
2300
+ };
2301
+ }
2302
+
2303
+ // ../../src/core/database/schema/grammars/mysqlGrammar.ts
2304
+ var MySqlGrammar = createGrammar("mysql");
2305
+
2306
+ // ../../src/core/database/schema/grammars/postgresGrammar.ts
2307
+ var PostgresGrammar = createGrammar("pgsql");
2308
+
2309
+ // ../../src/core/database/schema/grammars/sqliteGrammar.ts
2310
+ var SqliteGrammar = createGrammar("sqlite");
2311
+
2312
+ // ../../src/core/database/schema/grammars/index.ts
2313
+ function grammarForDriver(driver) {
2314
+ switch (driver) {
2315
+ case "pgsql":
2316
+ return PostgresGrammar;
2317
+ case "mysql":
2318
+ return MySqlGrammar;
2319
+ case "sqlite":
2320
+ return SqliteGrammar;
2321
+ default:
2322
+ throw new Error(`Unsupported database driver: ${driver}`);
2323
+ }
2324
+ }
2325
+ // ../../src/core/database/schema/schema.ts
2326
+ class SchemaBuilder {
2327
+ #driver;
2328
+ #statements = [];
2329
+ constructor(driver) {
2330
+ this.#driver = driver;
2331
+ }
2332
+ create(table, callback) {
2333
+ const blueprint = new Blueprint(table, "create");
2334
+ callback(blueprint);
2335
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2336
+ return this;
2337
+ }
2338
+ table(table, callback) {
2339
+ const blueprint = new Blueprint(table, "alter");
2340
+ callback(blueprint);
2341
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2342
+ return this;
2343
+ }
2344
+ drop(table) {
2345
+ const blueprint = new Blueprint(table, "drop");
2346
+ this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
2347
+ return this;
2348
+ }
2349
+ toSql() {
2350
+ return [...this.#statements];
2351
+ }
2352
+ async execute(db2) {
2353
+ for (const statement of this.#statements) {
2354
+ await db2.unsafe(statement);
2355
+ }
2356
+ }
2357
+ }
2358
+
2359
+ class Schema {
2360
+ static builder(driver) {
2361
+ return new SchemaBuilder(driver ?? resolveDatabaseDriver());
2362
+ }
2363
+ static async run(db2, driver, callback) {
2364
+ const schema = Schema.builder(driver);
2365
+ await callback(schema);
2366
+ await schema.execute(db2);
2367
+ }
2368
+ }
2369
+ function createSchemaBuilder(db2, driver) {
2370
+ const builder = Schema.builder(driver);
2371
+ return Object.assign(builder, {
2372
+ async commit() {
2373
+ await builder.execute(db2);
2374
+ }
2375
+ });
2376
+ }
2377
+ // ../../src/core/database/table.ts
2378
+ function defineTable(definition) {
2379
+ return definition;
2380
+ }
2381
+ // ../../src/core/database/transaction.ts
2382
+ async function runInTransaction(operation) {
2383
+ return await connection_default.begin(async (transaction) => {
2384
+ return await operation(createDatabaseConnection2(transaction));
2385
+ });
2386
+ }
2387
+ // ../../src/modules/user/apiTokenTable.ts
2388
+ var apiTokenTable = defineTable({
2389
+ name: "api_token",
2390
+ primaryKey: "id",
2391
+ columns: [
2392
+ "id",
2393
+ "user_id",
2394
+ "name",
2395
+ "token_hash",
2396
+ "abilities",
2397
+ "last_used_at",
2398
+ "expires_at",
2399
+ "created_at"
2400
+ ],
2401
+ defaultOrderBy: { column: "id", direction: "ASC" }
2402
+ });
2403
+
2404
+ // ../../src/core/auth/password.ts
2405
+ async function hashPassword(password) {
2406
+ return await Bun.password.hash(password, {
2407
+ algorithm: "bcrypt",
2408
+ cost: 10
2409
+ });
2410
+ }
2411
+ async function verifyPassword(password, passwordHash) {
2412
+ return await Bun.password.verify(password, passwordHash);
2413
+ }
2414
+
2415
+ // ../../src/core/crypto/fieldEncryption.ts
2416
+ import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
2417
+ var ENCRYPTION_PREFIX = "enc:v1:";
2418
+ var IV_LENGTH = 12;
2419
+ var TAG_LENGTH = 16;
2420
+ function resolveEncryptionKey() {
2421
+ const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
2422
+ if (!raw) {
2423
+ return null;
2424
+ }
2425
+ if (/^[0-9a-f]{64}$/i.test(raw)) {
2426
+ return Buffer.from(raw, "hex");
2427
+ }
2428
+ const decoded = Buffer.from(raw, "base64");
2429
+ if (decoded.length === 32) {
2430
+ return decoded;
2431
+ }
2432
+ throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
2433
+ }
2434
+ function isFieldEncryptionEnabled() {
2435
+ const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
2436
+ if (featureFlag === "false") {
2437
+ return false;
2438
+ }
2439
+ if (featureFlag === "true") {
2440
+ return true;
2441
+ }
2442
+ return (process.env.APP_ENV ?? "local") === "production";
2443
+ }
2444
+ function encryptField(plaintext, key) {
2445
+ const iv = randomBytes(IV_LENGTH);
2446
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
2447
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2448
+ const tag = cipher.getAuthTag();
2449
+ const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
2450
+ return `${ENCRYPTION_PREFIX}${payload}`;
2451
+ }
2452
+ function decryptField(value, key) {
2453
+ if (!value.startsWith(ENCRYPTION_PREFIX)) {
2454
+ return value;
2455
+ }
2456
+ const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
2457
+ const iv = payload.subarray(0, IV_LENGTH);
2458
+ const tag = payload.subarray(payload.length - TAG_LENGTH);
2459
+ const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
2460
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
2461
+ decipher.setAuthTag(tag);
2462
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
2463
+ }
2464
+ function hashLookupValue(normalizedValue, key) {
2465
+ return createHmac("sha256", key).update(normalizedValue).digest("hex");
2466
+ }
2467
+ function normalizeEmail(email) {
2468
+ return email.trim().toLowerCase();
2469
+ }
2470
+ function protectEmail(email) {
2471
+ const normalized = normalizeEmail(email);
2472
+ const key = resolveEncryptionKey();
2473
+ if (!key || !isFieldEncryptionEnabled()) {
2474
+ return { storedEmail: normalized, emailLookup: normalized };
2475
+ }
2476
+ return {
2477
+ storedEmail: encryptField(normalized, key),
2478
+ emailLookup: hashLookupValue(normalized, key)
2479
+ };
2480
+ }
2481
+ function revealEmail(storedEmail) {
2482
+ const key = resolveEncryptionKey();
2483
+ if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
2484
+ return storedEmail;
2485
+ }
2486
+ return decryptField(storedEmail, key);
2487
+ }
2488
+ function emailLookupForQuery(email) {
2489
+ const normalized = normalizeEmail(email);
2490
+ const key = resolveEncryptionKey();
2491
+ if (!key || !isFieldEncryptionEnabled()) {
2492
+ return normalized;
2493
+ }
2494
+ return hashLookupValue(normalized, key);
2495
+ }
2496
+
2497
+ // ../../src/core/crypto/mfaSecret.ts
2498
+ function protectMfaSecret(secret) {
2499
+ const key = resolveEncryptionKey();
2500
+ if (!isFieldEncryptionEnabled() || !key) {
2501
+ return secret;
2502
+ }
2503
+ return encryptField(secret, key);
2504
+ }
2505
+ function revealMfaSecret(stored) {
2506
+ if (!stored) {
2507
+ return null;
2508
+ }
2509
+ const key = resolveEncryptionKey();
2510
+ if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
2511
+ return stored;
2512
+ }
2513
+ return decryptField(stored, key);
2514
+ }
2515
+
2516
+ // ../../src/core/auth/authContext.ts
2517
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2518
+ var authContext = new AsyncLocalStorage2;
2519
+ function runWithAuthUser(user, callback) {
2520
+ return authContext.run(user, callback);
2521
+ }
2522
+ function currentAuthUser() {
2523
+ return authContext.getStore() ?? null;
2524
+ }
2525
+
2526
+ // ../../src/core/http/requestMetaContext.ts
2527
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
2528
+ var requestMetaContext = new AsyncLocalStorage3;
2529
+ function runWithRequestMeta(meta, callback) {
2530
+ return requestMetaContext.run(meta, callback);
2531
+ }
2532
+ function currentRequestMeta() {
2533
+ return requestMetaContext.getStore() ?? {
2534
+ ipAddress: null,
2535
+ userAgent: null
2536
+ };
2537
+ }
2538
+
2539
+ // ../../src/core/security/securityEvents.ts
2540
+ function logSecurityEvent(event, details = {}) {
2541
+ const meta = currentRequestMeta();
2542
+ const user = currentAuthUser();
2543
+ console.log(JSON.stringify({
2544
+ level: "security",
2545
+ event,
2546
+ timestamp: new Date().toISOString(),
2547
+ ip_address: meta.ipAddress ?? null,
2548
+ user_agent: meta.userAgent ?? null,
2549
+ user_id: user?.id ?? null,
2550
+ ...details
2551
+ }));
2552
+ }
2553
+
2554
+ // ../../src/core/security/tokenExpiry.ts
2555
+ function resolveDefaultTokenExpiryDays() {
2556
+ const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
2557
+ if (!raw) {
2558
+ return null;
2559
+ }
2560
+ const parsed = Number.parseInt(raw, 10);
2561
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2562
+ return null;
2563
+ }
2564
+ return parsed;
2565
+ }
2566
+
2567
+ // ../../src/core/security/totp.ts
2568
+ import { createHmac as createHmac2 } from "crypto";
2569
+ function decodeBase32(input) {
2570
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2571
+ const normalized = input.replace(/=+$/u, "").toUpperCase();
2572
+ let bits = "";
2573
+ for (const char of normalized) {
2574
+ const value = alphabet.indexOf(char);
2575
+ if (value === -1) {
2576
+ throw new Error("Invalid base32 character in MFA secret.");
2577
+ }
2578
+ bits += value.toString(2).padStart(5, "0");
2579
+ }
2580
+ const bytes = [];
2581
+ for (let index = 0;index + 8 <= bits.length; index += 8) {
2582
+ bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
2583
+ }
2584
+ return Buffer.from(bytes);
2585
+ }
2586
+ function generateTotp(secret, counter, digits = 6) {
2587
+ const key = decodeBase32(secret);
2588
+ const buffer = Buffer.alloc(8);
2589
+ buffer.writeBigUInt64BE(BigInt(counter));
2590
+ const digest = createHmac2("sha1", key).update(buffer).digest();
2591
+ const lastByte = digest[digest.length - 1] ?? 0;
2592
+ const offset = lastByte & 15;
2593
+ const b0 = digest[offset] ?? 0;
2594
+ const b1 = digest[offset + 1] ?? 0;
2595
+ const b2 = digest[offset + 2] ?? 0;
2596
+ const b3 = digest[offset + 3] ?? 0;
2597
+ const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
2598
+ return String(code % 10 ** digits).padStart(digits, "0");
2599
+ }
2600
+ function verifyTotp(secret, token, window = 1) {
2601
+ const normalized = token.trim();
2602
+ if (!/^\d{6}$/u.test(normalized)) {
2603
+ return false;
2604
+ }
2605
+ const timestep = Math.floor(Date.now() / 30000);
2606
+ for (let offset = -window;offset <= window; offset += 1) {
2607
+ if (generateTotp(secret, timestep + offset) === normalized) {
2608
+ return true;
2609
+ }
2610
+ }
2611
+ return false;
2612
+ }
2613
+
2614
+ // ../../src/core/tenant/tenantContext.ts
2615
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
2616
+ var tenantContext = new AsyncLocalStorage4;
2617
+ function runWithTenant(tenant, callback) {
2618
+ return tenantContext.run(tenant, callback);
2619
+ }
2620
+ function currentTenant() {
2621
+ return tenantContext.getStore() ?? null;
2622
+ }
2623
+ function currentTenantId() {
2624
+ return currentTenant()?.id ?? 1;
2625
+ }
2626
+ function rateLimitMultiplierForPlan(plan) {
2627
+ switch (plan) {
2628
+ case "enterprise":
2629
+ return 4;
2630
+ case "pro":
2631
+ return 2;
2632
+ default:
2633
+ return 1;
2634
+ }
2635
+ }
2636
+
2637
+ // ../../src/domain/abilities.ts
2638
+ var MEMBER_ABILITIES = [
2639
+ "organizations:read",
2640
+ "projects:read",
2641
+ "projects:create",
2642
+ "tasks:read",
2643
+ "tasks:create",
2644
+ "comments:read",
2645
+ "comments:create",
2646
+ "attachments:read",
2647
+ "attachments:create",
2648
+ "auth:tokens:read",
2649
+ "auth:tokens:write"
2650
+ ];
2651
+ var ADMIN_ABILITIES = [
2652
+ ...MEMBER_ABILITIES,
2653
+ "organizations:create",
2654
+ "organizations:update",
2655
+ "organizations:delete",
2656
+ "projects:update",
2657
+ "projects:delete",
2658
+ "tasks:update",
2659
+ "tasks:delete",
2660
+ "comments:update",
2661
+ "comments:delete",
2662
+ "attachments:delete",
2663
+ "webhooks:read",
2664
+ "webhooks:write",
2665
+ "audit:read"
2666
+ ];
2667
+ var PLATFORM_ADMIN_ABILITIES = ["*"];
2668
+ function resolveAbilitiesForRole(role) {
2669
+ if (role === "admin") {
2670
+ return [...PLATFORM_ADMIN_ABILITIES];
2671
+ }
2672
+ return [...MEMBER_ABILITIES];
2673
+ }
2674
+
2675
+ // ../../src/modules/user/authService.ts
2676
+ class AuthService {
2677
+ users;
2678
+ tokens;
2679
+ oauthIdentities;
2680
+ oauthProviders = new Map;
2681
+ constructor(users, tokens, oauthIdentities) {
2682
+ this.users = users;
2683
+ this.tokens = tokens;
2684
+ this.oauthIdentities = oauthIdentities;
2685
+ }
2686
+ registerOAuthProvider(provider) {
2687
+ this.oauthProviders.set(provider.name, provider);
2688
+ }
2689
+ getOAuthProvider(name) {
2690
+ return this.oauthProviders.get(name);
2691
+ }
2692
+ async loginWithPassword(email, password, options = {}) {
2693
+ const user = await this.users.findByEmail(email);
2694
+ if (!user?.password_hash) {
2695
+ logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
2696
+ throw new UnauthorizedError("Invalid credentials.");
2697
+ }
2698
+ const valid = await verifyPassword(password, user.password_hash);
2699
+ if (!valid) {
2700
+ logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
2701
+ throw new UnauthorizedError("Invalid credentials.");
2702
+ }
2703
+ if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
2704
+ logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
2705
+ throw new UnauthorizedError("Email address is not verified.");
2706
+ }
2707
+ if (isFeatureEnabled("mfa") && user.mfa_enabled) {
2708
+ const mfaSecret = revealMfaSecret(user.mfa_secret);
2709
+ if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
2710
+ logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
2711
+ throw new UnauthorizedError("Invalid MFA code.");
2712
+ }
2713
+ }
2714
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
2715
+ return await this.tokens.createToken(user.id, {
2716
+ name: "password-login",
2717
+ abilities: resolveAbilitiesForRole(user.role),
2718
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2719
+ });
2720
+ }
2721
+ async loginWithOAuth(providerName, code) {
2722
+ const provider = this.oauthProviders.get(providerName);
2723
+ if (!provider) {
2724
+ throw new UnauthorizedError("Unsupported OAuth provider.");
2725
+ }
2726
+ const profile = await provider.exchangeCode(code);
2727
+ const user = await this.findOrCreateOAuthUser(providerName, profile);
2728
+ logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
2729
+ return await this.tokens.createToken(user.id, {
2730
+ name: `${providerName}-oauth`,
2731
+ abilities: resolveAbilitiesForRole(user.role),
2732
+ expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2733
+ });
2734
+ }
2735
+ buildOAuthAuthorizationUrl(providerName, state) {
2736
+ const provider = this.oauthProviders.get(providerName);
2737
+ if (!provider) {
2738
+ throw new UnauthorizedError("Unsupported OAuth provider.");
2739
+ }
2740
+ return provider.getAuthorizationUrl(state);
2741
+ }
2742
+ async findOrCreateOAuthUser(providerName, profile) {
2743
+ const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
2744
+ if (existingIdentity) {
2745
+ return await this.users.findByIdOrThrow(existingIdentity.user_id);
2746
+ }
2747
+ const existingUser = await this.users.findByEmail(profile.email);
2748
+ const user = existingUser ?? await this.users.create({
2749
+ name: profile.name,
2750
+ email: profile.email,
2751
+ role: "member",
2752
+ tenant_id: currentTenantId(),
2753
+ email_verified_at: new Date,
2754
+ created_at: new Date,
2755
+ updated_at: new Date
2756
+ });
2757
+ await this.oauthIdentities.create({
2758
+ user_id: user.id,
2759
+ provider: providerName,
2760
+ provider_user_id: profile.providerUserId,
2761
+ email: profile.email,
2762
+ created_at: new Date
2763
+ });
2764
+ return user;
2765
+ }
2766
+ }
2767
+
2768
+ // ../../src/modules/user/notificationTable.ts
2769
+ var notificationTable = defineTable({
2770
+ name: "notification",
2771
+ primaryKey: "id",
2772
+ columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
2773
+ defaultOrderBy: { column: "created_at", direction: "DESC" }
2774
+ });
2775
+
2776
+ // ../../src/modules/user/oauthIdentityRepository.ts
2777
+ var oauthIdentityTable = defineTable({
2778
+ name: "oauth_identity",
2779
+ primaryKey: "id",
2780
+ columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
2781
+ });
2782
+
2783
+ // ../../src/modules/user/table.ts
2784
+ var userTable = defineTable({
2785
+ name: "users",
2786
+ primaryKey: "id",
2787
+ columns: [
2788
+ "id",
2789
+ "name",
2790
+ "email",
2791
+ "email_lookup",
2792
+ "role",
2793
+ "tenant_id",
2794
+ "password_hash",
2795
+ "email_verified_at",
2796
+ "mfa_secret",
2797
+ "mfa_enabled",
2798
+ "created_at",
2799
+ "updated_at"
2800
+ ],
2801
+ defaultOrderBy: { column: "id", direction: "ASC" }
2802
+ });
2803
+
2804
+ // ../../src/core/auth/tokenHash.ts
2805
+ import { createHash, createHmac as createHmac3 } from "crypto";
2806
+ function resolveTokenPepper() {
2807
+ return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
2808
+ }
2809
+ function hashApiToken(token) {
2810
+ const pepper = resolveTokenPepper();
2811
+ if (pepper && pepper !== "workhub-dev-token-pepper") {
2812
+ return createHmac3("sha256", pepper).update(token).digest("hex");
2813
+ }
2814
+ return createHash("sha256").update(token).digest("hex");
2815
+ }
2816
+
2817
+ // ../../src/modules/user/provider.ts
2818
+ var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
2819
+
2820
+ // ../../src/core/http/csrfToken.ts
2821
+ import { createHmac as createHmac4, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
2822
+ var CSRF_COOKIE = "workhub_csrf";
2823
+ var CSRF_TTL_MS = 60 * 60 * 1000;
2824
+ function resolveCsrfSecret() {
2825
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
2826
+ }
2827
+ function signCsrfToken(token, issuedAt) {
2828
+ const payload = `${token}.${issuedAt}`;
2829
+ const signature = createHmac4("sha256", resolveCsrfSecret()).update(payload).digest("hex");
2830
+ return `${payload}.${signature}`;
2831
+ }
2832
+ function readCsrfCookie(request) {
2833
+ const cookieHeader = request.headers.get("cookie");
2834
+ if (!cookieHeader) {
2835
+ return null;
2836
+ }
2837
+ for (const part of cookieHeader.split(";")) {
2838
+ const [name, ...rest] = part.trim().split("=");
2839
+ if (name === CSRF_COOKIE) {
2840
+ return decodeURIComponent(rest.join("="));
2841
+ }
2842
+ }
2843
+ return null;
2844
+ }
2845
+ function parseSignedCsrfValue(cookieValue) {
2846
+ const parts = cookieValue.split(".");
2847
+ if (parts.length !== 3) {
2848
+ return null;
2849
+ }
2850
+ const [token, issuedAtRaw, cookieSignature] = parts;
2851
+ if (!token || !issuedAtRaw || !cookieSignature) {
2852
+ return null;
2853
+ }
2854
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
2855
+ if (!Number.isFinite(issuedAt)) {
2856
+ return null;
2857
+ }
2858
+ if (Date.now() - issuedAt > CSRF_TTL_MS) {
2859
+ return null;
2860
+ }
2861
+ const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
2862
+ if (!expectedSignature) {
2863
+ return null;
2864
+ }
2865
+ const expectedBuffer = Buffer.from(expectedSignature);
2866
+ const actualBuffer = Buffer.from(cookieSignature);
2867
+ if (expectedBuffer.length !== actualBuffer.length) {
2868
+ return null;
2869
+ }
2870
+ if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
2871
+ return null;
2872
+ }
2873
+ return { token, issuedAt };
2874
+ }
2875
+ function createCsrfTokenCookie() {
2876
+ const token = randomBytes2(24).toString("hex");
2877
+ const issuedAt = Date.now();
2878
+ const value = signCsrfToken(token, issuedAt);
2879
+ return {
2880
+ token,
2881
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
2882
+ };
2883
+ }
2884
+ function resolveCsrfToken(request) {
2885
+ const cookieValue = readCsrfCookie(request);
2886
+ if (cookieValue) {
2887
+ const parsed = parseSignedCsrfValue(cookieValue);
2888
+ if (parsed) {
2889
+ return { token: parsed.token };
2890
+ }
2891
+ }
2892
+ return createCsrfTokenCookie();
2893
+ }
2894
+ function readSubmittedCsrfToken(request) {
2895
+ const headerToken = request.headers.get("x-csrf-token")?.trim();
2896
+ if (headerToken) {
2897
+ return headerToken;
2898
+ }
2899
+ return null;
2900
+ }
2901
+ async function readSubmittedCsrfTokenFromBody(request) {
2902
+ const headerToken = readSubmittedCsrfToken(request);
2903
+ if (headerToken) {
2904
+ return headerToken;
2905
+ }
2906
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
2907
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
2908
+ const formData = await request.clone().formData();
2909
+ const field = formData.get("_token");
2910
+ if (typeof field === "string" && field.trim().length > 0) {
2911
+ return field.trim();
2912
+ }
2913
+ }
2914
+ return null;
2915
+ }
2916
+ function verifyCsrfToken(request, submittedToken) {
2917
+ if (!submittedToken) {
2918
+ return false;
2919
+ }
2920
+ const cookieValue = readCsrfCookie(request);
2921
+ if (!cookieValue) {
2922
+ return false;
2923
+ }
2924
+ const parsed = parseSignedCsrfValue(cookieValue);
2925
+ if (!parsed) {
2926
+ return false;
2927
+ }
2928
+ const submittedBuffer = Buffer.from(submittedToken);
2929
+ const expectedBuffer = Buffer.from(parsed.token);
2930
+ if (submittedBuffer.length !== expectedBuffer.length) {
2931
+ return false;
2932
+ }
2933
+ return timingSafeEqual(submittedBuffer, expectedBuffer);
2934
+ }
2935
+ function resolveCsrfTokenForRequest(request) {
2936
+ const metaToken = currentRequestMeta().csrfToken;
2937
+ if (metaToken) {
2938
+ return metaToken;
2939
+ }
2940
+ return resolveCsrfToken(request).token;
2941
+ }
2942
+
2943
+ // ../../src/core/http/flashSession.ts
2944
+ import { createHmac as createHmac5, timingSafeEqual as timingSafeEqual2 } from "crypto";
2945
+ var FLASH_COOKIE = "workhub_flash";
2946
+ var FLASH_TTL_MS = 60 * 1000;
2947
+ function resolveFlashSecret() {
2948
+ return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
2949
+ }
2950
+ function signFlashPayload(payload, issuedAt) {
2951
+ const signature = createHmac5("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
2952
+ return `${payload}.${issuedAt}.${signature}`;
2953
+ }
2954
+ function readFlashCookie(request) {
2955
+ const cookieHeader = request.headers.get("cookie");
2956
+ if (!cookieHeader) {
2957
+ return null;
2958
+ }
2959
+ for (const part of cookieHeader.split(";")) {
2960
+ const [name, ...rest] = part.trim().split("=");
2961
+ if (name === FLASH_COOKIE) {
2962
+ return decodeURIComponent(rest.join("="));
2963
+ }
2964
+ }
2965
+ return null;
2966
+ }
2967
+ function parseFlashCookie(cookieValue) {
2968
+ const parts = cookieValue.split(".");
2969
+ if (parts.length < 3) {
2970
+ return null;
2971
+ }
2972
+ const signature = parts.pop();
2973
+ const issuedAtRaw = parts.pop();
2974
+ const payload = parts.join(".");
2975
+ if (!signature || !issuedAtRaw || !payload) {
2976
+ return null;
2977
+ }
2978
+ const issuedAt = Number.parseInt(issuedAtRaw, 10);
2979
+ if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
2980
+ return null;
2981
+ }
2982
+ const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
2983
+ if (!expectedSignature) {
2984
+ return null;
2985
+ }
2986
+ const expectedBuffer = Buffer.from(expectedSignature);
2987
+ const actualBuffer = Buffer.from(signature);
2988
+ if (expectedBuffer.length !== actualBuffer.length) {
2989
+ return null;
2990
+ }
2991
+ if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
2992
+ return null;
2993
+ }
2994
+ try {
2995
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
2996
+ if (!parsed?.message || typeof parsed.message !== "string") {
2997
+ return null;
2998
+ }
2999
+ if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
3000
+ return null;
3001
+ }
3002
+ return parsed;
3003
+ } catch {
3004
+ return null;
3005
+ }
3006
+ }
3007
+ function createFlashCookie(message) {
3008
+ const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
3009
+ const issuedAt = Date.now();
3010
+ const value = signFlashPayload(payload, issuedAt);
3011
+ return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
3012
+ }
3013
+ function clearFlashCookie() {
3014
+ return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
3015
+ }
3016
+ function pullFlash(request) {
3017
+ const cookieValue = readFlashCookie(request);
3018
+ if (!cookieValue) {
3019
+ return null;
3020
+ }
3021
+ return parseFlashCookie(cookieValue);
3022
+ }
3023
+ function flashResponse(response, message) {
3024
+ const headers = new Headers(response.headers);
3025
+ headers.append("set-cookie", createFlashCookie(message));
3026
+ return new Response(response.body, {
3027
+ status: response.status,
3028
+ statusText: response.statusText,
3029
+ headers
3030
+ });
3031
+ }
3032
+ function withFlashClear(response) {
3033
+ const headers = new Headers(response.headers);
3034
+ headers.append("set-cookie", clearFlashCookie());
3035
+ return new Response(response.body, {
3036
+ status: response.status,
3037
+ statusText: response.statusText,
3038
+ headers
3039
+ });
3040
+ }
3041
+
3042
+ // ../../src/core/view/webLayoutData.ts
3043
+ async function resolveWebLayoutData(container, request) {
3044
+ const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
3045
+ const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
3046
+ const authUser = currentAuthUser();
3047
+ if (!authUser) {
3048
+ return { authUser: null, csrfToken, flash };
3049
+ }
3050
+ const userId = Number(authUser.id);
3051
+ if (!Number.isInteger(userId) || userId <= 0) {
3052
+ return { authUser: null, csrfToken, flash };
3053
+ }
3054
+ if (!container.has(tokenServiceToken)) {
3055
+ return {
3056
+ authUser: {
3057
+ id: userId,
3058
+ email: "",
3059
+ role: authUser.role ?? "member"
3060
+ },
3061
+ csrfToken,
3062
+ flash
3063
+ };
3064
+ }
3065
+ const tokenService = container.resolve(tokenServiceToken);
3066
+ try {
3067
+ const user = await tokenService.findByIdOrThrow(userId);
3068
+ return {
3069
+ authUser: {
3070
+ id: userId,
3071
+ email: user.email ?? "",
3072
+ role: authUser.role ?? user.role ?? "member"
3073
+ },
3074
+ csrfToken,
3075
+ flash
3076
+ };
3077
+ } catch {
3078
+ return { authUser: null, csrfToken, flash };
3079
+ }
3080
+ }
3081
+ // ../../src/core/http/contentNegotiation.ts
3082
+ function requestPrefersJson(request) {
3083
+ if (!request) {
3084
+ return true;
3085
+ }
3086
+ if (request.headers.get("HX-Request") === "true") {
3087
+ return false;
3088
+ }
3089
+ const accept = request.headers.get("accept")?.toLowerCase() ?? "";
3090
+ if (accept.includes("text/html")) {
3091
+ return false;
3092
+ }
3093
+ if (accept.includes("application/json")) {
3094
+ return true;
3095
+ }
3096
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3097
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
3098
+ return false;
3099
+ }
3100
+ const pathname = new URL(request.url).pathname;
3101
+ return pathname.startsWith("/api/");
3102
+ }
3103
+
3104
+ // ../../src/core/http/webErrorResponse.ts
3105
+ function normalizeFieldErrors(details) {
3106
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
3107
+ return {};
3108
+ }
3109
+ const errors = {};
3110
+ for (const [field, messages] of Object.entries(details)) {
3111
+ if (Array.isArray(messages)) {
3112
+ errors[field] = messages.map(String);
3113
+ continue;
3114
+ }
3115
+ if (typeof messages === "string") {
3116
+ errors[field] = [messages];
3117
+ }
3118
+ }
3119
+ return errors;
3120
+ }
3121
+ function webErrorResponse(error, request) {
3122
+ if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
3123
+ return null;
3124
+ }
3125
+ const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
3126
+ if (mappedError instanceof UnauthorizedError) {
3127
+ const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
3128
+ return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
3129
+ }
3130
+ if (mappedError instanceof ValidationError) {
3131
+ const errors = normalizeFieldErrors(mappedError.details);
3132
+ const fieldSummary = Object.entries(errors).flatMap(([field, messages]) => messages.map((message) => `${field}: ${message}`)).join(`
3133
+ `);
3134
+ return htmlResponse(`<section class="page-header"><h1>Validation failed</h1><pre>${fieldSummary || mappedError.message}</pre><p><a href="javascript:history.back()">Go back</a></p></section>`, { status: mappedError.status });
3135
+ }
3136
+ return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
3137
+ status: mappedError.status
3138
+ });
3139
+ }
3140
+ // ../../src/core/http/authMiddleware.ts
3141
+ function createAuthMiddleware(auth) {
3142
+ return async (request, next) => {
3143
+ const user = await auth.resolve(request);
3144
+ return await runWithAuthUser(user, async () => {
3145
+ const response = await next();
3146
+ if (user) {
3147
+ const headers = new Headers(response.headers);
3148
+ headers.set("x-authenticated-user-id", String(user.id));
3149
+ return new Response(response.body, {
3150
+ status: response.status,
3151
+ statusText: response.statusText,
3152
+ headers
3153
+ });
3154
+ }
3155
+ return response;
3156
+ });
3157
+ };
3158
+ }
3159
+ // ../../src/core/http/authorizeMiddleware.ts
3160
+ function createAuthorizeMiddleware(gate, auth, resource, action) {
3161
+ return async (request, next) => {
3162
+ const user = await auth.resolve(request);
3163
+ if (!gate.allows(resource, action, user)) {
3164
+ const error = new ForbiddenError;
3165
+ return Response.json({ error: error.message }, { status: error.status });
3166
+ }
3167
+ return await next();
3168
+ };
3169
+ }
3170
+ // ../../src/core/http/etag.ts
3171
+ import { createHash as createHash2 } from "crypto";
3172
+ function isEtagEnabled() {
3173
+ return (process.env.FEATURE_ETAG ?? "true") !== "false";
3174
+ }
3175
+ function formatWeakEtag(digest) {
3176
+ return `W/"${digest}"`;
3177
+ }
3178
+ function computeEtagFromJson(data) {
3179
+ const digest = createHash2("sha256").update(JSON.stringify(data)).digest("hex").slice(0, 32);
3180
+ return formatWeakEtag(digest);
3181
+ }
3182
+ function etagFromResource(resource) {
3183
+ const version = resource.updated_at ?? resource.created_at ?? "";
3184
+ const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
3185
+ const digest = createHash2("sha256").update(`${String(resource.id ?? "0")}:${versionText}`).digest("hex").slice(0, 32);
3186
+ return formatWeakEtag(digest);
3187
+ }
3188
+ function normalizeEtag(value) {
3189
+ return value.trim();
3190
+ }
3191
+ function etagValuesMatch(left, right) {
3192
+ return normalizeEtag(left) === normalizeEtag(right);
3193
+ }
3194
+ function parseEtagList(header) {
3195
+ if (!header) {
3196
+ return [];
3197
+ }
3198
+ return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
3199
+ }
3200
+ function ifNoneMatchSatisfied(request, etag) {
3201
+ const header = request.headers.get("if-none-match");
3202
+ if (!header) {
3203
+ return false;
3204
+ }
3205
+ if (header.trim() === "*") {
3206
+ return true;
3207
+ }
3208
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3209
+ }
3210
+ function ifMatchSatisfied(request, etag) {
3211
+ const header = request.headers.get("if-match");
3212
+ if (!header) {
3213
+ return false;
3214
+ }
3215
+ if (header.trim() === "*") {
3216
+ return true;
3217
+ }
3218
+ return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3219
+ }
3220
+ function assertIfMatch(request, etag, options = {}) {
3221
+ const header = request.headers.get("if-match");
3222
+ if (!header) {
3223
+ if (options.required) {
3224
+ throw new PreconditionFailedError("If-Match header is required.");
3225
+ }
3226
+ return;
3227
+ }
3228
+ if (!ifMatchSatisfied(request, etag)) {
3229
+ throw new PreconditionFailedError("Resource ETag does not match If-Match.");
3230
+ }
3231
+ }
3232
+ function applyEtagHeaders(headers, etag) {
3233
+ const next = new Headers(headers);
3234
+ next.set("ETag", etag);
3235
+ next.set("Cache-Control", "private, must-revalidate");
3236
+ next.append("Vary", "Authorization");
3237
+ next.append("Vary", "X-Tenant-Id");
3238
+ return next;
3239
+ }
3240
+ function notModifiedResponse(etag) {
3241
+ return new Response(null, {
3242
+ status: 304,
3243
+ headers: applyEtagHeaders(new Headers, etag)
3244
+ });
3245
+ }
3246
+ function applyConditionalGet(request, response, etag) {
3247
+ if (!isEtagEnabled()) {
3248
+ return response;
3249
+ }
3250
+ if (ifNoneMatchSatisfied(request, etag)) {
3251
+ return notModifiedResponse(etag);
3252
+ }
3253
+ const headers = applyEtagHeaders(new Headers(response.headers), etag);
3254
+ return new Response(response.body, {
3255
+ status: response.status,
3256
+ statusText: response.statusText,
3257
+ headers
3258
+ });
3259
+ }
3260
+
3261
+ // ../../src/core/http/conditionalResponse.ts
3262
+ function jsonResponse(data, init = {}) {
3263
+ return Response.json(data, {
3264
+ status: init.status ?? 200,
3265
+ headers: init.headers
3266
+ });
3267
+ }
3268
+ function conditionalJsonResponse(request, data, init = {}) {
3269
+ if (!request || !isEtagEnabled()) {
3270
+ return jsonResponse(data, init);
3271
+ }
3272
+ const etag = computeEtagFromJson(data);
3273
+ if (ifNoneMatchSatisfied(request, etag)) {
3274
+ return notModifiedResponse(etag);
3275
+ }
3276
+ const response = jsonResponse(data, init);
3277
+ const headers = new Headers(response.headers);
3278
+ headers.set("ETag", etag);
3279
+ headers.set("Cache-Control", "private, must-revalidate");
3280
+ headers.append("Vary", "Authorization");
3281
+ headers.append("Vary", "X-Tenant-Id");
3282
+ return new Response(response.body, {
3283
+ status: response.status,
3284
+ statusText: response.statusText,
3285
+ headers
3286
+ });
3287
+ }
3288
+ // ../../src/core/http/validation.ts
3289
+ function buildRequestCacheKey(fallbackPath, request) {
3290
+ if (!request) {
3291
+ return fallbackPath;
3292
+ }
3293
+ const url = new URL(request.url);
3294
+ const user = currentAuthUser();
3295
+ const authScope = user ? `u:${user.id}` : "guest";
3296
+ const tenantScope = `t:${currentTenantId()}`;
3297
+ return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
3298
+ }
3299
+ function getQueryParams(request) {
3300
+ if (!request) {
3301
+ return new URLSearchParams;
3302
+ }
3303
+ return new URL(request.url).searchParams;
3304
+ }
3305
+ function parseOptionalPositiveIntQueryParam(params, name) {
3306
+ const value = params.get(name);
3307
+ if (value === null || value.trim() === "") {
3308
+ return;
3309
+ }
3310
+ const parsed = Number.parseInt(value, 10);
3311
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3312
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
3313
+ }
3314
+ return parsed;
3315
+ }
3316
+ function parseOptionalBooleanQueryParam(params, name) {
3317
+ const value = params.get(name);
3318
+ if (value === null || value.trim() === "") {
3319
+ return;
3320
+ }
3321
+ switch (value.toLowerCase()) {
3322
+ case "true":
3323
+ case "1":
3324
+ return true;
3325
+ case "false":
3326
+ case "0":
3327
+ return false;
3328
+ default:
3329
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
3330
+ }
3331
+ }
3332
+ function parseOptionalEnumQueryParam(params, name, allowedValues) {
3333
+ const value = params.get(name);
3334
+ if (value === null || value.trim() === "") {
3335
+ return;
3336
+ }
3337
+ if (!allowedValues.includes(value)) {
3338
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
3339
+ }
3340
+ return value;
3341
+ }
3342
+ function expectObject(value, label = "request body") {
3343
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3344
+ throw new BadRequestError(`${label} must be a JSON object.`);
3345
+ }
3346
+ return value;
3347
+ }
3348
+ async function parseJsonBody(request, validator) {
3349
+ let payload;
3350
+ try {
3351
+ payload = await request.json();
3352
+ } catch {
3353
+ throw new BadRequestError("Request body must be valid JSON.");
3354
+ }
3355
+ return validator(payload);
3356
+ }
3357
+ function readRequiredString(payload, field, options = {}) {
3358
+ const value = payload[field];
3359
+ if (typeof value !== "string" || value.trim() === "") {
3360
+ throw new BadRequestError(`"${field}" is required and must be a string.`);
3361
+ }
3362
+ const trimmed = value.trim();
3363
+ if (options.minLength !== undefined && trimmed.length < options.minLength) {
3364
+ throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
3365
+ }
3366
+ if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
3367
+ throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
3368
+ }
3369
+ if (options.pattern && !options.pattern.test(trimmed)) {
3370
+ throw new BadRequestError(`"${field}" has an invalid format.`);
3371
+ }
3372
+ return trimmed;
3373
+ }
3374
+ function readOptionalString(payload, field, options = {}) {
3375
+ if (!(field in payload) || payload[field] === undefined) {
3376
+ return;
3377
+ }
3378
+ return readRequiredString(payload, field, options);
3379
+ }
3380
+ function readRequiredEnum(payload, field, allowedValues) {
3381
+ const value = readRequiredString(payload, field);
3382
+ if (!allowedValues.includes(value)) {
3383
+ throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
3384
+ }
3385
+ return value;
3386
+ }
3387
+ function readOptionalEnum(payload, field, allowedValues) {
3388
+ if (!(field in payload) || payload[field] === undefined) {
3389
+ return;
3390
+ }
3391
+ return readRequiredEnum(payload, field, allowedValues);
3392
+ }
3393
+ function readRequiredPositiveInt(payload, field) {
3394
+ const value = payload[field];
3395
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
3396
+ throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
3397
+ }
3398
+ return value;
3399
+ }
3400
+ function readOptionalPositiveInt(payload, field) {
3401
+ if (!(field in payload) || payload[field] === undefined) {
3402
+ return;
3403
+ }
3404
+ return readRequiredPositiveInt(payload, field);
3405
+ }
3406
+ function parsePositiveIntParam(value, name = "id") {
3407
+ const parsed = Number.parseInt(value, 10);
3408
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3409
+ throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
3410
+ }
3411
+ return parsed;
3412
+ }
3413
+
3414
+ // ../../src/core/http/formRequest.ts
3415
+ class FormRequest {
3416
+ authorize(_request) {
3417
+ return true;
3418
+ }
3419
+ async validate(request) {
3420
+ if (!await this.authorize(request)) {
3421
+ throw new ForbiddenError;
3422
+ }
3423
+ return await parseJsonBody(request, (payload) => this.parse(payload));
3424
+ }
3425
+ }
3426
+
3427
+ class QueryFormRequest {
3428
+ validate(request) {
3429
+ return this.parseQuery(request);
3430
+ }
3431
+ }
3432
+ // ../../src/core/http/middleware.ts
3433
+ function isRouteHandler(value) {
3434
+ return typeof value === "function";
3435
+ }
3436
+ function isMethodRouteMap(value) {
3437
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3438
+ return false;
3439
+ }
3440
+ const entries = Object.entries(value);
3441
+ return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
3442
+ }
3443
+ function composeMiddleware(...middleware) {
3444
+ return (handler) => {
3445
+ return async (request) => {
3446
+ let index = 0;
3447
+ const dispatch = async () => {
3448
+ if (index >= middleware.length) {
3449
+ return await handler(request);
3450
+ }
3451
+ const current = middleware[index];
3452
+ index += 1;
3453
+ if (!current) {
3454
+ return await handler(request);
3455
+ }
3456
+ return await current(request, dispatch);
3457
+ };
3458
+ return await dispatch();
3459
+ };
3460
+ };
3461
+ }
3462
+ async function requestIdMiddleware(request, next) {
3463
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
3464
+ const response = await next();
3465
+ const headers = new Headers(response.headers);
3466
+ headers.set("x-request-id", requestId);
3467
+ return new Response(response.body, {
3468
+ status: response.status,
3469
+ statusText: response.statusText,
3470
+ headers
3471
+ });
3472
+ }
3473
+ function wrapRouteHandler(handler, middleware) {
3474
+ if (isMethodRouteMap(handler)) {
3475
+ const wrapped = {};
3476
+ for (const [method, routeHandler] of Object.entries(handler)) {
3477
+ wrapped[method] = composeMiddleware(...middleware)(routeHandler);
3478
+ }
3479
+ return wrapped;
3480
+ }
3481
+ if (isRouteHandler(handler)) {
3482
+ return composeMiddleware(...middleware)(handler);
3483
+ }
3484
+ return handler;
3485
+ }
3486
+ function applyMiddlewareToRoutes(routes, middleware) {
3487
+ const wrapped = {};
3488
+ for (const [path, routeHandler] of Object.entries(routes)) {
3489
+ wrapped[path] = wrapRouteHandler(routeHandler, middleware);
3490
+ }
3491
+ return wrapped;
3492
+ }
3493
+ // ../../src/core/http/pagination.ts
3494
+ var DEFAULT_PER_PAGE = 15;
3495
+ var MAX_PER_PAGE = 100;
3496
+ function parseRequiredPositiveIntQueryParam(params, name) {
3497
+ const value = params.get(name);
3498
+ if (value === null || value.trim() === "") {
3499
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
3500
+ }
3501
+ const parsed = Number.parseInt(value, 10);
3502
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3503
+ throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
3504
+ }
3505
+ return parsed;
3506
+ }
3507
+ function parsePaginationQuery(request) {
3508
+ const params = getQueryParams(request);
3509
+ const pageParam = params.get("page");
3510
+ const perPageParam = params.get("per_page");
3511
+ const page = pageParam === null || pageParam.trim() === "" ? 1 : parseRequiredPositiveIntQueryParam(params, "page");
3512
+ if (perPageParam === null || perPageParam.trim() === "") {
3513
+ return { page, perPage: DEFAULT_PER_PAGE };
3514
+ }
3515
+ const perPage = parseRequiredPositiveIntQueryParam(params, "per_page");
3516
+ if (perPage > MAX_PER_PAGE) {
3517
+ throw new BadRequestError(`Invalid query parameter "per_page". Maximum allowed value is ${MAX_PER_PAGE}.`);
3518
+ }
3519
+ return { page, perPage };
3520
+ }
3521
+ function paginatedResponse(data, meta, init = {}) {
3522
+ return Response.json({ data, meta }, init);
3523
+ }
3524
+ // ../../src/core/http/requireAuthMiddleware.ts
3525
+ function createRequireAuthMiddleware(auth) {
3526
+ return async (request, next) => {
3527
+ if (!await auth.check(request)) {
3528
+ const error = new UnauthorizedError;
3529
+ return Response.json({ error: error.message }, { status: error.status });
3530
+ }
3531
+ return await next();
3532
+ };
3533
+ }
3534
+ // ../../src/core/http/resources.ts
3535
+ function serializeDate(value) {
3536
+ return value instanceof Date ? value.toISOString() : value;
3537
+ }
3538
+ function toResourceCollection(items, transformer) {
3539
+ return items.map(transformer);
3540
+ }
3541
+ function toPaginatedResourceCollection(items, meta, transformer) {
3542
+ return {
3543
+ data: toResourceCollection(items, transformer),
3544
+ meta
3545
+ };
3546
+ }
3547
+ // ../../src/core/http/routeMiddleware.ts
3548
+ function withMiddleware(...middleware) {
3549
+ const wrap = composeMiddleware(...middleware);
3550
+ return (handler) => {
3551
+ return wrap(handler);
3552
+ };
3553
+ }
3554
+ // ../../src/core/http/routeModelBinding.ts
3555
+ function bindRouteModel(param, resolver, handler) {
3556
+ return async (request) => {
3557
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
3558
+ const model = await resolver(id, request);
3559
+ return await handler(request, model);
3560
+ };
3561
+ }
3562
+ // ../../src/core/logging/logger.ts
3563
+ class Logger {
3564
+ channel;
3565
+ constructor(channel = "app") {
3566
+ this.channel = channel;
3567
+ }
3568
+ write(level, message, context = {}) {
3569
+ const entry = {
3570
+ level,
3571
+ channel: this.channel,
3572
+ message,
3573
+ timestamp: new Date().toISOString(),
3574
+ ...context
3575
+ };
3576
+ const line = JSON.stringify(entry);
3577
+ if (level === "error") {
3578
+ console.error(line);
3579
+ return;
3580
+ }
3581
+ console.log(line);
3582
+ }
3583
+ debug(message, context) {
3584
+ this.write("debug", message, context);
3585
+ }
3586
+ info(message, context) {
3587
+ this.write("info", message, context);
3588
+ }
3589
+ warn(message, context) {
3590
+ this.write("warn", message, context);
3591
+ }
3592
+ error(message, context) {
3593
+ this.write("error", message, context);
3594
+ }
3595
+ }
3596
+ var appLogger = new Logger("app");
3597
+
3598
+ // ../../src/bootstrap/contracts.ts
3599
+ class ServiceContainer {
3600
+ services = new Map;
3601
+ singletonFactories = new Map;
3602
+ bindings = new Map;
3603
+ set(key, value) {
3604
+ this.singletonFactories.delete(key);
3605
+ this.bindings.delete(key);
3606
+ this.services.set(key, value);
3607
+ return value;
3608
+ }
3609
+ singleton(key, factory) {
3610
+ this.bindings.delete(key);
3611
+ this.services.delete(key);
3612
+ this.singletonFactories.set(key, factory);
3613
+ }
3614
+ bind(key, factory) {
3615
+ this.singletonFactories.delete(key);
3616
+ this.services.delete(key);
3617
+ this.bindings.set(key, factory);
3618
+ }
3619
+ get(key) {
3620
+ if (this.services.has(key)) {
3621
+ return this.services.get(key);
3622
+ }
3623
+ const singletonFactory = this.singletonFactories.get(key);
3624
+ if (singletonFactory) {
3625
+ const value = singletonFactory(this);
3626
+ this.services.set(key, value);
3627
+ return value;
3628
+ }
3629
+ const binding = this.bindings.get(key);
3630
+ if (binding) {
3631
+ return binding(this);
3632
+ }
3633
+ throw new Error(`Service "${key}" is not registered.`);
3634
+ }
3635
+ resolve(key) {
3636
+ return this.get(key);
3637
+ }
3638
+ has(key) {
3639
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
3640
+ }
3641
+ }
3642
+
3643
+ class ConfigStore {
3644
+ values = new Map;
3645
+ set(key, value) {
3646
+ this.values.set(key, value);
3647
+ return value;
3648
+ }
3649
+ get(key) {
3650
+ return this.values.get(key);
3651
+ }
3652
+ require(key) {
3653
+ if (!this.values.has(key)) {
3654
+ throw new Error(`Config key "${key}" is not defined.`);
3655
+ }
3656
+ return this.values.get(key);
3657
+ }
3658
+ has(key) {
3659
+ return this.values.has(key);
3660
+ }
3661
+ }
3662
+ function getRequiredDependency(dependencies, key) {
3663
+ const dependency = dependencies[key];
3664
+ if (dependency === undefined) {
3665
+ throw new Error(`Required dependency "${key}" is not registered.`);
3666
+ }
3667
+ return dependency;
3668
+ }
3669
+ function resolveService(dependencies, token) {
3670
+ return dependencies.container.resolve(token);
3671
+ }
3672
+
3673
+ // ../../src/bootstrap/applicationRegistry.ts
3674
+ var activeContext;
3675
+ function requireActiveApplicationContext() {
3676
+ if (!activeContext) {
3677
+ throw new Error("The application context has not been bootstrapped.");
3678
+ }
3679
+ return activeContext;
3680
+ }
3681
+ function resolveApplicationCache() {
3682
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
3683
+ }
3684
+ function resolveApplicationQueue() {
3685
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
3686
+ }
3687
+ function resolveApplicationAuth() {
3688
+ return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
3689
+ }
3690
+ function resolveApplicationPolicyGate() {
3691
+ return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
3692
+ }
3693
+ function resolveApplicationConfig() {
3694
+ return requireActiveApplicationContext().config;
3695
+ }
3696
+ function resolveApplicationLogger() {
3697
+ return appLogger;
3698
+ }
3699
+ function resolveApplicationDependencies() {
3700
+ return requireActiveApplicationContext().dependencies;
3701
+ }
3702
+
3703
+ // ../../src/core/http/securedRouteModelBinding.ts
3704
+ function isMutatingPolicyAction(action) {
3705
+ return action === "update" || action === "delete";
3706
+ }
3707
+ function securedBindRouteModel(param, resolver, authorization, handler) {
3708
+ return async (request) => {
3709
+ const id = parsePositiveIntParam(String(request.params[param]), String(param));
3710
+ const model = await resolver(id, request);
3711
+ const gate = resolveApplicationPolicyGate();
3712
+ const auth = resolveApplicationAuth();
3713
+ gate.authorize(authorization.resource, authorization.action, await auth.resolve(request), model);
3714
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
3715
+ assertIfMatch(request, etagFromResource(model), {
3716
+ required: authorization.requireIfMatch ?? true
3717
+ });
3718
+ }
3719
+ const response = await handler(request, model);
3720
+ if (isEtagEnabled() && authorization.action === "view") {
3721
+ return applyConditionalGet(request, response, etagFromResource(model));
3722
+ }
3723
+ return response;
3724
+ };
3725
+ }
3726
+ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
3727
+ return async (request) => {
3728
+ const key = String(request.params[param] ?? "").trim();
3729
+ if (!key) {
3730
+ throw new BadRequestError(`Missing route parameter "${String(param)}".`);
3731
+ }
3732
+ const model = await resolver(key, request);
3733
+ const gate = resolveApplicationPolicyGate();
3734
+ const auth = resolveApplicationAuth();
3735
+ gate.authorize(authorization.resource, authorization.action, await auth.resolve(request), model);
3736
+ if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
3737
+ assertIfMatch(request, etagFromResource(model), {
3738
+ required: authorization.requireIfMatch ?? true
3739
+ });
3740
+ }
3741
+ const response = await handler(request, model);
3742
+ if (isEtagEnabled() && authorization.action === "view") {
3743
+ return applyConditionalGet(request, response, etagFromResource(model));
3744
+ }
3745
+ return response;
3746
+ };
3747
+ }
3748
+ // ../../src/config/uploads.ts
3749
+ var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
3750
+ var ALLOWED_UPLOAD_MIME_TYPES = new Set([
3751
+ "application/pdf",
3752
+ "application/json",
3753
+ "application/zip",
3754
+ "application/x-zip-compressed",
3755
+ "image/jpeg",
3756
+ "image/png",
3757
+ "image/gif",
3758
+ "image/webp",
3759
+ "text/plain",
3760
+ "text/csv"
3761
+ ]);
3762
+ function resolveMaxUploadBytes() {
3763
+ const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
3764
+ if (!raw) {
3765
+ return DEFAULT_MAX_UPLOAD_BYTES;
3766
+ }
3767
+ const parsed = Number.parseInt(raw, 10);
3768
+ if (!Number.isInteger(parsed) || parsed <= 0) {
3769
+ return DEFAULT_MAX_UPLOAD_BYTES;
3770
+ }
3771
+ return parsed;
3772
+ }
3773
+ function normalizeMimeType(mimeType) {
3774
+ return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
3775
+ }
3776
+ function isAllowedMimeType(mimeType) {
3777
+ const normalized = normalizeMimeType(mimeType);
3778
+ if (!normalized || normalized === "application/octet-stream") {
3779
+ return true;
3780
+ }
3781
+ return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
3782
+ }
3783
+
3784
+ // ../../src/core/http/parseMultipartUpload.ts
3785
+ function normalizeMimeType2(mimeType) {
3786
+ return mimeType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
3787
+ }
3788
+ function sanitizeUploadFileName(name) {
3789
+ const base = name.split(/[/\\]/).pop()?.trim() ?? "upload";
3790
+ const sanitized = base.replace(/[^\w.\-()+ ]+/g, "_").slice(0, 200);
3791
+ return sanitized.length > 0 ? sanitized : "upload";
3792
+ }
3793
+ async function parseMultipartUpload(request, fieldName = "file") {
3794
+ const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3795
+ if (!contentType.includes("multipart/form-data")) {
3796
+ throw new BadRequestError("Expected multipart form data.");
3797
+ }
3798
+ const formData = await request.formData();
3799
+ const value = formData.get(fieldName);
3800
+ if (!(value instanceof File)) {
3801
+ throw new BadRequestError(`Missing upload field "${fieldName}".`);
3802
+ }
3803
+ if (value.size <= 0) {
3804
+ throw new BadRequestError("Uploaded file is empty.");
3805
+ }
3806
+ const maxBytes = resolveMaxUploadBytes();
3807
+ if (value.size > maxBytes) {
3808
+ throw new PayloadTooLargeError(`Upload exceeds the ${maxBytes} byte limit.`);
3809
+ }
3810
+ const mimeType = normalizeMimeType2(value.type.trim() || "application/octet-stream");
3811
+ if (!isAllowedMimeType(mimeType)) {
3812
+ throw new BadRequestError(`File type "${mimeType}" is not allowed.`);
3813
+ }
3814
+ return {
3815
+ fileName: sanitizeUploadFileName(value.name),
3816
+ mimeType,
3817
+ size: value.size,
3818
+ contents: new Uint8Array(await value.arrayBuffer())
3819
+ };
3820
+ }
3821
+ // ../../src/core/http/route.ts
3822
+ function getRouteParams(request) {
3823
+ return request.params;
3824
+ }
3825
+
3826
+ // ../../src/core/http/index.ts
3827
+ function jsonResponse2(data, init = {}) {
3828
+ return Response.json(data, {
3829
+ status: init.status ?? 200,
3830
+ headers: init.headers
3831
+ });
3832
+ }
3833
+ function createdResponse(data, init = {}) {
3834
+ return jsonResponse2(data, { ...init, status: init.status ?? 201 });
3835
+ }
3836
+ function noContentResponse() {
3837
+ return new Response(null, { status: 204 });
3838
+ }
3839
+ function errorResponse(error) {
3840
+ const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
3841
+ return Response.json({
3842
+ error: mappedError.message,
3843
+ ...mappedError.details === undefined ? {} : { details: mappedError.details }
3844
+ }, { status: mappedError.status });
3845
+ }
3846
+ function withErrorHandling(handler) {
3847
+ return async (...args) => {
3848
+ try {
3849
+ return await handler(...args);
3850
+ } catch (error) {
3851
+ const request = args.find((arg) => arg instanceof Request);
3852
+ const webResponse = webErrorResponse(error, request);
3853
+ if (webResponse) {
3854
+ return webResponse;
3855
+ }
3856
+ return errorResponse(error);
3857
+ }
3858
+ };
3859
+ }
3860
+ export {
3861
+ wrapRouteHandler,
3862
+ withMiddleware,
3863
+ withErrorHandling,
3864
+ toResourceCollection,
3865
+ toPaginatedResourceCollection,
3866
+ serializeDate,
3867
+ securedBindRouteModelByKey,
3868
+ securedBindRouteModel,
3869
+ sanitizeUploadFileName,
3870
+ requestIdMiddleware,
3871
+ readRequiredString,
3872
+ readRequiredPositiveInt,
3873
+ readRequiredEnum,
3874
+ readOptionalString,
3875
+ readOptionalPositiveInt,
3876
+ readOptionalEnum,
3877
+ parsePositiveIntParam,
3878
+ parsePaginationQuery,
3879
+ parseOptionalPositiveIntQueryParam,
3880
+ parseOptionalEnumQueryParam,
3881
+ parseOptionalBooleanQueryParam,
3882
+ parseMultipartUpload,
3883
+ parseJsonBody,
3884
+ paginatedResponse,
3885
+ notModifiedResponse,
3886
+ noContentResponse,
3887
+ jsonResponse2 as jsonResponse,
3888
+ isEtagEnabled,
3889
+ ifNoneMatchSatisfied,
3890
+ ifMatchSatisfied,
3891
+ getRouteParams,
3892
+ getQueryParams,
3893
+ expectObject,
3894
+ etagValuesMatch,
3895
+ etagFromResource,
3896
+ errorResponse,
3897
+ createdResponse,
3898
+ createRequireAuthMiddleware,
3899
+ createAuthorizeMiddleware,
3900
+ createAuthMiddleware,
3901
+ conditionalJsonResponse,
3902
+ computeEtagFromJson,
3903
+ composeMiddleware,
3904
+ buildRequestCacheKey,
3905
+ buildPaginationMeta,
3906
+ bindRouteModel,
3907
+ assertIfMatch,
3908
+ applyMiddlewareToRoutes,
3909
+ applyConditionalGet,
3910
+ ValidationError,
3911
+ UnprocessableEntityError,
3912
+ UnauthorizedError,
3913
+ QueryFormRequest,
3914
+ PreconditionFailedError,
3915
+ PayloadTooLargeError,
3916
+ NotFoundError,
3917
+ MAX_PER_PAGE,
3918
+ HttpError,
3919
+ FormRequest,
3920
+ ForbiddenError,
3921
+ DEFAULT_PER_PAGE,
3922
+ ConflictError,
3923
+ BadRequestError
3924
+ };