@getstrata/core 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -33
- package/README.md +36 -43
- package/dist/core/database/baseRepository.d.ts +2 -1
- package/dist/core/database/dialect.d.ts +5 -1
- package/dist/core/database/errors.d.ts +4 -0
- package/dist/core/database/mysqlConnection.d.ts +3 -1
- package/dist/core/errors/http.d.ts +4 -1
- package/dist/core/http/clientIp.d.ts +6 -3
- package/dist/core/http/webErrorResponse.d.ts +3 -1
- package/dist/core/runtime/appEnv.d.ts +4 -0
- package/dist/core/tenant/tenancyConfig.d.ts +1 -0
- package/dist/entries/audit/exportAuditLogs.js +7 -5
- package/dist/entries/auth/scimAuthMiddleware.js +7 -5
- package/dist/entries/contracts/authUserDirectory.js +1 -0
- package/dist/entries/database/errors.js +45 -6
- package/dist/entries/database/mysqlConnection.js +18 -2
- package/dist/entries/database/sqliteConnection.js +5 -0
- package/dist/entries/http/clientIp.js +24 -6
- package/dist/entries/http/corsMiddleware.js +14 -3
- package/dist/entries/http/loginThrottleMiddleware.js +25 -8
- package/dist/entries/http/memoryThrottleMiddleware.js +25 -8
- package/dist/entries/http/response.js +66 -7
- package/dist/entries/http/scimThrottleMiddleware.js +23 -6
- package/dist/entries/http/throttleMiddleware.js +25 -8
- package/dist/entries/http/webErrorResponse.js +66 -7
- package/dist/entries/jobs/exportAuditLogsJob.js +7 -5
- package/dist/entries/logging/requestLoggingMiddleware.js +29 -10
- package/dist/entries/openapi/generator.js +1 -1
- package/dist/entries/runtime/appEnv.js +8 -0
- package/dist/entries/tenant/databaseTenantContext.js +7 -5
- package/dist/entries/tenant/tenancyConfig.js +7 -5
- package/dist/entries/tenant/tenantDatabaseScope.js +7 -5
- package/dist/framework/public-api.d.ts +3 -3
- package/dist/index.js +161 -39
- package/package.json +16 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
- Lockstep with `create-strata` 1.0.2. No runtime changes.
|
|
6
|
+
|
|
7
|
+
## 1.0.1
|
|
8
|
+
|
|
9
|
+
- Publish the `contracts/authUserDirectory` subpath. Generated cookie and token apps import `AuthUserDirectory` from it, and without the export their `tsc --noEmit` failed with TS2307.
|
|
10
|
+
- `SqlDialect` gains `timestampValue()`, plus a `sqlTimestamp()` helper. MySQL `DATETIME` rejects the ISO-8601 `T` separator and trailing `Z`, which broke cookie session inserts on MySQL.
|
|
11
|
+
- `DatabaseConnection.close?()` now returns `void | Promise<void>`, matching the synchronous `close()` on the SQLite connection it is supposed to describe.
|
|
12
|
+
- `engines.bun` is declared.
|
|
13
|
+
- MySQL pools are UTC end to end: `createMysqlConnection()` passes `timezone: "Z"` to mysql2 and runs `SET time_zone = '+00:00'` on every new connection. Before, `DATETIME` values written as UTC by `timestampValue()` were read back shifted by the host offset, and `expires_at > NOW()` depended on the server time zone. `createMysqlPool(url)` exposes the configured raw pool.
|
|
14
|
+
- SQLite `nowExpression()` is `strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` instead of `CURRENT_TIMESTAMP`, matching the ISO-8601 text that `timestampValue()` writes. With `CURRENT_TIMESTAMP` the `T` separator sorted after the space, so an expired session compared as valid until the next UTC day.
|
|
15
|
+
- Unknown exceptions map to `InternalServerError` (500, generic message) instead of a 400 that echoed the raw error text. SQLite (`SQLITE_CONSTRAINT_*`) and MySQL (`ER_DUP_ENTRY`, `ER_NO_REFERENCED_ROW_2`, `ER_ROW_IS_REFERENCED_2`, `ER_BAD_NULL_ERROR`, `ER_CHECK_CONSTRAINT_VIOLATED`) constraint errors map to 409/422/400 like Postgres SQLSTATEs. 5xx responses are logged with the original message and stack.
|
|
16
|
+
- Client IP: `readClientIp()` falls back to the socket address the web server records in the request context, and with `TRUST_FORWARDED_FOR=true` takes the rightmost public `X-Forwarded-For` hop instead of the first (client-controlled) one. Throttles no longer collapse every client into an `unknown` bucket.
|
|
17
|
+
- CORS: with `CORS_ALLOWED_ORIGINS` unset, production sends no `Access-Control-Allow-Origin` (same-origin only) instead of reflecting any origin; outside production the default stays `*`. Listed origins are reflected; unlisted ones get no header.
|
|
18
|
+
- `TENANCY_DRIVER` must be `none`, `column`, or `rls`. Unknown values throw instead of enabling rls.
|
|
19
|
+
- SQLite connections enable WAL, `busy_timeout = 5000`, and `synchronous = NORMAL` for file databases.
|
|
20
|
+
- New subpath `runtime/appEnv` with `isProductionEnv()` (`APP_ENV` or `NODE_ENV`), used for the cookie `Secure` flag and for hiding 500 messages in HTML.
|
|
21
|
+
|
|
3
22
|
## 1.0.0
|
|
4
23
|
|
|
5
24
|
- First stable release of the public `@getstrata/core` API.
|
|
@@ -19,7 +38,7 @@
|
|
|
19
38
|
|
|
20
39
|
## 0.7.2
|
|
21
40
|
|
|
22
|
-
- `withJsonErrorHandling` always maps thrown errors to JSON. `requestPrefersJson` treats `/api/` as JSON even when `Accept` is HTML, so hybrid
|
|
41
|
+
- `withJsonErrorHandling` always maps thrown errors to JSON. `requestPrefersJson` treats `/api/` as JSON even when `Accept` is HTML, so hybrid HTML cannot remap JSON API errors.
|
|
23
42
|
|
|
24
43
|
## 0.7.1
|
|
25
44
|
|
|
@@ -28,20 +47,20 @@
|
|
|
28
47
|
|
|
29
48
|
## 0.7.0
|
|
30
49
|
|
|
31
|
-
- `FRONTEND_MODE=hybrid` turns on
|
|
50
|
+
- `FRONTEND_MODE=hybrid` turns on HTML at `/` and a SPA under `SPA_PREFIX` together. Views are on for `server-htmx` and `hybrid`. The SPA is on for `spa-react` and `hybrid`.
|
|
32
51
|
- `parseFrontendMode`, `FRONTEND_MODES`, and `FRONTEND_MODE_PATTERN` are the allowed-value list. Apps should reuse that pattern in env schemas.
|
|
33
52
|
- Named database connections: `registerNamedConnection`, `runOnNamedConnection`, SQLite (`bun:sqlite`), and MySQL (`mysql2`). `runWithSqlDialect` keeps the dialect across `await` via AsyncLocalStorage.
|
|
34
53
|
- New subpaths: `database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`, `database/connectionContext`.
|
|
35
|
-
- OpenAPI treats
|
|
54
|
+
- OpenAPI treats unauthenticated login routes as public.
|
|
36
55
|
|
|
37
56
|
## 0.6.0
|
|
38
57
|
|
|
39
58
|
- **Breaking:** cookie session signatures use HMAC-SHA256. Existing HMAC cookies signed with the previous digest will not verify. Rotate `SESSION_SECRET` or sign users in again.
|
|
40
|
-
- **Breaking:** default `MEMBER_ABILITIES` are profile and token scopes (`profile:read`, `auth:tokens:*`), not org/project/task. Apps replace the catalog with `configureAbilityCatalog`. HiroApp maps admin /
|
|
59
|
+
- **Breaking:** default `MEMBER_ABILITIES` are profile and token scopes (`profile:read`, `auth:tokens:*`), not org/project/task. Apps replace the catalog with `configureAbilityCatalog`. Generated HiroApp maps admin / member.
|
|
41
60
|
- Named auth guards: opaque Bearer tokens, JWT HS256, HTTP Basic, and cookie sessions. `AuthManager` picks a guard from the `Authorization` scheme. CSRF is skipped for Bearer and Basic.
|
|
42
|
-
- OpenAPI treats HiroApp login
|
|
61
|
+
- OpenAPI treats HiroApp login and JWT mint (`POST /api/auth/token`) as unauthenticated. Partner ping and audit export require credentials.
|
|
43
62
|
- SQL dialect helpers (`pgsql`, `mysql`, `sqlite`) for placeholders, quoting, `ILIKE`/`LIKE`, `RETURNING`, and `NULLS LAST`. Full-text `tsMatch` stays Postgres-only and throws on other engines.
|
|
44
|
-
- HiroApp is the
|
|
63
|
+
- HiroApp is the in-repo generated example. The leftover `src/db` schema is a test fixture, not a second app.
|
|
45
64
|
- New subpaths: `auth/jwt`, `auth/jwtGuard`, `auth/basicAuthGuard`, `auth/tokenAbilityChecker`, `database/dialect`, `http/statelessAuth`.
|
|
46
65
|
|
|
47
66
|
## 0.5.101
|
|
@@ -60,12 +79,12 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names. `hashe
|
|
|
60
79
|
|
|
61
80
|
## 0.5.99
|
|
62
81
|
|
|
63
|
-
HiroApp dogfood of 0.5.98 found lookalike APIs. This release matches the
|
|
82
|
+
HiroApp dogfood of 0.5.98 found lookalike APIs. This release matches the existing call shape and SQL, not just export names.
|
|
64
83
|
|
|
65
84
|
- Relation queries are thenable: `await user.applications()` delegates to `get()`.
|
|
66
85
|
- `Model.with()` / `where()` / `whereHas()` return a `ModelQuery` that hydrates models and chains into `where` / `find` / `findOrFail` / `first` / `get`. `first()` / `find()` use `LIMIT` / PK lookup.
|
|
67
86
|
- `belongsTo.where()` threads constraints into `whereHas` EXISTS (HiroApp application search).
|
|
68
|
-
- `{ ilike }` uses the value as-is
|
|
87
|
+
- `{ ilike }` uses the value as-is. Pass `%term%` yourself; the operator no longer wraps extra `%`.
|
|
69
88
|
- Morph type defaults to `$morphClass` / class name, not `table.name`. Override with `$morphClass = "App\\Models\\User"` or the last `morphMany` argument. `morphTo()` no longer silently defaults to `imageable_*`.
|
|
70
89
|
- `primaryKey()` defaults to the table PK (`id`).
|
|
71
90
|
- Nested `load("a.b")` / `with("a.b")` skip already-loaded heads and batch the next level.
|
|
@@ -80,13 +99,13 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
|
|
|
80
99
|
|
|
81
100
|
## 0.5.98
|
|
82
101
|
|
|
83
|
-
-
|
|
102
|
+
- Model relations: `this.hasMany(Related)` returns a relation query (`get`/`where`/`create`/`attach`). `load()` / `loaded()` and `Model.with()` load related rows. `whereHas`/`has`/`doesntHave`, morph* methods, and nested `with("a.b")` are supported.
|
|
84
103
|
- Model `$hidden`/`$visible`/`$appends`, `toArray()`/`toJSON()`, `makeHidden`/`makeVisible`/`append`, and `observe()`.
|
|
85
104
|
- `Model.where()`, `firstOrNew()`, `firstOrCreate()`, and `updateOrCreate()`.
|
|
86
105
|
- Factory `count`/`state`/`sequence`/`for`/`has`/`recycle` plus `afterMaking`/`afterCreating`.
|
|
87
106
|
- `JsonResource` (`wrap`, `whenLoaded`, `additional`, `collection`).
|
|
88
|
-
-
|
|
89
|
-
- Parity audit reports design score separately.
|
|
107
|
+
- Convenience aliases: container `make`/`instance`, EventBus `on`/`emit`, query `whereNull`/`whereIn`/`whereExists`.
|
|
108
|
+
- Parity audit reports design score separately. Queue dashboards, admin UIs, and CLI stay Bun-native.
|
|
90
109
|
|
|
91
110
|
## 0.5.97
|
|
92
111
|
|
|
@@ -103,7 +122,7 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
|
|
|
103
122
|
|
|
104
123
|
## 0.5.90
|
|
105
124
|
|
|
106
|
-
- **Breaking identity defaults:** `appKeyPrefix()` is `strata` and `appDisplayName()` is `Strata` when `APP_KEY_PREFIX` / `APP_NAME` are unset (were `strata` / `
|
|
125
|
+
- **Breaking identity defaults:** `appKeyPrefix()` is `strata` and `appDisplayName()` is `Strata` when `APP_KEY_PREFIX` / `APP_NAME` are unset (were `strata` / `HiroApp`). HiroApp pins those env vars.
|
|
107
126
|
- `Schedule.command()` rejects cron strings other than `* * * * *` and `*/N * * * *` instead of silently never running them.
|
|
108
127
|
- OpenAPI marks `GET /users/me/*` as bearer-authenticated.
|
|
109
128
|
- OpenAPI `/users/me/current-organization` summaries no longer say team invitations.
|
|
@@ -115,11 +134,11 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
|
|
|
115
134
|
## 0.5.88
|
|
116
135
|
|
|
117
136
|
- Exported cookie name constants (`SESSION_COOKIE`, `CSRF_COOKIE`, `FLASH_COOKIE`, `INTENDED_URL_COOKIE`, `PASSWORD_CONFIRM_COOKIE`) are `appCookieName(...)` so they match the default prefix instead of a hardcoded `strata_` string.
|
|
118
|
-
- `@getstrata/core/jobs/dispatchWebhookJob` is a deprecated compatibility re-export of the
|
|
137
|
+
- `@getstrata/core/jobs/dispatchWebhookJob` is a deprecated compatibility re-export of the HiroApp webhook job.
|
|
119
138
|
|
|
120
139
|
## 0.5.87
|
|
121
140
|
|
|
122
|
-
- `AuthUserDirectory.hasActiveBrowserSession?(userId, issuedAt)` is optional. `SessionGuard` calls it after `session_valid_after` and rejects the HMAC cookie when it returns false so
|
|
141
|
+
- `AuthUserDirectory.hasActiveBrowserSession?(userId, issuedAt)` is optional. `SessionGuard` calls it after `session_valid_after` and rejects the HMAC cookie when it returns false so HiroApp can revoke a single browser session by deleting the `sessions` row.
|
|
123
142
|
|
|
124
143
|
## 0.5.86
|
|
125
144
|
|
|
@@ -144,11 +163,11 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
|
|
|
144
163
|
|
|
145
164
|
## 0.5.81
|
|
146
165
|
|
|
147
|
-
- `@getstrata/core/auth/intendedUrlCookie` (`createIntendedUrlCookie`, `readIntendedUrl`, `clearIntendedUrlCookie`, `createIntendedUrlCookieFromRequest`). HTML auth intended URL after HTML email verification: register with `redirect=` and
|
|
166
|
+
- `@getstrata/core/auth/intendedUrlCookie` (`createIntendedUrlCookie`, `readIntendedUrl`, `clearIntendedUrlCookie`, `createIntendedUrlCookieFromRequest`). HTML auth intended URL after HTML email verification: register with `redirect=` and verified HTML redirects stash `${APP_KEY_PREFIX}_intended` (`INTENDED_URL_COOKIE_NAME`, default `strata_intended`; TTL `INTENDED_URL_TTL_SECONDS`, default 86400s). `GET /verify-email` honors and clears it.
|
|
148
167
|
|
|
149
168
|
## 0.5.80
|
|
150
169
|
|
|
151
|
-
- `appCookieName()` / `appDevSecret()` on `@getstrata/core/runtime/appKeyPrefix`. HMAC session, CSRF, flash, password-confirm, signed-URL, OAuth-state, and token-pepper fallbacks follow `APP_KEY_PREFIX` (
|
|
170
|
+
- `appCookieName()` / `appDevSecret()` on `@getstrata/core/runtime/appKeyPrefix`. HMAC session, CSRF, flash, password-confirm, signed-URL, OAuth-state, and token-pepper fallbacks follow `APP_KEY_PREFIX` (HiroApp defaults unchanged).
|
|
152
171
|
|
|
153
172
|
## 0.5.79
|
|
154
173
|
|
|
@@ -168,12 +187,12 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
|
|
|
168
187
|
|
|
169
188
|
## 0.5.75
|
|
170
189
|
|
|
171
|
-
-
|
|
190
|
+
- Password confirm: `@getstrata/core/auth/passwordConfirmCookie` (`createPasswordConfirmCookie`, `hasFreshPasswordConfirmation`, `clearPasswordConfirmCookie`) and `createRequirePasswordConfirmMiddleware()` (HTML 302 `/confirm-password`, JSON 423). Cookie name `PASSWORD_CONFIRM_COOKIE_NAME` (default `strata_password_confirmed`), TTL `PASSWORD_CONFIRM_TIMEOUT` (default 10800s).
|
|
172
191
|
|
|
173
192
|
## 0.5.74
|
|
174
193
|
|
|
175
194
|
- `AuthUser.emailVerifiedAt` and `@getstrata/core/auth/emailVerification` (`isEmailVerificationRequired`, `hasVerifiedEmail`). `null` means unverified; missing is treated as verified (GuestGuard / legacy).
|
|
176
|
-
- `createRequireVerifiedMiddleware()`
|
|
195
|
+
- `createRequireVerifiedMiddleware()` requires a verified email (HTML 302 `/email/verify`, JSON 403).
|
|
177
196
|
|
|
178
197
|
## 0.5.73
|
|
179
198
|
|
|
@@ -198,40 +217,40 @@ Still non-conforming (honest): Bun cannot infer `morphTo()` method names (`debug
|
|
|
198
217
|
|
|
199
218
|
## 0.5.68
|
|
200
219
|
|
|
201
|
-
- Published core no longer imports
|
|
220
|
+
- Published core no longer imports HiroApp `src/config`. Frontend mode, queue retries, CORS, and upload limits read env (`FRONTEND_MODE`, `QUEUE_*`, `CORS_ALLOWED_ORIGINS`, `MAX_UPLOAD_BYTES`).
|
|
202
221
|
- `@getstrata/core/runtime/frontendMode` exports `readFrontendMode` / `isViewsEnabled` / `isSpaEnabled`.
|
|
203
222
|
|
|
204
223
|
## 0.5.67
|
|
205
224
|
|
|
206
|
-
- OpenAPI title, server URLs, and generated SDK class name come from `APP_NAME` / `APP_URL` / `API_PREFIX` / `APP_SDK_CLASS` instead of importing
|
|
207
|
-
- `appEnv()`, `appUrl()`, `apiPrefix()`, and `sdkClientClassName()` live on `@getstrata/core/runtime/appKeyPrefix`. SIEM export, HSTS, and `safeFetch` DNS resolve use `appEnv()` instead of
|
|
225
|
+
- OpenAPI title, server URLs, and generated SDK class name come from `APP_NAME` / `APP_URL` / `API_PREFIX` / `APP_SDK_CLASS` instead of importing HiroApp `src/config/app`.
|
|
226
|
+
- `appEnv()`, `appUrl()`, `apiPrefix()`, and `sdkClientClassName()` live on `@getstrata/core/runtime/appKeyPrefix`. SIEM export, HSTS, and `safeFetch` DNS resolve use `appEnv()` instead of HiroApp `appConfig`.
|
|
208
227
|
|
|
209
228
|
## 0.5.66
|
|
210
229
|
|
|
211
|
-
- `createValidateSignatureMiddleware()` on `@getstrata/core/http/signedUrl`
|
|
230
|
+
- `createValidateSignatureMiddleware()` on `@getstrata/core/http/signedUrl` rejects invalid or expired signed links with `ForbiddenError`.
|
|
212
231
|
|
|
213
232
|
## 0.5.65
|
|
214
233
|
|
|
215
|
-
- Identity helpers on `@getstrata/core/runtime/appKeyPrefix` (`smtpEhloHost`, `siemEventType`, `appUserAgent`, `otelServiceName`, `appDisplayName`, `webhookSignatureHeader`) so sibling apps are not stuck with
|
|
216
|
-
- SIEM `event_type` and CEF vendor follow `SIEM_EVENT_TYPE` / `APP_NAME` (defaults stay `strata.audit` / `
|
|
234
|
+
- Identity helpers on `@getstrata/core/runtime/appKeyPrefix` (`smtpEhloHost`, `siemEventType`, `appUserAgent`, `otelServiceName`, `appDisplayName`, `webhookSignatureHeader`) so sibling apps are not stuck with HiroApp SMTP/SIEM/OTEL/OAuth names.
|
|
235
|
+
- SIEM `event_type` and CEF vendor follow `SIEM_EVENT_TYPE` / `APP_NAME` (defaults stay `strata.audit` / `HiroApp`).
|
|
217
236
|
|
|
218
237
|
## 0.5.64
|
|
219
238
|
|
|
220
|
-
- `APP_KEY_PREFIX` (default `strata`) namespaces Redis cache, queue, and throttle keys so sibling apps do not share
|
|
221
|
-
- `DispatchWebhookJob` implementation lives in the
|
|
239
|
+
- `APP_KEY_PREFIX` (default `strata`) namespaces Redis cache, queue, and throttle keys so sibling apps do not share HiroApp's keyspace.
|
|
240
|
+
- `DispatchWebhookJob` implementation lives in the HiroApp webhook module. `@getstrata/core/jobs/dispatchWebhookJob` remains a compatibility re-export.
|
|
222
241
|
|
|
223
242
|
## 0.5.63
|
|
224
243
|
|
|
225
|
-
- Flash cookies honor `FLASH_COOKIE_NAME` (default `strata_flash`) so sibling apps do not inherit
|
|
244
|
+
- Flash cookies honor `FLASH_COOKIE_NAME` (default `strata_flash`) so sibling apps do not inherit HiroApp's cookie name.
|
|
226
245
|
|
|
227
246
|
## 0.5.62
|
|
228
247
|
|
|
229
|
-
|
|
248
|
+
URL signing, Bun-native markdown mail, and session-auth cleanup.
|
|
230
249
|
|
|
231
250
|
- `temporarySignedUrl` / `signedUrl` / `hasValidSignature` / `assertValidSignature` on `@getstrata/core/http/signedUrl`. HMAC uses `SIGNED_URL_SECRET` or `SESSION_SECRET`. Paths must be same-origin (`/` only; reject `//` and `://`).
|
|
232
251
|
- `markdownToHtml()` uses `Bun.markdown.html()` plus `sanitizeMailHtml` (allowlist). Scripts, `javascript:` links, and unknown tags are stripped.
|
|
233
252
|
- `ExportAuditLogsJob` (`@getstrata/core/jobs/exportAuditLogsJob`) wraps SIEM export so the scheduler can dispatch a real job.
|
|
234
|
-
- `DispatchWebhookJob` reads `APP_ENV` and `WEBHOOK_SIGNATURE_HEADER` instead of
|
|
253
|
+
- `DispatchWebhookJob` reads `APP_ENV` and `WEBHOOK_SIGNATURE_HEADER` instead of HiroApp `appConfig`.
|
|
235
254
|
- `createRequireWebAuthMiddleware` runs the handler inside `runWithAuthUser` so `currentAuthUser()` works on HTMX session routes.
|
|
236
255
|
- `generateTotpSecret()` / `buildOtpauthUrl()` on `@getstrata/core/security/totp`.
|
|
237
256
|
- `createRequireAbilityMiddleware` rethrows `ForbiddenError` for HTML views so HTMX routes render a styled 403 instead of JSON.
|
|
@@ -255,13 +274,13 @@ HTMX HTML kernel gaps that sibling apps could not work around without weakening
|
|
|
255
274
|
|
|
256
275
|
## 0.5.59
|
|
257
276
|
|
|
258
|
-
Sibling HTMX apps can consume published packages without
|
|
277
|
+
Sibling HTMX apps can consume published packages without HiroApp-only glue.
|
|
259
278
|
|
|
260
279
|
- Emit `@getstrata/core/facades` types at `dist/core/facades/index.d.ts` (CI checks every `exports.*.types` path after build).
|
|
261
|
-
- Session and CSRF cookie names are configurable (`SESSION_COOKIE_NAME`, `CSRF_COOKIE_NAME`). Defaults remain `strata_session` and `strata_csrf` for
|
|
280
|
+
- Session and CSRF cookie names are configurable (`SESSION_COOKIE_NAME`, `CSRF_COOKIE_NAME`). Defaults remain `strata_session` and `strata_csrf` for HiroApp.
|
|
262
281
|
- Split `CORE_ABILITY_CHECKER_TOKEN` and `CORE_AUTH_USER_DIRECTORY_TOKEN` from `CORE_TOKEN_SERVICE_TOKEN`. HttpKernel prefers the ability-checker token; HMAC `SessionGuard` / layout data prefer the user directory. A compatibility shim uses `CORE_TOKEN_SERVICE_TOKEN` only when the bound value matches the requested type.
|
|
263
|
-
- `configureWebLayoutData` lets apps choose `currentUser` vs `authUser`, a custom user loader, and extra template fields.
|
|
282
|
+
- `configureWebLayoutData` lets apps choose `currentUser` vs `authUser`, a custom user loader, and extra template fields. HiroApp still gets `{ authUser, csrfToken, flash }`.
|
|
264
283
|
- `redirectResponse`, `notFoundHtmlResponse`, `textResponse`, and `xmlResponse` join `htmlResponse` / `isHtmxRequest` on `@getstrata/core/view`.
|
|
265
284
|
- `LOGIN_RATE_LIMIT_WINDOW_MS` is a deprecated alias for `LOGIN_RATE_LIMIT_WINDOW_SECONDS`.
|
|
266
285
|
|
|
267
|
-
HMAC `SessionGuard` is unchanged for
|
|
286
|
+
HMAC `SessionGuard` is unchanged for HiroApp API/token apps. Sibling HTMX apps should use `@getstrata/bootstrap` `CookieSessionStore` + `createCookieSessionAuthManager`.
|
package/README.md
CHANGED
|
@@ -1,71 +1,64 @@
|
|
|
1
1
|
# @getstrata/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runtime library for [Strata](https://github.com/EyK-26/strata) apps: HTTP, auth, database, queue, mail, and security. Requires Bun (tested on 1.4.x).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
**Repository:** [EyK-26/strata](https://github.com/EyK-26/strata), directory `packages/strata-core`
|
|
5
|
+
Generate an app rather than wiring this by hand:
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
```bash
|
|
8
|
+
bunx create-strata my-app
|
|
9
|
+
```
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## Import subpaths, not the root
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Apps, and anything they import, should use subpaths:
|
|
13
14
|
|
|
14
15
|
```typescript
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { Policy } from "@getstrata/core/auth/policy";
|
|
16
|
+
import { createAuthMiddleware } from "@getstrata/core/http/authMiddleware";
|
|
17
|
+
import { bindDatabaseConnection } from "@getstrata/core/database/bindConnection";
|
|
18
18
|
import { BaseRepository } from "@getstrata/core/database/baseRepository";
|
|
19
19
|
import { FormRequest } from "@getstrata/core/http/formRequest";
|
|
20
|
+
import { Policy } from "@getstrata/core/auth/policy";
|
|
20
21
|
import { withErrorHandling } from "@getstrata/core/http/response";
|
|
21
|
-
import {
|
|
22
|
+
import { ValidationError } from "@getstrata/core/errors/http";
|
|
23
|
+
import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
**Dependency:** `eta` is a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database **engine** is your choice. Generated HiroApp uses **Bun's built-in `Bun.sql`** (Postgres): `createBunSqlPool()`, then `registerDefaultDatabasePool()` and `bindDatabaseConnection()`. `bindBunSql()` is a helper that registers both. Extra engines register with `registerNamedConnection` (`database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`).
|
|
27
|
-
|
|
28
|
-
`orderBy` accepts `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`. `{ ilike }` uses the value as-is. Pass `%term%` yourself.
|
|
26
|
+
The root `@getstrata/core` import resolves, but nothing stops you from ending up with two copies of process-wide state: the database pool, the dialect override, async-local auth and tenant context, and the `HttpError` base class used with `instanceof`. Some subpaths re-export the main bundle for exactly that reason. Prefer subpaths everywhere and the problem does not arise.
|
|
29
27
|
|
|
30
|
-
##
|
|
28
|
+
## Database
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
- `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
|
|
34
|
-
- `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
|
|
30
|
+
Set `DATABASE_URL` before the first query; the connection is created lazily.
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
```typescript
|
|
33
|
+
process.env.DATABASE_URL ??= "postgresql://postgres:postgres@localhost:5432/myapp";
|
|
34
|
+
```
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
The engine is your choice, and an app should have one primary engine. Postgres apps use Bun's built-in `Bun.sql` through `createBunSqlPool()`, then `registerDefaultDatabasePool()` and `bindDatabaseConnection()`; `bindBunSql()` registers both. SQLite and MySQL register through `database/sqliteConnection` and `database/mysqlConnection`, or `registerNamedConnection` from `database/namedConnections`.
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
bun run build:framework
|
|
42
|
-
bun run verify:framework
|
|
43
|
-
bun run verify:shared-subpaths
|
|
44
|
-
```
|
|
38
|
+
`orderBy` takes `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`. `{ ilike }` uses the value as-is, so pass `%term%` yourself.
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
Full-text `tsMatch` is PostgreSQL only.
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
`mysql2` and `eta` are direct dependencies, so they install even for a SQLite JSON API app.
|
|
49
43
|
|
|
50
|
-
|
|
44
|
+
## Views
|
|
51
45
|
|
|
52
|
-
|
|
46
|
+
`.eta` files are HTML plus Eta tags (`<% %>`, `<%= %>`, `<%~ include() %>`). Pug-style class or id shorthand such as `section.section` throws at render time and names the offending template.
|
|
53
47
|
|
|
54
|
-
|
|
48
|
+
## Admin and queue helpers
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
import { ValidationError } from "@getstrata/core/errors/http";
|
|
60
|
-
import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
61
|
-
```
|
|
50
|
+
- `AdminResourceRegistry`, `formatAdminValue`: read-only resource browsers
|
|
51
|
+
- `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
|
|
52
|
+
- `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
|
|
62
53
|
|
|
63
|
-
|
|
54
|
+
Generated apps do not include an admin dashboard, and the registry is optional.
|
|
64
55
|
|
|
65
|
-
|
|
56
|
+
## Docs
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
- [Getting started](https://github.com/EyK-26/strata/blob/main/docs/GETTING-STARTED.md)
|
|
59
|
+
- [Building apps](https://github.com/EyK-26/strata/blob/main/docs/BUILDING-APPS.md)
|
|
60
|
+
- [Auth choices](https://github.com/EyK-26/strata/blob/main/docs/AUTH.md)
|
|
61
|
+
- [Databases](https://github.com/EyK-26/strata/blob/main/docs/DATABASE.md)
|
|
62
|
+
- [Production](https://github.com/EyK-26/strata/blob/main/docs/PRODUCTION.md)
|
|
70
63
|
|
|
71
|
-
|
|
64
|
+
Contributing to the framework itself: [CONTRIBUTING.md](https://github.com/EyK-26/strata/blob/main/CONTRIBUTING.md).
|
|
@@ -10,7 +10,8 @@ type ExtendedQueryOptions<TEntity extends object> = QueryOptions<TEntity> & {
|
|
|
10
10
|
interface DatabaseConnection {
|
|
11
11
|
unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
|
|
12
12
|
begin?<T>(callback: (transaction: DatabaseConnection) => Promise<T>): Promise<T>;
|
|
13
|
-
|
|
13
|
+
/** SQLite closes synchronously; pooled drivers return a promise. */
|
|
14
|
+
close?(): void | Promise<void>;
|
|
14
15
|
}
|
|
15
16
|
interface SqlDatabaseConnection extends DatabaseConnection {
|
|
16
17
|
(strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
|
|
@@ -4,6 +4,8 @@ interface SqlDialect {
|
|
|
4
4
|
placeholder(index: number): string;
|
|
5
5
|
quoteIdentifier(identifier: string): string;
|
|
6
6
|
nowExpression(): string;
|
|
7
|
+
/** Render a Date as a literal this engine accepts for a timestamp column. */
|
|
8
|
+
timestampValue(value: Date): string;
|
|
7
9
|
returningClause(columns: string): string;
|
|
8
10
|
ilikeOperator(): "ILIKE" | "LIKE";
|
|
9
11
|
nullsLastSuffix(): string;
|
|
@@ -12,7 +14,9 @@ interface SqlDialect {
|
|
|
12
14
|
declare function dialectFor(driver: DatabaseDriver): SqlDialect;
|
|
13
15
|
declare function currentSqlDialect(): SqlDialect;
|
|
14
16
|
declare function useSqlDialect(driver: DatabaseDriver): SqlDialect;
|
|
17
|
+
/** Format a timestamp parameter for whichever engine is active. */
|
|
18
|
+
declare function sqlTimestamp(value?: Date): string;
|
|
15
19
|
declare function resetSqlDialect(): void;
|
|
16
20
|
declare function runWithSqlDialect<T>(driver: DatabaseDriver, callback: () => T | Promise<T>): T | Promise<T>;
|
|
17
21
|
export type { SqlDialect };
|
|
18
|
-
export { currentSqlDialect, dialectFor, resetSqlDialect, runWithSqlDialect, useSqlDialect };
|
|
22
|
+
export { currentSqlDialect, dialectFor, resetSqlDialect, runWithSqlDialect, sqlTimestamp, useSqlDialect, };
|
|
@@ -7,6 +7,10 @@ interface PostgresErrorLike {
|
|
|
7
7
|
message?: string;
|
|
8
8
|
}
|
|
9
9
|
declare function isPostgresError(error: unknown): error is PostgresErrorLike;
|
|
10
|
+
/**
|
|
11
|
+
* Constraint violations become 4xx with a fixed message. Anything else is a
|
|
12
|
+
* 500 with a generic message; the raw driver text never reaches the client.
|
|
13
|
+
*/
|
|
10
14
|
declare function mapDatabaseError(error: unknown): HttpError;
|
|
11
15
|
declare function withDatabaseErrorHandling<TValue>(operation: () => Promise<TValue>): Promise<TValue>;
|
|
12
16
|
export { isPostgresError, mapDatabaseError, withDatabaseErrorHandling };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import mysql from "mysql2/promise";
|
|
1
2
|
import type { ActiveDatabaseHandle } from "./connectionContext.ts";
|
|
2
3
|
type MysqlExecutable = {
|
|
3
4
|
execute: (sql: string, params?: unknown[]) => Promise<[unknown, unknown]>;
|
|
@@ -7,6 +8,7 @@ type MysqlConnection = ActiveDatabaseHandle & {
|
|
|
7
8
|
close(): Promise<void>;
|
|
8
9
|
};
|
|
9
10
|
declare function createMysqlConnectionFromPool(pool: MysqlExecutable): MysqlConnection;
|
|
11
|
+
declare function createMysqlPool(url: string): mysql.Pool;
|
|
10
12
|
declare function createMysqlConnection(url: string): MysqlConnection;
|
|
11
13
|
export type { MysqlConnection, MysqlExecutable };
|
|
12
|
-
export { createMysqlConnection, createMysqlConnectionFromPool };
|
|
14
|
+
export { createMysqlConnection, createMysqlConnectionFromPool, createMysqlPool };
|
|
@@ -30,6 +30,9 @@ declare class PayloadTooLargeError extends HttpError {
|
|
|
30
30
|
declare class PreconditionFailedError extends HttpError {
|
|
31
31
|
constructor(message?: string, details?: unknown);
|
|
32
32
|
}
|
|
33
|
+
declare class InternalServerError extends HttpError {
|
|
34
|
+
constructor(message?: string, details?: unknown);
|
|
35
|
+
}
|
|
33
36
|
interface HttpErrorLike {
|
|
34
37
|
status: number;
|
|
35
38
|
message: string;
|
|
@@ -37,4 +40,4 @@ interface HttpErrorLike {
|
|
|
37
40
|
}
|
|
38
41
|
declare function isHttpErrorLike(error: unknown): error is HttpErrorLike;
|
|
39
42
|
declare function toHttpError(error: unknown): HttpError | null;
|
|
40
|
-
export { BadRequestError, ConflictError, ForbiddenError, HttpError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, };
|
|
43
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, InternalServerError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, };
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
declare function
|
|
3
|
-
|
|
1
|
+
type EnvRecord = Record<string, string | undefined>;
|
|
2
|
+
declare function trustForwardedFor(env?: EnvRecord): boolean;
|
|
3
|
+
declare function isPrivateAddress(address: string): boolean;
|
|
4
|
+
/** Forwarded headers only when trusted; otherwise the socket address the server recorded. */
|
|
5
|
+
declare function readClientIp(request: Request, env?: EnvRecord): string | undefined;
|
|
6
|
+
export { isPrivateAddress, readClientIp, trustForwardedFor };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type HttpError } from "@getstrata/core/errors/http";
|
|
1
2
|
type FieldErrors = Record<string, string[]>;
|
|
2
3
|
declare function normalizeFieldErrors(details: unknown): FieldErrors;
|
|
4
|
+
declare function logServerError(error: unknown, mappedError: HttpError): void;
|
|
3
5
|
declare function webErrorResponse(error: unknown, request?: Request): Promise<Response | null>;
|
|
4
6
|
export type { FieldErrors };
|
|
5
|
-
export { normalizeFieldErrors, webErrorResponse };
|
|
7
|
+
export { logServerError, normalizeFieldErrors, webErrorResponse };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
type TenancyDriver = "rls" | "column" | "none";
|
|
2
|
+
/** Unset means rls (the Postgres example). Anything else must be an exact driver name. */
|
|
2
3
|
declare function readTenancyDriver(env?: Record<string, string | undefined>): TenancyDriver;
|
|
3
4
|
declare function isTenancyEnabled(env?: Record<string, string | undefined>): boolean;
|
|
4
5
|
/** Postgres `SET LOCAL` / `set_config` for row-level security. SQLite and MySQL cannot do this. */
|
|
@@ -191,14 +191,16 @@ async function safeFetch(input, init = {}, options = {}) {
|
|
|
191
191
|
import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
|
|
192
192
|
|
|
193
193
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
194
|
+
var TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
194
195
|
function readTenancyDriver(env = process.env) {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const raw = env.TENANCY_DRIVER?.trim();
|
|
197
|
+
if (raw === undefined || raw === "") {
|
|
198
|
+
return "rls";
|
|
197
199
|
}
|
|
198
|
-
if (
|
|
199
|
-
return
|
|
200
|
+
if (TENANCY_DRIVERS.includes(raw)) {
|
|
201
|
+
return raw;
|
|
200
202
|
}
|
|
201
|
-
|
|
203
|
+
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
202
204
|
}
|
|
203
205
|
function isTenancyEnabled(env = process.env) {
|
|
204
206
|
return readTenancyDriver(env) !== "none";
|
|
@@ -78,14 +78,16 @@ function resolveScimTenantFromToken(token) {
|
|
|
78
78
|
import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
|
|
79
79
|
|
|
80
80
|
// ../../src/core/tenant/tenancyConfig.ts
|
|
81
|
+
var TENANCY_DRIVERS = ["rls", "column", "none"];
|
|
81
82
|
function readTenancyDriver(env = process.env) {
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const raw = env.TENANCY_DRIVER?.trim();
|
|
84
|
+
if (raw === undefined || raw === "") {
|
|
85
|
+
return "rls";
|
|
84
86
|
}
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
87
|
+
if (TENANCY_DRIVERS.includes(raw)) {
|
|
88
|
+
return raw;
|
|
87
89
|
}
|
|
88
|
-
|
|
90
|
+
throw new Error(`TENANCY_DRIVER must be one of ${TENANCY_DRIVERS.join(", ")}; received "${raw}".`);
|
|
89
91
|
}
|
|
90
92
|
function isTenancyEnabled(env = process.env) {
|
|
91
93
|
return readTenancyDriver(env) !== "none";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
BadRequestError,
|
|
5
5
|
ConflictError,
|
|
6
|
+
InternalServerError,
|
|
6
7
|
toHttpError,
|
|
7
8
|
UnprocessableEntityError
|
|
8
9
|
} from "@getstrata/core/errors/http";
|
|
@@ -21,14 +22,55 @@ function getPostgresSqlState(error) {
|
|
|
21
22
|
}
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
25
|
+
var MYSQL_ERRNO_MESSAGES = {
|
|
26
|
+
1062: () => new ConflictError("A record with these values already exists."),
|
|
27
|
+
1451: () => new UnprocessableEntityError("Record is still referenced by other records."),
|
|
28
|
+
1452: () => new UnprocessableEntityError("Referenced record does not exist."),
|
|
29
|
+
1048: () => new BadRequestError("Required field is missing."),
|
|
30
|
+
3819: () => new BadRequestError("Value violates a database constraint.")
|
|
31
|
+
};
|
|
32
|
+
function mapSqliteError(error) {
|
|
33
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
34
|
+
if (!code.startsWith("SQLITE_")) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
38
|
+
return new ConflictError("A record with these values already exists.");
|
|
39
|
+
}
|
|
40
|
+
if (code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
|
|
41
|
+
return new UnprocessableEntityError("Referenced record does not exist.");
|
|
42
|
+
}
|
|
43
|
+
if (code === "SQLITE_CONSTRAINT_NOTNULL") {
|
|
44
|
+
return new BadRequestError("Required field is missing.");
|
|
45
|
+
}
|
|
46
|
+
if (code.startsWith("SQLITE_CONSTRAINT")) {
|
|
47
|
+
return new BadRequestError("Value violates a database constraint.");
|
|
48
|
+
}
|
|
49
|
+
return new InternalServerError("Database operation failed.");
|
|
50
|
+
}
|
|
51
|
+
function mapMysqlError(error) {
|
|
52
|
+
const code = typeof error.code === "string" ? error.code : "";
|
|
53
|
+
if (!code.startsWith("ER_")) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const factory = typeof error.errno === "number" ? MYSQL_ERRNO_MESSAGES[error.errno] : undefined;
|
|
57
|
+
return factory ? factory() : new InternalServerError("Database operation failed.");
|
|
58
|
+
}
|
|
24
59
|
function mapDatabaseError(error) {
|
|
25
60
|
const httpError = toHttpError(error);
|
|
26
61
|
if (httpError) {
|
|
27
62
|
return httpError;
|
|
28
63
|
}
|
|
29
64
|
if (!isPostgresError(error)) {
|
|
30
|
-
|
|
31
|
-
|
|
65
|
+
return new InternalServerError;
|
|
66
|
+
}
|
|
67
|
+
const sqlite = mapSqliteError(error);
|
|
68
|
+
if (sqlite) {
|
|
69
|
+
return sqlite;
|
|
70
|
+
}
|
|
71
|
+
const mysql = mapMysqlError(error);
|
|
72
|
+
if (mysql) {
|
|
73
|
+
return mysql;
|
|
32
74
|
}
|
|
33
75
|
const sqlState = getPostgresSqlState(error);
|
|
34
76
|
switch (sqlState) {
|
|
@@ -49,10 +91,7 @@ function mapDatabaseError(error) {
|
|
|
49
91
|
constraint: error.constraint
|
|
50
92
|
});
|
|
51
93
|
default:
|
|
52
|
-
return new
|
|
53
|
-
code: error.code,
|
|
54
|
-
sqlState
|
|
55
|
-
});
|
|
94
|
+
return new InternalServerError("Database operation failed.");
|
|
56
95
|
}
|
|
57
96
|
}
|
|
58
97
|
async function withDatabaseErrorHandling(operation) {
|
|
@@ -23,13 +23,29 @@ function createMysqlConnectionFromPool(pool) {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
var MYSQL_SESSION_UTC = "SET time_zone = '+00:00'";
|
|
27
|
+
function pinSessionToUtc(connection) {
|
|
28
|
+
connection.query(MYSQL_SESSION_UTC, (error) => {
|
|
29
|
+
if (error) {
|
|
30
|
+
console.warn(`[mysql] Could not set the session time zone to UTC; DATETIME comparisons may drift: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function createMysqlPool(url) {
|
|
35
|
+
const pool = mysql.createPool({ uri: url, timezone: "Z" });
|
|
36
|
+
pool.on("connection", (connection) => {
|
|
37
|
+
pinSessionToUtc(connection);
|
|
38
|
+
});
|
|
39
|
+
return pool;
|
|
40
|
+
}
|
|
26
41
|
function createMysqlConnection(url) {
|
|
27
42
|
if (!url.trim()) {
|
|
28
43
|
throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
|
|
29
44
|
}
|
|
30
|
-
return createMysqlConnectionFromPool(
|
|
45
|
+
return createMysqlConnectionFromPool(createMysqlPool(url));
|
|
31
46
|
}
|
|
32
47
|
export {
|
|
33
48
|
createMysqlConnection,
|
|
34
|
-
createMysqlConnectionFromPool
|
|
49
|
+
createMysqlConnectionFromPool,
|
|
50
|
+
createMysqlPool
|
|
35
51
|
};
|
|
@@ -14,6 +14,11 @@ function createSqliteConnection(filename) {
|
|
|
14
14
|
}
|
|
15
15
|
const db = new Database(filename, { create: true });
|
|
16
16
|
db.exec("PRAGMA foreign_keys = ON");
|
|
17
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
18
|
+
if (filename !== ":memory:") {
|
|
19
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
20
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
21
|
+
}
|
|
17
22
|
return {
|
|
18
23
|
async unsafe(query, params = []) {
|
|
19
24
|
const statement = db.query(query);
|