@c9up/ream 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +6 -1
- package/dist/Application.js.map +1 -1
- package/dist/Provider.d.ts +20 -2
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +7 -3
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +3 -2
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/commands/MigrateBase.d.ts.map +1 -1
- package/dist/commands/MigrateBase.js +4 -1
- package/dist/commands/MigrateBase.js.map +1 -1
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +7 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/types.d.ts +14 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/console/types.js.map +1 -1
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +6 -1
- package/dist/container/Container.js.map +1 -1
- package/dist/container/types.d.ts +11 -1
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -1
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +4 -2
- package/dist/env/Env.js.map +1 -1
- package/dist/env/interpolate.d.ts.map +1 -1
- package/dist/env/interpolate.js +5 -3
- package/dist/env/interpolate.js.map +1 -1
- package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
- package/dist/errors/FuzzyMatcher.js +30 -13
- package/dist/errors/FuzzyMatcher.js.map +1 -1
- package/dist/errors/PipelineStageError.d.ts.map +1 -1
- package/dist/errors/PipelineStageError.js +4 -2
- package/dist/errors/PipelineStageError.js.map +1 -1
- package/dist/events/Emitter.d.ts +21 -4
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +6 -2
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/testing/FakeBus.d.ts +1 -1
- package/dist/events/testing/FakeBus.d.ts.map +1 -1
- package/dist/events/testing/FakeBus.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +2 -2
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +4 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/bytes.d.ts.map +1 -1
- package/dist/helpers/bytes.js +13 -2
- package/dist/helpers/bytes.js.map +1 -1
- package/dist/helpers/string.js +3 -3
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/Exception.d.ts +8 -4
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +14 -8
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +10 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/mime.d.ts.map +1 -1
- package/dist/http/mime.js +2 -1
- package/dist/http/mime.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/native/generated.d.ts +12 -8
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.js +6 -4
- package/dist/openapi/OpenApiGenerator.js.map +1 -1
- package/dist/router/Router.d.ts +10 -1
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +22 -10
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +16 -3
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +34 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Schedule.d.ts +6 -1
- package/dist/scheduler/Schedule.d.ts.map +1 -1
- package/dist/scheduler/Schedule.js +5 -0
- package/dist/scheduler/Schedule.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +12 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.js +3 -2
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +23 -3
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +10 -2
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +10 -11
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.js +6 -6
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +6 -2
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +4 -0
- package/dist/utils/Macroable.js.map +1 -1
- package/dist/utils/objectPath.d.ts.map +1 -1
- package/dist/utils/objectPath.js +14 -6
- package/dist/utils/objectPath.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +3 -3
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-napi-types.mjs +28 -12
- package/src/Application.ts +6 -1
- package/src/Provider.ts +21 -2
- package/src/bodyparser/MultipartFile.ts +7 -3
- package/src/bodyparser/parseSize.ts +3 -2
- package/src/commands/MigrateBase.ts +4 -1
- package/src/console/BaseCommand.ts +1 -1
- package/src/console/Kernel.ts +8 -0
- package/src/console/types.ts +15 -0
- package/src/container/Container.ts +15 -6
- package/src/container/types.ts +11 -1
- package/src/decorators/Service.ts +1 -1
- package/src/env/Env.ts +4 -2
- package/src/env/interpolate.ts +5 -3
- package/src/errors/FuzzyMatcher.ts +34 -13
- package/src/errors/PipelineStageError.ts +4 -2
- package/src/events/Emitter.ts +34 -8
- package/src/events/testing/FakeBus.ts +12 -5
- package/src/graphql/GraphQLEngine.ts +11 -7
- package/src/helpers/bytes.ts +13 -2
- package/src/helpers/string.ts +3 -3
- package/src/http/Exception.ts +14 -8
- package/src/http/Request.ts +8 -5
- package/src/http/mime.ts +2 -1
- package/src/index.ts +2 -2
- package/src/native/generated.ts +12 -8
- package/src/openapi/OpenApiGenerator.ts +6 -4
- package/src/router/Router.ts +31 -11
- package/src/rpc/RpcRouter.ts +38 -8
- package/src/scheduler/Schedule.ts +8 -1
- package/src/security/CookieSigner.ts +15 -5
- package/src/security/SignedUrl.ts +3 -2
- package/src/session/drivers/DatabaseDriver.ts +36 -8
- package/src/session/drivers/RedisDriver.ts +13 -5
- package/src/testing/ApiResponse.ts +9 -10
- package/src/testing/TestClient.ts +6 -6
- package/src/utils/Macroable.ts +6 -2
- package/src/utils/objectPath.ts +12 -6
|
@@ -170,9 +170,9 @@ export class OpenApiGenerator {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (route.validators.length > 0 && ['post', 'put', 'patch'].includes(method)) {
|
|
173
|
-
const validatorName = route.validators
|
|
174
|
-
const schema = this.#runeSchemas.get(validatorName)
|
|
175
|
-
if (schema) {
|
|
173
|
+
const [validatorName] = route.validators
|
|
174
|
+
const schema = validatorName === undefined ? undefined : this.#runeSchemas.get(validatorName)
|
|
175
|
+
if (schema && validatorName !== undefined) {
|
|
176
176
|
operation.requestBody = {
|
|
177
177
|
required: true,
|
|
178
178
|
content: { 'application/json': { schema: this.#runeToJsonSchema(schema) } },
|
|
@@ -197,7 +197,9 @@ export class OpenApiGenerator {
|
|
|
197
197
|
/** Extract parameter names from path. */
|
|
198
198
|
#extractPathParams(path: string): string[] {
|
|
199
199
|
const matches = path.matchAll(/:(\w+)\??/g)
|
|
200
|
-
|
|
200
|
+
// Group 1 is required by the pattern, so every match carries it; the
|
|
201
|
+
// filter is how that is stated instead of asserted.
|
|
202
|
+
return [...matches].map((m) => m[1]).filter((name) => name !== undefined)
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
/** Generate a summary from route metadata. */
|
package/src/router/Router.ts
CHANGED
|
@@ -25,7 +25,16 @@ export interface SignedUrlOptions {
|
|
|
25
25
|
|
|
26
26
|
// ─── Types ──────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* A route handler. Whatever it returns is awaited and dropped — the response
|
|
30
|
+
* is written through `ctx.response`.
|
|
31
|
+
*
|
|
32
|
+
* `unknown`, not `Promise<void> | void`: a bare `void` return type accepts a
|
|
33
|
+
* function returning anything, but a UNION containing it does not, so
|
|
34
|
+
* `router.get('/x', () => 'ok')` was a type error for returning a value the
|
|
35
|
+
* kernel already ignores.
|
|
36
|
+
*/
|
|
37
|
+
export type RouteHandlerFunction = (ctx: HttpContext) => unknown
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* Controller tuple: [ControllerClass, 'methodName'].
|
|
@@ -267,7 +276,13 @@ export class RouteBuilder extends Macroable {
|
|
|
267
276
|
|
|
268
277
|
/** The primary definition — accessors read from it (all verbs share pattern/name). */
|
|
269
278
|
#primary(): RouteDefinition {
|
|
270
|
-
|
|
279
|
+
const [first] = this.#routes
|
|
280
|
+
if (first === undefined) {
|
|
281
|
+
// A route is constructed with at least one verb, so this cannot happen —
|
|
282
|
+
// and this is where that is stated rather than asserted past.
|
|
283
|
+
throw new Error('Route has no definitions')
|
|
284
|
+
}
|
|
285
|
+
return first
|
|
271
286
|
}
|
|
272
287
|
|
|
273
288
|
/** Name this route (for URL generation and redirects). */
|
|
@@ -1557,9 +1572,13 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
|
|
|
1557
1572
|
const params: MatchedParams = {}
|
|
1558
1573
|
for (let i = 0; i < wildcardIdx; i++) {
|
|
1559
1574
|
const part = patternParts[i]
|
|
1575
|
+
const actual = actualParts[i]
|
|
1576
|
+
// Both are read out of arrays whose length was checked above; naming
|
|
1577
|
+
// them is what turns that check into something the compiler shares.
|
|
1578
|
+
if (part === undefined || actual === undefined) return null
|
|
1560
1579
|
if (part.startsWith(':')) {
|
|
1561
|
-
params[part.substring(1)] = safeDecodeURIComponent(
|
|
1562
|
-
} else if (part !==
|
|
1580
|
+
params[part.substring(1)] = safeDecodeURIComponent(actual)
|
|
1581
|
+
} else if (part !== actual) {
|
|
1563
1582
|
return null
|
|
1564
1583
|
}
|
|
1565
1584
|
}
|
|
@@ -1579,22 +1598,22 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
|
|
|
1579
1598
|
|
|
1580
1599
|
const params: MatchedParams = {}
|
|
1581
1600
|
|
|
1582
|
-
for (
|
|
1583
|
-
const part = patternParts[i]
|
|
1601
|
+
for (const [i, part] of patternParts.entries()) {
|
|
1584
1602
|
const isOptional = part.endsWith('?')
|
|
1603
|
+
const actual = actualParts[i]
|
|
1585
1604
|
|
|
1586
1605
|
if (part.startsWith(':')) {
|
|
1587
1606
|
const paramName = isOptional ? part.slice(1, -1) : part.substring(1)
|
|
1588
|
-
if (
|
|
1607
|
+
if (actual !== undefined) {
|
|
1589
1608
|
// Percent-decode the captured segment (query strings already are) so
|
|
1590
1609
|
// `/users/Jos%C3%A9` yields `José` and matchers test the real value.
|
|
1591
1610
|
// safeDecode falls back to the raw segment on malformed input.
|
|
1592
|
-
params[paramName] = safeDecodeURIComponent(
|
|
1611
|
+
params[paramName] = safeDecodeURIComponent(actual)
|
|
1593
1612
|
}
|
|
1594
1613
|
// Optional param with no actual part — skip (param not set)
|
|
1595
|
-
} else if (
|
|
1614
|
+
} else if (actual !== undefined && part !== actual) {
|
|
1596
1615
|
return null
|
|
1597
|
-
} else if (
|
|
1616
|
+
} else if (actual === undefined && !isOptional) {
|
|
1598
1617
|
return null
|
|
1599
1618
|
}
|
|
1600
1619
|
}
|
|
@@ -1612,7 +1631,8 @@ function matchPath(pattern: string, actual: string): MatchedParams | null {
|
|
|
1612
1631
|
* Comparing them verbatim sends that request to a fallback, or to a 404.
|
|
1613
1632
|
*/
|
|
1614
1633
|
function matchDomain(pattern: string, host: string): boolean {
|
|
1615
|
-
const
|
|
1634
|
+
const [hostname = ''] = host.split(':') // strip port
|
|
1635
|
+
const actualHost = hostname.toLowerCase()
|
|
1616
1636
|
const wanted = pattern.toLowerCase()
|
|
1617
1637
|
if (wanted === actualHost) return true
|
|
1618
1638
|
if (wanted.startsWith('*.')) {
|
package/src/rpc/RpcRouter.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type RpcHandler = (ctx: HttpContext, params: unknown) => Promise<unknown>
|
|
|
30
30
|
* the shared `'middleware'` registry, and `validator:<name>` schemas.
|
|
31
31
|
*/
|
|
32
32
|
interface RpcContainer {
|
|
33
|
-
make<T>(target: new (...args:
|
|
33
|
+
make<T>(target: abstract new (...args: never[]) => T): Promise<T>
|
|
34
34
|
resolve<T>(token: string): Promise<T>
|
|
35
35
|
has(token: string): boolean
|
|
36
36
|
}
|
|
@@ -151,11 +151,21 @@ export class RpcRouter {
|
|
|
151
151
|
return new RpcMethodBuilder(def)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Register methods from a controller — auto-registers all public methods.
|
|
156
|
+
*
|
|
157
|
+
* The parameter used to demand
|
|
158
|
+
* `new (...args: unknown[]) => Record<string, (...args: unknown[]) => unknown>`,
|
|
159
|
+
* which NO real controller satisfies: a class with any non-method property
|
|
160
|
+
* fails the `Record`, and a method declared `bump(): number` fails the
|
|
161
|
+
* `unknown[]` parameters, which are compared contravariantly. So the
|
|
162
|
+
* documented call — `rpc.namespace('task', TaskController)` — did not
|
|
163
|
+
* typecheck, and nothing noticed because the tests were outside `tsc`.
|
|
164
|
+
*
|
|
165
|
+
* Any class, then. The body already checks that what it found is callable
|
|
166
|
+
* and names the method when it is not.
|
|
167
|
+
*/
|
|
168
|
+
namespace(prefix: string, controller: abstract new (...args: never[]) => object): void {
|
|
159
169
|
const proto = controller.prototype
|
|
160
170
|
const methods = Object.getOwnPropertyNames(proto).filter(
|
|
161
171
|
(m) => m !== 'constructor' && typeof proto[m] === 'function',
|
|
@@ -164,8 +174,18 @@ export class RpcRouter {
|
|
|
164
174
|
this.method(`${prefix}.${methodName}`, async (ctx, params) => {
|
|
165
175
|
// Resolve through the container on every call (fresh DI per request,
|
|
166
176
|
// like GraphQLEngine), falling back to a bare `new` when unset.
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
// `Reflect.construct`, not `new controller()`: the token may be an
|
|
178
|
+
// abstract class, which the runtime builds happily and `new` refuses.
|
|
179
|
+
const instance: object = this.#container
|
|
180
|
+
? await this.#container.make(controller)
|
|
181
|
+
: Reflect.construct(controller, [])
|
|
182
|
+
const handler = Reflect.get(instance, methodName)
|
|
183
|
+
if (typeof handler !== 'function') {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`RPC method '${prefix}.${methodName}' is not a function on ${controller.name}`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
return handler.call(instance, ctx, params)
|
|
169
189
|
})
|
|
170
190
|
}
|
|
171
191
|
}
|
|
@@ -199,6 +219,16 @@ export class RpcRouter {
|
|
|
199
219
|
|
|
200
220
|
// Batch support (max 50 to prevent DoS)
|
|
201
221
|
if (Array.isArray(body)) {
|
|
222
|
+
// §6: "If the batch rpc call itself fails to be recognized as an valid
|
|
223
|
+
// JSON or as an Array with at least one value, the response from the
|
|
224
|
+
// Server MUST be a single Response object." An empty batch answered with
|
|
225
|
+
// 204 No Content, which is what this replies when every call in a batch
|
|
226
|
+
// was a notification — so a client that sent nothing was told its calls
|
|
227
|
+
// had run.
|
|
228
|
+
if (body.length === 0) {
|
|
229
|
+
ctx.response.status(200).json(rpcError(-32600, 'Invalid Request', null))
|
|
230
|
+
return
|
|
231
|
+
}
|
|
202
232
|
if (body.length > 50) {
|
|
203
233
|
ctx.response.status(400).json(rpcError(-32600, 'Batch too large (max 50)', null))
|
|
204
234
|
return
|
|
@@ -90,7 +90,14 @@ export function Schedule(cronExpr: string): MethodDecorator {
|
|
|
90
90
|
* none). Returns a shallow copy so callers cannot mutate the stored
|
|
91
91
|
* metadata by accident.
|
|
92
92
|
*/
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* `abstract new`, not `Constructor`: reading metadata off a class never
|
|
95
|
+
* constructs it, and the service registry holds abstract ones too since a
|
|
96
|
+
* contextual binding binds against an abstract base.
|
|
97
|
+
*/
|
|
98
|
+
export function getScheduleMetadata(
|
|
99
|
+
target: abstract new (...args: never[]) => unknown,
|
|
100
|
+
): ScheduleMetadata[] {
|
|
94
101
|
const stored: ScheduleMetadata[] = Reflect.getOwnMetadata(SCHEDULE_METADATA_KEY, target) ?? []
|
|
95
102
|
return [...stored]
|
|
96
103
|
}
|
|
@@ -225,12 +225,22 @@ export class CookieSigner {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
decrypt<T = string>(encrypted: string, purpose?: string): T | null {
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
// Destructured rather than indexed, and `extra` is what keeps the old
|
|
229
|
+
// length check honest: three names bind on a four-part string too, so the
|
|
230
|
+
// fourth has to be looked at to reject it.
|
|
231
|
+
const [ivPart, dataPart, tagPart, extra] = encrypted.split('.')
|
|
232
|
+
if (
|
|
233
|
+
ivPart === undefined ||
|
|
234
|
+
dataPart === undefined ||
|
|
235
|
+
tagPart === undefined ||
|
|
236
|
+
extra !== undefined
|
|
237
|
+
) {
|
|
238
|
+
return null
|
|
239
|
+
}
|
|
230
240
|
try {
|
|
231
|
-
const iv = Buffer.from(
|
|
232
|
-
const data = Buffer.from(
|
|
233
|
-
const tag = Buffer.from(
|
|
241
|
+
const iv = Buffer.from(ivPart, 'base64url')
|
|
242
|
+
const data = Buffer.from(dataPart, 'base64url')
|
|
243
|
+
const tag = Buffer.from(tagPart, 'base64url')
|
|
234
244
|
const decipher = createDecipheriv('aes-256-gcm', this.#keyBuffer, iv)
|
|
235
245
|
decipher.setAuthTag(tag)
|
|
236
246
|
const raw = Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
|
|
@@ -55,8 +55,9 @@ function parseExpiry(value: string | number): number {
|
|
|
55
55
|
if (typeof value === 'number') return value
|
|
56
56
|
const match = value.match(/^(\d+)(s|m|h|d)$/)
|
|
57
57
|
if (!match) return 3600
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
const [, digits, unit] = match
|
|
59
|
+
const num = parseInt(digits ?? '0', 10)
|
|
60
|
+
switch (unit) {
|
|
60
61
|
case 's':
|
|
61
62
|
return num
|
|
62
63
|
case 'm':
|
|
@@ -8,10 +8,28 @@ import type { SessionDriverWithTagging, TaggedSession } from '../Session.js'
|
|
|
8
8
|
* app's own wrapper.
|
|
9
9
|
*/
|
|
10
10
|
export interface SessionDbConnection {
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Rows, as rows: `Record<string, unknown>`, not a shape the caller names.
|
|
13
|
+
*
|
|
14
|
+
* `query<T>` let this driver ASSERT what a SELECT returned — a promise the
|
|
15
|
+
* database never made and the connection cannot keep. It also made the
|
|
16
|
+
* interface unimplementable by anything but a real driver: a generic method
|
|
17
|
+
* must work for every `T`, so no fake could satisfy it, and this file's tests
|
|
18
|
+
* sat outside the typecheck because of it.
|
|
19
|
+
*
|
|
20
|
+
* The columns are read through {@link textColumn} below instead, which is
|
|
21
|
+
* what a row from a database deserves.
|
|
22
|
+
*/
|
|
23
|
+
query(sql: string, params?: unknown[]): Promise<Array<Record<string, unknown>>>
|
|
12
24
|
execute(sql: string, params?: unknown[]): Promise<{ rowsAffected: number }>
|
|
13
25
|
}
|
|
14
26
|
|
|
27
|
+
/** A column that must be text to be usable, or `null`. */
|
|
28
|
+
function textColumn(row: Record<string, unknown>, name: string): string | null {
|
|
29
|
+
const value = row[name]
|
|
30
|
+
return typeof value === 'string' ? value : null
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export interface DatabaseDriverOptions {
|
|
16
34
|
connection: SessionDbConnection
|
|
17
35
|
/** Table holding the sessions. Default `sessions`. */
|
|
@@ -71,17 +89,20 @@ export class DatabaseDriver implements SessionDriverWithTagging {
|
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
74
|
-
const rows = await this.#db.query
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
92
|
+
const rows = await this.#db.query(`SELECT data, expires_at FROM ${this.#table} WHERE id = ?`, [
|
|
93
|
+
sessionId,
|
|
94
|
+
])
|
|
78
95
|
const row = rows[0]
|
|
79
96
|
if (!row) return null
|
|
80
97
|
if (Number(row.expires_at) < Date.now()) {
|
|
81
98
|
await this.destroy(sessionId)
|
|
82
99
|
return null
|
|
83
100
|
}
|
|
84
|
-
|
|
101
|
+
const data = textColumn(row, 'data')
|
|
102
|
+
// A row whose payload is not text is not a session — the same outcome as a
|
|
103
|
+
// row that will not parse.
|
|
104
|
+
if (data === null) return null
|
|
105
|
+
return parseData(data)
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
async write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void> {
|
|
@@ -157,11 +178,18 @@ export class DatabaseDriver implements SessionDriverWithTagging {
|
|
|
157
178
|
|
|
158
179
|
/** Every non-expired session this user holds — their active devices. */
|
|
159
180
|
async tagged(userId: string | number): Promise<TaggedSession[]> {
|
|
160
|
-
const rows = await this.#db.query
|
|
181
|
+
const rows = await this.#db.query(
|
|
161
182
|
`SELECT id, data FROM ${this.#table} WHERE user_id = ? AND expires_at >= ?`,
|
|
162
183
|
[String(userId), Date.now()],
|
|
163
184
|
)
|
|
164
|
-
|
|
185
|
+
const sessions: TaggedSession[] = []
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
const id = textColumn(row, 'id')
|
|
188
|
+
const data = textColumn(row, 'data')
|
|
189
|
+
if (id === null || data === null) continue
|
|
190
|
+
sessions.push({ id, data: parseData(data) })
|
|
191
|
+
}
|
|
192
|
+
return sessions
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
/** Delete every expired row — for a scheduled job, since nothing else does. */
|
|
@@ -67,11 +67,19 @@ export class RedisDriver implements SessionDriverWithTagging {
|
|
|
67
67
|
}
|
|
68
68
|
if (!this.#pending) {
|
|
69
69
|
const resolver = this.#source
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
// `finally`, not the success path: clearing it only on success left a
|
|
71
|
+
// REJECTED promise in the slot, and every later call returned that same
|
|
72
|
+
// rejection. One refused connection at boot — Redis still starting, a
|
|
73
|
+
// network blip — and the driver never tried again for the life of the
|
|
74
|
+
// process. Bay and echo already clear it this way.
|
|
75
|
+
this.#pending = Promise.resolve(resolver())
|
|
76
|
+
.then((client) => {
|
|
77
|
+
this.#resolved = client
|
|
78
|
+
return client
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
this.#pending = undefined
|
|
82
|
+
})
|
|
75
83
|
}
|
|
76
84
|
return this.#pending
|
|
77
85
|
}
|
|
@@ -149,15 +149,14 @@ function parseMultipartResponse(
|
|
|
149
149
|
// Trim the trailing CRLF before the next boundary.
|
|
150
150
|
let content = part.subarray(headerEnd + 4)
|
|
151
151
|
if (content.subarray(-2).toString('binary') === '\r\n') content = content.subarray(0, -2)
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
if (fileMatch) {
|
|
152
|
+
const [, name] = headerText.match(/name="([^"]*)"/i) ?? []
|
|
153
|
+
if (name === undefined) continue
|
|
154
|
+
const [, filename] = headerText.match(/filename="([^"]*)"/i) ?? []
|
|
155
|
+
if (filename !== undefined) {
|
|
157
156
|
const typeMatch = headerText.match(/content-type:\s*([^\r\n]+)/i)
|
|
158
157
|
files[name] = {
|
|
159
158
|
fieldName: name,
|
|
160
|
-
filename
|
|
159
|
+
filename,
|
|
161
160
|
type: typeMatch?.[1]?.trim() ?? 'application/octet-stream',
|
|
162
161
|
size: content.length,
|
|
163
162
|
content,
|
|
@@ -355,8 +354,8 @@ export class ApiResponse {
|
|
|
355
354
|
/** Charset from `Content-Type`, if any — helix `charset()`. */
|
|
356
355
|
charset(): string | undefined {
|
|
357
356
|
const ct = this.#raw.headers['content-type'] ?? ''
|
|
358
|
-
const
|
|
359
|
-
return
|
|
357
|
+
const [, charset] = ct.match(/charset=([^;]+)/i) ?? []
|
|
358
|
+
return charset?.trim()
|
|
360
359
|
}
|
|
361
360
|
|
|
362
361
|
/** All response cookies, parsed from `Set-Cookie` — helix `cookies()`. */
|
|
@@ -380,8 +379,8 @@ export class ApiResponse {
|
|
|
380
379
|
if (!link) return {}
|
|
381
380
|
const out: Record<string, string> = {}
|
|
382
381
|
for (const part of link.split(',')) {
|
|
383
|
-
const
|
|
384
|
-
if (
|
|
382
|
+
const [, url, rel] = part.match(/<([^>]+)>\s*;\s*rel="?([^";]+)"?/) ?? []
|
|
383
|
+
if (url !== undefined && rel !== undefined) out[rel] = url
|
|
385
384
|
}
|
|
386
385
|
return out
|
|
387
386
|
}
|
|
@@ -368,15 +368,15 @@ export function parseHttpResponse(raw: string | Buffer): TestResponse {
|
|
|
368
368
|
|
|
369
369
|
const lines = headerSection.split('\r\n')
|
|
370
370
|
const statusLine = lines[0] ?? ''
|
|
371
|
-
const
|
|
372
|
-
const status =
|
|
371
|
+
const [, statusDigits] = statusLine.match(/HTTP\/\d+\.\d+ (\d+)/) ?? []
|
|
372
|
+
const status = statusDigits === undefined ? 0 : Number.parseInt(statusDigits, 10)
|
|
373
373
|
|
|
374
374
|
const headers: Dict = {}
|
|
375
|
-
for (
|
|
376
|
-
const colonIdx =
|
|
375
|
+
for (const line of lines.slice(1)) {
|
|
376
|
+
const colonIdx = line.indexOf(':')
|
|
377
377
|
if (colonIdx === -1) continue
|
|
378
|
-
const key =
|
|
379
|
-
const value =
|
|
378
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase()
|
|
379
|
+
const value = line.slice(colonIdx + 1).trim()
|
|
380
380
|
headers[key] = value
|
|
381
381
|
}
|
|
382
382
|
|
package/src/utils/Macroable.ts
CHANGED
|
@@ -38,7 +38,11 @@ export class Macroable {
|
|
|
38
38
|
* `HttpContext`, and two subclasses declaring the same name clobbered each
|
|
39
39
|
* other silently.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
// `{ prototype: object }`, not `typeof Macroable`: a subclass whose
|
|
42
|
+
// constructor takes arguments — `Request`, say — has a different static side,
|
|
43
|
+
// so `Request.macro(...)` was "the 'this' context of type 'typeof Request' is
|
|
44
|
+
// not assignable". What this needs from `this` is the prototype it defines on.
|
|
45
|
+
static macro(this: { prototype: object }, name: string, fn: MacroFn): void {
|
|
42
46
|
Object.defineProperty(this.prototype, name, {
|
|
43
47
|
value: fn,
|
|
44
48
|
writable: true,
|
|
@@ -51,7 +55,7 @@ export class Macroable {
|
|
|
51
55
|
* `getter`). With `singleton`, the value is computed once per instance and
|
|
52
56
|
* cached.
|
|
53
57
|
*/
|
|
54
|
-
static getter(this:
|
|
58
|
+
static getter(this: { prototype: object }, name: string, fn: GetterFn, singleton = false): void {
|
|
55
59
|
if (!singleton) {
|
|
56
60
|
Object.defineProperty(this.prototype, name, { get: fn, configurable: true })
|
|
57
61
|
return
|
package/src/utils/objectPath.ts
CHANGED
|
@@ -77,9 +77,13 @@ export function hasPath(source: unknown, path: string): boolean {
|
|
|
77
77
|
*/
|
|
78
78
|
export function setPath(target: UnknownRecord, path: string, value: unknown): UnknownRecord {
|
|
79
79
|
const segments = toSegments(path)
|
|
80
|
+
// The last segment is the one written; everything before it is walked. Split
|
|
81
|
+
// off by name rather than by index, so neither has to be read back out of
|
|
82
|
+
// the array afterwards and re-checked for existence.
|
|
83
|
+
const last = segments.pop()
|
|
84
|
+
if (last === undefined) return target
|
|
80
85
|
let current: UnknownRecord = target
|
|
81
|
-
for (
|
|
82
|
-
const key = segments[i]
|
|
86
|
+
for (const key of segments) {
|
|
83
87
|
const next = current[key]
|
|
84
88
|
if (!isIndexable(next)) {
|
|
85
89
|
const created: UnknownRecord = {}
|
|
@@ -89,7 +93,7 @@ export function setPath(target: UnknownRecord, path: string, value: unknown): Un
|
|
|
89
93
|
current = next
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
|
-
current[
|
|
96
|
+
current[last] = value
|
|
93
97
|
return target
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -115,12 +119,14 @@ export function omitPaths(source: UnknownRecord, paths: string[]): UnknownRecord
|
|
|
115
119
|
const result = structuredClone(source)
|
|
116
120
|
for (const path of paths) {
|
|
117
121
|
const segments = toSegments(path)
|
|
122
|
+
const last = segments.pop()
|
|
123
|
+
if (last === undefined) continue
|
|
118
124
|
let current: unknown = result
|
|
119
|
-
for (
|
|
125
|
+
for (const key of segments) {
|
|
120
126
|
if (!isIndexable(current)) break
|
|
121
|
-
current = current[
|
|
127
|
+
current = current[key]
|
|
122
128
|
}
|
|
123
|
-
if (isIndexable(current)) delete current[
|
|
129
|
+
if (isIndexable(current)) delete current[last]
|
|
124
130
|
}
|
|
125
131
|
return result
|
|
126
132
|
}
|