Skip to content

Conversation

@SaltyAom
Copy link
Member

@SaltyAom SaltyAom commented Sep 14, 2025

Bug fix:

  • standard schema incorrectly validate cookie and coercion

Summary by CodeRabbit

  • Breaking Changes

    • Model coercion for referenced models changed (adjust affected models).
    • Public type signatures expanded/adjusted; update dependent TypeScript typings.
  • Bug Fixes

    • Fixed cookie extraction, validation/coercion, and propagation.
    • Corrected merging behavior for nested guard/group schemas and intersections.
  • Documentation

    • Added CHANGELOG entry for v1.4.5 summarizing improvements and breaking change.
  • Tests

    • Added/updated tests for nested guards/groups, cookie handling, and numeric-type behavior.
  • Chores

    • package.json version bumped to 1.4.5.

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Updated docs and bumped version to 1.4.5. Core changes: cookie validation now mutates and propagates decoded cookie values; schema import/ref resolution refined and JSON-string defaults removed; deepClone util added; new IntersectIfObjectSchema types and expanded route/group typings; multiple tests and examples updated to model-driven t.* types.

Changes

Cohort / File(s) Summary of Changes
Changelog & Version
CHANGELOG.md, package.json
Added v1.4.5 changelog entry and bumped package version to 1.4.5.
Cookie processing
src/compose.ts, test/standard-schema/validate.test.ts
Make cookieValue mutable, validate into it, propagate values into c.cookie[*].value, apply optional validator.cookie.Decode, and add a test for cookie coercion.
Schema resolution & defaults
src/schema.ts
Inline non-ref imported defs when possible; wrap in a module only when necessary; remove automatic JSON-stringified defaults for ObjectString/ArrayString.
Utilities
src/utils.ts
Harden mergeHook guards/merges (avoid optional chaining), adjust standaloneSchema merging, and add exported deepClone<T>(...) with cycle protection.
Types & public API typings
src/types.ts, src/index.ts
Add IntersectIfObject / IntersectIfObjectSchema, make UnwrapRoute Path in/out, broaden GuardLocalHook generics/defaults, export new types, and update many overloads to use intersecting schema types and MacroContext generics.
Routing / guard/group merging & tests
src/index.ts, test/path/group.test.ts, test/path/guard.test.ts, test/types/lifecycle/soundness.ts
Detect/propagate standaloneSchema in merging; add tests verifying nested guard/group query schema composition and macro-context soundness.
Examples & tests: model-driven changes
example/a.ts, test/extends/models.test.ts, test/validator/query.test.ts, test/types/index.ts
Replace zod/valibot usage in example with t.* model types; update tests to use t.Numeric() where applicable, add/adjust tests, and comment out one coercion test.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Server as Elysia
  participant Composer as src/compose.ts
  participant Validator as StandardValidator
  participant CookieBag as c.cookie

  Client->>Server: HTTP request (Cookie header)
  Server->>Composer: extract cookieValue
  Composer->>Validator: validate(cookieValue)
  alt validation succeeds
    Validator-->>Composer: vac.value
    Composer->>Composer: cookieValue = vac.value
    Composer->>CookieBag: for each key set c.cookie[key].value = cookieValue[key]
    alt validator.cookie.hasTransform
      Composer->>Validator: Decode(cookieValue)
      Validator-->>Composer: decoded values
      Composer->>CookieBag: assign decoded c.cookie[*].value
    end
    Composer-->>Server: proceed with updated cookie values
    Server-->>Client: handler response
  else validation fails
    Validator-->>Server: error (422)
    Server-->>Client: error response
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

Possibly related PRs

  • 1.4.5 patch #1403 — Nearly identical patch set touching types, utils, schema, compose, tests, and CHANGELOG (same 1.4.5 update).
  • Elysia 1.4: Weirs #1398 — Overlapping changes around cookie/compose flow, schema import/ref resolution, deepClone/merge behavior, and related tests.

Poem

I nibble notes on changelog lines,
Cookies parsed and values shine,
Types entwine where macros play,
Deep clones guard each looping way,
A happy rabbit hops—release day! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "1.4.5 patch" denotes a release/version bump and matches the package.json and CHANGELOG.md updates in this PR, so it is related to the changeset; however it is minimal and does not describe the main bug fixes or type-level API changes included.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6f84eb and a964826.

📒 Files selected for processing (3)
  • src/index.ts (22 hunks)
  • src/types.ts (4 hunks)
  • test/types/lifecycle/soundness.ts (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 14, 2025

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1403

commit: a964826

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
CHANGELOG.md (1)

1-4: Polish the 1.4.5 entry (date + wording).

To match prior entries and improve clarity:

-# 1.4.5
-Bug fix:
-- standard schema incorrectly validate cookie and coercion
+# 1.4.5 - 14 Sep 2025
+Fix:
+- standard schema: incorrect cookie validation/coercion
package.json (1)

4-4: Version/changelog mismatch (beta vs stable).

CHANGELOG says 1.4.5 (stable) but package.json is 1.4.5-beta.0. Confirm intent and align both artifacts.

Would you like me to update CHANGELOG to “1.4.5-beta.0 - 14 Sep 2025”, or bump package.json to 1.4.5?

src/compose.ts (1)

1499-1505: Likely merge target mistake when refreshing validator cookie config.

This merges the config with itself; intention appears to be merging in app.config.cookie.

-			validator.cookie.config = mergeCookie(
-				validator.cookie.config,
-				validator.cookie?.config ?? {}
-			)
+			validator.cookie.config = mergeCookie(
+				validator.cookie.config,
+				app.config.cookie as any
+			)
example/a.ts (1)

4-14: Tidy example: correct typeof, drop stray log, and use absolute route.

-const app = new Elysia().get(
-	'test',
-	({ cookie: { test } }) => {
-		console.log(test.value)
-
-		return typeof test
-	},
+const app = new Elysia().get(
+	'/test',
+	({ cookie: { test } }) => typeof test.value,
 	{
 		cookie: z.object({ test: z.coerce.number() })
 	}
 )
src/cookies.ts (1)

263-265: Expose a stable public API — don't write to a private setter from generated code

compose.ts emits c.cookie[k].innerValue=... (src/compose.ts:1524) while src/cookies.ts defines a private setter at lines ~263–265; expose a public helper and update the generator to call it.

-	private set innerValue(value: T) {
-		this.value = value
-	}
+	/** Assign value post-validation without mutating other options */
+	public assignValidated(value: T) {
+		this.value = value
+	}

Replace generator/compose usages of .innerValue with .assignValidated(...).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bd3d7e and dee78dc.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • example/a.ts (1 hunks)
  • package.json (1 hunks)
  • src/compose.ts (2 hunks)
  • src/cookies.ts (1 hunks)
  • test/standard-schema/validate.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/standard-schema/validate.test.ts (1)
src/cookies.ts (2)
  • value (154-156)
  • value (158-160)
example/a.ts (1)
src/cookies.ts (2)
  • value (154-156)
  • value (158-160)
🔇 Additional comments (4)
test/standard-schema/validate.test.ts (1)

303-323: Good addition: exercises cookie coercion path.

Covers zod z.coerce.number() from Cookie header to typed value. Looks solid.

src/compose.ts (3)

1507-1510: Collecting cookieValue is correct.

Building a plain object snapshot from c.cookie before validation is the right move.


1515-1521: Standard-provider validation flow LGTM.

Validates with standard schema and uses the validated value.


1526-1530: Bug: cookie branch checks body schema flag.

This should reference validator.cookie, not validator.body.

-} else if (validator.body?.schema?.noValidate !== true) {
+} else if (validator.cookie?.schema?.noValidate !== true) {

Likely an incorrect or invalid review comment.

Comment on lines +1531 to +1538
if (validator.cookie.hasTransform)
fnLiteral += coerceTransformDecodeError(
`for(const [key,value] of Object.entries(validator.cookie.Decode(cookieValue))){` +
`c.cookie[key].value=value` +
`}`,
'cookie'
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure propagation for non-standard providers without transform; unify assignment.

After a successful non-standard Check(cookieValue), propagate values into c.cookie even when no transform is present. Also prefer the public setter:

- if (validator.cookie.hasTransform)
+ // Always reflect validated values
+ for (const k of Object.keys(cookieValue))
+   c.cookie[k].value = cookieValue[k]
+
+ if (validator.cookie.hasTransform)
   ${coerceTransformDecodeError(
     `for(const [key,value] of Object.entries(validator.cookie.Decode(cookieValue))){` +
-      `c.cookie[key].value=value` +
+      `c.cookie[key].value=value` +
       `}`,
     'cookie'
   )}

If you prefer minimal churn, I can submit a follow-up PR with tests covering the non-standard provider path.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Search for occurrences of 'validator.cookie' and 'cookieValue'..."
rg -n --hidden "validator\.cookie" -S || true
rg -n --hidden "\bcookieValue\b" -S || true

echo
echo "Show src/compose.ts around the reported lines (1400-1560):"
if [ -f src/compose.ts ]; then
  sed -n '1400,1560p' src/compose.ts || true
else
  echo "src/compose.ts not found"
fi

Length of output: 6862


Propagate validated cookie values for non-standard providers and unify assignment

When validator.cookie.Check succeeds for non-standard providers we must reflect the validated cookieValue into c.cookie for all keys (use the public .value setter), then let Decode override it when hasTransform is true — current code only updates on hasTransform and uses inconsistent properties.

- if (validator.cookie.hasTransform)
+ // Always reflect validated values
+ for (const k of Object.keys(cookieValue))
+   c.cookie[k].value = cookieValue[k]
+
+ if (validator.cookie.hasTransform)
   ${coerceTransformDecodeError(
     `for(const [key,value] of Object.entries(validator.cookie.Decode(cookieValue))){` +
-      `c.cookie[key].value=value` +
+      `c.cookie[key].value=value` +
       `}`,
     'cookie'
   )}

src/compose.ts (around lines ~1523-1536).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/compose.ts around lines 1531 to 1538, when validator.cookie.Check
succeeds for non-standard providers we must first propagate the validated
cookieValue into c.cookie for every key using the public .value setter (e.g.,
for(const [k,v] of Object.entries(cookieValue)){ c.cookie[k].value = v }), then
if validator.cookie.hasTransform is true call Decode and let its results
override those same keys (again assigning via the .value setter); replace the
current conditional that only updates on hasTransform and ensure all assignments
use the unified .value property and the coerceTransformDecodeError-wrapped
Decode invocation so transforms still run and override validated values.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/schema.ts (1)

1108-1115: Bug: hasRef getter calls hasTransform.

get hasRef() returns hasTransform(schema) instead of hasRef(schema). This misreports metadata and can mislead downstream logic.

-          return (this['~hasRef'] = hasTransform(schema))
+          return (this['~hasRef'] = hasRef(schema))
🧹 Nitpick comments (7)
src/utils.ts (1)

1166-1171: Redundant typeof check.

typeof source === 'function' is implied by typeof source !== 'object'; you can drop it for clarity.

example/a.ts (2)

5-11: Example won’t coerce string numerics with t.Number(); use t.Numeric() (or send numbers).

As written, sending strings ('1', '2') will fail validation. Either switch to t.Numeric() or send numeric JSON.

 const app = new Elysia()
   .model({
-    q: t.Number(),
+    q: t.Numeric(),
     number: t.Object({
-      value: t.Number(),
+      value: t.Numeric(),
       number: t.Ref('q')
     })
   })

14-20: Optional: assert output instead of console.log for a runnable sample.

Consider converting this to an assertion or comment expected output to avoid noisy logs in examples.

test/validator/query.test.ts (1)

1098-1099: Use the correct query key to keep the test meaningful.

The schema expects party, but the invalid request uses filter, which weakens the test’s intent. Use ?party=[] to assert failed population for the targeted field.

-    const invalid2 = await app.handle(
-      new Request(`http://localhost:3000?filter=[]`)
-    )
+    const invalid2 = await app.handle(
+      new Request(`http://localhost:3000?party=[]`)
+    )
test/extends/models.test.ts (1)

228-251: Don’t silently disable the “coerce with reference model” test.

If behavior changed, replace with an updated assertion or add a TODO explaining why it’s skipped. Silent commenting hides regressions.

src/schema.ts (2)

535-555: ObjectString/ArrayString default injection removed: verify transform still applied.

Transform is now attached only when v.default equals {} or []. Ensure t.ObjectString/t.ArrayString provide those defaults by design; otherwise, transforms may be skipped. Consider an explicit flag instead of default-string checks.


822-844: Potential gap: coercion skipped for Import with nested refs.

In the Import branch, if the referenced def still has refs, coercion isn’t applied. This appears intentional (tests now use t.Numeric()), but it’s a behavior change. Please confirm and document: “coercion is not applied inside referenced models; use t.Numeric()/t.BooleanString in models.”

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dee78dc and f620038.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • example/a.ts (1 hunks)
  • src/schema.ts (3 hunks)
  • src/utils.ts (1 hunks)
  • test/extends/models.test.ts (4 hunks)
  • test/validator/query.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 Additional context used
🧬 Code graph analysis (3)
src/schema.ts (2)
src/index.ts (4)
  • TSchema (8201-8201)
  • model (7552-7637)
  • t (8085-8085)
  • modules (8078-8080)
src/utils.ts (1)
  • randomId (927-930)
test/extends/models.test.ts (1)
src/index.ts (3)
  • Elysia (184-8081)
  • Elysia (8083-8083)
  • t (8085-8085)
example/a.ts (3)
src/index.ts (4)
  • Elysia (184-8081)
  • Elysia (8083-8083)
  • t (8085-8085)
  • post (5733-5818)
src/type-system/index.ts (1)
  • t (657-657)
test/utils.ts (1)
  • post (46-67)
🔇 Additional comments (3)
test/validator/query.test.ts (1)

838-840: Good: reference model now uses Numeric.

Switching to t.Numeric() for both the model and the ref removes reliance on implicit coercion.

test/extends/models.test.ts (2)

256-256: Good: defaulted numeric reference uses t.Numeric().

This aligns defaults with new coercion semantics.


284-286: Good: cookie model token switched to Numeric.

Prevents relying on implicit numeric coercion in cookies.

Comment on lines 1164 to 1200

export function deepClone<T>(source: T, weak = new WeakMap<object, any>()): T {
if (
source === null ||
typeof source !== 'object' ||
typeof source === 'function'
)
return source

// Circular‑reference guard
if (weak.has(source as object)) return weak.get(source as object)

if (Array.isArray(source)) {
const copy: any[] = new Array(source.length)
weak.set(source, copy)

for (let i = 0; i < source.length; i++)
copy[i] = deepClone(source[i], weak)

return copy as any
}

if (typeof source === 'object') {
const keys = Object.keys(source).concat(
Object.getOwnPropertySymbols(source) as any[]
)

const cloned: Partial<T> = {}

for (const key of keys)
cloned[key as keyof T] = deepClone((source as any)[key], weak)

return cloned as T
}

return source
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix circular reference for plain objects (WeakMap set happens too late).

Objects aren’t added to the WeakMap before descending, so self/recursive structures will loop indefinitely. Set the placeholder before recursing (arrays already do this).

 export function deepClone<T>(source: T, weak = new WeakMap<object, any>()): T {
@@
   if (Array.isArray(source)) {
     const copy: any[] = new Array(source.length)
     weak.set(source, copy)
@@
     return copy as any
   }
 
   if (typeof source === 'object') {
-    const keys = Object.keys(source).concat(
-      Object.getOwnPropertySymbols(source) as any[]
-    )
-
-    const cloned: Partial<T> = {}
-
-    for (const key of keys)
-      cloned[key as keyof T] = deepClone((source as any)[key], weak)
-
-    return cloned as T
+    const keys = Object.keys(source).concat(
+      Object.getOwnPropertySymbols(source) as any[]
+    )
+    const cloned: Partial<T> = {}
+    weak.set(source as object, cloned)
+    for (const key of keys) {
+      (cloned as any)[key] = deepClone((source as any)[key as any], weak)
+    }
+    return cloned as T
   }
 
   return source
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function deepClone<T>(source: T, weak = new WeakMap<object, any>()): T {
if (
source === null ||
typeof source !== 'object' ||
typeof source === 'function'
)
return source
// Circular‑reference guard
if (weak.has(source as object)) return weak.get(source as object)
if (Array.isArray(source)) {
const copy: any[] = new Array(source.length)
weak.set(source, copy)
for (let i = 0; i < source.length; i++)
copy[i] = deepClone(source[i], weak)
return copy as any
}
if (typeof source === 'object') {
const keys = Object.keys(source).concat(
Object.getOwnPropertySymbols(source) as any[]
)
const cloned: Partial<T> = {}
for (const key of keys)
cloned[key as keyof T] = deepClone((source as any)[key], weak)
return cloned as T
}
return source
}
export function deepClone<T>(source: T, weak = new WeakMap<object, any>()): T {
if (
source === null ||
typeof source !== 'object' ||
typeof source === 'function'
)
return source
// Circular‑reference guard
if (weak.has(source as object)) return weak.get(source as object)
if (Array.isArray(source)) {
const copy: any[] = new Array(source.length)
weak.set(source, copy)
for (let i = 0; i < source.length; i++)
copy[i] = deepClone(source[i], weak)
return copy as any
}
if (typeof source === 'object') {
const keys = Object.keys(source).concat(
Object.getOwnPropertySymbols(source) as any[]
)
const cloned: Partial<T> = {}
weak.set(source as object, cloned)
for (const key of keys) {
(cloned as any)[key] = deepClone((source as any)[key as any], weak)
}
return cloned as T
}
return source
}
🤖 Prompt for AI Agents
In src/utils.ts around lines 1164 to 1200, the deepClone implementation adds
plain objects to the WeakMap only after recursing, causing infinite loops on
self-referential structures; fix it by creating the cloned placeholder object
(e.g., const cloned: Partial<T> = {}) and immediately calling weak.set(source as
object, cloned) before iterating over keys and calling deepClone for each
property, mirroring the array branch’s behavior so circular references are
handled correctly.

Comment on lines +359 to +398
it("coerce model when there's no reference", async () => {
const app = new Elysia()
.model({
idParam: t.Object({
id: t.Union([
t.String({ format: 'uuid' }),
t.Number({
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER
})
])
}),
response200: t.Object({
message: t.String(),
content: t.Array(
t.Object({
id: t.Union([t.String(), t.Number()])
})
)
})
})
.get(
'/:id',
({ params: { id } }) => ({
message: 'ok',
content: [{ id }]
}),
{
params: 'idParam',
response: 'response200'
}
)
.listen(3000)

const value = await app
.handle(new Request('http://localhost/3'))
.then((x) => x.json())

expect(value).toEqual({ message: 'ok', content: [{ id: 3 }] })
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid binding a real port in tests (.listen(3000)).

Use app.handle(...) without .listen() to keep tests hermetic and prevent flaky port conflicts.

       )
-      .listen(3000)
 
   const value = await app
     .handle(new Request('http://localhost/3'))
     .then((x) => x.json())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("coerce model when there's no reference", async () => {
const app = new Elysia()
.model({
idParam: t.Object({
id: t.Union([
t.String({ format: 'uuid' }),
t.Number({
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER
})
])
}),
response200: t.Object({
message: t.String(),
content: t.Array(
t.Object({
id: t.Union([t.String(), t.Number()])
})
)
})
})
.get(
'/:id',
({ params: { id } }) => ({
message: 'ok',
content: [{ id }]
}),
{
params: 'idParam',
response: 'response200'
}
)
.listen(3000)
const value = await app
.handle(new Request('http://localhost/3'))
.then((x) => x.json())
expect(value).toEqual({ message: 'ok', content: [{ id: 3 }] })
})
it("coerce model when there's no reference", async () => {
const app = new Elysia()
.model({
idParam: t.Object({
id: t.Union([
t.String({ format: 'uuid' }),
t.Number({
minimum: 1,
maximum: Number.MAX_SAFE_INTEGER
})
])
}),
response200: t.Object({
message: t.String(),
content: t.Array(
t.Object({
id: t.Union([t.String(), t.Number()])
})
)
})
})
.get(
'/:id',
({ params: { id } }) => ({
message: 'ok',
content: [{ id }]
}),
{
params: 'idParam',
response: 'response200'
}
)
const value = await app
.handle(new Request('http://localhost/3'))
.then((x) => x.json())
expect(value).toEqual({ message: 'ok', content: [{ id: 3 }] })
})
🤖 Prompt for AI Agents
In test/extends/models.test.ts around lines 359 to 398, the test binds a real
port by calling .listen(3000) which can cause flaky tests and port conflicts;
remove the .listen(3000) call and use the Elysia app instance directly with
app.handle(new Request(...)) (or await app.ready() if required by framework) so
the test remains hermetic and does not open a network port.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/utils.ts (1)

1188-1223: Critical: deepClone loops on circular plain objects; set WeakMap entry before descending.

Arrays are guarded, but plain objects aren’t added to weak until after recursion, so self-references recurse forever. Register the placeholder first.

Apply:

  if (typeof source === 'object') {
     const keys = Object.keys(source).concat(
       Object.getOwnPropertySymbols(source) as any[]
     )
 
-    const cloned: Partial<T> = {}
-
-    for (const key of keys)
-      cloned[key as keyof T] = deepClone((source as any)[key], weak)
+    const cloned: Partial<T> = {}
+    weak.set(source as object, cloned)
+    for (const key of keys) {
+      (cloned as any)[key] = deepClone((source as any)[key as any], weak)
+    }
 
     return cloned as T
   }

Minor nit: the guard redundantly checks typeof source === 'function' which is already covered by typeof source !== 'object'.

-	if (
-		source === null ||
-		typeof source !== 'object' ||
-		typeof source === 'function'
-	)
+	if (source === null || typeof source !== 'object')
 		return source
🧹 Nitpick comments (4)
src/utils.ts (1)

310-329: Simplify standaloneSchema merge; current nested ternary is hard to read and error-prone.

Equivalent, clearer logic:

-		// @ts-ignore
-		standaloneSchema:
-			// @ts-ignore
-			a.standaloneSchema || b.standaloneSchema
-				? // @ts-ignore
-					a.standaloneSchema && !b.standaloneSchema
-					? // @ts-ignore
-						a.standaloneSchema
-					: // @ts-ignore
-						b.standaloneSchema && !a.standaloneSchema
-						? b.standaloneSchema
-						: [
-								// @ts-ignore
-								...(a.standaloneSchema ?? []),
-								...(b.standaloneSchema ?? [])
-							]
-				: undefined
+		standaloneSchema:
+			(a.standaloneSchema?.length || b.standaloneSchema?.length)
+				? ([...(a.standaloneSchema ?? []), ...(b.standaloneSchema ?? [])] as any)
+				: undefined
src/index.ts (1)

4642-4644: Duplicate standalone schema detection logic.

The hasStandaloneSchema check is duplicated from the group method. Consider extracting this into a helper function to maintain DRY principles.

Consider extracting the schema detection logic:

private hasStandaloneSchema(schema: AnyLocalHook): boolean {
    const { body, headers, query, params, cookie, response } = schema;
    return !!(body || headers || query || params || cookie || response);
}

Then use it in both methods:

- const hasStandaloneSchema =
-     body || headers || query || params || cookie || response
+ const hasStandaloneSchema = this.hasStandaloneSchema(hook)
example/a.ts (2)

1-3: Clean up unused import.

The zod import on line 2 is no longer used after migrating to the t type system.

import { Elysia, t } from '../src'
-import z from 'zod'
import { req } from '../test/utils'

40-44: Consider uncommenting the assertion for clarity.

The commented assertion would make the expected behavior clearer for documentation purposes.

console.log(value)
-// expect(value).toEqual({
-// 	name: 'lilith',
-// 	playing: true,
-// 	limit: 10
-// })
+// Expected output:
+// {
+//   name: 'lilith',
+//   playing: true,
+//   limit: 10
+// }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55ce809 and 631d292.

📒 Files selected for processing (5)
  • example/a.ts (1 hunks)
  • src/index.ts (4 hunks)
  • src/utils.ts (2 hunks)
  • test/path/group.test.ts (1 hunks)
  • test/path/guard.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
test/path/group.test.ts (2)
src/index.ts (4)
  • Elysia (184-8098)
  • Elysia (8100-8100)
  • t (8102-8102)
  • error (3087-3127)
test/utils.ts (1)
  • req (1-2)
src/index.ts (2)
src/utils.ts (1)
  • mergeHook (220-334)
src/types.ts (1)
  • AnyLocalHook (1571-1571)
test/path/guard.test.ts (2)
src/index.ts (4)
  • Elysia (184-8098)
  • Elysia (8100-8100)
  • t (8102-8102)
  • error (3087-3127)
test/utils.ts (1)
  • req (1-2)
example/a.ts (3)
src/index.ts (3)
  • Elysia (184-8098)
  • Elysia (8100-8100)
  • t (8102-8102)
src/type-system/index.ts (1)
  • t (657-657)
test/utils.ts (1)
  • req (1-2)
🔇 Additional comments (10)
src/utils.ts (2)

265-274: Confirm precedence changes for local validators and detail.

  • Local validators now prefer b.* ?? a.* (body/headers/params/query/cookie) while type prefers a.type || b.type. Please confirm this asymmetry is intentional.
  • detail is merged as mergeDeep(b.detail ?? {}, a.detail ?? {}), meaning a.detail overrides b.detail. Verify this matches your merge semantics.

Also applies to: 281-287


289-309: LGTM: Hook arrays merged without side effects and deduped via checksum seed.

Using mergeObjectArray for parse/transform/afterHandle/mapResponse/afterResponse/trace/error looks correct and avoids in-place mutation.

test/path/guard.test.ts (1)

442-479: Test demonstrates correct nested guard schema merging behavior.

The test validates that:

  1. Multiple nested guards properly compose their query schemas (name, limit, playing)
  2. All schema constraints are enforced (literal 'lilith', numeric limit, boolean playing)
  3. Missing required fields correctly return 422 status

This aligns with the framework's guard composition semantics.

test/path/group.test.ts (2)

270-290: LGTM! Test validates basic nested group functionality.

The test confirms that nested groups without schemas properly pass through query parameters to the handler.


292-332: Comprehensive test coverage for nested guard groups with schemas.

The test properly validates:

  1. Schema inheritance across nested groups (outer → inner → route-level)
  2. Correct merging of query validators at each level
  3. Appropriate error handling when required fields are missing

The test structure mirrors the guard test, ensuring consistent behavior across both APIs.

src/index.ts (4)

4040-4042: LGTM! Proper conditional logic for standalone schema detection.

The hasStandaloneSchema flag correctly identifies when any schema component is present, which determines whether to append a new standalone validator entry.


4631-4640: Consistent variable naming improves code clarity.

The renaming from hooks to localHook and extraction of guardHook makes the code more readable and clearly distinguishes between the route's local hooks and the guard-level hooks being applied.


4662-4674: Consistent standalone validator handling across guard and group.

The implementation correctly mirrors the group method's approach, ensuring consistent behavior when composing standalone validators in nested guard contexts.


4060-4073: Approve: standalone validator array construction confirmed. Found insertStandaloneValidator (src/utils.ts) and matching merges/pushes in src/index.ts and adapter/bun/compose.ts; the conditional preserves existing validators when no schema and appends the new validator object when schema components exist.

example/a.ts (1)

5-33: Example effectively demonstrates nested guard schema composition.

The code shows how multiple guard levels can progressively add schema requirements:

  1. Outer guard requires name: 'lilith'
  2. Middle guard adds limit: Number
  3. Route-level schema adds playing: Boolean

This mirrors the test cases and provides a clear usage example.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/index.ts (1)

4650-4694: Guard merge should also retain guardHook.standaloneValidator.

As with group(), guard merge overwrites any guard-specified standaloneValidator. Merge both sources first.

Apply this diff:

-                const hasStandaloneSchema =
+                const hasStandaloneSchema =
                     body || headers || query || params || cookie || response
+                const existingStandalone = [
+                    ...(guardHook.standaloneValidator ?? []),
+                    ...(localHook.standaloneValidator ?? [])
+                ]

...
-                    mergeHook(guardHook as AnyLocalHook, {
+                    mergeHook(guardHook as AnyLocalHook, {
                         ...((localHook || {}) as AnyLocalHook),
...
-                        standaloneValidator: !hasStandaloneSchema
-                            ? localHook.standaloneValidator
-                            : [
-                                    ...(localHook.standaloneValidator ?? []),
-                                    {
-                                        body,
-                                        headers,
-                                        query,
-                                        params,
-                                        cookie,
-                                        response
-                                    }
-                                ]
+                        standaloneValidator: hasStandaloneSchema
+                            ? [
+                                  ...existingStandalone,
+                                  { body, headers, query, params, cookie, response }
+                              ]
+                            : existingStandalone
♻️ Duplicate comments (1)
src/utils.ts (1)

254-256: Good fix: avoid calling Object.values on possibly undefined.

Swapping the null-guards before Object.values(b) prevents a runtime TypeError.

🧹 Nitpick comments (8)
CHANGELOG.md (1)

1-12: Polish wording and confirm release date.

  • Suggest clearer phrasing:
    • “standard schema incorrectly validate cookie and coercion” → “fix incorrect cookie validation/coercion in Standard Schema”
    • “overwrite primitive value if collide” → “overwrite primitive values on conflict”
  • Date says “15 Sep 2025” while PR opened on 14 Sep 2025. Please confirm final release date before merge.
-# 1.4.5 - 15 Sep 2025
+# 1.4.5 - 15 Sep 2025
 Improvement:
 - soundness for guard, group
-- overwrite primitive value if collide (intersectIfObject)
+- overwrite primitive values on conflict (intersectIfObject)
 
 Bug fix:
-- standard schema incorrectly validate cookie and coercion
+- fix incorrect cookie validation/coercion in Standard Schema
 - merge nested guard, group standalone schema properly
 
 Breaking Change:
 - no longer coerce type for .model with `t.Ref` by default
src/types.ts (1)

708-716: Refine IntersectIfObject to match existing object checks.

Current condition uses Record<any, any> and may behave unexpectedly for non-plain objects. Reuse the existing IsBothObject<A,B> helper to keep semantics consistent with other merge utilities.

-export type IntersectIfObject<A, B> =
-	A extends Record<any, any>
-		? B extends Record<any, any>
-			? A & B
-			: A
-		: B extends Record<any, any>
-			? B
-			: A
+export type IntersectIfObject<A, B> =
+  IsBothObject<A, B> extends true
+    ? A & B
+    : A extends Record<any, any>
+      ? A
+      : B extends Record<any, any>
+        ? B
+        : A
src/utils.ts (3)

284-287: Consider precedence for detail merge (minor).

Elsewhere, local b fields take precedence (b.body ?? a.body), but detail currently does mergeDeep(b.detail, a.detail) which lets a override b when keys collide. If local should win, flip the order:

- detail: mergeDeep(
-   b.detail ?? {},
-   a.detail ?? {}
- ),
+ detail: mergeDeep(
+   a.detail ?? {},
+   b.detail ?? {}
+ ),

310-329: Simplify standaloneSchema merge (readability).

The nested conditional is correct but hard to scan. Consider normalizing to arrays then concatenating:

- standaloneSchema:
-   a.standaloneSchema || b.standaloneSchema
-     ? a.standaloneSchema && !b.standaloneSchema
-       ? a.standaloneSchema
-       : b.standaloneSchema && !a.standaloneSchema
-         ? b.standaloneSchema
-         : [ ...(a.standaloneSchema ?? []), ...(b.standaloneSchema ?? []) ]
-     : undefined
+ standaloneSchema: (() => {
+   const A = a.standaloneSchema ?? []
+   const B = b.standaloneSchema ?? []
+   return A.length || B.length ? ([...A, ...B] as any) : undefined
+ })()

1188-1224: deepClone: solid baseline with circular-reference guard; note non-plain objects.

  • Good: arrays pre‑registered in WeakMap; objects registered before descent.
  • Caveat: special objects (Date/RegExp/Map/Set/TypedArray/ArrayBuffer) and property descriptors aren’t preserved. If deepClone will only handle plain JSON-like data, add a brief comment; otherwise, add targeted handlers.

Example handler stubs (optional):

+  // Handle common built-ins
+  if (source instanceof Date) return new Date(source.getTime()) as any
+  if (source instanceof RegExp) return new RegExp(source) as any
+  if (source instanceof Map) {
+    const m = new Map()
+    weak.set(source as object, m)
+    for (const [k, v] of source) m.set(deepClone(k as any, weak), deepClone(v as any, weak))
+    return m as any
+  }
+  if (source instanceof Set) {
+    const s = new Set()
+    weak.set(source as object, s)
+    for (const v of source) s.add(deepClone(v as any, weak))
+    return s as any
+  }
src/index.ts (3)

4048-4050: Preserve any existing standaloneValidator from schemaOrRun when merging.

Current merge path discards schemaOrRun.standaloneValidator in favor of localHook’s. Merge both before appending the synthesized validator.

Apply this diff:

-                    const hasStandaloneSchema =
+                    const hasStandaloneSchema =
                         body || headers || query || params || cookie || response
+                    const existingStandalone = [
+                        ...(hook.standaloneValidator ?? []),
+                        ...(localHook.standaloneValidator ?? [])
+                    ]

...
-                            standaloneValidator: !hasStandaloneSchema
-                                ? localHook.standaloneValidator
-                                : [
-                                        ...(localHook.standaloneValidator ??
-                                            []),
-                                        {
-                                            body,
-                                            headers,
-                                            query,
-                                            params,
-                                            cookie,
-                                            response
-                                        }
-                                    ]
+                            standaloneValidator: hasStandaloneSchema
+                                ? [
+                                        ...existingStandalone,
+                                        {
+                                            body,
+                                            headers,
+                                            query,
+                                            params,
+                                            cookie,
+                                            response
+                                        }
+                                  ]
+                                : existingStandalone

Also applies to: 4069-4081


6721-6739: ws MacroContext missing Definitions param.

MacroToContext here omits Definitions['typebox'] (present elsewhere), which may degrade ref resolution in macro-driven schemas for ws.

Apply this diff:

-        const MacroContext extends MacroToContext<
-            Metadata['macroFn'],
-            Omit<Input, NonResolvableMacroKey>
-        >
+        const MacroContext extends MacroToContext<
+            Metadata['macroFn'],
+            Omit<Input, NonResolvableMacroKey>,
+            Definitions['typebox']
+        >

5670-5685: Optional: consider reusing a shared Schema alias to reduce duplication.

All route builders repeat the same IntersectIfObjectSchema generic. A top-level type alias (e.g., RouteBuilderSchema<BasePath, Path, Input, Definitions, Volatile, Ephemeral, Metadata>) would improve maintainability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 631d292 and ee201e4.

📒 Files selected for processing (8)
  • CHANGELOG.md (1 hunks)
  • package.json (1 hunks)
  • src/index.ts (20 hunks)
  • src/types.ts (2 hunks)
  • src/utils.ts (3 hunks)
  • test/path/guard.test.ts (1 hunks)
  • test/types/index.ts (3 hunks)
  • test/types/lifecycle/soundness.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • test/path/guard.test.ts
🧰 Additional context used
🧬 Code graph analysis (3)
test/types/lifecycle/soundness.ts (2)
src/index.ts (4)
  • Elysia (186-8139)
  • Elysia (8141-8141)
  • t (8143-8143)
  • status (8184-8184)
src/error.ts (1)
  • status (72-80)
src/types.ts (1)
src/index.ts (1)
  • RouteSchema (8208-8208)
src/index.ts (2)
src/types.ts (9)
  • MergeSchema (726-765)
  • MacroToContext (1008-1077)
  • NonResolvableMacroKey (1651-1653)
  • LocalHook (1661-1722)
  • Prettify (301-303)
  • AnyLocalHook (1589-1589)
  • IntersectIfObjectSchema (717-724)
  • UnwrapRoute (525-564)
  • JoinPath (2460-2463)
src/utils.ts (1)
  • mergeHook (220-334)
🔇 Additional comments (9)
src/types.ts (2)

528-529: Variance tweak looks consistent.

Using “in out” on Path aligns with existing generics in this file. No issues.


717-724: Schema-level intersection helper looks good.

Field-wise application of IntersectIfObject is clear and matches intent. Ensure it’s exported from the public surface where needed (e.g., index.ts) to avoid type import duplication.

test/types/index.ts (2)

867-887: LGTM: merged body shape expectation now includes username.

Matches guard+inner body intersection.


1053-1081: LGTM: prefer local params schema.

Switching to t.Number() and asserting id: number is consistent with local override.

test/types/lifecycle/soundness.ts (2)

1887-1899: LGTM: standalone schema on GET coverage.

Validates that standalone body schema is respected even on GET.


1901-1942: LGTM: merged guard schemas + local route schema.

Type assertion for { playing, name, limit } confirms the new intersection utility works as intended across nesting.

src/index.ts (3)

3908-3921: Schema typing in group(): good direction.

Using merged route + global schemas here makes sense. No issues spotted.


4433-4444: Guard() schema typing: good.

Extending Schema with merged volatile/ephemeral/metadata + standalone intersections reads correctly.


5670-5685: LGTM: IntersectIfObjectSchema across route builders.

Intersecting per-route with standalone schemas only when both sides are objects tightens typings without breaking existing merges. Looks good across get/post/put/patch/delete/options/all/head/connect/route.

Also applies to: 5775-5790, 5881-5895, 5985-5999, 6089-6103, 6193-6207, 6297-6311, 6401-6415, 6505-6519, 6610-6624

Comment on lines +165 to 168
MaybeValueOrVoidFunction,
IntersectIfObject,
IntersectIfObjectSchema
} from './types'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Re-export new public types or drop unused import.

You import IntersectIfObject and IntersectIfObjectSchema for typing, but they aren’t re-exported in this file’s public surface. If the intent is to expose them (as per PR summary), add a re-export; otherwise, remove IntersectIfObject to avoid an unused type import.

Apply this diff (safe to place near other type re-exports at the bottom):

+export type { IntersectIfObject, IntersectIfObjectSchema } from './types'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
MaybeValueOrVoidFunction,
IntersectIfObject,
IntersectIfObjectSchema
} from './types'
MaybeValueOrVoidFunction,
IntersectIfObject,
IntersectIfObjectSchema
} from './types'
export type { IntersectIfObject, IntersectIfObjectSchema } from './types'
🤖 Prompt for AI Agents
In src/index.ts around lines 165 to 168, the types IntersectIfObject and
IntersectIfObjectSchema are imported but not re-exported; if the intent (per PR
summary) is to expose them, add them to the file’s public type re-exports near
the other type exports at the bottom (e.g., include IntersectIfObject and
IntersectIfObjectSchema in the export list); otherwise, remove these two imports
to eliminate the unused type import.

Comment on lines 3941 to 3960
derive: Singleton['derive']
resolve: Prettify<
Singleton['resolve'] &
Ephemeral['resolve'] &
Volatile['resolve']
// @ts-ignore
MacroContext['resolve']
>
},
Definitions,
{
schema: Schema
standaloneSchema: Metadata['standaloneSchema']
standaloneSchema: Metadata['standaloneSchema'] &
Schema &
MacroContext
macro: Metadata['macro']
macroFn: Metadata['macroFn']
parser: Metadata['parser']
response: Metadata['response']
response: Metadata['response'] &
// @ts-ignore
MacroContext['response']
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use SimplifyToSchema for standaloneSchema to avoid leaking non-schema keys.

StandaloneSchema currently intersects MacroContext directly, which can pull in non-schema keys (eg resolve). Prefer narrowing to schema-like keys.

Apply this diff:

-                    standaloneSchema: Metadata['standaloneSchema'] &
-                        Schema &
-                        MacroContext
+                    standaloneSchema: Metadata['standaloneSchema'] &
+                        Schema &
+                        SimplifyToSchema<MacroContext>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
derive: Singleton['derive']
resolve: Prettify<
Singleton['resolve'] &
Ephemeral['resolve'] &
Volatile['resolve']
// @ts-ignore
MacroContext['resolve']
>
},
Definitions,
{
schema: Schema
standaloneSchema: Metadata['standaloneSchema']
standaloneSchema: Metadata['standaloneSchema'] &
Schema &
MacroContext
macro: Metadata['macro']
macroFn: Metadata['macroFn']
parser: Metadata['parser']
response: Metadata['response']
response: Metadata['response'] &
// @ts-ignore
MacroContext['response']
},
derive: Singleton['derive']
resolve: Prettify<
Singleton['resolve'] &
// @ts-ignore
MacroContext['resolve']
>
},
Definitions,
{
schema: Schema
standaloneSchema: Metadata['standaloneSchema'] &
Schema &
SimplifyToSchema<MacroContext>
macro: Metadata['macro']
macroFn: Metadata['macroFn']
parser: Metadata['parser']
response: Metadata['response'] &
// @ts-ignore
MacroContext['response']
},
🤖 Prompt for AI Agents
In src/index.ts around lines 3941 to 3960, standaloneSchema currently intersects
MacroContext directly which can leak non-schema keys; wrap the intersection in
SimplifyToSchema to narrow to schema-like keys. Replace the standaloneSchema
type from "Metadata['standaloneSchema'] & Schema & MacroContext" to
"SimplifyToSchema<Metadata['standaloneSchema'] & Schema & MacroContext>" (and
ensure SimplifyToSchema is imported/available) so non-schema keys like resolve
are not exposed.

@SaltyAom
Copy link
Member Author

LGTM

@SaltyAom SaltyAom merged commit a27b433 into main Sep 15, 2025
3 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/types.ts (1)

1726-1786: Fix duplicate type alias: GuardLocalHook (TS2300)

Two declarations of GuardLocalHook exist in src/types.ts — delete the earlier definition (lines 1726–1786); the remaining definition begins at line 1787.

src/index.ts (1)

674-691: AOT vs non‑AOT inconsistency: headers.additionalProperties differs.

Precompiled path sets additionalProperties: true, dynamic path uses !normalize. This can change behavior between aot=true/false. Align to a single behavior (suggest true, like precompiled).

-                                    additionalProperties: !normalize,
+                                    additionalProperties: true,
♻️ Duplicate comments (3)
src/index.ts (3)

165-168: Re‑export newly imported public types (or drop unused import).

You import IntersectIfObject and IntersectIfObjectSchema but don’t re‑export them; IntersectIfObject is also unused locally. If these are part of the public surface for 1.4.5, re‑export; otherwise remove IntersectIfObject. This mirrors the prior suggestion.

[ suggest_recommended_refactor ]

Apply this diff near the bottom where other type exports live:

+export type { IntersectIfObject, IntersectIfObjectSchema } from './types'

To remove instead:

-    IntersectIfObject,
     IntersectIfObjectSchema

Run to verify:

#!/bin/bash
rg -n "export type \{[^}]*IntersectIfObjectSchema" -n
rg -n "IntersectIfObject[^S]" src/index.ts

Also applies to: 8244-8289


3963-3966: Use SimplifyToSchema for standaloneSchema to avoid leaking non‑schema keys.

Intersecting MacroContext directly may leak non‑schema keys (e.g., resolve). Wrap it.

-                    standaloneSchema: Metadata['standaloneSchema'] &
-                        Schema &
-                        MacroContext
+                    standaloneSchema: Metadata['standaloneSchema'] &
+                        Schema &
+                        SimplifyToSchema<MacroContext>

4499-4502: Apply the same SimplifyToSchema guard in guard(...run) return type.

Keeps standaloneSchema narrowed to schema‑like keys.

-                    standaloneSchema: Metadata['standaloneSchema'] &
-                        Schema &
-                        MacroContext
+                    standaloneSchema: Metadata['standaloneSchema'] &
+                        Schema &
+                        SimplifyToSchema<MacroContext>
🧹 Nitpick comments (4)
test/types/lifecycle/soundness.ts (1)

1944-1985: Comment says “group” but code uses guard; align naming to avoid confusion.

Either switch to .group(...) if that’s intended, or update the comment.

Apply (comment-only tweak):

-// Merge multiple group schema
+// Merge multiple guard schema (comment corrected)
example/a.ts (1)

1-17: Remove unused imports.

zod and req aren’t used in this snippet.

-import z from 'zod'
-import { req } from '../test/utils'
src/index.ts (2)

579-592: Filter out empty cookie validators to avoid passing undefined entries.

Passing undefineds downstream is avoidable noise.

-                        validators: standaloneValidators.map((x) => x.cookie),
+                        validators: standaloneValidators
+                            .map((x) => x.cookie)
+                            .filter(Boolean),

4049-4094: Normalize standalone response in group() like guard().

guard() wraps string/TypeBox/Standard responses into {200: ...}; group() pushes response as-is. Normalize for consistency.

-                            standaloneValidator: !hasStandaloneSchema
+                            standaloneValidator: !hasStandaloneSchema
                                 ? localHook.standaloneValidator
                                 : [
                                         ...(localHook.standaloneValidator ??
                                             []),
-                                        {
-                                            body,
-                                            headers,
-                                            query,
-                                            params,
-                                            cookie,
-                                            response
-                                        }
+                                        (() => {
+                                            const normalized =
+                                                !response
+                                                    ? undefined
+                                                    : typeof response === 'string' ||
+                                                      (response as any) &&
+                                                      (Kind in (response as any) ||
+                                                       '~standard' in (response as any))
+                                                        ? { 200: response }
+                                                        : response
+                                            return {
+                                                body,
+                                                headers,
+                                                query,
+                                                params,
+                                                cookie,
+                                                response: normalized
+                                            }
+                                        })()
                                     ]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee201e4 and d6f84eb.

📒 Files selected for processing (4)
  • example/a.ts (1 hunks)
  • src/index.ts (21 hunks)
  • src/types.ts (4 hunks)
  • test/types/lifecycle/soundness.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
test/types/lifecycle/soundness.ts (1)
src/index.ts (4)
  • Elysia (186-8168)
  • Elysia (8170-8170)
  • t (8172-8172)
  • status (8213-8213)
src/types.ts (1)
src/index.ts (13)
  • RouteSchema (8237-8237)
  • MaybeArray (8263-8263)
  • OptionalHandler (8245-8245)
  • AfterHandler (8275-8275)
  • ErrorHandler (8247-8247)
  • LifeCycleType (8250-8250)
  • DocumentDecoration (8254-8254)
  • BodyHandler (8244-8244)
  • TransformHandler (8278-8278)
  • MapResponse (8258-8258)
  • AfterResponseHandler (8246-8246)
  • BaseMacro (8259-8259)
  • SingletonBase (8229-8229)
example/a.ts (2)
src/index.ts (3)
  • Elysia (186-8168)
  • Elysia (8170-8170)
  • t (8172-8172)
src/type-system/index.ts (1)
  • t (657-657)
src/index.ts (2)
src/types.ts (11)
  • MergeSchema (729-768)
  • MacroToContext (1011-1079)
  • NonResolvableMacroKey (1653-1655)
  • MaybeArray (261-261)
  • OptionalHandler (1190-1205)
  • AfterHandler (1207-1235)
  • ErrorHandler (1369-1544)
  • Prettify (301-303)
  • AnyLocalHook (1591-1591)
  • IntersectIfObjectSchema (717-727)
  • UnwrapRoute (525-564)
src/utils.ts (1)
  • mergeHook (220-334)
🪛 Biome (2.1.2)
src/types.ts

[error] 1787-1787: Shouldn't redeclare 'GuardLocalHook'. Consider to delete it or rename it.

'GuardLocalHook' is defined here:

(lint/suspicious/noRedeclare)

🪛 GitHub Actions: Build and Test
src/types.ts

[error] 1726-1726: TypeScript error TS2300: Duplicate identifier 'GuardLocalHook'.


[error] 1787-1787: TypeScript error TS2300: Duplicate identifier 'GuardLocalHook'.

🔇 Additional comments (7)
src/types.ts (3)

708-727: New IntersectIfObject and IntersectIfObjectSchema look correct.

The conditional intersection behavior matches the intended schema-merge semantics.

If desired, we can tighten the constraint to avoid any:

-export type IntersectIfObject<A, B> =
-	A extends Record<any, any>
+export type IntersectIfObject<A, B> =
+	A extends Record<keyof any, unknown>
 		? B extends Record<any, any>
 			? A & B
 			: A
-		: B extends Record<any, any>
+		: B extends Record<keyof any, unknown>
 			? B
 			: A

1066-1076: MacroToContext intersection order change seems fine.

The added intersection keeps recursive macro context accumulation intact.


528-529: Keep UnwrapRoute's Path default as '' — intentional and safe.

ResolvePath treats '' as "no path parameters" (PathParameterLike requires a "/:/*" pattern), so UnwrapRoute's default yields {} for params and does not change inference for routes without path params; handler types use Path | undefined for handler call-site flexibility.

test/types/lifecycle/soundness.ts (3)

1887-1899: Strengthen the GET-schema test with an explicit body type check.

Right now it returns body but doesn’t assert the merged shape.

[ suggest_optional_refactor ]
Apply:

-		.get('/user/:id', ({ body }) => body, {
+		.get('/user/:id', ({ body }) => {
+			expectTypeOf(body).toEqualTypeOf<{ id: number; name: 'lilith' }>()
+			return body
+		}, {
 			body: t.Object({
 				name: t.Literal('lilith')
 			})
 		})

1987-2027: Duplicate of the previous block.

If intentional, add a note; otherwise remove to cut noise.

[ suggest_nitpick_refactor ]


2030-2045: Macro context inheritance check looks good.

Asserting user from upstream macro in the downstream macro is correct.

src/index.ts (1)

1-8169: Local verification required — ripgrep returned no output

Scan returned no output; cannot confirm whether:

  • MacroContext['response'] is present in resolve positions
  • standaloneSchema intersections are missing SimplifyToSchema
  • headers validator uses different additionalProperties between AOT/dynamic

Run from the repository root and paste the outputs:
rg -n -C2 "resolve[^\\n]MacroContext\['response'\]" src/index.ts
rg -n -C1 "standaloneSchema:.
&\sSchema\s&\s*MacroContext" src/index.ts
rg -n -C2 "getSchemaValidator\(cloned\.headers,[^)]+additionalProperties:" src/index.ts

Comment on lines 3934 to 3941
Schema & MacroContext,
Singleton & {
derive: Ephemeral['derive'] & Volatile['derive']
resolve: Ephemeral['resolve'] & Volatile['resolve']
resolve: Ephemeral['resolve'] &
Volatile['resolve'] &
// @ts-ignore
MacroContext['response']
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: resolve type incorrectly includes MacroContext['response'].

This should be MacroContext['resolve']; current form pollutes resolve with response shape.

-                resolve: Ephemeral['resolve'] &
-                    Volatile['resolve'] &
-                    // @ts-ignore
-                    MacroContext['response']
+                resolve: Ephemeral['resolve'] &
+                    Volatile['resolve'] &
+                    // @ts-ignore
+                    MacroContext['resolve']
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Schema & MacroContext,
Singleton & {
derive: Ephemeral['derive'] & Volatile['derive']
resolve: Ephemeral['resolve'] & Volatile['resolve']
resolve: Ephemeral['resolve'] &
Volatile['resolve'] &
// @ts-ignore
MacroContext['response']
},
Schema & MacroContext,
Singleton & {
derive: Ephemeral['derive'] & Volatile['derive']
resolve: Ephemeral['resolve'] &
Volatile['resolve'] &
// @ts-ignore
MacroContext['resolve']
},
🤖 Prompt for AI Agents
In src/index.ts around lines 3934 to 3941, the resolve type is incorrectly
including MacroContext['response'] (and using a @ts-ignore); change that to
MacroContext['resolve'] so resolve combines Ephemeral['resolve'],
Volatile['resolve'], and MacroContext['resolve'] instead of the response shape,
and remove the unnecessary @ts-ignore; ensure the combined type remains an
intersection as intended.

src/index.ts Outdated
Comment on lines 4161 to 4165
resolve: Ephemeral['resolve'] &
Volatile['resolve'] &
// @ts-ignore
MacroContext['response']
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same bug in guard() overload: resolve includes MacroContext['response'].

Mirror the fix here.

-                resolve: Ephemeral['resolve'] &
-                    Volatile['resolve'] &
-                    // @ts-ignore
-                    MacroContext['response']
+                resolve: Ephemeral['resolve'] &
+                    Volatile['resolve'] &
+                    // @ts-ignore
+                    MacroContext['resolve']
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resolve: Ephemeral['resolve'] &
Volatile['resolve'] &
// @ts-ignore
MacroContext['response']
},
resolve: Ephemeral['resolve'] &
Volatile['resolve'] &
// @ts-ignore
MacroContext['resolve']
},
🤖 Prompt for AI Agents
In src/index.ts around lines 4161 to 4165, the guard() overload incorrectly
includes MacroContext['response'] in the resolve union; change that to
MacroContext['resolve'] (or the correct resolve member on MacroContext) and
remove the // @ts-ignore, so the resolve type becomes Ephemeral['resolve'] &
Volatile['resolve'] & MacroContext['resolve'] (or the appropriate existing key)
to mirror the fix applied elsewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants