Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .beads/metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl"
}
}
4 changes: 3 additions & 1 deletion packages/fmodata/src/client/builders/default-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export function getDefaultSelectFields(
fields.push("ROWID", "ROWMODID");
}

return fields;
// Return undefined (meaning "all") when schema has no fields with validators,
// rather than an empty array which would generate an empty $select=
return fields.length > 0 ? fields : undefined;
}

if (Array.isArray(defaultSelect)) {
Expand Down
4 changes: 3 additions & 1 deletion packages/fmodata/src/client/builders/expand-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ export class ExpandBuilder {
if (opts.select) {
const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];
const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);
parts.push(`$select=${selectFields}`);
if (selectFields) {
parts.push(`$select=${selectFields}`);
}
}

if (opts.filter) {
Expand Down
97 changes: 96 additions & 1 deletion packages/fmodata/tests/expands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* DO NOT RUN THESE TESTS YET - they define the API we want to build.
*/

import { eq, fmTableOccurrence, numberField, textField } from "@proofkit/fmodata";
import { eq, FMServerConnection, fmTableOccurrence, numberField, textField } from "@proofkit/fmodata";
import { assert, describe, expect, expectTypeOf, it } from "vitest";
import { z } from "zod/v4";
import { mockResponses } from "./fixtures/responses";
Expand Down Expand Up @@ -578,3 +578,98 @@ describe("Expand API Specification", () => {
});
});
});

/**
* GitHub Issue #109: Nested expand generates empty $select= causing OData parse error
*
* When expanding a table that has no readValidators and uses the default "schema"
* defaultSelect, getDefaultSelectFields returns an empty array. This causes
* buildExpandParts to generate "$select=" (empty) inside the expand, which
* FileMaker OData cannot parse.
*
* @see https://github.com/proofkit/proofkit/issues/109
*/
const EMPTY_SELECT_CLOSE_PAREN = /\$select=\)/;
const EMPTY_SELECT_SEMICOLON = /\$select=;/;
const EMPTY_SELECT_END = /\$select=$/;
const EXPAND_CAPTURE = /\$expand=([^&]+)/;
const SELECT_CAPTURE = /\$select=([^;)]+)/;

describe("Issue #109: Empty $select in expand", () => {
// Tables matching the issue reproduction: entity IDs, no defaultSelect (defaults to "schema"),
// no readValidators on any fields
const Parent = fmTableOccurrence(
"Parent",
{
_pk: textField().primaryKey().entityId("FMFID:1"),
},
{
entityId: "FMTID:1",
navigationPaths: ["Child"],
},
);

const Child = fmTableOccurrence(
"Child",
{
_pk: textField().primaryKey().entityId("FMFID:2"),
data: textField().entityId("FMFID:3"),
},
{
entityId: "FMTID:2",
navigationPaths: ["Parent"],
},
);

const connection = new FMServerConnection({
serverUrl: "https://example.com",
auth: { username: "test", password: "test" },
});

const db = connection.database("Test.fmp12", { useEntityIds: true });

it("should not generate empty $select= inside expand on .get()", () => {
const queryString = db.from(Parent).get("test-id").expand(Child).getQueryString();

// The query should NOT contain an empty $select= (i.e. "$select=)" or "$select=;")
expect(queryString).not.toMatch(EMPTY_SELECT_CLOSE_PAREN);
expect(queryString).not.toMatch(EMPTY_SELECT_SEMICOLON);
expect(queryString).not.toMatch(EMPTY_SELECT_END);
});

it("should not generate empty $select= inside expand on .list()", () => {
const queryString = db.from(Parent).list().expand(Child).getQueryString();

expect(queryString).not.toMatch(EMPTY_SELECT_CLOSE_PAREN);
expect(queryString).not.toMatch(EMPTY_SELECT_SEMICOLON);
});

it("should either omit $select or include all fields in expand without callback", () => {
const queryString = db.from(Parent).get("test-id").expand(Child).getQueryString();

// If expand includes $select, it must have actual field names
if (queryString.includes("$expand=")) {
const expandMatch = queryString.match(EXPAND_CAPTURE);
expect(expandMatch).toBeTruthy();
const expandContent = expandMatch?.[1];
// If there's a $select inside the expand parens, it must not be empty
if (expandContent?.includes("$select=")) {
const selectMatch = expandContent.match(SELECT_CAPTURE);
expect(selectMatch).toBeTruthy();
expect(selectMatch?.[1]?.length).toBeGreaterThan(0);
}
}
});

it("should not generate empty $select= in nested expand without explicit select", () => {
const queryString = db
.from(Parent)
.get("test-id")
.expand(Child, (b) => b.expand(Parent))
.getQueryString();

// Neither the Child expand nor the nested Parent expand should have empty $select=
expect(queryString).not.toMatch(EMPTY_SELECT_CLOSE_PAREN);
expect(queryString).not.toMatch(EMPTY_SELECT_SEMICOLON);
});
});
1 change: 0 additions & 1 deletion packages/typegen/web/src/components/ui/data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { createContext, type ReactNode, useContext } from "react";
import { cn } from "@/lib/utils";

declare module "@tanstack/react-table" {
// biome-ignore lint/correctness/noUnusedVariables: TValue is required by library interface
interface ColumnMeta<TData extends RowData, TValue> {
headerTitle?: string;
headerClassName?: string;
Expand Down
Loading