@proofkit/fmodata
Version:
FileMaker OData API client
150 lines (139 loc) • 5.93 kB
text/typescript
import type { FMTable } from "../../orm/table";
import { getTableName } from "../../orm/table";
import { buildRecordPath, type RecordLocator } from "../builders/mutation-helpers";
import { resolveTableId } from "../builders/table-utils";
/**
* Configuration for navigation from RecordBuilder or EntitySet
*/
export interface NavigationConfig {
recordLocator?: RecordLocator;
relation: string;
relationEntityId?: string;
sourceTableName: string;
sourceTableEntityId?: string;
baseRelation?: string; // For chained navigations from navigated EntitySets
baseRelationEntityId?: string;
basePath?: string; // Full base path for chained entity set navigations
basePathEntityId?: string;
}
/**
* Builds OData query URLs for different navigation modes.
* Handles:
* - Record navigation: /database/sourceTable('recordId')/relation
* - Entity set navigation: /database/sourceTable/relation
* - Count endpoint: /database/tableId/$count
* - Standard queries: /database/tableId
*/
export class QueryUrlBuilder {
private readonly databaseName: string;
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
private readonly occurrence: FMTable<any, any>;
private readonly useEntityIds: boolean;
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
constructor(databaseName: string, occurrence: FMTable<any, any>, useEntityIds: boolean) {
this.databaseName = databaseName;
this.occurrence = occurrence;
this.useEntityIds = useEntityIds;
}
/**
* Builds the full URL for a query request.
*
* @param queryString - The OData query string (e.g., "?$filter=...&$select=...")
* @param options - Options including whether this is a count query, useEntityIds override, and navigation config
*/
build(
queryString: string,
options: {
isCount?: boolean;
useEntityIds?: boolean;
navigation?: NavigationConfig;
},
): string {
return `/${this.databaseName}${this.buildPath(queryString, options)}`;
}
/**
* Builds a query string path (without database prefix) for getQueryString().
* Used when the full URL is not needed.
*/
buildPath(
queryString: string,
options?: { isCount?: boolean; useEntityIds?: boolean; navigation?: NavigationConfig },
): string {
const effectiveUseEntityIds = options?.useEntityIds ?? this.useEntityIds;
const navigation = options?.navigation;
const tableId = resolveTableId(this.occurrence, getTableName(this.occurrence), effectiveUseEntityIds);
const suffix = options?.isCount ? "/$count" : "";
if (navigation?.recordLocator !== undefined && navigation?.relation) {
const sourceTable = effectiveUseEntityIds
? (navigation.sourceTableEntityId ?? navigation.sourceTableName)
: navigation.sourceTableName;
const baseRelation = effectiveUseEntityIds
? (navigation.baseRelationEntityId ?? navigation.baseRelation)
: navigation.baseRelation;
const relation = effectiveUseEntityIds
? (navigation.relationEntityId ?? navigation.relation)
: navigation.relation;
const { recordLocator } = navigation;
if (recordLocator === undefined) {
throw new Error("recordLocator is required for record navigation");
}
const base = baseRelation
? buildRecordPath(`${sourceTable}/${baseRelation}`, recordLocator)
: buildRecordPath(sourceTable, recordLocator);
return queryString ? `/${base}/${relation}${suffix}${queryString}` : `/${base}/${relation}${suffix}`;
}
if (navigation?.relation) {
const sourceTable = effectiveUseEntityIds
? (navigation.sourceTableEntityId ?? navigation.sourceTableName)
: navigation.sourceTableName;
const basePath = effectiveUseEntityIds
? (navigation.basePathEntityId ?? navigation.basePath)
: navigation.basePath;
const relation = effectiveUseEntityIds
? (navigation.relationEntityId ?? navigation.relation)
: navigation.relation;
const base = basePath || sourceTable;
return queryString ? `/${base}/${relation}${suffix}${queryString}` : `/${base}/${relation}${suffix}`;
}
return queryString ? `/${tableId}${suffix}${queryString}` : `/${tableId}${suffix}`;
}
/**
* Build URL for record operations (single record by ID).
* Used by RecordBuilder to build URLs like /database/table('id').
*
* @param recordLocator - The record locator
* @param queryString - The OData query string (e.g., "?$select=...")
* @param options - Options including operation type and useEntityIds override
*/
buildRecordUrl(
recordLocator: RecordLocator,
queryString: string,
options?: {
operation?: "getSingleField";
operationParam?: string;
useEntityIds?: boolean;
isNavigateFromEntitySet?: boolean;
navigateSourceTableName?: string;
navigateRelation?: string;
},
): string {
const effectiveUseEntityIds = options?.useEntityIds ?? this.useEntityIds;
const tableId = resolveTableId(this.occurrence, getTableName(this.occurrence), effectiveUseEntityIds);
// Build the base URL depending on whether this came from a navigated EntitySet
let url: string;
if (options?.isNavigateFromEntitySet && options.navigateSourceTableName && options.navigateRelation) {
// From navigated EntitySet: /sourceTable/relation('recordId')
url = `/${this.databaseName}/${buildRecordPath(
`${options.navigateSourceTableName}/${options.navigateRelation}`,
recordLocator,
)}`;
} else {
// Normal record: /tableName('recordId') - use FMTID if configured
url = `/${this.databaseName}/${buildRecordPath(tableId, recordLocator)}`;
}
if (options?.operation === "getSingleField" && options.operationParam) {
url += `/${options.operationParam}`;
}
return url + queryString;
}
}