Skip to content

Fix fmodata mutation Prefer header merging for special columns#212

Merged
eluce2 merged 3 commits intomainfrom
codex/find-includespecialcolumns
Apr 15, 2026
Merged

Fix fmodata mutation Prefer header merging for special columns#212
eluce2 merged 3 commits intomainfrom
codex/find-includespecialcolumns

Conversation

@eluce2
Copy link
Copy Markdown
Collaborator

@eluce2 eluce2 commented Apr 15, 2026

Summary

  • preserve merged Prefer values for insert() and update(..., { returnFullRecord: true })
  • carry includeSpecialColumns through mutation execute-option defaults and request builders
  • return typed ROWID/ROWMODID on full-record mutation responses when special columns are enabled
  • add regression tests for merged Prefer headers and special-column mutation responses
  • add a patch changeset for @proofkit/fmodata

Testing

  • pnpm run ci

Summary by CodeRabbit

  • New Features

    • Return special columns (ROWID, ROWMODID) in full-record insert/update responses via includeSpecialColumns (configurable at DB or per-request); typings updated to reflect this.
  • Bug Fixes

    • Prefer header values are merged, de-duplicated and ordered predictably when combining return, entity-ID, and special-column flags.
    • Request header handling now correctly respects and composes caller-provided Prefer values.
  • Tests

    • Added tests covering header merging and special-column inclusion in mutation responses.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
proofkit-docs Ready Ready Preview Apr 15, 2026 10:50pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 15, 2026

🦋 Changeset detected

Latest commit: 7c971ff

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@proofkit/fmodata Patch
@proofkit/better-auth Patch
@proofkit/typegen Patch
@proofkit/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 15, 2026

Open in StackBlitz

@proofkit/better-auth

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/better-auth@212

@proofkit/cli

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/cli@212

create-proofkit

pnpm add https://pkg.pr.new/proofsh/proofkit/create-proofkit@212

@proofkit/fmdapi

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/fmdapi@212

@proofkit/fmodata

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/fmodata@212

@proofkit/typegen

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/typegen@212

@proofkit/webviewer

pnpm add https://pkg.pr.new/proofsh/proofkit/@proofkit/webviewer@212

commit: bd0dccf

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47665e52-86d8-4295-b217-0574361c4ee5

📥 Commits

Reviewing files that changed from the base of the PR and between bd0dccf and 7c971ff.

📒 Files selected for processing (3)
  • packages/fmodata/src/client/filemaker-odata.ts
  • packages/fmodata/tests/include-special-columns.test.ts
  • packages/fmodata/tests/use-entity-ids-override.test.ts

📝 Walkthrough

Walkthrough

Threads an includeSpecialColumns flag through mutation builders and request/response paths, adds Prefer-header merging utilities, updates header construction to use the Headers API, and adds tests verifying Prefer composition and inclusion of FileMaker special columns (ROWID/ROWMODID) in full-record mutation responses.

Changes

Cohort / File(s) Summary
Changeset
\.changeset/fix-fmodata-mutation-special-columns.md
Adds a patch changeset documenting the fix: merged Prefer headers preserved and special columns returned in full-record mutation responses.
Mutation helpers
packages/fmodata/src/client/builders/mutation-helpers.ts
mergeMutationExecuteOptions gains databaseIncludeSpecialColumns and returns includeSpecialColumns; new exported mergePreferHeaderValues(...values) flattens, trims, dedups, and joins Prefer fragments.
Request plumbing
packages/fmodata/src/client/filemaker-odata.ts
Switched to new Headers(...); sets Authorization/Content-Type/Accept; computes merged Prefer via mergePreferHeaderValues, deletes Prefer if empty; logging reads from Headers.entries() (omits authorization).
Delete builder
packages/fmodata/src/client/delete-builder.ts
ExecutableDeleteBuilder.execute() now forwards config.includeSpecialColumns into option merging so include-special-columns influences request building.
Insert builder
packages/fmodata/src/client/insert-builder.ts
Adds generic DatabaseIncludeSpecialColumns; execution options include includeSpecialColumns?; builds Prefer to include fmodata.include-specialcolumns when needed; processResponse/execute propagate include flag and conditionally include special columns in representation results.
Update builder
packages/fmodata/src/client/update-builder.ts
Adds generic DatabaseIncludeSpecialColumns; byId/where propagate it; execute/processResponse become generic over ExecuteOptions, merge includeSpecialColumns, compute merged Prefer via mergePreferHeaderValues, transform/validate responses with include-special-columns awareness and conditionally return special columns.
Entity set typing
packages/fmodata/src/client/entity-set.ts
insert and update overloads updated to propagate DatabaseIncludeSpecialColumns through returned InsertBuilder/UpdateBuilder types and constructor calls.
Tests
packages/fmodata/tests/include-special-columns.test.ts, packages/fmodata/tests/use-entity-ids-override.test.ts
Adds tests verifying Prefer header merging/order and that ROWID/ROWMODID appear in representation responses when includeSpecialColumns is enabled; updates entity-ID override test to assert merged Prefer values.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing Prefer header merging for special columns in fmodata mutations, which is directly reflected in the changeset and code modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/find-includespecialcolumns

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

Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (3)
packages/fmodata/src/client/insert-builder.ts (2)

147-181: ⚠️ Potential issue | 🟠 Major

Canonicalize callerHeaders to Headers instance before writing.

The code reads callerHeaders through new Headers(...) but then spreads callerHeaders as a plain object in the request. This is unsafe:

  • If callerHeaders is a Headers instance, the spread operator doesn't iterate its entries—caller headers are lost.
  • If callerHeaders is a plain object, both prefer and Prefer keys can coexist, and the merged Prefer value gets added separately, creating duplicates on the wire.

Consistently use a Headers instance throughout to avoid data loss and conflicts.

Suggested fix
+    const requestHeaders = new Headers(callerHeaders);
     const preferHeader = mergePreferHeaderValues(
-      new Headers(callerHeaders).get("Prefer") ?? undefined,
+      requestHeaders.get("Prefer") ?? undefined,
       this.returnPreference === "minimal" ? "return=minimal" : "return=representation",
       shouldUseIds ? "fmodata.entity-ids" : undefined,
       includeSpecialColumns ? "fmodata.include-specialcolumns" : undefined,
     );
+    requestHeaders.set("Content-Type", "application/json");
+    if (preferHeader) {
+      requestHeaders.set("Prefer", preferHeader);
+    }

     const pipeline = Effect.gen(this, function* () {
       // ...
       const responseData = yield* requestFromService<any>(url, {
         ...requestOptions,
         method: "POST",
-        headers: {
-          ...(callerHeaders || {}),
-          "Content-Type": "application/json",
-          ...(preferHeader ? { Prefer: preferHeader } : {}),
-        },
+        headers: requestHeaders,
         body: JSON.stringify(transformedData),
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/src/client/insert-builder.ts` around lines 147 - 181, The
request currently mixes a raw callerHeaders value and a computed preferHeader
string (see preferHeader, callerHeaders, mergePreferHeaderValues) which can drop
or duplicate headers; fix by canonicalizing callerHeaders to a Headers instance
once (e.g., const canonicalHeaders = new Headers(callerHeaders || {})),
read/merge Prefer from that Headers (using mergePreferHeaderValues as done) and
then pass headers to requestFromService as an object built from canonicalHeaders
(e.g., Object.fromEntries(canonicalHeaders.entries()) or by iterating entries)
while ensuring Prefer is set to preferHeader only once; update the code paths
around preferHeader creation and the requestFromService call to use
canonicalHeaders and derived plain-object headers consistently.

288-300: ⚠️ Potential issue | 🟡 Minor

processResponse() return type doesn't reflect the request-level special-column override.

Line 385 reads options?.includeSpecialColumns, and line 417 passes it to validation. However, the Promise return type (line 292) is fixed to DatabaseIncludeSpecialColumns, unlike the execute() method which uses NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"], DatabaseIncludeSpecialColumns>. A caller using processResponse(response, { includeSpecialColumns: true }) on a DB configured with false will get ROWID/ROWMODID at runtime but TypeScript won't reflect this. Make processResponse() generic like execute():

Suggested fix
-  async processResponse(
-    response: Response,
-    options?: ExecuteOptions,
-  ): Promise<
-    Result<
-      ReturnPreference extends "minimal"
-        ? { ROWID: number }
-        : ConditionallyWithSpecialColumns<
-            InferSchemaOutputFromFMTable<NonNullable<Occ>>,
-            DatabaseIncludeSpecialColumns,
-            false
-          >
-    >
-  > {
+  async processResponse<EO extends ExecuteOptions>(
+    response: Response,
+    options?: EO,
+  ): Promise<
+    Result<
+      ReturnPreference extends "minimal"
+        ? { ROWID: number }
+        : ConditionallyWithSpecialColumns<
+            InferSchemaOutputFromFMTable<NonNullable<Occ>>,
+            NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"], DatabaseIncludeSpecialColumns>,
+            false
+          >
-    >
-  > {
+    >
+  > {

Also applies to: 383-418

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/src/client/insert-builder.ts` around lines 288 - 300,
processResponse's return type doesn't honor the request-level
includeSpecialColumns override; make processResponse generic over an
ExecuteOptions type parameter (e.g. <EO extends ExecuteOptions | undefined>)
like execute(), use NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"],
DatabaseIncludeSpecialColumns> in the resolved Promise return type, and thread
EO through any internal validation calls (where options?.includeSpecialColumns
is read) so the type reflects when a caller passes includeSpecialColumns: true;
apply the same change to the other processResponse overloads mentioned (around
lines 383–418) and ensure function signature and internal usages (options,
validation calls) reference the new EO generic.
packages/fmodata/src/client/update-builder.ts (1)

185-223: ⚠️ Potential issue | 🟠 Major

Use a Headers object throughout to avoid case-variant header duplication.

Object.fromEntries(new Headers(callerHeaders).entries()) normalizes the existing key to lowercase prefer, but then headers.Prefer = preferHeader adds an uppercase variant to the plain object. When both keys exist in the object passed to fetch, it can result in duplicate Prefer header values being sent, bypassing the merge deduplication logic.

Suggested fix
-    const headers: Record<string, string> = { "Content-Type": "application/json" };
+    const requestHeaders = new Headers(callerHeaders);
+    requestHeaders.set("Content-Type", "application/json");
     const preferHeader = mergePreferHeaderValues(
-      new Headers(callerHeaders).get("Prefer") ?? undefined,
+      requestHeaders.get("Prefer") ?? undefined,
       this.returnPreference === "representation" ? "return=representation" : undefined,
       shouldUseIds ? "fmodata.entity-ids" : undefined,
       includeSpecialColumns ? "fmodata.include-specialcolumns" : undefined,
     );
     if (preferHeader) {
-      headers.Prefer = preferHeader;
+      requestHeaders.set("Prefer", preferHeader);
+    } else {
+      requestHeaders.delete("Prefer");
     }
...
-      const requestHeaders = {
-        ...Object.fromEntries(new Headers(callerHeaders).entries()),
-        ...headers,
-      };
-
       const response = yield* requestFromService(url, {
         ...requestOptions,
         method: "PATCH",
         headers: requestHeaders,
         body: JSON.stringify(transformedData),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/src/client/update-builder.ts` around lines 185 - 223, The
problem is header duplication caused by mixing a plain object and a Headers map
(Object.fromEntries(new Headers(callerHeaders).entries()) plus headers.Prefer =
...). To fix, build and manipulate a Headers instance throughout: create a
Headers from callerHeaders, set or update Prefer via headersObj.set("Prefer",
preferHeader) (or delete if undefined), apply any other header entries from the
existing plain headers map to that Headers instance, then pass that Headers
instance as the request headers into requestFromService (replace requestHeaders
plain-object usage with the Headers instance). Reference symbols: callerHeaders,
headers (the plain headers map), preferHeader (from mergePreferHeaderValues),
requestHeaders, and requestFromService.
🧹 Nitpick comments (1)
packages/fmodata/tests/include-special-columns.test.ts (1)

654-720: Add a mutation regression that merges an existing caller-supplied Prefer header.

These new cases only assert builder-generated values. The bug this PR is fixing is about preserving merged Prefer values, so please add at least one execute({ headers: { Prefer: ... } }) case and assert the final header contains each token exactly once. That would cover the path most likely to regress here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/tests/include-special-columns.test.ts` around lines 654 -
720, Add a test variation to the existing "include special columns" insert
and/or update tests that passes a caller-supplied Prefer header via execute({
headers: { Prefer: "..." } }) and asserts the final merged Prefer header
(captured via the before hook's req.headers.get("Prefer")) contains each token
exactly once (e.g., "return=representation, fmodata.include-specialcolumns"
merged with an existing token like "handling=lenient" should become
"return=representation, fmodata.include-specialcolumns, handling=lenient" with
no duplicates). Locate the two existing tests using
db.from(contactsTO).insert(...).execute(...) and
db.from(contactsTO).update(...).execute(...) and add an execute call that
supplies headers.Prefer with one or more tokens, then assert preferHeader equals
the expected merged string to ensure caller headers are preserved and
de-duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/fmodata/src/client/update-builder.ts`:
- Around line 298-310: The return type of processResponse and the UpdateResponse
alias are currently fixed to DatabaseIncludeSpecialColumns, so per-call
options?.includeSpecialColumns overrides aren't reflected in types; update both
places to use the normalized override type (the same pattern execute() uses):
replace DatabaseIncludeSpecialColumns with
NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"],
DatabaseIncludeSpecialColumns> (or the correct local ExecuteOptions generic if
named differently) so processResponse's Promise and UpdateResponse alias reflect
options?.includeSpecialColumns, and keep
validateSingleResponse(options?.includeSpecialColumns) as-is.

---

Outside diff comments:
In `@packages/fmodata/src/client/insert-builder.ts`:
- Around line 147-181: The request currently mixes a raw callerHeaders value and
a computed preferHeader string (see preferHeader, callerHeaders,
mergePreferHeaderValues) which can drop or duplicate headers; fix by
canonicalizing callerHeaders to a Headers instance once (e.g., const
canonicalHeaders = new Headers(callerHeaders || {})), read/merge Prefer from
that Headers (using mergePreferHeaderValues as done) and then pass headers to
requestFromService as an object built from canonicalHeaders (e.g.,
Object.fromEntries(canonicalHeaders.entries()) or by iterating entries) while
ensuring Prefer is set to preferHeader only once; update the code paths around
preferHeader creation and the requestFromService call to use canonicalHeaders
and derived plain-object headers consistently.
- Around line 288-300: processResponse's return type doesn't honor the
request-level includeSpecialColumns override; make processResponse generic over
an ExecuteOptions type parameter (e.g. <EO extends ExecuteOptions | undefined>)
like execute(), use NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"],
DatabaseIncludeSpecialColumns> in the resolved Promise return type, and thread
EO through any internal validation calls (where options?.includeSpecialColumns
is read) so the type reflects when a caller passes includeSpecialColumns: true;
apply the same change to the other processResponse overloads mentioned (around
lines 383–418) and ensure function signature and internal usages (options,
validation calls) reference the new EO generic.

In `@packages/fmodata/src/client/update-builder.ts`:
- Around line 185-223: The problem is header duplication caused by mixing a
plain object and a Headers map (Object.fromEntries(new
Headers(callerHeaders).entries()) plus headers.Prefer = ...). To fix, build and
manipulate a Headers instance throughout: create a Headers from callerHeaders,
set or update Prefer via headersObj.set("Prefer", preferHeader) (or delete if
undefined), apply any other header entries from the existing plain headers map
to that Headers instance, then pass that Headers instance as the request headers
into requestFromService (replace requestHeaders plain-object usage with the
Headers instance). Reference symbols: callerHeaders, headers (the plain headers
map), preferHeader (from mergePreferHeaderValues), requestHeaders, and
requestFromService.

---

Nitpick comments:
In `@packages/fmodata/tests/include-special-columns.test.ts`:
- Around line 654-720: Add a test variation to the existing "include special
columns" insert and/or update tests that passes a caller-supplied Prefer header
via execute({ headers: { Prefer: "..." } }) and asserts the final merged Prefer
header (captured via the before hook's req.headers.get("Prefer")) contains each
token exactly once (e.g., "return=representation,
fmodata.include-specialcolumns" merged with an existing token like
"handling=lenient" should become "return=representation,
fmodata.include-specialcolumns, handling=lenient" with no duplicates). Locate
the two existing tests using db.from(contactsTO).insert(...).execute(...) and
db.from(contactsTO).update(...).execute(...) and add an execute call that
supplies headers.Prefer with one or more tokens, then assert preferHeader equals
the expected merged string to ensure caller headers are preserved and
de-duplicated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d984a048-9508-41a4-bbbe-d16e8c19700b

📥 Commits

Reviewing files that changed from the base of the PR and between 175b0bd and 3d8cd82.

📒 Files selected for processing (8)
  • .changeset/fix-fmodata-mutation-special-columns.md
  • packages/fmodata/src/client/builders/mutation-helpers.ts
  • packages/fmodata/src/client/delete-builder.ts
  • packages/fmodata/src/client/entity-set.ts
  • packages/fmodata/src/client/insert-builder.ts
  • packages/fmodata/src/client/update-builder.ts
  • packages/fmodata/tests/include-special-columns.test.ts
  • packages/fmodata/tests/use-entity-ids-override.test.ts

Comment thread packages/fmodata/src/client/update-builder.ts Outdated
Comment on lines +231 to +233
if (!headers.has("Prefer") && preferValues.length > 0) {
headers.set("Prefer", preferValues.join(", "));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This logic fails to merge Prefer headers when user-provided headers already contain a Prefer value. If options?.headers includes a Prefer header, the database-level preferValues (like fmodata.include-specialcolumns) will be silently ignored instead of being merged.

This is inconsistent with how insert-builder.ts and update-builder.ts handle Prefer headers using mergePreferHeaderValues(). The fix should merge headers:

const existingPrefer = headers.get("Prefer");
const mergedPrefer = mergePreferHeaderValues(
  preferValues.length > 0 ? preferValues.join(", ") : undefined,
  existingPrefer ?? undefined
);
if (mergedPrefer) {
  headers.set("Prefer", mergedPrefer);
}
Suggested change
if (!headers.has("Prefer") && preferValues.length > 0) {
headers.set("Prefer", preferValues.join(", "));
}
const existingPrefer = headers.get("Prefer");
const mergedPrefer = mergePreferHeaderValues(
preferValues.length > 0 ? preferValues.join(", ") : undefined,
existingPrefer || undefined
);
if (mergedPrefer) {
headers.set("Prefer", mergedPrefer);
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/fmodata/src/client/filemaker-odata.ts`:
- Around line 231-233: The current logic skips setting Prefer when a
caller-supplied header exists, which drops fmodata-specific preferences; update
the block that checks headers and preferValues (the code around
headers.set("Prefer", ...) in filemaker-odata.ts) to merge rather than skip: if
headers.has("Prefer") then read the existing Prefer value, split on commas, trim
and combine with preferValues, deduplicate while preserving order, and set
headers.set("Prefer", mergedJoinedString); otherwise keep the existing behavior
of setting the joined preferValues when no Prefer header exists. Ensure you
reference and use the existing variables headers and preferValues so direct
_makeRequest callers retain their preferences plus fmodata.* preferences.

In `@packages/fmodata/src/client/insert-builder.ts`:
- Around line 148-159: The current Prefer header construction in
insert-builder.ts merges the caller-provided "Prefer" including any return=*
directive, which can override the builder-controlled response shape; change the
logic around mergePreferHeaderValues so you first parse/sanitize the caller
prefer directives from canonicalHeaders.get("Prefer") to remove any return=*
entry, merge only the non-return directives with fmodata.entity-ids /
fmodata.include-specialcolumns, and then append the builder-controlled return
value determined by this.returnPreference (e.g., "return=minimal" or
"return=representation") before setting Prefer; apply the same
sanitization/append pattern in ExecutableUpdateBuilder.execute() to prevent
caller return=* from overriding the builder.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f2483d7-374e-4050-932c-5fa6a38120e6

📥 Commits

Reviewing files that changed from the base of the PR and between 3d8cd82 and bd0dccf.

📒 Files selected for processing (4)
  • packages/fmodata/src/client/filemaker-odata.ts
  • packages/fmodata/src/client/insert-builder.ts
  • packages/fmodata/src/client/update-builder.ts
  • packages/fmodata/tests/include-special-columns.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/fmodata/tests/include-special-columns.test.ts

Comment thread packages/fmodata/src/client/filemaker-odata.ts Outdated
Comment on lines +148 to +159
const preferHeader = mergePreferHeaderValues(
this.returnPreference === "minimal" ? "return=minimal" : "return=representation",
shouldUseIds ? "fmodata.entity-ids" : undefined,
includeSpecialColumns ? "fmodata.include-specialcolumns" : undefined,
canonicalHeaders.get("Prefer") ?? undefined,
);
canonicalHeaders.set("Content-Type", "application/json");
if (preferHeader) {
canonicalHeaders.set("Prefer", preferHeader);
} else {
canonicalHeaders.delete("Prefer");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not merge caller return=* preferences into the builder-controlled response shape.

Line 140 says caller options should not override the required request shape, but canonicalHeaders.get("Prefer") is merged verbatim here. A caller can send Prefer: return=minimal on a representation insert (or the inverse), and execute() will then parse the response using the wrong shape.

Apply the merge only to non-return= directives, then append the builder-selected return= value. The same sanitization is needed in ExecutableUpdateBuilder.execute().

🔧 Proposed fix
     const shouldUseIds = mergedOptions.useEntityIds ?? this.config.useEntityIds;
     const includeSpecialColumns = mergedOptions.includeSpecialColumns ?? this.config.includeSpecialColumns;
     const canonicalHeaders = new Headers(callerHeaders || {});
+    const callerPrefer = canonicalHeaders
+      .get("Prefer")
+      ?.split(",")
+      .map((value) => value.trim())
+      .filter((value) => value && !value.startsWith("return="))
+      .join(", ");
     const preferHeader = mergePreferHeaderValues(
       this.returnPreference === "minimal" ? "return=minimal" : "return=representation",
       shouldUseIds ? "fmodata.entity-ids" : undefined,
       includeSpecialColumns ? "fmodata.include-specialcolumns" : undefined,
-      canonicalHeaders.get("Prefer") ?? undefined,
+      callerPrefer || undefined,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/src/client/insert-builder.ts` around lines 148 - 159, The
current Prefer header construction in insert-builder.ts merges the
caller-provided "Prefer" including any return=* directive, which can override
the builder-controlled response shape; change the logic around
mergePreferHeaderValues so you first parse/sanitize the caller prefer directives
from canonicalHeaders.get("Prefer") to remove any return=* entry, merge only the
non-return directives with fmodata.entity-ids / fmodata.include-specialcolumns,
and then append the builder-controlled return value determined by
this.returnPreference (e.g., "return=minimal" or "return=representation") before
setting Prefer; apply the same sanitization/append pattern in
ExecutableUpdateBuilder.execute() to prevent caller return=* from overriding the
builder.

@eluce2 eluce2 merged commit f4c5a64 into main Apr 15, 2026
8 of 10 checks passed
@eluce2 eluce2 deleted the codex/find-includespecialcolumns branch April 15, 2026 22:48
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.

1 participant