Skip to content

fix(fmodata): strip otto prefix and .fmp12 from batch sub-request URLs#208

Merged
eluce2 merged 2 commits intomainfrom
fix/batch-sub-request-urls
Mar 26, 2026
Merged

fix(fmodata): strip otto prefix and .fmp12 from batch sub-request URLs#208
eluce2 merged 2 commits intomainfrom
fix/batch-sub-request-urls

Conversation

@chriscors
Copy link
Copy Markdown
Collaborator

@chriscors chriscors commented Mar 25, 2026

Summary

  • Fixes batch $batch sub-request URLs to use the canonical FileMaker OData path format
  • Strips the /otto/ proxy prefix and .fmp12 file extension from database names in sub-request URLs inside multipart batch bodies
  • Adds toBatchSubRequestUrl() helper with top-level regex constants

Problem

db.batch() fails because sub-request URLs inside the multipart body include the Otto proxy prefix (/otto/) and .fmp12 extension. FileMaker's OData engine processes sub-requests directly and rejects these:

  • /otto/ prefix → error -1000 (Incorrect OData service root path)
  • .fmp12 extension → error -1032 (Operation database does not match batch request database)

Fix

Sub-request URLs are now transformed from:

https://host/otto/fmi/odata/v4/MyDB.fmp12/table?$query

to:

/fmi/odata/v4/MyDB/table?$query

Test plan

  • Added unit tests for toBatchSubRequestUrl() covering all URL variants
  • Added integration test for formatBatchRequest verifying sub-request lines
  • All existing tests pass (pnpm run ci)

Closes #207

Summary by CodeRabbit

  • Bug Fixes

    • Corrected batch sub-request URL formatting for FileMaker OData paths (removes proxy prefix and file extension) and made insert handling tolerant when Location headers are missing by returning ROWID = -1 instead of failing.
  • Tests

    • Added test coverage for batch sub-request URL canonicalization and batch request formatting.

Batch sub-request URLs inside the multipart body are processed directly
by FileMaker's OData engine, not through the Otto proxy. The engine
rejects URLs containing the /otto/ prefix (-1000 error) or the .fmp12
extension (-1032 error).

Add toBatchSubRequestUrl() to transform full URLs into the canonical
/fmi/odata/v4/{database}/{table} format before writing them into the
batch body.

Closes #207
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 25, 2026

🦋 Changeset detected

Latest commit: 3584bff

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

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

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

Project Deployment Actions Updated (UTC)
proofkit-docs Ready Ready Preview Mar 25, 2026 8:02pm

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 25, 2026

Open in StackBlitz

@proofkit/better-auth

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

@proofkit/cli

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

create-proofkit

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

@proofkit/fmdapi

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

@proofkit/fmodata

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

@proofkit/typegen

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

@proofkit/webviewer

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

commit: 3584bff

@chriscors chriscors requested a review from eluce2 March 25, 2026 19:46
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Adds an exported helper toBatchSubRequestUrl() to canonicalize FileMaker OData batch sub-request paths (removes /otto/ proxy prefix and .fmp12 DB extension) and updates batch formatting to use it; updates InsertBuilder behavior to gracefully handle missing Location headers for return=minimal by returning ROWID: -1; includes a changeset and tests.

Changes

Cohort / File(s) Summary
Changeset Entry
\.changeset/fix-batch-sub-request-urls.md
Patch-level changeset documenting the fix for @proofkit/fmodata.
Batch request helper & formatting
packages/fmodata/src/client/batch-request.ts
Added exported toBatchSubRequestUrl(fullUrl: string) and regexes to strip /otto/ prefix and .fmp12 extensions; formatSubRequest() now applies canonicalization to the HTTP request line (preserves query strings).
Tests for canonicalization & formatting
packages/fmodata/tests/batch-sub-request-url.test.ts
New Vitest suite covering toBatchSubRequestUrl() (prefix/extension stripping, query preservation) and formatBatchRequest() integration for absolute and relative sub-request URLs.
InsertBuilder: minimal return handling
packages/fmodata/src/client/insert-builder.ts
Changed handling for returnPreference === "minimal" to avoid throwing InvalidLocationHeaderError when Location is missing; returns { ROWID: -1 } (or parsed ROWID when available) instead of erroring.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ 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 clearly and specifically describes the main change: stripping otto prefix and .fmp12 from batch sub-request URLs, which is the primary fix addressed in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/batch-sub-request-urls

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.

🧹 Nitpick comments (2)
packages/fmodata/tests/batch-sub-request-url.test.ts (1)

4-28: Consider adding a test case for URLs with only /otto/ prefix (no .fmp12).

The current tests cover both transformations together, .fmp12 only, and neither. Adding a test for /otto/ prefix without .fmp12 would complete the matrix.

📝 Suggested additional test
it("strips /otto/ prefix without .fmp12 extension", () => {
  const result = toBatchSubRequestUrl("https://host.example.com/otto/fmi/odata/v4/MyDB/contacts");
  expect(result).toBe("/fmi/odata/v4/MyDB/contacts");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/fmodata/tests/batch-sub-request-url.test.ts` around lines 4 - 28,
Add a unit test to cover the missing case where the URL contains only the
"/otto/" prefix but no ".fmp12" extension by adding an it block that calls
toBatchSubRequestUrl with
"https://host.example.com/otto/fmi/odata/v4/MyDB/contacts" and asserts the
result equals "/fmi/odata/v4/MyDB/contacts"; locate the tests in the
describe("toBatchSubRequestUrl") block and follow the existing test style for
consistency.
packages/fmodata/src/client/batch-request.ts (1)

13-14: Consider anchoring the .fmp12 regex to the database segment.

The FMPRO_EXT_REGEX will match .fmp12 anywhere in the path, not just in the database name segment. While unlikely in practice, a table or field name containing .fmp12 would be incorrectly modified.

A more precise approach would anchor the pattern to the expected OData path structure (e.g., matching only after /v4/ and before the next /).

💡 Optional: More precise regex
-const FMPRO_EXT_REGEX = /\.fmp12/;
+const FMPRO_EXT_REGEX = /(?<=\/v4\/[^/]+)\.fmp12(?=\/|$|\?)/;

Alternatively, keep it simple if FileMaker database names are the only place .fmp12 can appear in practice.

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

In `@packages/fmodata/src/client/batch-request.ts` around lines 13 - 14,
FMPRO_EXT_REGEX currently matches `.fmp12` anywhere in the path; tighten it so
it only matches the database segment (i.e., the portion immediately after the
OData version segment and before the next `/` or end). Update the
FMPRO_EXT_REGEX definition used in batch-request.ts to anchor the pattern to the
OData path structure (match `.fmp12` only when it occurs in the database name
following `/v4/` and before the next `/` or end), and adjust any replacement
logic that uses FMPRO_EXT_REGEX accordingly (referencing the FMPRO_EXT_REGEX
constant and any code paths that strip `.fmp12` from database names).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/fmodata/src/client/batch-request.ts`:
- Around line 13-14: FMPRO_EXT_REGEX currently matches `.fmp12` anywhere in the
path; tighten it so it only matches the database segment (i.e., the portion
immediately after the OData version segment and before the next `/` or end).
Update the FMPRO_EXT_REGEX definition used in batch-request.ts to anchor the
pattern to the OData path structure (match `.fmp12` only when it occurs in the
database name following `/v4/` and before the next `/` or end), and adjust any
replacement logic that uses FMPRO_EXT_REGEX accordingly (referencing the
FMPRO_EXT_REGEX constant and any code paths that strip `.fmp12` from database
names).

In `@packages/fmodata/tests/batch-sub-request-url.test.ts`:
- Around line 4-28: Add a unit test to cover the missing case where the URL
contains only the "/otto/" prefix but no ".fmp12" extension by adding an it
block that calls toBatchSubRequestUrl with
"https://host.example.com/otto/fmi/odata/v4/MyDB/contacts" and asserts the
result equals "/fmi/odata/v4/MyDB/contacts"; locate the tests in the
describe("toBatchSubRequestUrl") block and follow the existing test style for
consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6a0de79-92c9-4055-afcc-234bb68d548b

📥 Commits

Reviewing files that changed from the base of the PR and between 06173e6 and b075656.

📒 Files selected for processing (3)
  • .changeset/fix-batch-sub-request-urls.md
  • packages/fmodata/src/client/batch-request.ts
  • packages/fmodata/tests/batch-sub-request-url.test.ts

…onses

FileMaker batch sub-responses may not include the Location header that
standalone responses provide. Instead of throwing InvalidLocationHeaderError,
return ROWID -1 when the header is absent in a batch context.

This fixes the follow-up issue from #207 where batch writes (INSERT/UPDATE)
failed with InvalidLocationHeaderError after the URL fix.
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

🤖 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/insert-builder.ts`:
- Around line 278-280: processResponse() currently duplicates Location header
parsing and can throw from this.parseLocationHeader(), causing a rejected
promise instead of returning the usual { data, error } shape; create a small
helper (e.g., safeParseRowIdFromLocation or parseLocationSafe) used by both
places to return a constant FALLBACK_ROWID (replace the magic -1 with a named
constant) and to catch parse exceptions and convert them into a structured error
result rather than throwing; update calls in processResponse() and the other
branch that handles minimal responses (the code using parseLocationHeader and
returning { data: { ROWID: ... } }) to use this helper so parsing failures
become part of the { data, error } return path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 07cefb89-cf4b-4785-a09e-7f23428a5ec9

📥 Commits

Reviewing files that changed from the base of the PR and between b075656 and 3584bff.

📒 Files selected for processing (2)
  • .changeset/fix-batch-sub-request-urls.md
  • packages/fmodata/src/client/insert-builder.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-batch-sub-request-urls.md

Comment on lines +278 to +280
const rowid = locationHeader ? this.parseLocationHeader(locationHeader) : -1;
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
return { data: { ROWID: rowid } as any, error: undefined };
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 | 🟡 Minor

Centralize minimal-response ROWID parsing and keep processResponse() non-throwing.

These two branches duplicate the same Location handling, and this.parseLocationHeader() can still throw on a malformed header. In processResponse(), that means a rejected promise instead of the usual { data, error } result. A small helper would make the -1 fallback explicit and keep parse failures on the structured error path.

♻️ Proposed refactor
+const UNKNOWN_ROWID = -1 as const;
+
+  private processMinimalInsertResponse(response: Response): Result<{ ROWID: number }> {
+    const locationHeader = getLocationHeader(response.headers);
+    if (!locationHeader) {
+      return { data: { ROWID: UNKNOWN_ROWID }, error: undefined };
+    }
+
+    try {
+      return {
+        data: { ROWID: this.parseLocationHeader(locationHeader) },
+        error: undefined,
+      };
+    } catch (error) {
+      return {
+        data: undefined,
+        error:
+          error instanceof Error
+            ? error
+            : new BuilderInvariantError("InsertBuilder.processResponse", String(error)),
+      };
+    }
+  }
...
       if (this.returnPreference === "minimal") {
-        const locationHeader = getLocationHeader(response.headers);
-        const rowid = locationHeader ? this.parseLocationHeader(locationHeader) : -1;
-        // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
-        return { data: { ROWID: rowid } as any, error: undefined };
+        // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
+        return this.processMinimalInsertResponse(response) as any;
       }
...
     if (this.returnPreference === "minimal") {
-      const locationHeader = getLocationHeader(response.headers);
-      const rowid = locationHeader ? this.parseLocationHeader(locationHeader) : -1;
-      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
-      return { data: { ROWID: rowid } as any, error: undefined };
+      // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
+      return this.processMinimalInsertResponse(response) as any;
     }

As per coding guidelines, "Use meaningful variable names instead of magic numbers - extract constants with descriptive names".

Also applies to: 293-300

🤖 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 278 - 280,
processResponse() currently duplicates Location header parsing and can throw
from this.parseLocationHeader(), causing a rejected promise instead of returning
the usual { data, error } shape; create a small helper (e.g.,
safeParseRowIdFromLocation or parseLocationSafe) used by both places to return a
constant FALLBACK_ROWID (replace the magic -1 with a named constant) and to
catch parse exceptions and convert them into a structured error result rather
than throwing; update calls in processResponse() and the other branch that
handles minimal responses (the code using parseLocationHeader and returning {
data: { ROWID: ... } }) to use this helper so parsing failures become part of
the { data, error } return path.

@eluce2 eluce2 merged commit c3740de into main Mar 26, 2026
14 checks passed
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.

Batch sub-request URLs incompatible with FileMaker OData

2 participants