UNPKG

@proofkit/fmodata

Version:

FileMaker OData API client

859 lines (797 loc) 34.2 kB
/** biome-ignore-all lint/complexity/noBannedTypes: Empty object type represents no expands by default */ import type { FFetchOptions } from "@fetchkit/ffetch"; import { Effect } from "effect"; import { requestFromService, runLayerResult } from "../effect"; import { BuilderInvariantError } from "../errors"; import type { InternalLogger } from "../logger"; import type { Column } from "../orm/column"; import type { ExtractTableName, FMTable, InferSchemaOutputFromFMTable, ValidExpandTarget } from "../orm/table"; import { getNavigationPaths, getTableName, isUsingEntityIds } from "../orm/table"; import type { FMODataLayer, ODataConfig } from "../services"; import type { ConditionallyWithODataAnnotations, ConditionallyWithSpecialColumns, ExecutableBuilder, ExecuteMethodOptions, ExecuteOptions, NormalizeIncludeSpecialColumns, ODataFieldResponse, Result, } from "../types"; import { buildSelectExpandQueryString, cloneRecordReadBuilderState, createInitialRecordReadBuilderState, createODataRequest, ExpandBuilder, type ExpandConfig, type ExpandedRelations, mergeExecuteOptions, processRecordResponse, processSelectWithRenames, resolveTableId, } from "./builders/index"; import { buildRecordPath, type RecordLocator } from "./builders/mutation-helpers"; import { parseErrorResponse } from "./error-parser"; import type { ResolveExpandedRelations, SystemColumnsFromOption, SystemColumnsOption } from "./query/types"; import { QueryBuilder } from "./query-builder"; import { createClientRuntime } from "./runtime"; import { safeJsonParse } from "./sanitize-json"; /** * Extract the value type from a Column. * This uses the phantom type stored in Column to get the actual value type. */ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer type ExtractColumnType<C> = C extends Column<infer T, any> ? T : never; /** * Map a select object to its return type. * For each key in the select object, extract the type from the corresponding Column. */ type MapSelectToReturnType< // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration TSelect extends Record<string, Column<any, any, any, any>>, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any schema shape _TSchema extends Record<string, any>, > = { [K in keyof TSelect]: ExtractColumnType<TSelect[K]>; }; // Return type for RecordBuilder execute export type RecordReturnType< // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any schema shape Schema extends Record<string, any>, IsSingleField extends boolean, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration FieldColumn extends Column<any, any, any, any> | undefined, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration, accepts any FMTable Selected extends keyof Schema | Record<string, Column<any, any, ExtractTableName<FMTable<any, any>>>>, Expands extends ExpandedRelations, SystemCols extends SystemColumnsOption | undefined = undefined, > = IsSingleField extends true ? // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer FieldColumn extends Column<infer TOutput, any, any, any> ? TOutput : never : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration [Selected] extends [Record<string, Column<any, any, any, any>>] ? MapSelectToReturnType<Selected, Schema> & ResolveExpandedRelations<Expands> & SystemColumnsFromOption<SystemCols> : // Use tuple wrapping to prevent distribution over union of keys [Selected] extends [keyof Schema] ? Pick<Schema, Selected> & ResolveExpandedRelations<Expands> & SystemColumnsFromOption<SystemCols> : never; type RecordBuilderHasSelect< // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration Occ extends FMTable<any, any>, IsSingleField extends boolean, Selected, > = IsSingleField extends true ? false : // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration Selected extends Record<string, Column<any, any, any>> ? true : Selected extends keyof InferSchemaOutputFromFMTable<NonNullable<Occ>> ? false : true; type ExecutableRecordBuilderReturn< // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration Occ extends FMTable<any, any>, IsSingleField extends boolean, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration FieldColumn extends Column<any, any, any, any> | undefined, Selected extends | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>> // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration | Record<string, Column<any, any, ExtractTableName<NonNullable<Occ>>>>, Expands extends ExpandedRelations, SystemCols extends SystemColumnsOption | undefined, > = | ConditionallyWithODataAnnotations< ConditionallyWithSpecialColumns< RecordReturnType< InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands, SystemCols >, true, RecordBuilderHasSelect<Occ, IsSingleField, Selected> >, true > | ConditionallyWithODataAnnotations< ConditionallyWithSpecialColumns< RecordReturnType< InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands, SystemCols >, true, RecordBuilderHasSelect<Occ, IsSingleField, Selected> >, false > | ConditionallyWithODataAnnotations< ConditionallyWithSpecialColumns< RecordReturnType< InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands, SystemCols >, false, RecordBuilderHasSelect<Occ, IsSingleField, Selected> >, true > | ConditionallyWithODataAnnotations< ConditionallyWithSpecialColumns< RecordReturnType< InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands, SystemCols >, false, RecordBuilderHasSelect<Occ, IsSingleField, Selected> >, false >; export class RecordBuilder< // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, default allows untyped tables Occ extends FMTable<any, any> = FMTable<any, any>, IsSingleField extends boolean = false, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration FieldColumn extends Column<any, any, any, any> | undefined = undefined, Selected extends | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>> // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration | Record<string, Column<any, any, ExtractTableName<NonNullable<Occ>>>> = keyof InferSchemaOutputFromFMTable< NonNullable<Occ> >, Expands extends ExpandedRelations = {}, DatabaseIncludeSpecialColumns extends boolean = false, SystemCols extends SystemColumnsOption | undefined = undefined, > implements ExecutableBuilder<ExecutableRecordBuilderReturn<Occ, IsSingleField, FieldColumn, Selected, Expands, SystemCols>> { private readonly table: Occ; private readonly recordLocator: RecordLocator; private readonly operation?: "getSingleField" | "navigate"; private readonly operationParam?: string; // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration private readonly operationColumn?: Column<any, any, any, any>; private readonly isNavigateFromEntitySet?: boolean; private readonly navigateRelation?: string; private readonly navigateRelationEntityId?: string; private readonly navigateSourceTableName?: string; private readonly navigateSourceTableEntityId?: string; private readState = createInitialRecordReadBuilderState(); private readonly layer: FMODataLayer; private readonly config: ODataConfig; private readonly logger: InternalLogger; // Compatibility accessors for internal modules that inspect builder internals via `as any`. private get selectedFields(): string[] | undefined { return this.readState.selectedFields; } private set selectedFields(selectedFields: string[] | undefined) { this.readState = cloneRecordReadBuilderState(this.readState, { selectedFields, }); } private get expandConfigs(): ExpandConfig[] { return this.readState.expandConfigs; } private set expandConfigs(expandConfigs: ExpandConfig[]) { this.readState = cloneRecordReadBuilderState(this.readState, { expandConfigs, }); } private get fieldMapping(): Record<string, string> | undefined { return this.readState.fieldMapping; } private set fieldMapping(fieldMapping: Record<string, string> | undefined) { this.readState = cloneRecordReadBuilderState(this.readState, { fieldMapping, }); } private get systemColumns(): SystemColumnsOption | undefined { return this.readState.systemColumns; } private set systemColumns(systemColumns: SystemColumnsOption | undefined) { this.readState = cloneRecordReadBuilderState(this.readState, { systemColumns, }); } constructor(config: { occurrence: Occ; layer: FMODataLayer; recordLocator: RecordLocator; }) { this.table = config.occurrence; this.recordLocator = config.recordLocator; const runtime = createClientRuntime(config.layer); this.layer = runtime.layer; this.config = runtime.config; this.logger = runtime.logger; } /** * Helper to merge database-level useEntityIds and includeSpecialColumns with per-request options */ private mergeExecuteOptions(options?: RequestInit & FFetchOptions & ExecuteOptions): RequestInit & FFetchOptions & { useEntityIds?: boolean; includeSpecialColumns?: boolean; } { const merged = mergeExecuteOptions(options, this.config.useEntityIds); return { ...merged, includeSpecialColumns: options?.includeSpecialColumns ?? this.config.includeSpecialColumns, }; } /** * Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name * @param useEntityIds - Optional override for entity ID usage */ private getTableId(useEntityIds?: boolean): string { if (!this.table) { throw new BuilderInvariantError("RecordBuilder", "table occurrence is required"); } return resolveTableId(this.table, getTableName(this.table), useEntityIds ?? this.config.useEntityIds); } /** * Creates a new RecordBuilder with modified configuration. * Used by select() to create new instances. */ private cloneWithChanges< NewSelected extends | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>> // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration | Record<string, Column<any, any, ExtractTableName<NonNullable<Occ>>>> = Selected, NewSystemCols extends SystemColumnsOption | undefined = SystemCols, >(changes: { selectedFields?: string[]; fieldMapping?: Record<string, string>; systemColumns?: NewSystemCols; }): RecordBuilder<Occ, false, FieldColumn, NewSelected, Expands, DatabaseIncludeSpecialColumns, NewSystemCols> { const newBuilder = new RecordBuilder< Occ, false, FieldColumn, NewSelected, Expands, DatabaseIncludeSpecialColumns, NewSystemCols >({ occurrence: this.table, layer: this.layer, recordLocator: this.recordLocator, }); // Use type assertion to allow assignment to readonly properties on new instance // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern const mutableBuilder = newBuilder as any; mutableBuilder.readState = cloneRecordReadBuilderState(this.readState, { selectedFields: "selectedFields" in changes ? changes.selectedFields : this.selectedFields, fieldMapping: "fieldMapping" in changes ? changes.fieldMapping : this.fieldMapping, systemColumns: changes.systemColumns !== undefined ? changes.systemColumns : this.systemColumns, expandConfigs: this.expandConfigs, }); // Preserve navigation context mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet; mutableBuilder.navigateRelation = this.navigateRelation; mutableBuilder.navigateRelationEntityId = this.navigateRelationEntityId; mutableBuilder.navigateSourceTableName = this.navigateSourceTableName; mutableBuilder.navigateSourceTableEntityId = this.navigateSourceTableEntityId; mutableBuilder.operationColumn = this.operationColumn; return newBuilder; } // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration getSingleField<TColumn extends Column<any, any, ExtractTableName<NonNullable<Occ>>, any>>( column: TColumn, ): RecordBuilder< Occ, true, TColumn, keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>, {}, DatabaseIncludeSpecialColumns > { // Runtime validation: ensure column is from the correct table const tableName = getTableName(this.table); if (!column.isFromTable(tableName)) { throw new BuilderInvariantError( "RecordBuilder.getSingleField", `column ${column.toString()} is not from table ${tableName}`, ); } const newBuilder = new RecordBuilder< Occ, true, TColumn, keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>, {}, DatabaseIncludeSpecialColumns >({ occurrence: this.table, layer: this.layer, recordLocator: this.recordLocator, }); // Use type assertion to allow assignment to readonly properties on new instance // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern const mutableBuilder = newBuilder as any; mutableBuilder.operation = "getSingleField"; mutableBuilder.operationColumn = column; // Preserve navigation context mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet; mutableBuilder.navigateRelation = this.navigateRelation; mutableBuilder.navigateSourceTableName = this.navigateSourceTableName; return newBuilder; } /** * Select fields using column references, or pass "all" to clear any defaultSelect and fetch all fields. * Allows renaming fields by using different keys in the object. * Container fields cannot be selected and will cause a type error. * * @example * db.from(contacts).get("uuid").select({ * name: contacts.name, * userEmail: contacts.email // renamed! * }) * * @example * // Include system columns (ROWID, ROWMODID) when using select() * db.from(contacts).get("uuid").select( * { name: contacts.name }, * { ROWID: true, ROWMODID: true } * ) * * @example * // Override defaultSelect to fetch all fields * db.from(contacts).get("uuid").select("all") * * @param fields - Object mapping output keys to column references (container fields excluded), or "all" to select all fields * @param systemColumns - Optional object to request system columns (ROWID, ROWMODID) * @returns RecordBuilder with updated selected fields */ select( fields: "all", ): RecordBuilder< Occ, false, FieldColumn, keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>, Expands, DatabaseIncludeSpecialColumns, undefined >; select< // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration TSelect extends Record<string, Column<any, any, ExtractTableName<Occ>, false>>, TSystemCols extends SystemColumnsOption = {}, >( fields: TSelect, systemColumns?: TSystemCols, ): RecordBuilder<Occ, false, FieldColumn, TSelect, Expands, DatabaseIncludeSpecialColumns, TSystemCols>; // biome-ignore lint/suspicious/noExplicitAny: Implementation signature hidden from callers select(fields: any, systemColumns?: any): any { if (fields === "all") { return this.cloneWithChanges({ selectedFields: undefined, fieldMapping: undefined, // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter systemColumns: undefined as any, }); } const tableName = getTableName(this.table); const { selectedFields, fieldMapping } = processSelectWithRenames(fields, tableName, this.logger); // Add system columns to selectedFields if requested const finalSelectedFields = [...selectedFields]; if (systemColumns?.ROWID) { finalSelectedFields.push("ROWID"); } if (systemColumns?.ROWMODID) { finalSelectedFields.push("ROWMODID"); } return this.cloneWithChanges({ selectedFields: finalSelectedFields, fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined, // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter systemColumns: systemColumns as any, // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type }) as any; } /** * Expand a navigation property to include related records. * Supports nested select, filter, orderBy, and expand operations. * * @example * ```typescript * // Simple expand with FMTable object * const contact = await db.from(contacts).get("uuid").expand(users).execute(); * * // Expand with select * const contact = await db.from(contacts).get("uuid") * .expand(users, b => b.select({ username: users.username, email: users.email })) * .execute(); * ``` */ expand< // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration TargetTable extends FMTable<any, any>, TSelected extends | keyof InferSchemaOutputFromFMTable<TargetTable> | Record< string, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration Column<any, any, ExtractTableName<TargetTable>> > = keyof InferSchemaOutputFromFMTable<TargetTable>, TNestedExpands extends ExpandedRelations = {}, >( targetTable: ValidExpandTarget<Occ, TargetTable>, callback?: ( builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}, false>, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands, any, any>, ): RecordBuilder< Occ, false, FieldColumn, Selected, Expands & { [K in ExtractTableName<TargetTable>]: { schema: InferSchemaOutputFromFMTable<TargetTable>; selected: TSelected; nested: TNestedExpands; }; }, DatabaseIncludeSpecialColumns, SystemCols > { // Create new builder with updated types // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExpandedRelations const newBuilder = new RecordBuilder<Occ, false, FieldColumn, Selected, any, DatabaseIncludeSpecialColumns>({ occurrence: this.table, layer: this.layer, recordLocator: this.recordLocator, }); // Use type assertion to allow assignment to readonly properties on new instance // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern const mutableBuilder = newBuilder as any; // Copy existing state mutableBuilder.readState = cloneRecordReadBuilderState(this.readState, { selectedFields: this.selectedFields, fieldMapping: this.fieldMapping, systemColumns: this.systemColumns, expandConfigs: this.expandConfigs, }); mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet; mutableBuilder.navigateRelation = this.navigateRelation; mutableBuilder.navigateRelationEntityId = this.navigateRelationEntityId; mutableBuilder.navigateSourceTableName = this.navigateSourceTableName; mutableBuilder.navigateSourceTableEntityId = this.navigateSourceTableEntityId; mutableBuilder.operationColumn = this.operationColumn; // Use ExpandBuilder.processExpand to handle the expand logic const expandBuilder = new ExpandBuilder(this.config.useEntityIds, this.logger); type TargetBuilder = QueryBuilder< TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}, false >; const expandConfig = expandBuilder.processExpand<TargetTable, TargetBuilder>( targetTable, this.table ?? undefined, callback as ((builder: TargetBuilder) => TargetBuilder) | undefined, () => // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration new QueryBuilder<TargetTable, any, any, any, any, any, DatabaseIncludeSpecialColumns, undefined>({ occurrence: targetTable, layer: this.layer, }), ); mutableBuilder.readState = cloneRecordReadBuilderState(mutableBuilder.readState, { expandConfigs: [...this.expandConfigs, expandConfig], }); // biome-ignore lint/suspicious/noExplicitAny: Type assertion needed as expand changes generic parameters in complex way that TypeScript cannot infer return newBuilder as any; } // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration navigate<TargetTable extends FMTable<any, any>>( targetTable: ValidExpandTarget<Occ, TargetTable>, ): QueryBuilder< TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}, false, DatabaseIncludeSpecialColumns, undefined > { // Extract name and validate const relationName = getTableName(targetTable); // Runtime validation: Check if relation name is in navigationPaths if (this.table) { const navigationPaths = getNavigationPaths(this.table); if (navigationPaths && !navigationPaths.includes(relationName)) { this.logger.warn( `Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`, ); } } // Create QueryBuilder with target table // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration const builder = new QueryBuilder<TargetTable, any, any, any, any, any, DatabaseIncludeSpecialColumns, undefined>({ occurrence: targetTable, layer: this.layer, }); // Store the navigation info - resolve entity ID for relation if needed const relationEntityId = isUsingEntityIds(targetTable) ? resolveTableId(targetTable, relationName, true) : relationName; // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path let sourceTableName: string; let sourceTableEntityId: string; let baseRelation: string | undefined; let baseRelationEntityId: string | undefined; if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) { // Build the base path: /sourceTable/relation('recordId')/newRelation sourceTableName = this.navigateSourceTableName; sourceTableEntityId = this.navigateSourceTableEntityId ?? sourceTableName; baseRelation = this.navigateRelation; baseRelationEntityId = this.navigateRelationEntityId ?? baseRelation; } else { // Normal record navigation: /tableName('recordId')/relation // Use table ID if available, otherwise table name if (!this.table) { throw new BuilderInvariantError("RecordBuilder.navigate", "table occurrence is required for navigation"); } sourceTableName = getTableName(this.table); sourceTableEntityId = isUsingEntityIds(this.table) ? resolveTableId(this.table, sourceTableName, true) : sourceTableName; } // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern (builder as any).navigation = { recordLocator: this.recordLocator, relation: relationName, relationEntityId, sourceTableName, sourceTableEntityId, baseRelation, baseRelationEntityId, }; return builder; } /** * Builds the complete query string including $select and $expand parameters. */ private buildQueryString(includeSpecialColumns?: boolean, useEntityIds?: boolean): string { // Use merged includeSpecialColumns if provided, otherwise use database-level default const finalIncludeSpecialColumns = includeSpecialColumns ?? this.config.includeSpecialColumns; // Use merged useEntityIds if provided, otherwise use database-level default const finalUseEntityIds = useEntityIds ?? this.config.useEntityIds; return buildSelectExpandQueryString({ selectedFields: this.selectedFields, expandConfigs: this.expandConfigs, table: this.table, useEntityIds: finalUseEntityIds, logger: this.logger, includeSpecialColumns: finalIncludeSpecialColumns, }); } private buildRecordResourcePath(useEntityIds: boolean): string { if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) { const sourceSegment = useEntityIds ? (this.navigateSourceTableEntityId ?? this.navigateSourceTableName) : this.navigateSourceTableName; const relationSegment = useEntityIds ? (this.navigateRelationEntityId ?? this.navigateRelation) : this.navigateRelation; return `/${buildRecordPath(`${sourceSegment}/${relationSegment}`, this.recordLocator)}`; } const tableId = this.getTableId(useEntityIds); return `/${buildRecordPath(tableId, this.recordLocator)}`; } execute<EO extends ExecuteOptions>( options?: ExecuteMethodOptions<EO>, ): Promise< Result< ConditionallyWithODataAnnotations< ConditionallyWithSpecialColumns< RecordReturnType< InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands, SystemCols >, // Use the merged value: if explicitly provided in options, use that; otherwise use database default NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"], DatabaseIncludeSpecialColumns>, // Check if select was applied: if Selected is Record (object select) or a subset of keys, select was applied IsSingleField extends true ? false // Single field operations don't include special columns : // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration Selected extends Record<string, Column<any, any, any>> ? true : Selected extends keyof InferSchemaOutputFromFMTable<NonNullable<Occ>> ? false : true >, EO["includeODataAnnotations"] extends true ? true : false > > > { const mergedOptions = this.mergeExecuteOptions(options); let url = `/${this.config.databaseName}${this.buildRecordResourcePath( mergedOptions.useEntityIds ?? this.config.useEntityIds, )}`; if (this.operation === "getSingleField" && this.operationColumn) { url += `/${this.operationColumn.getFieldIdentifier(mergedOptions.useEntityIds)}`; } else if (this.operation === "getSingleField" && this.operationParam) { url += `/${this.operationParam}`; } else { // Add query string for select/expand (only when not getting a single field) const queryString = this.buildQueryString(mergedOptions.includeSpecialColumns, mergedOptions.useEntityIds); url += queryString; } const pipeline = Effect.gen(this, function* () { // Make GET request via DI // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API const response = yield* requestFromService<any>(url, { method: "GET", ...mergedOptions, }); // Handle single field operation if (this.operation === "getSingleField") { // Single field returns a JSON object with @context and value // The type is extracted from the Column stored in FieldColumn generic // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API const fieldResponse = response as ODataFieldResponse<any>; // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type extraction return fieldResponse.value as any; } const result = yield* Effect.tryPromise({ try: () => processRecordResponse(response, { table: this.table, selectedFields: this.selectedFields, expandConfigs: this.expandConfigs, skipValidation: options?.skipValidation, useEntityIds: mergedOptions.useEntityIds, includeSpecialColumns: mergedOptions.includeSpecialColumns, fieldMapping: this.fieldMapping, logger: this.logger, }), catch: (e) => (e instanceof Error ? e : new Error(String(e))), }); if (result.error) { return yield* Effect.fail(result.error); } return result.data; }); const result = runLayerResult(this.layer, pipeline, "fmodata.record.get", { "fmodata.table": getTableName(this.table), }); // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type return result as Promise<Result<any>>; } // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value getRequestConfig(): { method: string; url: string; body?: any } { let url = `/${this.config.databaseName}${this.buildRecordResourcePath(this.config.useEntityIds)}`; if (this.operation === "getSingleField" && this.operationColumn) { // Use the column's getFieldIdentifier to support entity IDs url += `/${this.operationColumn.getFieldIdentifier(this.config.useEntityIds)}`; } else if (this.operation === "getSingleField" && this.operationParam) { // Fallback for backwards compatibility (shouldn't happen in normal flow) url += `/${this.operationParam}`; } else { // Add query string for select/expand (only when not getting a single field) const queryString = this.buildQueryString(); url += queryString; } return { method: "GET", url, }; } /** * Returns the query string for this record builder (for testing purposes). */ getQueryString(options?: { useEntityIds?: boolean }): string { const useEntityIds = options?.useEntityIds ?? this.config.useEntityIds; const path = this.buildRecordResourcePath(useEntityIds); if (this.operation === "getSingleField" && this.operationColumn) { return `${path}/${this.operationColumn.getFieldIdentifier(useEntityIds)}`; } if (this.operation === "getSingleField" && this.operationParam) { // Fallback for backwards compatibility (shouldn't happen in normal flow) return `${path}/${this.operationParam}`; } const queryString = this.buildQueryString(undefined, useEntityIds); return `${path}${queryString}`; } toRequest(baseUrl: string, options?: ExecuteOptions): Request { const config = this.getRequestConfig(); return createODataRequest(baseUrl, config, { ...options, normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName, }); } async processResponse( response: Response, options?: ExecuteOptions, ): Promise< Result< RecordReturnType< InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands, SystemCols > > > { // Check for error responses (important for batch operations) if (!response.ok) { const tableName = this.table ? getTableName(this.table) : "unknown"; const error = await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${tableName}`); return { data: undefined, error }; } // Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values const rawResponse = await safeJsonParse(response); // Handle single field operation if (this.operation === "getSingleField") { // Single field returns a JSON object with @context and value // The type is extracted from the Column stored in FieldColumn generic // biome-ignore lint/suspicious/noExplicitAny: Type parameter inferred from FieldColumn generic const fieldResponse = rawResponse as ODataFieldResponse<any>; // biome-ignore lint/suspicious/noExplicitAny: Type parameter inferred from FieldColumn generic return { data: fieldResponse.value as any, error: undefined }; } // Use shared response processor const mergedOptions = this.mergeExecuteOptions(options); return processRecordResponse(rawResponse, { table: this.table, selectedFields: this.selectedFields, expandConfigs: this.expandConfigs, skipValidation: options?.skipValidation, useEntityIds: mergedOptions.useEntityIds, includeSpecialColumns: mergedOptions.includeSpecialColumns, fieldMapping: this.fieldMapping, logger: this.logger, }); } }