UNPKG

@proofkit/fmodata

Version:

FileMaker OData API client

371 lines (325 loc) 12.6 kB
import type { FFetchOptions } from "@fetchkit/ffetch"; import type { StandardSchemaV1 } from "@standard-schema/spec"; export interface BasicAuth { username: string; password: string; } export interface ApiKeyAuth { apiKey: string; } export interface ClarisIdAuth { clarisId: { username: string; password: string; }; } export type Auth = BasicAuth | ApiKeyAuth | ClarisIdAuth; export interface CountedListResult<T> { records: T[]; count: number; } export interface ExecutableBuilder<T> { execute(options?: ExecuteOptions): Promise<Result<T>>; // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value getRequestConfig(): { method: string; url: string; body?: any }; /** * Convert this builder to a native Request object for batch processing. * @param baseUrl - The base URL for the OData service * @param options - Optional execution options (e.g., includeODataAnnotations) * @returns A native Request object */ toRequest(baseUrl: string, options?: ExecuteOptions): Request; /** * Process a raw Response object into a typed Result. * This allows builders to apply their own validation and transformation logic. * @param response - The native Response object from the batch operation * @param options - Optional execution options (e.g., skipValidation, includeODataAnnotations) * @returns A typed Result with the builder's expected return type */ processResponse(response: Response, options?: ExecuteOptions): Promise<Result<T>>; } export interface ExecutionContext { /** * @internal * Returns the Effect Layer for this context, enabling service-based Effect pipelines. * All HTTP requests are made through the Layer's HttpClient service. */ _getLayer?(): import("./services").FMODataLayer; } export type InferSchemaType<Schema extends Record<string, StandardSchemaV1>> = { // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer [K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output> ? Output : never; }; export type WithSpecialColumns<T> = // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape T extends Record<string, any> ? T & { ROWID: number; ROWMODID: number; } : never; // Helper type to exclude special columns from a union of keys // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any key type export type ExcludeSystemFields<T extends keyof any> = Exclude<T, "ROWID" | "ROWMODID">; // OData record metadata fields (present on each record) export interface ODataRecordMetadata { "@id": string; "@editLink": string; } // OData response wrapper (top-level, internal use only) export interface ODataListResponse<T> { "@context": string; value: (T & ODataRecordMetadata)[]; } export type ODataSingleResponse<T> = T & ODataRecordMetadata & { "@context": string; }; // OData response for single field values export interface ODataFieldResponse<T> { "@context": string; value: T; } // Result pattern for execute responses export type Result<T, E = import("./errors").FMODataErrorType> = | { data: T; error: undefined } | { data: undefined; error: E }; // Batch operation result types export interface BatchItemResult<T> { data: T | undefined; error: import("./errors").FMODataErrorType | undefined; status: number; // HTTP status code (0 for truncated) } // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any tuple type export interface BatchResult<T extends readonly any[]> { results: { [K in keyof T]: BatchItemResult<T[K]> }; successCount: number; errorCount: number; truncated: boolean; firstErrorIndex: number | null; } // Make specific keys required, rest optional export type MakeFieldsRequired<T, Keys extends keyof T> = Partial<T> & Required<Pick<T, Keys>>; // Extract keys from schema where validator doesn't allow null/undefined (auto-required fields) export type AutoRequiredKeys<Schema extends Record<string, StandardSchemaV1>> = { [K in keyof Schema]: Extract<StandardSchemaV1.InferOutput<Schema[K]>, null | undefined> extends never ? K : never; }[keyof Schema]; // Helper type to compute excluded fields (readOnly fields + idField) export type ExcludedFields< // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any key type IdField extends keyof any | undefined, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any array type ReadOnly extends readonly any[], // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any key type > = IdField extends keyof any ? IdField | ReadOnly[number] : ReadOnly[number]; // Helper type for InsertData computation type _ComputeInsertData< Schema extends Record<string, StandardSchemaV1>, IdField extends keyof Schema | undefined, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any array type Required extends readonly any[], // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any array type ReadOnly extends readonly any[], > = [Required[number]] extends [keyof InferSchemaType<Schema>] ? Required extends readonly (keyof InferSchemaType<Schema>)[] ? MakeFieldsRequired< Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>, Exclude<AutoRequiredKeys<Schema> | Required[number], ExcludedFields<IdField, ReadOnly>> > : MakeFieldsRequired< Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>, Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>> > : MakeFieldsRequired< Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>, Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>> >; /** * Configuration for automatic retry of transient errors. * Uses exponential backoff with optional jitter. */ export interface RetryPolicy { /** * Maximum number of retry attempts (default: 3) */ maxRetries?: number; /** * Base delay in milliseconds for exponential backoff (default: 500) */ baseDelay?: number; /** * Whether to add random jitter to delay to prevent thundering herd (default: true) */ jitter?: boolean; } export interface ExecuteOptions { includeODataAnnotations?: boolean; skipValidation?: boolean; /** * Overrides the default behavior of the database to normalize the database name in THIS REQUEST ONLY. * Defaults to true at the database level. */ normalizeDatabaseName?: boolean; /** * Overrides the default behavior of the database to use entity IDs (rather than field names) in THIS REQUEST ONLY */ useEntityIds?: boolean; /** * Overrides the default behavior of the database to include special columns (ROWID and ROWMODID) in THIS REQUEST ONLY. * Note: Special columns are only included when there is no $select query. */ includeSpecialColumns?: boolean; /** * Optional retry policy for transient errors (SchemaLockedError, NetworkError, TimeoutError, HTTP 5xx). * When set, failed requests matching transient error conditions will be retried * with exponential backoff. */ retryPolicy?: RetryPolicy; } /** * Type for the fetchHandler callback function. * This is a convenience type export that matches the fetchHandler signature in FFetchOptions. * * @example * ```typescript * import type { FetchHandler } from '@proofkit/fmodata'; * * const myFetchHandler: FetchHandler = (input, init) => { * console.log('Custom fetch:', input); * return fetch(input, init); * }; * * await query.execute({ * fetchHandler: myFetchHandler * }); * ``` */ export type FetchHandler = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>; /** * Combined type for execute() method options. * * Uses FFetchOptions from @fetchkit/ffetch to ensure proper type inference. * FFetchOptions is re-exported in the package to ensure type availability in consuming packages. */ export type ExecuteMethodOptions<EO extends ExecuteOptions = ExecuteOptions> = RequestInit & FFetchOptions & ExecuteOptions & EO; /** * Get the Accept header value based on includeODataAnnotations option * @param includeODataAnnotations - Whether to include OData annotations * @returns Accept header value */ export function getAcceptHeader(includeODataAnnotations?: boolean): string { return includeODataAnnotations === true ? "application/json" : "application/json;odata.metadata=none"; } type WithODataAnnotations<T> = T extends CountedListResult<infer U> ? CountedListResult<WithODataAnnotations<U>> : T extends readonly (infer U)[] ? WithODataAnnotations<U>[] : // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape T extends Record<string, any> ? T & ODataRecordMetadata : T; export type ConditionallyWithODataAnnotations< T, IncludeODataAnnotations extends boolean, > = IncludeODataAnnotations extends true ? WithODataAnnotations<T> : T; /** * Normalizes includeSpecialColumns with a database-level default. * Uses distributive conditional types to handle unions correctly. * @template IncludeSpecialColumns - The includeSpecialColumns value from execute options * @template DatabaseDefault - The database-level includeSpecialColumns setting (defaults to false) */ export type NormalizeIncludeSpecialColumns< IncludeSpecialColumns extends boolean | undefined, DatabaseDefault extends boolean = false, > = [IncludeSpecialColumns] extends [true] ? true : [IncludeSpecialColumns] extends [false] ? false : DatabaseDefault; // When undefined, use database-level default type WithSpecialColumnsDeep<T> = T extends CountedListResult<infer U> ? CountedListResult<WithSpecialColumnsDeep<U>> : T extends readonly (infer U)[] ? WithSpecialColumnsDeep<U>[] : // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape T extends Record<string, any> ? T & { ROWID: number; ROWMODID: number; } : T; /** * Conditionally adds ROWID and ROWMODID special columns to a type. * Special columns are only included when: * - includeSpecialColumns is true AND * - hasSelect is false (no $select query was applied) AND * - T is an object type (not a primitive like string or number) * * Handles both single objects and arrays of objects. */ export type ConditionallyWithSpecialColumns< T, IncludeSpecialColumns extends boolean, HasSelect extends boolean, > = IncludeSpecialColumns extends true ? (HasSelect extends false ? WithSpecialColumnsDeep<T> : T) : T; // Helper type to extract schema from a FMTable export type ExtractSchemaFromOccurrence<Occ> = Occ extends { baseTable: { schema: infer S }; } ? S extends Record<string, StandardSchemaV1> ? S : Record<string, StandardSchemaV1> : Record<string, StandardSchemaV1>; export interface GenericFieldMetadata { $Nullable?: boolean; "@Index"?: boolean; "@Calculation"?: boolean; "@Summary"?: boolean; "@Global"?: boolean; "@Org.OData.Core.V1.Permissions"?: "Org.OData.Core.V1.Permission@Read"; } export type StringFieldMetadata = GenericFieldMetadata & { $Type: "Edm.String"; $DefaultValue?: "USER" | "USERNAME" | "CURRENT_USER"; $MaxLength?: number; }; export type DecimalFieldMetadata = GenericFieldMetadata & { $Type: "Edm.Decimal"; "@AutoGenerated"?: boolean; }; export type DateFieldMetadata = GenericFieldMetadata & { $Type: "Edm.Date"; $DefaultValue?: "CURDATE" | "CURRENT_DATE"; }; export type TimeOfDayFieldMetadata = GenericFieldMetadata & { $Type: "Edm.TimeOfDay"; $DefaultValue?: "CURTIME" | "CURRENT_TIME"; }; export type DateTimeOffsetFieldMetadata = GenericFieldMetadata & { $Type: "Edm.Date"; $DefaultValue?: "CURTIMESTAMP" | "CURRENT_TIMESTAMP"; "@VersionId"?: boolean; }; export interface StreamFieldMetadata { $Type: "Edm.Stream"; $Nullable?: boolean; "@EnclosedPath": string; "@ExternalOpenPath": string; "@ExternalSecurePath"?: string; } export type FieldMetadata = | StringFieldMetadata | DecimalFieldMetadata | DateFieldMetadata | TimeOfDayFieldMetadata | DateTimeOffsetFieldMetadata | StreamFieldMetadata; export type EntityType = { $Kind: "EntityType"; $Key: string[]; } & Record<string, FieldMetadata>; export interface EntitySet { $Kind: "EntitySet"; $Type: string; } export type Metadata = Record<string, EntityType | EntitySet>;