UNPKG

@proofkit/fmodata

Version:

FileMaker OData API client

5,265 lines 198 kB
#!/usr/bin/env node
import { createRequire } from "node:module";
import { Command } from "commander";
import createClient, { AbortError, CircuitOpenError, NetworkError, RetryLimitError, TimeoutError } from "@fetchkit/ffetch";
import { Context, Effect, Layer, Schedule } from "effect";
import { get } from "es-toolkit/compat";
import { AuthenticationDetails, CognitoUser, CognitoUserPool } from "amazon-cognito-identity-js";
import buildQuery from "odata-query";
import Table from "cli-table3";
//#region src/errors.ts
/**
* Base class for all fmodata errors
*/
var FMODataError = class extends Error {
	timestamp;
	constructor(message, options) {
		super(message, options);
		this.name = this.constructor.name;
		this.timestamp = /* @__PURE__ */ new Date();
	}
};
var HTTPError = class extends FMODataError {
	kind = "HTTPError";
	url;
	status;
	statusText;
	response;
	constructor(url, status, statusText, response) {
		super(`HTTP ${status} ${statusText} for ${url}`);
		this.url = url;
		this.status = status;
		this.statusText = statusText;
		this.response = response;
	}
	is4xx() {
		return this.status >= 400 && this.status < 500;
	}
	is5xx() {
		return this.status >= 500 && this.status < 600;
	}
	isNotFound() {
		return this.status === 404;
	}
	isUnauthorized() {
		return this.status === 401;
	}
	isForbidden() {
		return this.status === 403;
	}
};
var ODataError = class extends FMODataError {
	kind = "ODataError";
	url;
	code;
	details;
	constructor(url, message, code, details) {
		super(`OData error: ${message}`);
		this.url = url;
		this.code = code;
		this.details = details;
	}
};
var SchemaLockedError = class extends FMODataError {
	kind = "SchemaLockedError";
	url;
	code;
	details;
	constructor(url, message, details) {
		super(`OData error: ${message}`);
		this.url = url;
		this.code = "303";
		this.details = details;
	}
};
var ValidationError = class extends FMODataError {
	kind = "ValidationError";
	field;
	issues;
	value;
	constructor(message, issues, options) {
		super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
		this.field = options?.field;
		this.issues = issues;
		this.value = options?.value;
	}
};
var ResponseStructureError = class extends FMODataError {
	kind = "ResponseStructureError";
	expected;
	received;
	constructor(expected, received) {
		super(`Invalid response structure: expected ${expected}`);
		this.expected = expected;
		this.received = received;
	}
};
var RecordCountMismatchError = class extends FMODataError {
	kind = "RecordCountMismatchError";
	expected;
	received;
	constructor(expected, received) {
		super(`Expected ${typeof expected === "number" ? expected : expected} record(s), but received ${received}`);
		this.expected = expected;
		this.received = received;
	}
};
var InvalidLocationHeaderError = class extends FMODataError {
	kind = "InvalidLocationHeaderError";
	locationHeader;
	constructor(message, locationHeader) {
		super(message);
		this.locationHeader = locationHeader;
	}
};
var ResponseParseError = class extends FMODataError {
	kind = "ResponseParseError";
	url;
	rawText;
	constructor(url, message, options) {
		super(message, options?.cause ? { cause: options.cause } : void 0);
		this.url = url;
		this.rawText = options?.rawText;
	}
};
var BatchTruncatedError = class extends FMODataError {
	kind = "BatchTruncatedError";
	operationIndex;
	failedAtIndex;
	constructor(operationIndex, failedAtIndex) {
		super(`Operation ${operationIndex} was not executed because operation ${failedAtIndex} failed`);
		this.operationIndex = operationIndex;
		this.failedAtIndex = failedAtIndex;
	}
};
var MissingLayerServiceError = class extends FMODataError {
	kind = "MissingLayerServiceError";
	service;
	constructor(service, options) {
		super(`Required layer service "${service}" is not available`, options?.cause ? { cause: options.cause } : void 0);
		this.service = service;
	}
};
var MetadataNotFoundError = class extends FMODataError {
	kind = "MetadataNotFoundError";
	databaseName;
	constructor(databaseName) {
		super(`Metadata for database "${databaseName}" not found in response`);
		this.databaseName = databaseName;
	}
};
var BuilderInvariantError = class extends FMODataError {
	kind = "BuilderInvariantError";
	builder;
	constructor(builder, message, options) {
		super(`${builder} invariant violation: ${message}`, options?.cause ? { cause: options.cause } : void 0);
		this.builder = builder;
	}
};
var SchemaValidationFailedError = class extends FMODataError {
	kind = "SchemaValidationFailedError";
	operation;
	issues;
	constructor(operation, message, options) {
		super(`${operation} schema validation failed: ${message}`, options?.cause ? { cause: options.cause } : void 0);
		this.operation = operation;
		this.issues = options?.issues;
	}
};
function isFMODataError(error) {
	return error instanceof FMODataError;
}
/**
* Determines whether an error is transient and safe to retry.
* Transient errors include:
* - SchemaLockedError (FM code 303 — file locked temporarily)
* - NetworkError (connection issues)
* - TimeoutError (request timed out)
* - HTTP 5xx errors (server-side failures)
*/
function isTransientError(error) {
	if (error instanceof SchemaLockedError) return true;
	if (error && typeof error === "object") {
		const name = Reflect.get(error, "name");
		if (typeof name === "string" && (name === "NetworkError" || name === "TimeoutError")) return true;
	}
	if (error instanceof HTTPError && error.is5xx()) return true;
	return false;
}
//#endregion
//#region src/services.ts
const HttpClient = Context.GenericTag("@proofkit/fmodata/HttpClient");
const ODataConfig = Context.GenericTag("@proofkit/fmodata/ODataConfig");
const ODataLogger = Context.GenericTag("@proofkit/fmodata/ODataLogger");
/**
* Extracts ODataConfig and ODataLogger values from a Layer synchronously.
* Used by builders to access config in non-Effect methods (getRequestConfig, toRequest, etc.)
*/
function extractConfigFromLayer(layer) {
	const effect = Effect.gen(function* () {
		const config = yield* ODataConfig.pipe(Effect.mapError((error) => new MissingLayerServiceError("ODataConfig", { cause: error })));
		const { logger } = yield* ODataLogger.pipe(Effect.mapError((error) => new MissingLayerServiceError("ODataLogger", { cause: error })));
		return {
			config,
			logger
		};
	});
	return Effect.runSync(Effect.provide(effect, layer));
}
/**
* Creates a database-scoped Layer by overriding ODataConfig with database-specific values.
* The HttpClient and ODataLogger services are preserved from the base layer.
*/
function createDatabaseLayer(baseLayer, overrides) {
	const { config: baseConfig } = extractConfigFromLayer(baseLayer);
	const dbConfigLayer = Layer.succeed(ODataConfig, {
		baseUrl: baseConfig.baseUrl,
		databaseName: overrides.databaseName,
		normalizeDatabaseName: overrides.normalizeDatabaseName,
		useEntityIds: overrides.useEntityIds,
		includeSpecialColumns: overrides.includeSpecialColumns
	});
	return Layer.merge(baseLayer, dbConfigLayer);
}
//#endregion
//#region src/effect.ts
/**
* Creates an Effect that yields the HttpClient service and makes a request.
* This is the primary way builders should make HTTP requests.
*/
function requestFromService(url, options) {
	return Effect.gen(function* () {
		const client = yield* HttpClient;
		const config = yield* ODataConfig;
		return yield* client.request(url, {
			...options,
			normalizeDatabaseName: options?.normalizeDatabaseName ?? config.normalizeDatabaseName
		});
	});
}
/**
* Runs an Effect pipeline and converts the result back to the fmodata Result type.
* This is the exit point from Effect back to the public API.
*/
function runAsResult(effect) {
	return Effect.runPromise(effect.pipe(Effect.map((data) => ({
		data,
		error: void 0
	})), Effect.catchAll((error) => Effect.succeed({
		data: void 0,
		error
	})))).catch((defect) => ({
		data: void 0,
		error: isFMODataError(defect) ? defect : new BuilderInvariantError("runAsResult", String(defect))
	}));
}
function withOptionalSpan(effect, spanName, attributes) {
	if (!spanName) return effect;
	return withSpan(effect, spanName, attributes);
}
/**
* Runs an Effect by providing the shared DI layer and returns fmodata Result<T>.
*/
function runLayerResult(layer, effect, spanName, attributes) {
	return runAsResult(Effect.provide(withOptionalSpan(effect, spanName, attributes), layer));
}
/**
* Runs an Effect by providing the shared DI layer and throws on fmodata errors.
*/
async function runLayerOrThrow(layer, effect, spanName, attributes) {
	const result = await runLayerResult(layer, effect, spanName, attributes);
	if (result.error) throw result.error;
	return result.data;
}
/**
* Wraps a sync/async function that may throw into an Effect that captures
* the error as a typed FMODataErrorType.
*/
function tryEffect(fn, mapError) {
	return Effect.tryPromise({
		try: () => Promise.resolve(fn()),
		catch: mapError
	});
}
/**
* Wraps a function that returns a validation-style result
* ({ valid: true, data } | { valid: false, error }) into an Effect.
*/
function fromValidation(fn) {
	return Effect.tryPromise({
		try: fn,
		catch: (e) => e
	}).pipe(Effect.flatMap((result) => result.valid ? Effect.succeed(result.data) : Effect.fail(result.error)));
}
/**
* Builds an Effect Schedule from a RetryPolicy configuration.
* Uses exponential backoff with optional jitter, only retrying transient errors.
*/
function buildRetrySchedule(policy) {
	const maxRetries = policy.maxRetries ?? 3;
	const baseDelay = `${policy.baseDelay ?? 500} millis`;
	const useJitter = policy.jitter !== false;
	const base = Schedule.exponential(baseDelay);
	return (useJitter ? Schedule.jittered(base) : base).pipe(Schedule.intersect(Schedule.recurs(maxRetries)), Schedule.whileInput((error) => isTransientError(error)));
}
/**
* Applies a retry policy to an Effect if the policy is defined.
* Only retries transient errors (SchemaLockedError, NetworkError, TimeoutError, HTTP 5xx).
*/
function withRetryPolicy(effect, retryPolicy) {
	if (!retryPolicy) return effect;
	return effect.pipe(Effect.retry(buildRetrySchedule(retryPolicy)));
}
/**
* Wraps an Effect with a tracing span for observability.
* Zero overhead when no OpenTelemetry tracer is configured.
*/
function withSpan(effect, name, attributes) {
	return effect.pipe(Effect.withSpan(name, { attributes: attributes ? attributes : void 0 }));
}
//#endregion
//#region src/logger.ts
const TTY_COLORS = {
	reset: "\x1B[0m",
	bright: "\x1B[1m",
	dim: "\x1B[2m",
	undim: "\x1B[22m",
	underscore: "\x1B[4m",
	blink: "\x1B[5m",
	reverse: "\x1B[7m",
	hidden: "\x1B[8m",
	fg: {
		black: "\x1B[30m",
		red: "\x1B[31m",
		green: "\x1B[32m",
		yellow: "\x1B[33m",
		blue: "\x1B[34m",
		magenta: "\x1B[35m",
		cyan: "\x1B[36m",
		white: "\x1B[37m"
	},
	bg: {
		black: "\x1B[40m",
		red: "\x1B[41m",
		green: "\x1B[42m",
		yellow: "\x1B[43m",
		blue: "\x1B[44m",
		magenta: "\x1B[45m",
		cyan: "\x1B[46m",
		white: "\x1B[47m"
	}
};
const levels = [
	"debug",
	"info",
	"success",
	"warn",
	"error"
];
function shouldPublishLog(currentLogLevel, logLevel) {
	return levels.indexOf(logLevel) >= levels.indexOf(currentLogLevel);
}
const levelColors = {
	info: TTY_COLORS.fg.blue,
	success: TTY_COLORS.fg.green,
	warn: TTY_COLORS.fg.yellow,
	error: TTY_COLORS.fg.red,
	debug: TTY_COLORS.fg.magenta
};
const formatMessage = (level, message, colorsEnabled) => {
	const timestamp = (/* @__PURE__ */ new Date()).toISOString();
	if (colorsEnabled) return `${TTY_COLORS.dim}${timestamp}${TTY_COLORS.reset} ${levelColors[level]}${level.toUpperCase()}${TTY_COLORS.reset} ${TTY_COLORS.bright}[FMODATA]:${TTY_COLORS.reset} ${message}`;
	return `${timestamp} ${level.toUpperCase()} [FMODATA]: ${message}`;
};
const createLogger = (options) => {
	const enabled = options?.disabled !== true;
	const logLevel = options?.level ?? "error";
	const colorsEnabled = options?.disableColors !== true;
	const LogFunc = (level, message, args = []) => {
		if (!(enabled && shouldPublishLog(logLevel, level))) return;
		const formattedMessage = formatMessage(level, message, colorsEnabled);
		if (!options || typeof options.log !== "function") {
			if (level === "error") console.error(formattedMessage, ...args);
			else if (level === "warn") console.warn(formattedMessage, ...args);
			else console.log(formattedMessage, ...args);
			return;
		}
		options.log(level === "success" ? "info" : level, message, ...args);
	};
	return {
		...Object.fromEntries(levels.map((level) => [level, (...[message, ...args]) => LogFunc(level, message, args)])),
		get level() {
			return logLevel;
		}
	};
};
createLogger();
//#endregion
//#region src/types.ts
/**
* Get the Accept header value based on includeODataAnnotations option
* @param includeODataAnnotations - Whether to include OData annotations
* @returns Accept header value
*/
function getAcceptHeader(includeODataAnnotations) {
	return includeODataAnnotations === true ? "application/json" : "application/json;odata.metadata=none";
}
//#endregion
//#region src/transform.ts
const WHITESPACE_SPLIT_REGEX = /\s+/;
/**
* Transforms field names to FileMaker field IDs (FMFID) in an object
* @param data - Object with field names as keys
* @param table - FMTable instance to get field IDs from
* @returns Object with FMFID keys instead of field names
*/
function transformFieldNamesToIds(data, table) {
	if (!getBaseTableConfig(table).fmfIds) return data;
	const transformed = {};
	for (const [fieldName, value] of Object.entries(data)) {
		const fieldId = getFieldId(table, fieldName);
		transformed[fieldId] = value;
	}
	return transformed;
}
/**
* Transforms response data by converting field IDs back to field names recursively.
* Handles both single records and arrays of records, as well as nested expand relationships.
*
* @param data - Response data from FileMaker (can be single record, array, or wrapped in value property)
* @param table - FMTable instance for the main table
* @param expandConfigs - Configuration for expanded relations (optional)
* @returns Transformed data with field names instead of IDs
*/
function transformResponseFields(data, table, expandConfigs) {
	if (!getBaseTableConfig(table).fmfIds) return data;
	if (data === null || data === void 0) return data;
	if (data.value && Array.isArray(data.value)) return {
		...data,
		value: data.value.map((record) => transformSingleRecord(record, table, expandConfigs))
	};
	if (Array.isArray(data)) return data.map((record) => transformSingleRecord(record, table, expandConfigs));
	return transformSingleRecord(data, table, expandConfigs);
}
/**
* Transforms a single record, converting field IDs to names and handling nested expands
*/
function transformSingleRecord(record, table, expandConfigs) {
	if (!record || typeof record !== "object") return record;
	const transformed = {};
	for (const [key, value] of Object.entries(record)) {
		if (key.startsWith("@")) {
			transformed[key] = value;
			continue;
		}
		let expandConfig = expandConfigs?.find((ec) => ec.relation === key);
		if (!expandConfig && key.startsWith("FMTID:")) expandConfig = expandConfigs?.find((ec) => ec.table && isUsingEntityIds(ec.table) && getTableId(ec.table) === key);
		if (expandConfig?.table) {
			const relationKey = expandConfig.relation;
			if (Array.isArray(value)) {
				if (!expandConfig.table) {
					transformed[relationKey] = value;
					continue;
				}
				const nestedTable = expandConfig.table;
				transformed[relationKey] = value.map((nestedRecord) => transformSingleRecord(nestedRecord, nestedTable, void 0));
			} else if (value && typeof value === "object") {
				if (!expandConfig.table) {
					transformed[relationKey] = value;
					continue;
				}
				transformed[relationKey] = transformSingleRecord(value, expandConfig.table, void 0);
			} else transformed[relationKey] = value;
			continue;
		}
		const fieldName = getFieldName(table, key);
		transformed[fieldName] = value;
	}
	return transformed;
}
/**
* Transforms an array of field names to FMFIDs
* @param fieldNames - Array of field names
* @param table - FMTable instance to get field IDs from
* @returns Array of FMFIDs or field names
*/
function transformFieldNamesArray(fieldNames, table) {
	if (!getBaseTableConfig(table).fmfIds) return fieldNames;
	return fieldNames.map((fieldName) => getFieldId(table, fieldName));
}
/**
* Transforms a field name in an orderBy string (e.g., "name desc" -> "FMFID:1 desc")
* @param orderByString - The orderBy string (field name with optional asc/desc)
* @param table - FMTable instance to get field ID from
* @returns Transformed orderBy string with FMFID
*/
function transformOrderByField(orderByString, table) {
	if (!table) return orderByString;
	if (!getBaseTableConfig(table)?.fmfIds) return orderByString;
	const parts = orderByString.trim().split(WHITESPACE_SPLIT_REGEX);
	const fieldName = parts[0];
	if (!fieldName) return orderByString;
	const direction = parts[1];
	const fieldId = getFieldId(table, fieldName);
	return direction ? `${fieldId} ${direction}` : fieldId;
}
//#endregion
//#region src/client/builders/select-utils.ts
const VALID_FIELD_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9]*$/;
/**
* Determines if a field name needs to be quoted in OData queries.
* Per FileMaker docs: field names with special characters (spaces, underscores, etc.) must be quoted.
* Also quotes "id" case-insensitively as it's an OData reserved word.
* Entity IDs (FMFID:*, FMTID:*) are not quoted as they're identifiers, not field names.
*
* @param fieldName - The field name or identifier to check
* @returns true if the field name should be quoted in OData queries
*/
function needsFieldQuoting(fieldName) {
	if (fieldName.startsWith("FMFID:") || fieldName.startsWith("FMTID:")) return false;
	if (fieldName.toLowerCase() === "id") return true;
	return fieldName.includes(" ") || fieldName.includes("_") || !VALID_FIELD_NAME_REGEX.test(fieldName);
}
/**
* Formats select fields for use in OData query strings.
* - Transforms field names to FMFIDs if using entity IDs
* - Wraps "id" fields in double quotes (OData reserved)
* - URL-encodes special characters but preserves spaces
*/
function formatSelectFields(select, table, useEntityIds) {
	if (!select || select.length === 0) return "";
	const selectArray = Array.isArray(select) ? select : [select];
	return (table && useEntityIds ? transformFieldNamesArray(selectArray.map(String), table) : selectArray.map(String)).map((field) => {
		if (needsFieldQuoting(field)) return `"${field}"`;
		return encodeURIComponent(field).replace(/%20/g, " ");
	}).join(",");
}
//#endregion
//#region src/orm/column.ts
/**
* Column represents a type-safe reference to a table field.
* Used in queries, filters, and operators to provide autocomplete and type checking.
*
* @template TOutput - The TypeScript type when reading from the database (output type)
* @template TInput - The TypeScript type when writing to the database (input type, for filters)
* @template TableName - The table name as a string literal type (for validation)
* @template IsContainer - Whether this column represents a container field (cannot be selected)
*/
var Column = class {
	fieldName;
	entityId;
	tableName;
	tableEntityId;
	inputValidator;
	fieldType;
	_phantomOutput;
	_phantomInput;
	_isContainer;
	constructor(config) {
		this.fieldName = config.fieldName;
		this.entityId = config.entityId;
		this.tableName = config.tableName;
		this.tableEntityId = config.tableEntityId;
		this.inputValidator = config.inputValidator;
		this.fieldType = config.fieldType;
	}
	/**
	* Get the field identifier (entity ID if available, otherwise field name).
	* Used when building OData queries.
	*/
	getFieldIdentifier(useEntityIds) {
		if (useEntityIds && this.entityId) return this.entityId;
		return this.fieldName;
	}
	/**
	* Get the table identifier (entity ID if available, otherwise table name).
	* Used when building OData queries.
	*/
	getTableIdentifier(useEntityIds) {
		if (useEntityIds && this.tableEntityId) return this.tableEntityId;
		return this.tableName;
	}
	/**
	* Check if this column is from a specific table.
	* Useful for validation in cross-table operations.
	*/
	isFromTable(tableName) {
		return this.tableName === tableName;
	}
	/**
	* Create a string representation for debugging.
	*/
	toString() {
		return `${this.tableName}.${this.fieldName}`;
	}
};
/**
* Type guard to check if a value is a Column instance.
*/
function isColumn(value) {
	return value instanceof Column;
}
/**
* ColumnFunction wraps a Column with an OData string function (tolower, toupper, trim).
* Since it extends Column, it passes `isColumn()` checks and works with all existing operators.
* Supports nesting: `tolower(trim(col))` → `tolower(trim(name))`.
*/
var ColumnFunction = class extends Column {
	fnName;
	innerColumn;
	constructor(fnName, innerColumn) {
		super({
			fieldName: innerColumn.fieldName,
			entityId: innerColumn.entityId,
			tableName: innerColumn.tableName,
			tableEntityId: innerColumn.tableEntityId,
			inputValidator: innerColumn.inputValidator,
			fieldType: innerColumn.fieldType
		});
		this.fnName = fnName;
		this.innerColumn = innerColumn;
	}
	toFilterString(useEntityIds) {
		if (isColumnFunction(this.innerColumn)) return `${this.fnName}(${this.innerColumn.toFilterString(useEntityIds)})`;
		const fieldIdentifier = this.innerColumn.getFieldIdentifier(useEntityIds);
		const quoted = needsFieldQuoting(fieldIdentifier) ? `"${fieldIdentifier}"` : fieldIdentifier;
		return `${this.fnName}(${quoted})`;
	}
};
/**
* Type guard to check if a value is a ColumnFunction instance.
*/
function isColumnFunction(value) {
	return value instanceof ColumnFunction;
}
//#endregion
//#region src/orm/table.ts
/**
* Internal Symbols for table properties (hidden from IDE autocomplete).
* These are used to store internal configuration that shouldn't be visible
* when users access table columns.
* @internal - Not exported from public API, only accessible via FMTable.Symbol
*/
const FMTableName = Symbol.for("fmodata:FMTableName");
const FMTableEntityId = Symbol.for("fmodata:FMTableEntityId");
const FMTableSchema = Symbol.for("fmodata:FMTableSchema");
const FMTableFields = Symbol.for("fmodata:FMTableFields");
const FMTableNavigationPaths = Symbol.for("fmodata:FMTableNavigationPaths");
const FMTableDefaultSelect = Symbol.for("fmodata:FMTableDefaultSelect");
const FMTableBaseTableConfig = Symbol.for("fmodata:FMTableBaseTableConfig");
const FMTableUseEntityIds = Symbol.for("fmodata:FMTableUseEntityIds");
const FMTableComment = Symbol.for("fmodata:FMTableComment");
/**
* Base table class with Symbol-based internal properties.
* This follows the Drizzle ORM pattern where internal configuration
* is stored via Symbols, keeping it hidden from IDE autocomplete.
*/
var FMTable = class {
	/**
	* Internal Symbols for accessing table metadata.
	* @internal - Not intended for public use. Access table properties via columns instead.
	*/
	static Symbol = {
		Name: FMTableName,
		EntityId: FMTableEntityId,
		UseEntityIds: FMTableUseEntityIds,
		Schema: FMTableSchema,
		Fields: FMTableFields,
		NavigationPaths: FMTableNavigationPaths,
		DefaultSelect: FMTableDefaultSelect,
		BaseTableConfig: FMTableBaseTableConfig,
		Comment: FMTableComment
	};
	/** @internal */
	[FMTableName];
	/** @internal */
	[FMTableEntityId];
	/** @internal */
	[FMTableUseEntityIds];
	/** @internal */
	[FMTableComment];
	/** @internal */
	[FMTableSchema];
	/** @internal */
	[FMTableFields];
	/** @internal */
	[FMTableNavigationPaths];
	/** @internal */
	[FMTableDefaultSelect];
	/** @internal */
	[FMTableBaseTableConfig];
	constructor(config) {
		this[FMTableName] = config.name;
		this[FMTableEntityId] = config.entityId;
		this[FMTableUseEntityIds] = config.useEntityIds;
		this[FMTableComment] = config.comment;
		this[FMTableSchema] = config.schema;
		this[FMTableFields] = config.fields;
		this[FMTableNavigationPaths] = config.navigationPaths;
		this[FMTableDefaultSelect] = config.defaultSelect;
		this[FMTableBaseTableConfig] = config.baseTableConfig;
	}
};
/**
* Get the table name from an FMTable instance.
* @param table - FMTable instance
* @returns The table name
*/
function getTableName(table) {
	return table[FMTableName];
}
/**
* Get the schema validator from an FMTable instance.
* @param table - FMTable instance
* @returns The StandardSchemaV1 validator record (partial - only fields with validators)
*/
function getTableSchema(table) {
	return table[FMTableSchema];
}
/**
* Get the navigation paths from an FMTable instance.
* @param table - FMTable instance
* @returns Array of navigation path names
*/
function getNavigationPaths(table) {
	return table[FMTableNavigationPaths];
}
/**
* Get the default select configuration from an FMTable instance.
* @param table - FMTable instance
* @returns Default select configuration
*/
function getDefaultSelect(table) {
	return table[FMTableDefaultSelect];
}
/**
* Get the base table configuration from an FMTable instance.
* This provides access to schema, idField, required fields, readOnly fields, and field IDs.
* @param table - FMTable instance
* @returns Base table configuration object
*/
function getBaseTableConfig(table) {
	return table[FMTableBaseTableConfig];
}
/**
* Check if an FMTable instance is using entity IDs (both FMTID and FMFIDs).
* @param table - FMTable instance
* @returns True if using entity IDs, false otherwise
*/
function isUsingEntityIds(table) {
	return table[FMTableEntityId] !== void 0 && table[FMTableBaseTableConfig].fmfIds !== void 0;
}
/**
* Get the field ID (FMFID) for a given field name, or the field name itself if not using IDs.
* @param table - FMTable instance
* @param fieldName - Field name to get the ID for
* @returns The FMFID string or the original field name
*/
function getFieldId(table, fieldName) {
	const config = table[FMTableBaseTableConfig];
	if (config.fmfIds && fieldName in config.fmfIds) {
		const fieldId = config.fmfIds[fieldName];
		if (fieldId) return fieldId;
	}
	return fieldName;
}
/**
* Get the field name for a given field ID (FMFID), or the ID itself if not found.
* @param table - FMTable instance
* @param fieldId - The FMFID to get the field name for
* @returns The field name or the original ID
*/
function getFieldName(table, fieldId) {
	const config = table[FMTableBaseTableConfig];
	if (config.fmfIds) {
		for (const [fieldName, fmfId] of Object.entries(config.fmfIds)) if (fmfId === fieldId) return fieldName;
	}
	return fieldId;
}
/**
* Get the table ID (FMTID or name) from an FMTable instance.
* Returns the FMTID if available, otherwise returns the table name.
* @param table - FMTable instance
* @returns The FMTID string or the table name
*/
function getTableId(table) {
	return table[FMTableEntityId] ?? table[FMTableName];
}
/**
* Get all columns from a table as an object.
* Useful for selecting all fields except some using destructuring.
*
* @example
* const { password, ...cols } = getTableColumns(users)
* db.from(users).list().select(cols)
*
* @param table - FMTable instance
* @returns Object with all columns from the table
*/
function getTableColumns(table) {
	const fields = table[FMTableFields];
	const tableName = table[FMTableName];
	const tableEntityId = table[FMTableEntityId];
	const baseConfig = table[FMTableBaseTableConfig];
	const columns = {};
	for (const [fieldName, builder] of Object.entries(fields)) {
		const config = builder._getConfig();
		columns[fieldName] = new Column({
			fieldName: String(fieldName),
			entityId: baseConfig.fmfIds?.[fieldName],
			tableName,
			tableEntityId,
			inputValidator: config.inputValidator,
			fieldType: config.fieldType
		});
	}
	return columns;
}
//#endregion
//#region src/client/database-name.ts
const FMP12_EXT_REGEX = /\.fmp12$/i;
function stripFmp12Extension(databaseName) {
	return databaseName.replace(FMP12_EXT_REGEX, "");
}
function ensureFmp12Extension(databaseName) {
	return FMP12_EXT_REGEX.test(databaseName) ? databaseName : `${databaseName}.fmp12`;
}
function normalizeDatabaseSegment(databaseName, normalizeDatabaseName, mode = "default") {
	if (!normalizeDatabaseName) return databaseName;
	return mode === "ensureExtension" ? ensureFmp12Extension(databaseName) : stripFmp12Extension(databaseName);
}
function normalizeDatabasePath(path, options) {
	if (!path.startsWith("/")) return path;
	const secondSlashIndex = path.indexOf("/", 1);
	const hasDatabaseSegment = secondSlashIndex !== -1;
	const databaseSegment = hasDatabaseSegment ? path.slice(1, secondSlashIndex) : path.slice(1);
	if (!databaseSegment || databaseSegment.startsWith("$")) return path;
	let normalizedInput = databaseSegment;
	try {
		normalizedInput = decodeURIComponent(databaseSegment);
	} catch {
		normalizedInput = databaseSegment;
	}
	const normalizedDatabaseSegment = normalizeDatabaseSegment(normalizedInput, options.normalizeDatabaseName, options.mode);
	const encodedDatabaseSegment = encodeURIComponent(normalizedDatabaseSegment);
	return hasDatabaseSegment ? `/${encodedDatabaseSegment}${path.slice(secondSlashIndex)}` : `/${encodedDatabaseSegment}`;
}
//#endregion
//#region src/client/builders/table-utils.ts
/**
* Resolves table identifier based on entity ID settings.
* Used by both QueryBuilder and RecordBuilder.
*/
function resolveTableId(table, fallbackTableName, useEntityIds) {
	if (!table) return fallbackTableName;
	if (useEntityIds) {
		if (!isUsingEntityIds(table)) throw new Error(`useEntityIds is true but table "${getTableName(table)}" does not have entity IDs configured`);
		return getTableId(table);
	}
	return getTableName(table);
}
/**
* Merges database-level useEntityIds with per-request options.
*/
function mergeEntityIdOptions(options, databaseDefault) {
	return {
		...options,
		useEntityIds: options?.useEntityIds ?? databaseDefault
	};
}
/**
* Type-safe helper for merging execute options with entity ID settings
*/
function mergeExecuteOptions(options, databaseUseEntityIds) {
	return mergeEntityIdOptions(options, databaseUseEntityIds);
}
/**
* Creates an OData Request object with proper headers.
* Used by both QueryBuilder and RecordBuilder to eliminate duplication.
*
* @param baseUrl - Base URL for the request
* @param config - Request configuration with method and url
* @param options - Optional execution options
* @returns Request object ready to use
*/
function createODataRequest(baseUrl, config, options) {
	const fullUrl = `${baseUrl}${normalizeDatabasePath(config.url, { normalizeDatabaseName: options?.normalizeDatabaseName ?? true })}`;
	return new Request(fullUrl, {
		method: config.method,
		headers: {
			"Content-Type": "application/json",
			Accept: getAcceptHeader(options?.includeODataAnnotations)
		}
	});
}
//#endregion
//#region src/client/builders/mutation-helpers.ts
const ROWID_MATCH_REGEX = /ROWID=(\d+)/;
const PAREN_VALUE_REGEX = /\(['"]?([^'"]+)['"]?\)/;
function isRowIdRecordLocator(recordLocator) {
	return typeof recordLocator === "object" && recordLocator !== null && "ROWID" in recordLocator;
}
function escapeODataStringLiteral(value) {
	return value.replaceAll("'", "''");
}
function buildRecordLocatorSegment(recordLocator) {
	if (isRowIdRecordLocator(recordLocator)) return `(ROWID=${recordLocator.ROWID})`;
	return `('${escapeODataStringLiteral(String(recordLocator))}')`;
}
function buildRecordPath(pathPrefix, recordLocator) {
	return `${pathPrefix}${buildRecordLocatorSegment(recordLocator)}`;
}
function mergeMutationExecuteOptions(options, databaseUseEntityIds, databaseIncludeSpecialColumns) {
	return {
		...options,
		useEntityIds: options?.useEntityIds ?? databaseUseEntityIds,
		includeSpecialColumns: options?.includeSpecialColumns ?? databaseIncludeSpecialColumns
	};
}
function mergePreferHeaderValues(...values) {
	const merged = [];
	const seen = /* @__PURE__ */ new Set();
	for (const value of values) {
		if (!value) continue;
		for (const part of value.split(",")) {
			const normalized = part.trim();
			if (!normalized || seen.has(normalized)) continue;
			seen.add(normalized);
			merged.push(normalized);
		}
	}
	return merged.length > 0 ? merged.join(", ") : void 0;
}
function resolveMutationTableId(table, useEntityIds, builderName) {
	if (!table) throw new BuilderInvariantError(builderName, "table occurrence is required");
	return resolveTableId(table, getTableName(table), useEntityIds);
}
function buildMutationUrl(config) {
	const { databaseName, tableId, tableName, mode, recordLocator, queryBuilder, useEntityIds, builderName } = config;
	if (mode === "byId") {
		if (recordLocator === void 0 || recordLocator === null || recordLocator === "") throw new BuilderInvariantError(builderName, "recordLocator is required for byId mode");
		return `/${databaseName}/${buildRecordPath(tableId, recordLocator)}`;
	}
	if (!queryBuilder) throw new BuilderInvariantError(builderName, "query builder is required for filter mode");
	return `/${databaseName}/${tableId}${stripTablePathPrefix(queryBuilder.getQueryString({ useEntityIds }), tableId, tableName)}`;
}
function stripTablePathPrefix(queryString, tableId, tableName) {
	if (queryString.startsWith(`/${tableId}`)) return queryString.slice(`/${tableId}`.length);
	if (queryString.startsWith(`/${tableName}`)) return queryString.slice(`/${tableName}`.length);
	return queryString;
}
function extractAffectedRows(response, headers, fallback = 0, countKey) {
	const headerValue = headers?.get("fmodata.affected_rows");
	if (headerValue) {
		const parsed = Number.parseInt(headerValue, 10);
		if (!Number.isNaN(parsed)) return parsed;
	}
	if (typeof response === "number") return response;
	if (response && typeof response === "object") {
		if (countKey && countKey in response) {
			const count = Number(response[countKey]);
			if (!Number.isNaN(count)) return count;
		}
		const affected = Number(response["fmodata.affected_rows"]);
		if (!Number.isNaN(affected)) return affected;
	}
	return fallback;
}
/**
* Parse ROWID from Location header.
* Expected formats:
* - contacts(ROWID=4583)
* - contacts('4583')
*/
function parseRowIdFromLocationHeader(locationHeader) {
	if (!locationHeader) throw new InvalidLocationHeaderError("Location header is required but was not provided");
	const rowidMatch = locationHeader.match(ROWID_MATCH_REGEX);
	if (rowidMatch?.[1]) return Number.parseInt(rowidMatch[1], 10);
	const parenMatch = locationHeader.match(PAREN_VALUE_REGEX);
	if (parenMatch?.[1]) {
		const value = Number.parseInt(parenMatch[1], 10);
		if (!Number.isNaN(value)) return value;
	}
	throw new InvalidLocationHeaderError(`Could not extract ROWID from Location header: ${locationHeader}`, locationHeader);
}
function getLocationHeader(headers) {
	return headers.get("Location") || headers.get("location") || void 0;
}
//#endregion
//#region src/client/claris-id.ts
const CLARIS_USER_POOL_URL = "https://www.ifmcloud.com/endpoint/userpool/2.2.0.my.claris.com.json";
const UNSUPPORTED_MFA_ERROR = "Claris ID MFA is not supported by @proofkit/fmodata yet. Use a non-MFA Claris ID account for now.";
var ClarisIdAuthManager = class {
	username;
	password;
	authenticationDetails;
	userPool = null;
	cognitoUser = null;
	userSession = null;
	idTokenPromise = null;
	constructor(config) {
		this.username = config.username;
		this.password = config.password;
	}
	async getAuthorizationHeader(fetchLike) {
		if (!this.idTokenPromise) this.idTokenPromise = this.getIdToken(fetchLike).finally(() => {
			this.idTokenPromise = null;
		});
		return `FMID ${await this.idTokenPromise}`;
	}
	getAuthenticationDetails() {
		if (!this.authenticationDetails) this.authenticationDetails = new AuthenticationDetails({
			Username: this.username,
			Password: this.password
		});
		return this.authenticationDetails;
	}
	async getIdToken(fetchLike) {
		if (this.userSession) return this.getStoredIdToken(this.userSession);
		return (await this.retrieveNewSession(fetchLike)).getIdToken().getJwtToken();
	}
	async getStoredIdToken(userSession) {
		return (userSession.isValid() ? userSession : await this.refreshSession(userSession)).getIdToken().getJwtToken();
	}
	async refreshSession(userSession) {
		const cognitoUser = await this.getCognitoUser();
		this.userSession = await new Promise((resolve, reject) => {
			cognitoUser.refreshSession(userSession.getRefreshToken(), async (error, session) => {
				if (error || !session) try {
					resolve(await this.retrieveNewSession());
					return;
				} catch (reauthError) {
					reject(reauthError);
					return;
				}
				resolve(session);
			});
		});
		return this.userSession;
	}
	async retrieveNewSession(fetchLike) {
		const cognitoUser = await this.getCognitoUser(fetchLike);
		const authenticationDetails = this.getAuthenticationDetails();
		this.userSession = await new Promise((resolve, reject) => {
			cognitoUser.authenticateUser(authenticationDetails, {
				onSuccess: (result) => {
					resolve(result);
				},
				onFailure: (error) => {
					reject(error);
				},
				mfaRequired: () => reject(/* @__PURE__ */ new Error(UNSUPPORTED_MFA_ERROR)),
				totpRequired: () => reject(/* @__PURE__ */ new Error(UNSUPPORTED_MFA_ERROR)),
				selectMFAType: () => reject(/* @__PURE__ */ new Error(UNSUPPORTED_MFA_ERROR)),
				mfaSetup: () => reject(/* @__PURE__ */ new Error(UNSUPPORTED_MFA_ERROR))
			});
		});
		return this.userSession;
	}
	async getCognitoUser(fetchLike) {
		if (this.cognitoUser) return this.cognitoUser;
		this.cognitoUser = new CognitoUser({
			Username: this.getAuthenticationDetails().getUsername(),
			Pool: await this.getUserPool(fetchLike)
		});
		return this.cognitoUser;
	}
	async getUserPool(fetchLike) {
		if (this.userPool) return this.userPool;
		const response = await (fetchLike ?? fetch)(CLARIS_USER_POOL_URL);
		if (!response.ok) throw new Error("Could not fetch Claris ID user pool config");
		const config = await response.json();
		const userPoolId = config.data?.UserPool_ID;
		const clientId = config.data?.Client_ID;
		if (!(userPoolId && clientId)) throw new Error("Invalid Claris ID user pool config response");
		this.userPool = new CognitoUserPool({
			UserPoolId: userPoolId,
			ClientId: clientId
		});
		return this.userPool;
	}
};
//#endregion
//#region src/client/batch-request.ts
/**
* Batch Request Utilities
*
* Utilities for formatting and parsing OData batch requests using multipart/mixed format.
* OData batch requests allow bundling multiple operations into a single HTTP request,
* with support for transactional changesets.
*/
const BOUNDARY_REGEX = /boundary=([^;]+)/;
const HTTP_STATUS_LINE_REGEX = /HTTP\/\d\.\d\s+(\d+)\s*(.*)/;
const CRLF_REGEX = /\r\n/;
const CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/;
const OTTO_PREFIX_REGEX = /^\/otto/;
const FMPRO_EXT_REGEX = /\.fmp12/;
/**
* Generates a random boundary string for multipart requests
* @param prefix - Prefix for the boundary (e.g., "batch_" or "changeset_")
* @returns A boundary string with the prefix and 32 random hex characters
*/
function generateBoundary(prefix = "batch_") {
	return `${prefix}${Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
}
/**
* Converts a native Request object to RequestConfig
* @param request - Native Request object
* @returns RequestConfig object
*/
async function requestToConfig(request) {
	const headers = {};
	request.headers.forEach((value, key) => {
		headers[key] = value;
	});
	let body;
	if (request.body) body = await request.clone().text();
	return {
		method: request.method,
		url: request.url,
		body,
		headers
	};
}
/**
* Transforms a full URL into the canonical path format required by FileMaker's
* OData batch processor. Strips proxy prefixes (e.g. /otto/) and the .fmp12
* file extension from the database name segment.
*/
function toBatchSubRequestUrl(fullUrl) {
	const url = new URL(fullUrl);
	return `${url.pathname.replace(OTTO_PREFIX_REGEX, "").replace(FMPRO_EXT_REGEX, "")}${url.search}`;
}
/**
* Formats a single HTTP request for inclusion in a batch
* @param request - The request configuration
* @param baseUrl - The base URL to prepend to relative URLs
* @returns Formatted request string with CRLF line endings
*
* Formatting rules for FileMaker OData:
* - GET (no body): request line → blank → blank
* - POST/PATCH (with body): request line → headers → blank → body (NO blank after!)
*/
function formatSubRequest(request, baseUrl) {
	const lines = [];
	lines.push("Content-Type: application/http");
	lines.push("Content-Transfer-Encoding: binary");
	lines.push("");
	const subRequestUrl = toBatchSubRequestUrl(request.url.startsWith("http") ? request.url : `${baseUrl}${request.url}`);
	lines.push(`${request.method} ${subRequestUrl} HTTP/1.1`);
	if (request.body) {
		if (request.headers) {
			for (const [key, value] of Object.entries(request.headers)) if (key.toLowerCase() !== "authorization") lines.push(`${key}: ${value}`);
		}
		if (!(request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === "content-type"))) lines.push("Content-Type: application/json");
		if (!(request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === "content-length"))) lines.push(`Content-Length: ${request.body.length}`);
		lines.push("");
		lines.push(request.body);
	} else {
		lines.push("");
		lines.push("");
	}
	return lines.join("\r\n");
}
/**
* Formats a changeset containing multiple non-GET operations
* @param requests - Array of request configurations (should be non-GET)
* @param baseUrl - The base URL to prepend to relative URLs
* @param changesetBoundary - Boundary string for the changeset
* @returns Formatted changeset string with CRLF line endings
*/
function formatChangeset(requests, baseUrl, changesetBoundary) {
	const lines = [];
	lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);
	lines.push("");
	for (const request of requests) {
		lines.push(`--${changesetBoundary}`);
		lines.push(formatSubRequest(request, baseUrl));
	}
	lines.push(`--${changesetBoundary}--`);
	return lines.join("\r\n");
}
/**
* Formats multiple Request objects into a batch request body
* Supports explicit changesets via Request arrays
* @param requests - Array of Request objects or Request arrays (for explicit changesets)
* @param baseUrl - The base URL to prepend to relative URLs
* @param batchBoundary - Optional boundary string for the batch (generated if not provided)
* @returns Promise resolving to object containing the formatted body and boundary
*/
async function formatBatchRequestFromNative(requests, baseUrl, batchBoundary) {
	const boundary = batchBoundary || generateBoundary("batch_");
	const lines = [];
	for (const item of requests) if (Array.isArray(item)) {
		const changesetBoundary = generateBoundary("changeset_");
		const changesetConfigs = [];
		for (const request of item) changesetConfigs.push(await requestToConfig(request));
		lines.push(`--${boundary}`);
		lines.push(formatChangeset(changesetConfigs, baseUrl, changesetBoundary));
	} else {
		const config = await requestToConfig(item);
		if (config.method === "GET") {
			lines.push(`--${boundary}`);
			lines.push(formatSubRequest(config, baseUrl));
		} else {
			const changesetBoundary = generateBoundary("changeset_");
			lines.push(`--${boundary}`);
			lines.push(formatChangeset([config], baseUrl, changesetBoundary));
		}
	}
	lines.push(`--${boundary}--`);
	return {
		body: lines.join("\r\n"),
		boundary
	};
}
/**
* Extracts the boundary from a Content-Type header
* @param contentType - The Content-Type header value
* @returns The boundary string, or null if not found
*/
function extractBoundary(contentType) {
	const match = contentType.match(BOUNDARY_REGEX);
	return match?.[1] ? match[1].trim() : null;
}
/**
* Parses an HTTP response line (status line)
* @param line - The HTTP status line (e.g., "HTTP/1.1 200 OK")
* @returns Object containing status code and status text
*/
function parseStatusLine(line) {
	const match = line.match(HTTP_STATUS_LINE_REGEX);
	if (!match?.[1]) return {
		status: 0,
		statusText: ""
	};
	return {
		status: Number.parseInt(match[1], 10),
		statusText: match[2]?.trim() || ""
	};
}
/**
* Parses headers from an array of header lines
* @param lines - Array of header lines
* @returns Object containing parsed headers
*/
function parseHeaders(lines) {
	const headers = {};
	for (const line of lines) {
		const colonIndex = line.indexOf(":");
		if (colonIndex > 0) {
			const key = line.substring(0, colonIndex).trim();
			const value = line.substring(colonIndex + 1).trim();
			headers[key.toLowerCase()] = value;
		}
	}
	return headers;
}
/**
* Parses a single HTTP response from a batch part
* @param part - The raw HTTP response string
* @returns Parsed response object
*/
function parseHttpResponse(part) {
	const lines = part.split(CRLF_REGEX);
	let statusLineIndex = -1;
	for (let i = 0; i < lines.length; i++) if (lines[i]?.startsWith("HTTP/")) {
		statusLineIndex = i;
		break;
	}
	if (statusLineIndex === -1) return {
		status: 0,
		statusText: "Invalid response",
		headers: {},
		body: null
	};
	const statusLine = lines[statusLineIndex];
	if (!statusLine) return {
		status: 0,
		statusText: "Invalid response",
		headers: {},
		body: null
	};
	const { status, statusText } = parseStatusLine(statusLine);
	const headerLines = [];
	let bodyStartIndex = lines.length;
	let foundEmptyLine = false;
	for (let i = statusLineIndex + 1; i < lines.length; i++) {
		const line = lines[i];
		if (line === "") {
			bodyStartIndex = i + 1;
			foundEmptyLine = true;
			break;
		}
		if (line?.startsWith("--")) break;
		if (line) headerLines.push(line);
	}
	const headers = parseHeaders(headerLines);
	let bodyText = "";
	if (foundEmptyLine && bodyStartIndex < lines.length) {
		const bodyLines = lines.slice(bodyStartIndex);
		const bodyLinesFiltered = [];
		for (const line of bodyLines) {
			if (line.startsWith("--")) break;
			bodyLinesFiltered.push(line);
		}
		bodyText = bodyLinesFiltered.join("\r\n").trim();
	}
	let body = null;
	if (bodyText) try {
		body = JSON.parse(bodyText);
	} catch {
		body = bodyText;
	}
	return {
		status,
		statusText,
		headers,
		body
	};
}
/**
* Parses a batch response into individual responses
* @param responseText - The raw batch response text
* @param contentType - The Content-Type header from the response
* @returns Array of parsed responses in the same order as the request
*/
function parseBatchResponse(responseText, contentType) {
	const boundary = extractBoundary(contentType);
	if (!boundary) throw new Error("Could not extract boundary from Content-Type header");
	const results = [];
	const boundaryPattern = `--${boundary}`;
	const parts = responseText.split(boundaryPattern);
	for (const part of parts) {
		const trimmedPart = part.trim();
		if (!trimmedPart || trimmedPart === "--") continue;
		if (trimmedPart.includes("Content-Type: multipart/mixed")) {
			const changesetContentTypeMatch = trimmedPart.match(CHANGESET_CONTENT_TYPE_REGEX);
			if (changesetContentTypeMatch) {
				const changesetPattern = `--${changesetContentTypeMatch?.[1]?.trim()}`;
				const changesetParts = trimmedPart.split(changesetPattern);
				for (const changesetPart of changesetParts) {
					const trimmedChangesetPart = changesetPart.trim();
					if (!trimmedChangesetPart || trimmedChangesetPart === "--") continue;
					if (trimmedChangesetPart.startsWith("Content-Type: multipart/mixed")) continue;
					const response = parseHttpResponse(trimmedChangesetPart);
					if (response.status > 0) results.push(response);
				}
			}
		} else {
			const response = parseHttpResponse(trimmedPart);
			if (response.status > 0) results.push(response);
		}
	}
	return results;
}
//#endregion
//#region src/client/runtime.ts
/**
* Single boundary for synchronous extraction of config/logger from the DI layer.
* Builder/manager constructors should call this once and pass runtime around.
*/
function createClientRuntime(layer) {
	const { config, logger } = extractConfigFromLayer(layer);
	return {
		layer,
		config,
		logger
	};
}
//#endregion
//#region src/client/batch-builder.ts
/**
* Converts a ParsedBatchResponse to a native Response object
* @param parsed - The parsed batch response
* @returns A native Response object
*/
function parsedToResponse(parsed) {
	const headers = new Headers(parsed.headers);
	if (parsed.body === null || parsed.body === void 0) return new Response(null, {
		status: parsed.status,
		statusText: parsed.statusText,
		headers
	});
	const bodyString = typeof parsed.body === "string" ? parsed.body : JSON.stringify(parsed.body);
	let status = parsed.status;
	if (status === 204 && bodyString && bodyString.trim() !== "") status = 200;
	return new Response(status === 204 ? null : bodyString, {
		status,
		statusText: parsed.statusText,
		headers
	});
}
/**
* Builder for batch operations that allows multiple queries to be executed together
* in a single transactional request.
*
* Note: BatchBuilder does not implement ExecutableBuilder because execute() returns
* BatchResult instead of Result, which is a different return type structure.
*/
var BatchBuilder = class {
	builders;
	layer;
	config;
	constructor(builders, layer) {
		this.builders = [...builders];
		const runtime = createClientRuntime(layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
	}
	/**
	* Add a request to the batch dynamically.
	* This allows building up batch operations programmatically.
	*
	* @param builder - An executable builder to add to the batch
	* @returns A BatchBuilder typed with the appended request result
	* @example
	* ```ts
	* const batch = db.batch([]);
	* batch.addRequest(db.from('contacts').list());
	* batch.addRequest(db.from('users').list());
	* const result = await batch.execute();
	* ```
	*/
	addRequest(builder) {
		this.builders.push(builder);
		return this;
	}
	/**
	* Get the request configuration for this batch operation.
	* This is used internally by the execution system.
	*/
	getRequestConfig() {
		return {
			method: "POST",
			url: `/${this.config.databaseName}/$batch`,
			body: void 0
		};
	}
	toRequest(baseUrl, _options) {
		const fullUrl = `${baseUrl}${normalizeDatabasePath(`/${this.config.databaseName}/$batch`, { normalizeDatabaseName: _options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName })}`;
		return new Request(fullUrl, {
			method: "POST",
			headers: {
				"Content-Type": "multipart/mixed",
				"OData-Version": "4.0"
			}
		});
	}
	processResponse(_response, _options) {
		return Promise.resolve({
			data: void 0,
			error: {
				name: "NotImplementedError",
				message: "Batch operations handle response processing internally",
				timestamp: /* @__PURE__ */ new Date()
			}
		});
	}
	/**
	* Creates a failed BatchResult where all operations are marked as failed with the given error.
	*/
	failAllResults(error) {
		const errorCount = this.builders.length;
		return {
			results: this.builders.map(() => ({
				data: void 0,
				error,
				status: 0
			})),
			successCount: 0,
			errorCount,
			truncated: false,
			firstErrorIndex: errorCount > 0 ? 0 : null
		};
	}
	/**
	* Execute the batch operation.
	*
	* @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
	* @returns A BatchResult containing individual results for each operation
	*/
	async execute(options) {
		const baseUrl = this.config.baseUrl;
		if (!baseUrl) return this.failAllResults({
			name: "ConfigurationError",
			message: "Base URL not available in ODataConfig",
			timestamp: /* @__PURE__ */ new Date()
		});
		const pipeline = Effect.gen(this, function* () {
			const requests = this.builders.map((builder) => builder.toRequest(baseUrl, options));
			const { body, boundary } = yield* Effect.tryPromise({
				try: () => formatBatchRequestFromNative(requests, baseUrl),
				catch: (e) => e
			});
			const responseData = yield* requestFromService(`/${this.config.databaseName}/$batch`, {
				...options,
				method: "POST",
				headers: {
					...options?.headers,
					"Content-Type": `multipart/mixed; boundary=${boundary}`,
					"OData-Version": "4.0"
				},
				body
			});
			const firstLine = responseData.split("\r\n")[0] || responseData.split("\n")[0] || "";
			const parsedResponses = parseBatchResponse(responseData, `multipart/mixed; boundary=${firstLine.startsWith("--") ? firstLine.substring(2) : boundary}`);
			const results = [];
			let successCount = 0;
			let errorCount = 0;
			let firstErrorIndex = null;
			const truncated = parsedResponses.length < this.builders.length;
			for (let i = 0; i < this.builders.length; i++) {
				const builder = this.builders[i];
				const parsed = parsedResponses[i];
				if (!parsed) {
					const failedAtIndex = firstErrorIndex ?? i;
					results.push({
						data: void 0,
						error: new BatchTruncatedError(i, failedAtIndex),
						status: 0
					});
					errorCount++;
					continue;
				}
				if (!builder) {
					results.push({
						data: void 0,
						error: {
							name: "BatchError",
							message: `Builder at index ${i} is undefined`,
							timestamp: /* @__PURE__ */ new Date()
						},
						status: parsed.status
					});
					errorCount++;
					if (firstErrorIndex === null) firstErrorIndex = i;
					continue;
				}
				const nativeResponse = parsedToResponse(parsed);
				const result = yield* Effect.tryPromise({
					try: () => builder.processResponse(nativeResponse, options),
					catch: (e) => e
				});
				if (result.error) {
					results.push({
						data: void 0,
						error: result.error,
						status: parsed.status
					});
					errorCount++;
					if (firstErrorIndex === null) firstErrorIndex = i;
				} else {
					results.push({
						data: result.data,
						error: void 0,
						status: parsed.status
					});
					successCount++;
				}
			}
			return {
				results,
				successCount,
				errorCount,
				truncated,
				firstErrorIndex
			};
		});
		const result = await runLayerResult(this.layer, pipeline, "fmodata.batch");
		if (result.error) return this.failAllResults(result.error);
		return result.data;
	}
};
//#endregion
//#region src/client/builders/default-select.ts
/**
* Helper function to get container field names from a table.
* Container fields cannot be selected via $select in FileMaker OData API.
*/
function getContainerFieldNames(table) {
	const baseTableConfig = getBaseTableConfig(table);
	if (!baseTableConfig?.containerFields) return [];
	return baseTableConfig.containerFields;
}
/**
* Gets default select fields from a table definition.
* Returns undefined if defaultSelect is "all".
* Automatically filters out container fields since they cannot be selected via $select.
*
* @param table - The table occurrence
* @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is "schema"
*/
function getDefaultSelectFields(table, includeSpecialColumns) {
	if (!table) return;
	const defaultSelect = table[FMTable.Symbol.DefaultSelect];
	const containerFields = getContainerFieldNames(table);
	if (defaultSelect === "schema") {
		const baseTableConfig = getBaseTableConfig(table);
		const allFields = Object.keys(baseTableConfig.schema);
		const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
		if (includeSpecialColumns) fields.push("ROWID", "ROWMODID");
		return fields.length > 0 ? fields : void 0;
	}
	if (Array.isArray(defaultSelect)) return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];
	if (typeof defaultSelect === "object" && defaultSelect !== null && !Array.isArray(defaultSelect)) {
		const fieldNames = [];
		for (const value of Object.values(defaultSelect)) if (isColumn(value)) fieldNames.push(value.fieldName);
		if (fieldNames.length > 0) return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];
	}
}
//#endregion
//#region src/client/builders/expand-builder.ts
const FILTER_QUERY_REGEX = /\$filter=([^&]+)/;
/**
* Builds OData expand query strings and validation configs.
* Handles nested expands recursively and transforms relation names to FMTIDs
* when using entity IDs.
*/
var ExpandBuilder = class {
	useEntityIds;
	logger;
	constructor(useEntityIds, logger) {
		this.useEntityIds = useEntityIds;
		this.logger = logger;
	}
	/**
	* Builds OData $expand query string from expand configurations.
	*/
	buildExpandString(configs) {
		if (configs.length === 0) return "";
		return configs.map((config) => this.buildSingleExpand(config)).join(",");
	}
	/**
	* Builds validation configs for expanded navigation properties.
	*/
	buildValidationConfigs(configs) {
		return configs.map((config) => {
			const targetTable = config.targetTable;
			let targetSchema;
			if (targetTable) {
				const baseTableConfig = getBaseTableConfig(targetTable);
				const containerFields = baseTableConfig.containerFields || [];
				const schema = { ...baseTableConfig.schema };
				for (const containerField of containerFields) delete schema[containerField];
				targetSchema = schema;
			}
			let selectedFields;
			if (config.options?.select) selectedFields = Array.isArray(config.options.select) ? config.options.select.map(String) : [String(config.options.select)];
			const nestedExpands = config.nestedExpandConfigs ? this.buildValidationConfigs(config.nestedExpandConfigs) : void 0;
			return {
				relation: config.relation,
				targetSchema,
				targetTable,
				table: targetTable,
				selectedFields,
				nestedExpands
			};
		});
	}
	/**
	* Process an expand() call and return the expand config.
	* Used by both QueryBuilder and RecordBuilder to eliminate duplication.
	*
	* @param targetTable - The target table to expand to
	* @param sourceTable - The source table (for validation)
	* @param callback - Optional callback to configure the expand query
	* @param builderFactory - Function that creates a QueryBuilder for the target table
	* @returns ExpandConfig to add to the builder's expandConfigs array
	*/
	processExpand(targetTable, sourceTable, callback, builderFactory) {
		const relationName = getTableName(targetTable);
		if (sourceTable) {
			const navigationPaths = getNavigationPaths(sourceTable);
			if (navigationPaths && !navigationPaths.includes(relationName)) this.logger.warn(`Cannot expand to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`);
		}
		if (callback && builderFactory) {
			const configuredBuilder = callback(builderFactory());
			const expandOptions = { ...configuredBuilder.queryOptions };
			const filterExpression = configuredBuilder.readState?.filterExpression;
			if (filterExpression && !expandOptions.filter && typeof filterExpression.toODataFilter === "function") expandOptions.filter = filterExpression.toODataFilter(this.useEntityIds);
			if (!expandOptions.select) {
				const defaultFields = getDefaultSelectFields(targetTable);
				if (defaultFields) expandOptions.select = defaultFields;
			}
			const nestedExpandConfigs = configuredBuilder.expandConfigs;
			if (nestedExpandConfigs?.length > 0) {
				const nestedExpandString = this.buildExpandString(nestedExpandConfigs);
				if (nestedExpandString) expandOptions.expand = nestedExpandString;
			}
			return {
				relation: relationName,
				options: expandOptions,
				targetTable,
				nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : void 0
			};
		}
		const defaultFields = getDefaultSelectFields(targetTable);
		if (defaultFields) return {
			relation: relationName,
			options: { select: defaultFields },
			targetTable
		};
		return {
			relation: relationName,
			targetTable
		};
	}
	/**
	* Builds a single expand string with its options.
	*/
	buildSingleExpand(config) {
		const relationName = this.resolveRelationName(config);
		const parts = this.buildExpandParts(config);
		if (parts.length === 0) return relationName;
		return `${relationName}(${parts.join(";")})`;
	}
	/**
	* Resolves relation name, using FMTID if entity IDs are enabled.
	*/
	resolveRelationName(config) {
		if (!this.useEntityIds) return config.relation;
		const targetTable = config.targetTable;
		if (targetTable && FMTable.Symbol.EntityId in targetTable) {
			const tableId = targetTable[FMTable.Symbol.EntityId];
			if (tableId) return tableId;
		}
		return config.relation;
	}
	/**
	* Builds expand parts (select, filter, orderBy, etc.) for a single expand.
	*/
	buildExpandParts(config) {
		if (!config.options || Object.keys(config.options).length === 0) return [];
		const parts = [];
		const opts = config.options;
		if (opts.select) {
			const selectFields = formatSelectFields(Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)], config.targetTable, this.useEntityIds);
			if (selectFields) parts.push(`$select=${selectFields}`);
		}
		if (opts.filter) if (typeof opts.filter === "string") parts.push(`$filter=${opts.filter}`);
		else {
			const match = buildQuery({ filter: opts.filter }).match(FILTER_QUERY_REGEX);
			if (match) parts.push(`$filter=${match[1]}`);
		}
		if (opts.orderBy) {
			const orderByValue = Array.isArray(opts.orderBy) ? opts.orderBy.join(",") : String(opts.orderBy);
			parts.push(`$orderby=${orderByValue}`);
		}
		if (opts.top !== void 0) parts.push(`$top=${opts.top}`);
		if (opts.skip !== void 0) parts.push(`$skip=${opts.skip}`);
		if (opts.expand && typeof opts.expand === "string") parts.push(`$expand=${opts.expand}`);
		return parts;
	}
};
//#endregion
//#region src/client/builders/query-string-builder.ts
/**
* Builds OData query string for $select and $expand parameters.
* Used by both QueryBuilder and RecordBuilder to eliminate duplication.
*
* @param config - Configuration object
* @returns Query string starting with ? or empty string if no parameters
*/
function buildSelectExpandQueryString(config) {
	const parts = [];
	const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);
	if (config.selectedFields && config.selectedFields.length > 0) {
		const selectString = formatSelectFields(config.selectedFields, config.table, config.useEntityIds);
		if (selectString) parts.push(`$select=${selectString}`);
	}
	const expandString = expandBuilder.buildExpandString(config.expandConfigs);
	if (expandString) parts.push(`$expand=${expandString}`);
	return parts.length > 0 ? `?${parts.join("&")}` : "";
}
//#endregion
//#region src/client/builders/read-builder-state.ts
function createInitialQueryReadBuilderState() {
	return {
		queryOptions: {},
		expandConfigs: [],
		singleMode: false,
		isCountMode: false,
		includeCountMode: false
	};
}
function cloneQueryReadBuilderState(state, changes) {
	let fieldMapping = state.fieldMapping ? { ...state.fieldMapping } : void 0;
	if ("fieldMapping" in (changes ?? {})) fieldMapping = changes?.fieldMapping ? { ...changes.fieldMapping } : void 0;
	return {
		...state,
		...changes,
		queryOptions: {
			...state.queryOptions,
			...changes?.queryOptions ?? {}
		},
		expandConfigs: changes?.expandConfigs ? [...changes.expandConfigs] : [...state.expandConfigs],
		fieldMapping
	};
}
function createInitialRecordReadBuilderState() {
	return { expandConfigs: [] };
}
function cloneRecordReadBuilderState(state, changes) {
	let selectedFields = state.selectedFields ? [...state.selectedFields] : void 0;
	if ("selectedFields" in (changes ?? {})) selectedFields = changes?.selectedFields ? [...changes.selectedFields] : void 0;
	let fieldMapping = state.fieldMapping ? { ...state.fieldMapping } : void 0;
	if ("fieldMapping" in (changes ?? {})) fieldMapping = changes?.fieldMapping ? { ...changes.fieldMapping } : void 0;
	return {
		...state,
		...changes,
		selectedFields,
		expandConfigs: changes?.expandConfigs ? [...changes.expandConfigs] : [...state.expandConfigs],
		fieldMapping
	};
}
//#endregion
//#region src/validation.ts
/**
* Validates and transforms input data for insert/update operations.
* Applies input validators (writeValidators) to transform user input to database format.
* Fields without input validators are passed through unchanged.
*
* @param data - The input data to validate and transform
* @param inputSchema - Optional schema containing input validators for each field
* @returns Transformed data ready to send to the server
* @throws ValidationError if any field fails validation
*/
async function validateAndTransformInput(data, inputSchema) {
	if (!inputSchema) return data;
	const transformedData = { ...data };
	const allIssues = [];
	const failedFields = [];
	for (const [fieldName, fieldSchema] of Object.entries(inputSchema)) {
		if (!fieldSchema) continue;
		if (fieldName in data) {
			const inputValue = data[fieldName];
			try {
				let result = fieldSchema["~standard"].validate(inputValue);
				if (result instanceof Promise) result = await result;
				if (result.issues) {
					for (const issue of result.issues) allIssues.push({
						...issue,
						path: issue.path ? [fieldName, ...issue.path] : [fieldName]
					});
					failedFields.push(fieldName);
					continue;
				}
				transformedData[fieldName] = result.value;
			} catch (error) {
				if (error instanceof ValidationError) for (const issue of error.issues) allIssues.push({
					...issue,
					path: issue.path ? [fieldName, ...issue.path] : [fieldName]
				});
				else allIssues.push({
					message: error instanceof Error ? error.message : String(error),
					path: [fieldName]
				});
				failedFields.push(fieldName);
			}
		}
	}
	if (allIssues.length > 0) throw new ValidationError(`Input validation failed for field${failedFields.length > 1 ? "s" : ""} '${failedFields.join("', '")}'`, allIssues, { field: failedFields[0] });
	return transformedData;
}
/**
* Validates a single record against a schema, only validating selected fields.
* Also validates expanded relations if expandConfigs are provided.
*/
async function validateRecord(record, schema, selectedFields, expandConfigs, includeSpecialColumns) {
	const { "@id": id, "@editLink": editLink, ...rest } = record;
	const metadata = {};
	if (id) metadata["@id"] = id;
	if (editLink) metadata["@editLink"] = editLink;
	if (!schema) {
		const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;
		const specialColumns = {};
		if (includeSpecialColumns) {
			if (ROWID !== void 0) specialColumns.ROWID = ROWID;
			if (ROWMODID !== void 0) specialColumns.ROWMODID = ROWMODID;
		}
		return {
			valid: true,
			data: {
				...restWithoutSystemFields,
				...specialColumns,
				...metadata
			}
		};
	}
	const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;
	const specialColumns = {};
	if (includeSpecialColumns) {
		if (ROWID !== void 0) specialColumns.ROWID = ROWID;
		if (ROWMODID !== void 0) specialColumns.ROWMODID = ROWMODID;
	}
	if (selectedFields && selectedFields.length > 0) {
		const validatedRecord = {};
		const allIssues = [];
		const failedFields = [];
		for (const field of selectedFields) {
			const fieldName = String(field);
			const fieldSchema = schema[fieldName];
			if (fieldSchema) {
				const input = rest[fieldName];
				try {
					let result = fieldSchema["~standard"].validate(input);
					if (result instanceof Promise) result = await result;
					if (result.issues) {
						for (const issue of result.issues) allIssues.push({
							...issue,
							path: issue.path ? [fieldName, ...issue.path] : [fieldName]
						});
						failedFields.push(fieldName);
						continue;
					}
					validatedRecord[fieldName] = result.value;
				} catch (originalError) {
					if (originalError instanceof ValidationError) for (const issue of originalError.issues) allIssues.push({
						...issue,
						path: issue.path ? [fieldName, ...issue.path] : [fieldName]
					});
					else allIssues.push({
						message: originalError instanceof Error ? originalError.message : String(originalError),
						path: [fieldName]
					});
					failedFields.push(fieldName);
				}
			} else if (fieldName === "ROWID" || fieldName === "ROWMODID") {
				if (fieldName === "ROWID" && ROWID !== void 0) validatedRecord[fieldName] = ROWID;
				else if (fieldName === "ROWMODID" && ROWMODID !== void 0) validatedRecord[fieldName] = ROWMODID;
			} else validatedRecord[fieldName] = rest[fieldName];
		}
		if (allIssues.length > 0) return {
			valid: false,
			error: new ValidationError(`Validation failed for field${failedFields.length > 1 ? "s" : ""} '${failedFields.join("', '")}'`, allIssues, {
				field: failedFields[0],
				value: record
			})
		};
		if (expandConfigs && expandConfigs.length > 0) for (const expandConfig of expandConfigs) {
			const expandValue = rest[expandConfig.relation];
			if (expandValue === void 0) {
				if (Array.isArray(rest.error) && rest.error.length > 0) {
					const errorDetail = rest.error[0]?.error;
					if (errorDetail?.message) {
						const errorMessage = errorDetail.message;
						if (errorMessage.toLowerCase().includes(expandConfig.relation.toLowerCase()) || expandConfig.selectedFields?.some((field) => errorMessage.toLowerCase().includes(field.toLowerCase()))) return {
							valid: false,
							error: new ValidationError(`Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`, [], { field: expandConfig.relation })
						};
					}
				}
			} else if (Array.isArray(expandValue)) {
				const validatedExpandedItems = [];
				for (let i = 0; i < expandValue.length; i++) {
					const item = expandValue[i];
					const itemValidation = await validateRecord(item, expandConfig.targetSchema, expandConfig.selectedFields, expandConfig.nestedExpands, includeSpecialColumns);
					if (!itemValidation.valid) return {
						valid: false,
						error: new ValidationError(`Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`, itemValidation.error.issues, {
							field: expandConfig.relation,
							cause: itemValidation.error.cause
						})
					};
					validatedExpandedItems.push(itemValidation.data);
				}
				validatedRecord[expandConfig.relation] = validatedExpandedItems;
			} else {
				const itemValidation = await validateRecord(expandValue, expandConfig.targetSchema, expandConfig.selectedFields, expandConfig.nestedExpands, includeSpecialColumns);
				if (!itemValidation.valid) return {
					valid: false,
					error: new ValidationError(`Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`, itemValidation.error.issues, {
						field: expandConfig.relation,
						cause: itemValidation.error.cause
					})
				};
				validatedRecord[expandConfig.relation] = itemValidation.data;
			}
		}
		return {
			valid: true,
			data: {
				...validatedRecord,
				...specialColumns,
				...metadata
			}
		};
	}
	const validatedRecord = { ...restWithoutSystemFields };
	const allIssues = [];
	const failedFields = [];
	for (const [fieldName, fieldSchema] of Object.entries(schema)) {
		if (!fieldSchema) continue;
		const input = rest[fieldName];
		try {
			let result = fieldSchema["~standard"].validate(input);
			if (result instanceof Promise) result = await result;
			if (result.issues) {
				for (const issue of result.issues) allIssues.push({
					...issue,
					path: issue.path ? [fieldName, ...issue.path] : [fieldName]
				});
				failedFields.push(fieldName);
				continue;
			}
			validatedRecord[fieldName] = result.value;
		} catch (originalError) {
			if (originalError instanceof ValidationError) for (const issue of originalError.issues) allIssues.push({
				...issue,
				path: issue.path ? [fieldName, ...issue.path] : [fieldName]
			});
			else allIssues.push({
				message: originalError instanceof Error ? originalError.message : String(originalError),
				path: [fieldName]
			});
			failedFields.push(fieldName);
		}
	}
	if (allIssues.length > 0) return {
		valid: false,
		error: new ValidationError(`Validation failed for field${failedFields.length > 1 ? "s" : ""} '${failedFields.join("', '")}'`, allIssues, {
			field: failedFields[0],
			value: record
		})
	};
	if (expandConfigs && expandConfigs.length > 0) for (const expandConfig of expandConfigs) {
		const expandValue = rest[expandConfig.relation];
		if (expandValue === void 0) {
			if (Array.isArray(rest.error) && rest.error.length > 0) {
				const errorDetail = rest.error[0]?.error;
				if (errorDetail?.message) {
					const errorMessage = errorDetail.message;
					if (errorMessage.toLowerCase().includes(expandConfig.relation.toLowerCase()) || expandConfig.selectedFields?.some((field) => errorMessage.toLowerCase().includes(field.toLowerCase()))) return {
						valid: false,
						error: new ValidationError(`Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`, [], { field: expandConfig.relation })
					};
				}
			}
		} else if (Array.isArray(expandValue)) {
			const validatedExpandedItems = [];
			for (let i = 0; i < expandValue.length; i++) {
				const item = expandValue[i];
				const itemValidation = await validateRecord(item, expandConfig.targetSchema, expandConfig.selectedFields, expandConfig.nestedExpands, includeSpecialColumns);
				if (!itemValidation.valid) return {
					valid: false,
					error: new ValidationError(`Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`, itemValidation.error.issues, {
						field: expandConfig.relation,
						cause: itemValidation.error.cause
					})
				};
				validatedExpandedItems.push(itemValidation.data);
			}
			validatedRecord[expandConfig.relation] = validatedExpandedItems;
		} else {
			const itemValidation = await validateRecord(expandValue, expandConfig.targetSchema, expandConfig.selectedFields, expandConfig.nestedExpands, includeSpecialColumns);
			if (!itemValidation.valid) return {
				valid: false,
				error: new ValidationError(`Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`, itemValidation.error.issues, {
					field: expandConfig.relation,
					cause: itemValidation.error.cause
				})
			};
			validatedRecord[expandConfig.relation] = itemValidation.data;
		}
	}
	return {
		valid: true,
		data: {
			...validatedRecord,
			...specialColumns,
			...metadata
		}
	};
}
/**
* Validates a list response against a schema.
*/
async function validateListResponse(response, schema, selectedFields, expandConfigs, includeSpecialColumns) {
	if (!response || typeof response !== "object") return {
		valid: false,
		error: new ResponseStructureError("an object", response)
	};
	const { "@context": context, value, ..._rest } = response;
	if (!Array.isArray(value)) return {
		valid: false,
		error: new ResponseStructureError("'value' property to be an array", value)
	};
	const validatedRecords = [];
	for (const record of value) {
		const validation = await validateRecord(record, schema, selectedFields, expandConfigs, includeSpecialColumns);
		if (!validation.valid) return {
			valid: false,
			error: validation.error
		};
		validatedRecords.push(validation.data);
	}
	return {
		valid: true,
		data: validatedRecords
	};
}
/**
* Validates a single record response against a schema.
*/
async function validateSingleResponse(response, schema, selectedFields, expandConfigs, mode = "maybe", includeSpecialColumns) {
	if (response.value && Array.isArray(response.value) && response.value.length > 1) return {
		valid: false,
		error: new RecordCountMismatchError(mode === "exact" ? "one" : "at-most-one", response.value.length)
	};
	if (!response || response.value && response.value.length === 0) {
		if (mode === "exact") return {
			valid: false,
			error: new RecordCountMismatchError("one", 0)
		};
		return {
			valid: true,
			data: null
		};
	}
	const validation = await validateRecord(response.value?.[0] ?? response, schema, selectedFields, expandConfigs, includeSpecialColumns);
	if (!validation.valid) return validation;
	return {
		valid: true,
		data: validation.data
	};
}
//#endregion
//#region src/client/builders/response-processor.ts
/**
* Processes OData response with transformation and validation.
* Shared by QueryBuilder and RecordBuilder.
*/
async function processODataResponse(rawResponse, config) {
	const { table, schema, singleMode, selectedFields, expandValidationConfigs, skipValidation, useEntityIds, includeSpecialColumns, fieldMapping } = config;
	let response = rawResponse;
	if (table && useEntityIds) response = transformResponseFields(response, table, expandValidationConfigs);
	if (skipValidation) {
		const result = extractRecords(response, singleMode);
		if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {
			if (result.error) return {
				data: void 0,
				error: result.error
			};
			return {
				data: renameFieldsInResponse(result.data, fieldMapping),
				error: void 0
			};
		}
		return result;
	}
	if (singleMode !== false) {
		const validation = await validateSingleResponse(response, schema, selectedFields, expandValidationConfigs, singleMode, includeSpecialColumns);
		if (!validation.valid) return {
			data: void 0,
			error: validation.error
		};
		if (fieldMapping && Object.keys(fieldMapping).length > 0) return {
			data: renameFieldsInResponse(validation.data, fieldMapping),
			error: void 0
		};
		return {
			data: validation.data,
			error: void 0
		};
	}
	const validation = await validateListResponse(response, schema, selectedFields, expandValidationConfigs, includeSpecialColumns);
	if (!validation.valid) return {
		data: void 0,
		error: validation.error
	};
	if (fieldMapping && Object.keys(fieldMapping).length > 0) return {
		data: renameFieldsInResponse(validation.data, fieldMapping),
		error: void 0
	};
	return {
		data: validation.data,
		error: void 0
	};
}
/**
* Extracts records from response without validation.
*/
function extractRecords(response, singleMode) {
	if (singleMode === false) return {
		data: response.value ?? [],
		error: void 0
	};
	const records = response.value ?? [response];
	const count = Array.isArray(records) ? records.length : 1;
	if (count > 1) return {
		data: void 0,
		error: new RecordCountMismatchError(singleMode === "exact" ? "one" : "at-most-one", count)
	};
	if (count === 0) {
		if (singleMode === "exact") return {
			data: void 0,
			error: new RecordCountMismatchError("one", 0)
		};
		return {
			data: null,
			error: void 0
		};
	}
	return {
		data: Array.isArray(records) ? records[0] : records,
		error: void 0
	};
}
/**
* Gets schema from a table occurrence, excluding container fields.
* Container fields are never returned in regular responses (only via getSingleField).
*/
function getSchemaFromTable(table) {
	if (!table) return;
	const baseTableConfig = getBaseTableConfig(table);
	const containerFields = baseTableConfig.containerFields || [];
	const schema = { ...baseTableConfig.schema };
	for (const containerField of containerFields) delete schema[containerField];
	return schema;
}
/**
* Renames fields in response data according to the field mapping.
* Used when select() is called with renamed fields (e.g., { userEmail: users.email }).
*/
function renameFieldsInResponse(data, fieldMapping) {
	if (!data || typeof data !== "object") return data;
	if (Array.isArray(data)) return data.map((item) => renameFieldsInResponse(item, fieldMapping));
	if ("value" in data && Array.isArray(data.value)) return {
		...data,
		value: data.value.map((item) => renameFieldsInResponse(item, fieldMapping))
	};
	const renamed = {};
	for (const [key, value] of Object.entries(data)) {
		const outputKey = fieldMapping[key];
		if (outputKey) renamed[outputKey] = value;
		else renamed[key] = value;
	}
	return renamed;
}
/**
* Processes query response with expand configs.
* This is a convenience wrapper that builds validation configs from expand configs.
*/
async function processQueryResponse(response, config) {
	const { occurrence, singleMode, queryOptions, expandConfigs, skipValidation, useEntityIds, includeSpecialColumns, includeCount, fieldMapping, logger } = config;
	const expandValidationConfigs = new ExpandBuilder(useEntityIds ?? false, logger).buildValidationConfigs(expandConfigs);
	let selectedFields;
	if (queryOptions.select) selectedFields = Array.isArray(queryOptions.select) ? queryOptions.select.map(String) : [String(queryOptions.select)];
	let processedResponse = await processODataResponse(response, {
		table: occurrence,
		schema: getSchemaFromTable(occurrence),
		singleMode,
		selectedFields,
		expandValidationConfigs,
		skipValidation,
		useEntityIds,
		includeSpecialColumns
	});
	if (processedResponse.data && fieldMapping && Object.keys(fieldMapping).length > 0) processedResponse = {
		...processedResponse,
		data: renameFieldsInResponse(processedResponse.data, fieldMapping)
	};
	if (includeCount) {
		if (processedResponse.error) return {
			data: void 0,
			error: processedResponse.error
		};
		if (singleMode !== false) return {
			data: void 0,
			error: new ResponseStructureError("list response for count-enabled query", response)
		};
		const rawCount = response?.["@odata.count"];
		let parsedCount = NaN;
		if (typeof rawCount === "number") parsedCount = rawCount;
		else if (typeof rawCount === "string" && rawCount.trim() !== "") parsedCount = Number(rawCount);
		if (!Number.isFinite(parsedCount)) return {
			data: void 0,
			error: new ResponseStructureError("response with valid @odata.count", response)
		};
		return {
			data: {
				records: processedResponse.data,
				count: parsedCount
			},
			error: void 0
		};
	}
	return processedResponse;
}
/**
* Processes record response by delegating to the canonical query processor.
* Record reads are query reads with singleMode fixed to "exact".
*/
async function processRecordResponse(response, config) {
	return await processQueryResponse(response, {
		occurrence: config.table,
		singleMode: "exact",
		queryOptions: { select: config.selectedFields },
		expandConfigs: config.expandConfigs,
		skipValidation: config.skipValidation,
		useEntityIds: config.useEntityIds,
		includeSpecialColumns: config.includeSpecialColumns,
		fieldMapping: config.fieldMapping,
		logger: config.logger
	});
}
//#endregion
//#region src/client/builders/select-mixin.ts
/**
* Processes select() calls with field renaming support.
* Validates columns belong to the correct table and builds field mapping for renamed fields.
* Used by both QueryBuilder and RecordBuilder to eliminate duplication.
*
* @param fields - Object mapping output keys to column references
* @param tableName - Expected table name for validation
* @returns Object with selectedFields array and fieldMapping for renamed fields
*/
function processSelectWithRenames(fields, tableName, logger) {
	const selectedFields = [];
	const fieldMapping = {};
	for (const [outputKey, column] of Object.entries(fields)) {
		if (!isColumn(column)) throw new Error(`select() expects column references, but got: ${typeof column}`);
		if (column.tableName !== tableName) logger.warn(`Column ${column.toString()} is from table "${column.tableName}", but query is for table "${tableName}"`);
		const fieldName = column.fieldName;
		selectedFields.push(fieldName);
		if (fieldName !== outputKey) fieldMapping[fieldName] = outputKey;
	}
	return {
		selectedFields,
		fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : {}
	};
}
//#endregion
//#region src/client/sanitize-json.ts
/**
* FileMaker OData API sometimes returns invalid JSON containing unquoted `?`
* characters as field values (e.g., `"fieldName": ?`), which causes JSON.parse()
* to fail. This module provides utilities to sanitize such responses before parsing.
*/
/**
* Sanitizes FileMaker OData JSON responses by replacing unquoted `?` values with `null`.
*
* FileMaker uses `?` to represent undefined/null values in its OData responses,
* but this is not valid JSON. This function converts those to proper `null` values.
*
* The regex uses two patterns:
* 1. `/:\s*\?(?=\s*[,}\]])/g` - for values in objects (after `:`)
* 2. `/(?<=[\[,])\s*\?(?=\s*[,\]])/g` - for values in arrays (after `[` or `,`)
*
* @param text - The raw response text from FileMaker OData API
* @returns Sanitized JSON string with `?` values replaced by `null`
*
* @example
* sanitizeFileMakerJson('{"field1": "valid", "field2": ?, "field3": null}')
* // Returns: '{"field1": "valid", "field2": null, "field3": null}'
*/
function sanitizeFileMakerJson(text) {
	let result = text.replace(/:\s*\?(?=\s*[,}\]])/g, ": null");
	result = result.replace(/(?<=[[,])\s*\?(?=\s*[,\]])/g, " null");
	return result;
}
/**
* Safely parses a Response body as JSON, handling FileMaker's invalid JSON responses.
*
* This function reads the response as text first, sanitizes any invalid `?` values,
* and then parses the sanitized JSON. This approach handles the case where FileMaker
* returns a Content-Type of application/json but the body contains invalid JSON.
*
* @param response - The fetch Response object
* @returns Parsed JSON data
* @throws ResponseParseError if the JSON is still invalid after sanitization (includes sanitized text for debugging)
*/
async function safeJsonParse(response) {
	const sanitized = sanitizeFileMakerJson(await response.text());
	try {
		return JSON.parse(sanitized);
	} catch (err) {
		throw new ResponseParseError(response.url, `Failed to parse response as JSON: ${err instanceof Error ? err.message : "Unknown error"}`, {
			rawText: sanitized,
			cause: err instanceof Error ? err : void 0
		});
	}
}
//#endregion
//#region src/client/error-parser.ts
/**
* Parses an error response and returns an appropriate error object.
* This helper is used by builder processResponse methods to handle error responses
* consistently, particularly important for batch operations where errors need to be
* properly parsed from the response body.
*
* @param response - The Response object (may be from batch or direct request)
* @param url - The URL that was requested (for error context)
* @returns An appropriate error object (ODataError, SchemaLockedError, or HTTPError)
*/
async function parseErrorResponse(response, url) {
	let errorBody;
	try {
		if (response.headers.get("content-type")?.includes("application/json")) errorBody = await safeJsonParse(response);
	} catch {}
	if (errorBody?.error) {
		const errorCode = errorBody.error.code;
		const errorMessage = errorBody.error.message || response.statusText;
		if (errorCode === "303" || errorCode === 303) return new SchemaLockedError(url, errorMessage, errorBody.error);
		return new ODataError(url, errorMessage, String(errorCode), errorBody.error);
	}
	return new HTTPError(url, response.status, response.statusText, errorBody);
}
//#endregion
//#region src/client/query/url-builder.ts
/**
* 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
*/
var QueryUrlBuilder = class {
	databaseName;
	occurrence;
	useEntityIds;
	constructor(databaseName, occurrence, useEntityIds) {
		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, options) {
		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, options) {
		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 !== void 0 && 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 === void 0) 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, queryString, options) {
		const effectiveUseEntityIds = options?.useEntityIds ?? this.useEntityIds;
		const tableId = resolveTableId(this.occurrence, getTableName(this.occurrence), effectiveUseEntityIds);
		let url;
		if (options?.isNavigateFromEntitySet && options.navigateSourceTableName && options.navigateRelation) url = `/${this.databaseName}/${buildRecordPath(`${options.navigateSourceTableName}/${options.navigateRelation}`, recordLocator)}`;
		else url = `/${this.databaseName}/${buildRecordPath(tableId, recordLocator)}`;
		if (options?.operation === "getSingleField" && options.operationParam) url += `/${options.operationParam}`;
		return url + queryString;
	}
};
//#endregion
//#region src/client/count-builder.ts
function normalizeCountBuildError(error) {
	if (isFMODataError(error)) return error;
	if (error instanceof Error) return new BuilderInvariantError("CountBuilder.execute", error.message, { cause: error });
	return new BuilderInvariantError("CountBuilder.execute", String(error));
}
var CountBuilder = class {
	occurrence;
	layer;
	config;
	urlBuilder;
	filterExpression;
	queryOptions = {};
	navigationConfig;
	constructor(config) {
		this.occurrence = config.occurrence;
		const runtime = createClientRuntime(config.layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
		this.urlBuilder = new QueryUrlBuilder(this.config.databaseName, this.occurrence, this.config.useEntityIds);
	}
	set navigation(navigation) {
		this.navigationConfig = navigation;
	}
	where(expression) {
		if (typeof expression === "string") {
			this.filterExpression = void 0;
			this.queryOptions.filter = expression;
			return this;
		}
		this.filterExpression = expression;
		this.queryOptions.filter = void 0;
		return this;
	}
	buildQueryString(useEntityIds) {
		const finalUseEntityIds = useEntityIds ?? this.config.useEntityIds;
		const queryOptions = { ...this.queryOptions };
		if (this.filterExpression) queryOptions.filter = this.filterExpression.toODataFilter(finalUseEntityIds);
		queryOptions.count = void 0;
		queryOptions.select = void 0;
		queryOptions.expand = void 0;
		queryOptions.top = void 0;
		queryOptions.skip = void 0;
		queryOptions.orderBy = void 0;
		return buildQuery(queryOptions);
	}
	parseCountValue(raw) {
		let count = NaN;
		if (typeof raw === "number") count = raw;
		else if (typeof raw === "string" && raw.trim() !== "") count = Number(raw);
		return Number.isFinite(count) ? count : new ResponseStructureError("numeric count response", raw);
	}
	execute(options) {
		const mergedOptions = mergeExecuteOptions(options, this.config.useEntityIds);
		let queryString;
		let url;
		try {
			queryString = this.buildQueryString(mergedOptions.useEntityIds);
			url = this.urlBuilder.build(queryString, {
				isCount: true,
				useEntityIds: mergedOptions.useEntityIds,
				navigation: this.navigationConfig
			});
		} catch (error) {
			return Promise.resolve({
				data: void 0,
				error: normalizeCountBuildError(error)
			});
		}
		const pipeline = requestFromService(url, mergedOptions).pipe(Effect.flatMap((data) => {
			const parsed = this.parseCountValue(data);
			return parsed instanceof ResponseStructureError ? Effect.fail(parsed) : Effect.succeed(parsed);
		}));
		return runLayerResult(this.layer, pipeline, "fmodata.query.count", { "fmodata.table": getTableName(this.occurrence) });
	}
	getQueryString(options) {
		const useEntityIds = options?.useEntityIds ?? this.config.useEntityIds;
		const queryString = this.buildQueryString(useEntityIds);
		return this.urlBuilder.buildPath(queryString, {
			isCount: true,
			useEntityIds,
			navigation: this.navigationConfig
		});
	}
	getRequestConfig() {
		const queryString = this.buildQueryString(this.config.useEntityIds);
		return {
			method: "GET",
			url: this.urlBuilder.build(queryString, {
				isCount: true,
				useEntityIds: this.config.useEntityIds,
				navigation: this.navigationConfig
			})
		};
	}
	toRequest(baseUrl, options) {
		return createODataRequest(baseUrl, this.getRequestConfig(), {
			...options,
			normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName
		});
	}
	async processResponse(response, _options) {
		if (!response.ok) return {
			data: void 0,
			error: await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${getTableName(this.occurrence)}/$count`)
		};
		const raw = await response.text();
		const parsed = this.parseCountValue(raw);
		if (parsed instanceof ResponseStructureError) return {
			data: void 0,
			error: parsed
		};
		return {
			data: parsed,
			error: void 0
		};
	}
};
//#endregion
//#region src/orm/operators.ts
/**
* FilterExpression represents a filter condition that can be used in where() clauses.
* Internal representation of operator expressions that get converted to OData filter syntax.
*/
var FilterExpression = class FilterExpression {
	operator;
	operands;
	constructor(operator, operands) {
		this.operator = operator;
		this.operands = operands;
	}
	/**
	* Convert this expression to OData filter syntax.
	* @internal Used by QueryBuilder
	*/
	toODataFilter(useEntityIds) {
		switch (this.operator) {
			case "eq": return this._binaryOp("eq", useEntityIds);
			case "ne": return this._binaryOp("ne", useEntityIds);
			case "gt": return this._binaryOp("gt", useEntityIds);
			case "gte": return this._binaryOp("ge", useEntityIds);
			case "lt": return this._binaryOp("lt", useEntityIds);
			case "lte": return this._binaryOp("le", useEntityIds);
			case "in": return this._inOp(useEntityIds);
			case "notIn": return this._notInOp(useEntityIds);
			case "contains": return this._functionOp("contains", useEntityIds);
			case "startsWith": return this._functionOp("startswith", useEntityIds);
			case "endsWith": return this._functionOp("endswith", useEntityIds);
			case "matchesPattern": return this._functionOp("matchesPattern", useEntityIds);
			case "isNull": return this._isNullOp(useEntityIds);
			case "isNotNull": return this._isNotNullOp(useEntityIds);
			case "and": return this._logicalOp("and", useEntityIds);
			case "or": return this._logicalOp("or", useEntityIds);
			case "not": return this._notOp(useEntityIds);
			default: throw new Error(`Unknown operator: ${this.operator}`);
		}
	}
	_binaryOp(op, useEntityIds) {
		const [left, right] = this.operands;
		let columnForValue;
		if (isColumn(left) && !isColumn(right)) columnForValue = left;
		else if (isColumn(right) && !isColumn(left)) columnForValue = right;
		else columnForValue = void 0;
		return `${this._operandToString(left, useEntityIds, columnForValue)} ${op} ${this._operandToString(right, useEntityIds, columnForValue)}`;
	}
	_functionOp(fnName, useEntityIds) {
		const [column, value] = this.operands;
		const columnInstance = isColumn(column) ? column : void 0;
		return `${fnName}(${this._operandToString(column, useEntityIds)}, ${this._operandToString(value, useEntityIds, columnInstance)})`;
	}
	_inOp(useEntityIds) {
		const [column, values] = this.operands;
		const columnInstance = isColumn(column) ? column : void 0;
		return `${this._operandToString(column, useEntityIds)} in (${values.map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(", ")})`;
	}
	_notInOp(useEntityIds) {
		const [column, values] = this.operands;
		const columnInstance = isColumn(column) ? column : void 0;
		return `not (${this._operandToString(column, useEntityIds)} in (${values.map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(", ")}))`;
	}
	_isNullOp(useEntityIds) {
		const [column] = this.operands;
		return `${this._operandToString(column, useEntityIds)} eq null`;
	}
	_isNotNullOp(useEntityIds) {
		const [column] = this.operands;
		return `${this._operandToString(column, useEntityIds)} ne null`;
	}
	_logicalOp(op, useEntityIds) {
		return this.operands.map((expr) => {
			if (expr instanceof FilterExpression) {
				const innerExpr = expr.toODataFilter(useEntityIds);
				if (expr.operator === "and" || expr.operator === "or") return `(${innerExpr})`;
				return innerExpr;
			}
			throw new Error("Logical operators require FilterExpression operands");
		}).join(` ${op} `);
	}
	_notOp(useEntityIds) {
		const [expr] = this.operands;
		if (expr instanceof FilterExpression) return `not (${expr.toODataFilter(useEntityIds)})`;
		throw new Error("NOT operator requires a FilterExpression operand");
	}
	_formatTemporalValue(value, fieldType) {
		if (!(value instanceof Date)) return String(value);
		if (fieldType === "date") return value.toISOString().slice(0, 10);
		if (fieldType === "time") return value.toISOString().slice(11, 19);
		return value.toISOString();
	}
	_operandToString(operand, useEntityIds, column) {
		if (isColumnFunction(operand)) return operand.toFilterString(useEntityIds);
		if (isColumn(operand)) {
			const fieldIdentifier = operand.getFieldIdentifier(useEntityIds);
			return needsFieldQuoting(fieldIdentifier) ? `"${fieldIdentifier}"` : fieldIdentifier;
		}
		let value = operand;
		if (column?.inputValidator) try {
			const result = column.inputValidator["~standard"].validate(value);
			if (result instanceof Promise) value = operand;
			else if ("issues" in result && result.issues) value = operand;
			else if ("value" in result) value = result.value;
		} catch (_error) {
			value = operand;
		}
		const ft = column?.fieldType;
		if (ft === "date" || ft === "time" || ft === "timestamp") return this._formatTemporalValue(value, ft);
		if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
		if (value === null || value === void 0) return "null";
		if (value instanceof Date) return value.toISOString();
		if (typeof value === "object") {
			const valueType = value?.constructor?.name ?? "Object";
			throw new Error(`Unsupported filter operand: received ${valueType}. Pass a table column or a primitive filter value.`);
		}
		return String(value);
	}
};
/**
* OrderByExpression represents a sort order specification for a column.
* Used in orderBy() clauses to provide type-safe sorting with direction.
*/
var OrderByExpression = class {
	column;
	direction;
	constructor(column, direction) {
		this.column = column;
		this.direction = direction;
	}
};
/**
* Type guard to check if a value is an OrderByExpression instance.
*/
function isOrderByExpression(value) {
	return value instanceof OrderByExpression;
}
//#endregion
//#region src/client/query/query-builder.ts
function normalizeQueryBuildError(error) {
	if (isFMODataError(error)) return error;
	if (error instanceof Error) return new BuilderInvariantError("QueryBuilder.execute", error.message, { cause: error });
	return new BuilderInvariantError("QueryBuilder.execute", String(error));
}
var QueryBuilder = class QueryBuilder {
	readState = createInitialQueryReadBuilderState();
	occurrence;
	expandBuilder;
	urlBuilder;
	layer;
	config;
	logger;
	get queryOptions() {
		return this.readState.queryOptions;
	}
	set queryOptions(queryOptions) {
		this.readState = cloneQueryReadBuilderState(this.readState, { queryOptions });
	}
	get expandConfigs() {
		return this.readState.expandConfigs;
	}
	set expandConfigs(expandConfigs) {
		this.readState = cloneQueryReadBuilderState(this.readState, { expandConfigs });
	}
	get singleMode() {
		return this.readState.singleMode;
	}
	set singleMode(singleMode) {
		this.readState = cloneQueryReadBuilderState(this.readState, { singleMode });
	}
	get isCountMode() {
		return this.readState.isCountMode;
	}
	set isCountMode(isCountMode) {
		this.readState = cloneQueryReadBuilderState(this.readState, { isCountMode });
	}
	get includeCountMode() {
		return this.readState.includeCountMode;
	}
	set includeCountMode(includeCountMode) {
		this.readState = cloneQueryReadBuilderState(this.readState, { includeCountMode });
	}
	get fieldMapping() {
		return this.readState.fieldMapping;
	}
	set fieldMapping(fieldMapping) {
		this.readState = cloneQueryReadBuilderState(this.readState, { fieldMapping });
	}
	get systemColumns() {
		return this.readState.systemColumns;
	}
	set systemColumns(systemColumns) {
		this.readState = cloneQueryReadBuilderState(this.readState, { systemColumns });
	}
	get navigation() {
		return this.readState.navigation;
	}
	set navigation(navigation) {
		this.setNavigation(navigation);
	}
	constructor(config) {
		this.occurrence = config.occurrence;
		const runtime = createClientRuntime(config.layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
		this.logger = runtime.logger;
		this.expandBuilder = new ExpandBuilder(this.config.useEntityIds, this.logger);
		this.urlBuilder = new QueryUrlBuilder(this.config.databaseName, this.occurrence, this.config.useEntityIds);
	}
	/**
	* Helper to merge database-level useEntityIds and includeSpecialColumns with per-request options
	*/
	mergeExecuteOptions(options) {
		return {
			...mergeExecuteOptions(options, this.config.useEntityIds),
			includeSpecialColumns: options?.includeSpecialColumns ?? this.config.includeSpecialColumns
		};
	}
	patchQueryOptions(patch) {
		this.readState = cloneQueryReadBuilderState(this.readState, { queryOptions: patch });
	}
	setFilterExpression(expression) {
		this.readState = cloneQueryReadBuilderState(this.readState, { filterExpression: expression });
	}
	setNavigation(navigation) {
		this.readState = cloneQueryReadBuilderState(this.readState, { navigation });
	}
	/**
	* Creates a new QueryBuilder with modified configuration.
	* Used by single(), maybeSingle(), count(), and select() to create new instances.
	*/
	cloneWithChanges(changes) {
		const newBuilder = new QueryBuilder({
			occurrence: this.occurrence,
			layer: this.layer
		});
		newBuilder.readState = cloneQueryReadBuilderState(this.readState, {
			queryOptions: changes.queryOptions,
			expandConfigs: this.readState.expandConfigs,
			singleMode: changes.singleMode ?? this.readState.singleMode,
			isCountMode: changes.isCountMode ?? this.readState.isCountMode,
			includeCountMode: changes.includeCountMode ?? this.readState.includeCountMode,
			fieldMapping: "fieldMapping" in changes ? changes.fieldMapping : this.readState.fieldMapping,
			systemColumns: changes.systemColumns !== void 0 ? changes.systemColumns : this.readState.systemColumns,
			navigation: this.readState.navigation
		});
		newBuilder.urlBuilder = new QueryUrlBuilder(this.config.databaseName, this.occurrence, this.config.useEntityIds);
		return newBuilder;
	}
	select(fields, systemColumns) {
		if (fields === "all") return this.cloneWithChanges({
			queryOptions: { select: void 0 },
			fieldMapping: void 0,
			systemColumns: void 0
		});
		const { selectedFields, fieldMapping } = processSelectWithRenames(fields, getTableName(this.occurrence), this.logger);
		const finalSelectedFields = [...selectedFields];
		if (systemColumns?.ROWID) finalSelectedFields.push("ROWID");
		if (systemColumns?.ROWMODID) finalSelectedFields.push("ROWMODID");
		return this.cloneWithChanges({
			selectedFields: fields,
			queryOptions: { select: finalSelectedFields },
			fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : void 0,
			systemColumns
		});
	}
	/**
	* Filter results using operator expressions (new ORM-style API).
	* Supports eq, gt, lt, and, or, etc. operators with Column references.
	* Also supports raw OData filter strings as an escape hatch.
	*
	* @example
	* .where(eq(users.hobby, "reading"))
	* .where(and(eq(users.active, true), gt(users.age, 18)))
	* .where("status eq 'active'")  // Raw OData string escape hatch
	*/
	where(expression) {
		if (typeof expression === "string") {
			this.setFilterExpression(void 0);
			this.patchQueryOptions({ filter: expression });
			return this;
		}
		this.setFilterExpression(expression);
		this.patchQueryOptions({ filter: void 0 });
		return this;
	}
	/**
	* Specify the sort order for query results.
	*
	* @example Single field (ascending by default)
	* ```ts
	* .orderBy("name")
	* .orderBy(users.name)  // Column reference
	* .orderBy(asc(users.name))  // Explicit ascending
	* ```
	*
	* @example Single field with explicit direction
	* ```ts
	* .orderBy(["name", "desc"])
	* .orderBy([users.name, "desc"])  // Column reference
	* .orderBy(desc(users.name))  // Explicit descending
	* ```
	*
	* @example Multiple fields with directions
	* ```ts
	* .orderBy([["name", "asc"], ["createdAt", "desc"]])
	* .orderBy([[users.name, "asc"], [users.createdAt, "desc"]])  // Column references
	* .orderBy(users.name, desc(users.age))  // Variadic with helpers
	* ```
	*/
	orderBy(...orderByArgs) {
		const tableName = getTableName(this.occurrence);
		if (orderByArgs.length > 1) {
			const orderByParts = orderByArgs.map((arg) => {
				if (isOrderByExpression(arg)) {
					if (arg.column.tableName !== tableName) this.logger.warn(`Column ${arg.column.toString()} is from table "${arg.column.tableName}", but query is for table "${tableName}"`);
					const fieldName = arg.column.fieldName;
					return `${this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName} ${arg.direction}`;
				}
				if (isColumn(arg)) {
					if (arg.tableName !== tableName) this.logger.warn(`Column ${arg.toString()} is from table "${arg.tableName}", but query is for table "${tableName}"`);
					const fieldName = arg.fieldName;
					return this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName;
				}
				throw new Error("Variadic orderBy() only accepts Column or OrderByExpression arguments");
			});
			this.patchQueryOptions({ orderBy: orderByParts });
			return this;
		}
		const orderBy = orderByArgs[0];
		if (isOrderByExpression(orderBy)) {
			if (orderBy.column.tableName !== tableName) this.logger.warn(`Column ${orderBy.column.toString()} is from table "${orderBy.column.tableName}", but query is for table "${tableName}"`);
			const fieldName = orderBy.column.fieldName;
			const transformedField = this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName;
			this.patchQueryOptions({ orderBy: `${transformedField} ${orderBy.direction}` });
			return this;
		}
		if (isColumn(orderBy)) {
			if (orderBy.tableName !== tableName) this.logger.warn(`Column ${orderBy.toString()} is from table "${orderBy.tableName}", but query is for table "${tableName}"`);
			const fieldName = orderBy.fieldName;
			this.patchQueryOptions({ orderBy: this.occurrence ? transformOrderByField(fieldName, this.occurrence) : fieldName });
			return this;
		}
		if (this.occurrence && orderBy) if (Array.isArray(orderBy)) if (orderBy.length === 2 && (typeof orderBy[0] === "string" || isColumn(orderBy[0])) && (orderBy[1] === "asc" || orderBy[1] === "desc")) {
			const field = isColumn(orderBy[0]) ? orderBy[0].fieldName : orderBy[0];
			const direction = orderBy[1];
			this.patchQueryOptions({ orderBy: `${transformOrderByField(field, this.occurrence)} ${direction}` });
		} else this.patchQueryOptions({ orderBy: orderBy.map(([fieldOrCol, direction]) => {
			const field = isColumn(fieldOrCol) ? fieldOrCol.fieldName : String(fieldOrCol);
			return `${this.occurrence ? transformOrderByField(field, this.occurrence) : field} ${direction}`;
		}) });
		else this.patchQueryOptions({ orderBy: transformOrderByField(String(orderBy), this.occurrence) });
		else if (Array.isArray(orderBy)) if (orderBy.length === 2 && (typeof orderBy[0] === "string" || isColumn(orderBy[0])) && (orderBy[1] === "asc" || orderBy[1] === "desc")) {
			const field = isColumn(orderBy[0]) ? orderBy[0].fieldName : orderBy[0];
			const direction = orderBy[1];
			this.patchQueryOptions({ orderBy: `${field} ${direction}` });
		} else this.patchQueryOptions({ orderBy: orderBy.map(([fieldOrCol, direction]) => {
			return `${isColumn(fieldOrCol) ? fieldOrCol.fieldName : String(fieldOrCol)} ${direction}`;
		}) });
		else this.patchQueryOptions({ orderBy });
		return this;
	}
	top(count) {
		this.patchQueryOptions({ top: count });
		return this;
	}
	skip(count) {
		this.patchQueryOptions({ skip: count });
		return this;
	}
	expand(targetTable, callback) {
		const expandConfig = this.expandBuilder.processExpand(targetTable, this.occurrence, callback, () => new QueryBuilder({
			occurrence: targetTable,
			layer: this.layer
		}));
		this.readState = cloneQueryReadBuilderState(this.readState, { expandConfigs: [...this.readState.expandConfigs, expandConfig] });
		return this;
	}
	single() {
		if (this.readState.includeCountMode) throw new BuilderInvariantError("QueryBuilder.single", "count-enabled list queries cannot use single()");
		return this.cloneWithChanges({ singleMode: "exact" });
	}
	maybeSingle() {
		if (this.readState.includeCountMode) throw new BuilderInvariantError("QueryBuilder.maybeSingle", "count-enabled list queries cannot use maybeSingle()");
		return this.cloneWithChanges({ singleMode: "maybe" });
	}
	count() {
		if (this.readState.singleMode !== false) throw new BuilderInvariantError("QueryBuilder.count", "single() and maybeSingle() cannot be combined with count()");
		return this.cloneWithChanges({
			includeCountMode: true,
			queryOptions: { count: true }
		});
	}
	/**
	* Builds the OData query string from current query options and expand configs.
	*/
	buildQueryString(includeSpecialColumns, useEntityIds) {
		const finalUseEntityIds = useEntityIds ?? this.config.useEntityIds;
		const queryOptionsWithoutExpandAndSelect = { ...this.readState.queryOptions };
		if (this.readState.filterExpression) queryOptionsWithoutExpandAndSelect.filter = this.readState.filterExpression.toODataFilter(finalUseEntityIds);
		const originalSelect = queryOptionsWithoutExpandAndSelect.select;
		queryOptionsWithoutExpandAndSelect.expand = void 0;
		queryOptionsWithoutExpandAndSelect.select = void 0;
		let queryString = buildQuery(queryOptionsWithoutExpandAndSelect);
		let selectArray;
		if (originalSelect) selectArray = Array.isArray(originalSelect) ? originalSelect.map(String) : [String(originalSelect)];
		const finalIncludeSpecialColumns = includeSpecialColumns ?? this.config.includeSpecialColumns;
		const selectExpandString = buildSelectExpandQueryString({
			selectedFields: selectArray,
			expandConfigs: this.readState.expandConfigs,
			table: this.occurrence,
			useEntityIds: finalUseEntityIds,
			logger: this.logger,
			includeSpecialColumns: finalIncludeSpecialColumns
		});
		if (selectExpandString) {
			const params = selectExpandString.startsWith("?") ? selectExpandString.slice(1) : selectExpandString;
			const separator = queryString.includes("?") ? "&" : "?";
			queryString = `${queryString}${separator}${params}`;
		}
		return queryString;
	}
	execute(options) {
		const mergedOptions = this.mergeExecuteOptions(options);
		let queryString;
		try {
			queryString = this.buildQueryString(mergedOptions.includeSpecialColumns, mergedOptions.useEntityIds);
		} catch (error) {
			return Promise.resolve({
				data: void 0,
				error: normalizeQueryBuildError(error)
			});
		}
		if (this.readState.isCountMode) {
			let url;
			try {
				url = this.urlBuilder.build(queryString, {
					isCount: true,
					useEntityIds: mergedOptions.useEntityIds,
					navigation: this.readState.navigation
				});
			} catch (error) {
				return Promise.resolve({
					data: void 0,
					error: normalizeQueryBuildError(error)
				});
			}
			const pipeline = requestFromService(url, mergedOptions).pipe(Effect.map((data) => {
				return typeof data === "string" ? Number(data) : data;
			}));
			return runLayerResult(this.layer, pipeline, "fmodata.query.count", { "fmodata.table": getTableName(this.occurrence) });
		}
		let url;
		try {
			url = this.urlBuilder.build(queryString, {
				isCount: this.readState.isCountMode,
				useEntityIds: mergedOptions.useEntityIds,
				navigation: this.readState.navigation
			});
		} catch (error) {
			return Promise.resolve({
				data: void 0,
				error: normalizeQueryBuildError(error)
			});
		}
		const pipeline = requestFromService(url, mergedOptions).pipe(Effect.flatMap((data) => Effect.tryPromise({
			try: () => processQueryResponse(data, {
				occurrence: this.occurrence,
				singleMode: this.readState.singleMode,
				queryOptions: this.readState.queryOptions,
				expandConfigs: this.readState.expandConfigs,
				skipValidation: options?.skipValidation,
				useEntityIds: mergedOptions.useEntityIds,
				includeSpecialColumns: mergedOptions.includeSpecialColumns,
				includeCount: this.readState.includeCountMode,
				fieldMapping: this.readState.fieldMapping,
				logger: this.logger
			}),
			catch: (e) => e instanceof Error ? e : new Error(String(e))
		})), Effect.flatMap((result) => result.error ? Effect.fail(result.error) : Effect.succeed(result.data)));
		return runLayerResult(this.layer, pipeline, this.readState.singleMode ? "fmodata.query.single" : "fmodata.query.list", { "fmodata.table": getTableName(this.occurrence) });
	}
	getQueryString(options) {
		const useEntityIds = options?.useEntityIds ?? this.config.useEntityIds;
		const queryString = this.buildQueryString(void 0, useEntityIds);
		return this.urlBuilder.buildPath(queryString, {
			useEntityIds,
			navigation: this.readState.navigation
		});
	}
	getRequestConfig() {
		const queryString = this.buildQueryString();
		return {
			method: "GET",
			url: this.urlBuilder.build(queryString, {
				isCount: this.readState.isCountMode,
				useEntityIds: this.config.useEntityIds,
				navigation: this.readState.navigation
			})
		};
	}
	toRequest(baseUrl, options) {
		return createODataRequest(baseUrl, this.getRequestConfig(), {
			...options,
			normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName
		});
	}
	async processResponse(response, options) {
		if (!response.ok) return {
			data: void 0,
			error: await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${getTableName(this.occurrence)}`)
		};
		if (response.status === 204) {
			if (this.readState.singleMode !== false) {
				if (this.readState.singleMode === "maybe") return {
					data: null,
					error: void 0
				};
				return {
					data: void 0,
					error: new RecordCountMismatchError("one", 0)
				};
			}
			return {
				data: [],
				error: void 0
			};
		}
		let rawData;
		try {
			rawData = await safeJsonParse(response);
		} catch (err) {
			if (err instanceof SyntaxError && response.status === 204) return {
				data: [],
				error: void 0
			};
			return {
				data: void 0,
				error: {
					name: "ResponseParseError",
					message: `Failed to parse response JSON: ${err instanceof Error ? err.message : "Unknown error"}`,
					timestamp: /* @__PURE__ */ new Date()
				}
			};
		}
		if (!rawData) return {
			data: void 0,
			error: {
				name: "ResponseError",
				message: "Response body was empty or null",
				timestamp: /* @__PURE__ */ new Date()
			}
		};
		const mergedOptions = this.mergeExecuteOptions(options);
		this.readState.queryOptions.select;
		return processQueryResponse(rawData, {
			occurrence: this.occurrence,
			singleMode: this.readState.singleMode,
			queryOptions: this.readState.queryOptions,
			expandConfigs: this.readState.expandConfigs,
			skipValidation: options?.skipValidation,
			useEntityIds: mergedOptions.useEntityIds,
			includeSpecialColumns: mergedOptions.includeSpecialColumns,
			includeCount: this.readState.includeCountMode,
			fieldMapping: this.readState.fieldMapping,
			logger: this.logger
		});
	}
};
//#endregion
//#region src/client/delete-builder.ts
/**
* Initial delete builder returned from EntitySet.delete()
* Requires calling .byId() or .where() before .execute() is available
*/
var DeleteBuilder = class {
	table;
	layer;
	config;
	constructor(config) {
		this.table = config.occurrence;
		const runtime = createClientRuntime(config.layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
	}
	/**
	* Delete a single record by ID
	*/
	byId(id) {
		return new ExecutableDeleteBuilder({
			occurrence: this.table,
			layer: this.layer,
			mode: "byId",
			recordLocator: id
		});
	}
	/**
	* Delete a single record by ROWID
	*/
	byRowId(rowId) {
		return new ExecutableDeleteBuilder({
			occurrence: this.table,
			layer: this.layer,
			mode: "byId",
			recordLocator: { ROWID: rowId }
		});
	}
	/**
	* Delete records matching a filter query
	* @param fn Callback that receives a QueryBuilder for building the filter
	*/
	where(fn) {
		const configuredBuilder = fn(new QueryBuilder({
			occurrence: this.table,
			layer: this.layer
		}));
		return new ExecutableDeleteBuilder({
			occurrence: this.table,
			layer: this.layer,
			mode: "byFilter",
			queryBuilder: configuredBuilder
		});
	}
};
/**
* Executable delete builder - has execute() method
* Returned after calling .byId() or .where()
*/
var ExecutableDeleteBuilder = class {
	table;
	mode;
	recordLocator;
	queryBuilder;
	layer;
	config;
	constructor(config) {
		this.table = config.occurrence;
		this.layer = config.layer;
		this.mode = config.mode;
		this.recordLocator = config.recordLocator;
		this.queryBuilder = config.queryBuilder;
		this.config = createClientRuntime(this.layer).config;
	}
	execute(options) {
		const mergedOptions = mergeMutationExecuteOptions(options, this.config.useEntityIds, this.config.includeSpecialColumns);
		const { method: _method, body: _body, ...requestOptions } = mergedOptions;
		const useEntityIds = mergedOptions.useEntityIds ?? this.config.useEntityIds;
		const tableId = resolveMutationTableId(this.table, useEntityIds, "ExecutableDeleteBuilder");
		const url = buildMutationUrl({
			databaseName: this.config.databaseName,
			tableId,
			tableName: getTableName(this.table),
			mode: this.mode,
			recordLocator: this.recordLocator,
			queryBuilder: this.queryBuilder,
			useEntityIds,
			builderName: "ExecutableDeleteBuilder"
		});
		const pipeline = Effect.gen(this, function* () {
			return { deletedCount: extractAffectedRows(yield* requestFromService(url, {
				...requestOptions,
				method: "DELETE"
			}), void 0, 0, "deletedCount") };
		});
		return runLayerResult(this.layer, pipeline, "fmodata.delete", { "fmodata.table": getTableName(this.table) });
	}
	getRequestConfig() {
		const tableId = resolveMutationTableId(this.table, this.config.useEntityIds, "ExecutableDeleteBuilder");
		return {
			method: "DELETE",
			url: buildMutationUrl({
				databaseName: this.config.databaseName,
				tableId,
				tableName: getTableName(this.table),
				mode: this.mode,
				recordLocator: this.recordLocator,
				queryBuilder: this.queryBuilder,
				useEntityIds: this.config.useEntityIds,
				builderName: "ExecutableDeleteBuilder"
			})
		};
	}
	toRequest(baseUrl, options) {
		const config = this.getRequestConfig();
		const fullUrl = `${baseUrl}${normalizeDatabasePath(config.url, { normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName })}`;
		return new Request(fullUrl, {
			method: config.method,
			headers: { Accept: getAcceptHeader(options?.includeODataAnnotations) }
		});
	}
	async processResponse(response, _options) {
		if (!response.ok) {
			const tableName = getTableName(this.table);
			return {
				data: void 0,
				error: await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${tableName}`)
			};
		}
		const text = await response.text();
		if (!text || text.trim() === "") return {
			data: { deletedCount: extractAffectedRows(void 0, response.headers, 1, "deletedCount") },
			error: void 0
		};
		return {
			data: { deletedCount: extractAffectedRows(JSON.parse(text), response.headers, 0, "deletedCount") },
			error: void 0
		};
	}
};
//#endregion
//#region src/client/insert-builder.ts
var InsertBuilder = class {
	table;
	data;
	returnPreference;
	layer;
	config;
	constructor(config) {
		this.table = config.occurrence;
		this.layer = config.layer;
		this.data = config.data;
		this.returnPreference = config.returnPreference || "representation";
		const runtime = createClientRuntime(this.layer);
		this.config = runtime.config;
	}
	/**
	* Helper to merge database-level useEntityIds with per-request options
	*/
	mergeExecuteOptions(options) {
		return mergeMutationExecuteOptions(options, this.config.useEntityIds, this.config.includeSpecialColumns);
	}
	/**
	* Parse ROWID from Location header
	* Expected formats:
	* - contacts(ROWID=4583)
	* - contacts('some-uuid')
	*/
	parseLocationHeader(locationHeader) {
		return parseRowIdFromLocationHeader(locationHeader);
	}
	/**
	* Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name
	* @param useEntityIds - Optional override for entity ID usage
	*/
	getTableId(useEntityIds) {
		if (!this.table) throw new BuilderInvariantError("InsertBuilder", "table occurrence is required");
		return resolveMutationTableId(this.table, useEntityIds ?? this.config.useEntityIds, "InsertBuilder");
	}
	/**
	* Builds the schema for validation, excluding container fields.
	*/
	getValidationSchema() {
		if (!this.table) return;
		const baseTableConfig = getBaseTableConfig(this.table);
		const containerFields = baseTableConfig.containerFields || [];
		const schema = { ...baseTableConfig.schema };
		for (const containerField of containerFields) delete schema[containerField];
		return schema;
	}
	execute(options) {
		const mergedOptions = this.mergeExecuteOptions(options);
		const { method: _method, headers: callerHeaders, body: _body, ...requestOptions } = mergedOptions;
		const tableId = this.getTableId(mergedOptions.useEntityIds);
		const url = `/${this.config.databaseName}/${tableId}`;
		const shouldUseIds = mergedOptions.useEntityIds ?? this.config.useEntityIds;
		const includeSpecialColumns = mergedOptions.includeSpecialColumns ?? this.config.includeSpecialColumns;
		const canonicalHeaders = new Headers(callerHeaders || {});
		const preferHeader = mergePreferHeaderValues(this.returnPreference === "minimal" ? "return=minimal" : "return=representation", shouldUseIds ? "fmodata.entity-ids" : void 0, includeSpecialColumns ? "fmodata.include-specialcolumns" : void 0, canonicalHeaders.get("Prefer") ?? void 0);
		canonicalHeaders.set("Content-Type", "application/json");
		if (preferHeader) canonicalHeaders.set("Prefer", preferHeader);
		else canonicalHeaders.delete("Prefer");
		const pipeline = Effect.gen(this, function* () {
			let validatedData = this.data;
			if (this.table) {
				const baseTableConfig = getBaseTableConfig(this.table);
				validatedData = yield* tryEffect(() => validateAndTransformInput(this.data, baseTableConfig.inputSchema), (e) => e instanceof Error ? e : new BuilderInvariantError("InsertBuilder.execute", String(e)));
			}
			const transformedData = this.table && shouldUseIds ? transformFieldNamesToIds(validatedData, this.table) : validatedData;
			const responseData = yield* requestFromService(url, {
				...requestOptions,
				method: "POST",
				headers: canonicalHeaders,
				body: JSON.stringify(transformedData)
			});
			if (this.returnPreference === "minimal") {
				if (!responseData?._location) return yield* Effect.fail(new InvalidLocationHeaderError("Location header is required when using return=minimal but was not found in response"));
				return { ROWID: this.parseLocationHeader(responseData._location) };
			}
			let response = responseData;
			if (this.table && shouldUseIds) response = transformResponseFields(response, this.table, void 0);
			const schema = this.getValidationSchema();
			const validated = yield* fromValidation(() => validateSingleResponse(response, schema, void 0, void 0, "exact", includeSpecialColumns));
			if (validated === null) return yield* Effect.fail(new BuilderInvariantError("InsertBuilder.execute", "insert operation returned null response"));
			return validated;
		});
		return runLayerResult(this.layer, pipeline, "fmodata.insert", this.table ? { "fmodata.table": getTableName(this.table) } : void 0);
	}
	getRequestConfig() {
		const tableId = this.getTableId(this.config.useEntityIds);
		const transformedData = this.table && this.config.useEntityIds ? transformFieldNamesToIds(this.data, this.table) : this.data;
		return {
			method: "POST",
			url: `/${this.config.databaseName}/${tableId}`,
			body: JSON.stringify(transformedData)
		};
	}
	toRequest(baseUrl, options) {
		const config = this.getRequestConfig();
		const fullUrl = `${baseUrl}${normalizeDatabasePath(config.url, { normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName })}`;
		const preferHeader = mergePreferHeaderValues(this.returnPreference === "minimal" ? "return=minimal" : "return=representation", options?.useEntityIds ?? this.config.useEntityIds ? "fmodata.entity-ids" : void 0, options?.includeSpecialColumns ?? this.config.includeSpecialColumns ? "fmodata.include-specialcolumns" : void 0);
		return new Request(fullUrl, {
			method: config.method,
			headers: {
				"Content-Type": "application/json",
				Accept: getAcceptHeader(options?.includeODataAnnotations),
				...preferHeader ? { Prefer: preferHeader } : {}
			},
			body: config.body
		});
	}
	async processResponse(response, options) {
		if (!response.ok) {
			const tableName = this.table ? getTableName(this.table) : "unknown";
			return {
				data: void 0,
				error: await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${tableName}`)
			};
		}
		if (response.status === 204) {
			if (this.returnPreference === "minimal") {
				const locationHeader = getLocationHeader(response.headers);
				return {
					data: { ROWID: locationHeader ? this.parseLocationHeader(locationHeader) : -1 },
					error: void 0
				};
			}
			return {
				data: {},
				error: void 0
			};
		}
		if (this.returnPreference === "minimal") {
			const locationHeader = getLocationHeader(response.headers);
			return {
				data: { ROWID: locationHeader ? this.parseLocationHeader(locationHeader) : -1 },
				error: void 0
			};
		}
		let rawResponse;
		try {
			rawResponse = await safeJsonParse(response);
		} catch (err) {
			if (response.status === 204) return {
				data: {},
				error: void 0
			};
			return {
				data: void 0,
				error: {
					name: "ResponseParseError",
					message: `Failed to parse response JSON: ${err instanceof Error ? err.message : "Unknown error"}`,
					timestamp: /* @__PURE__ */ new Date()
				}
			};
		}
		this.data;
		if (this.table) {
			const inputSchema = getBaseTableConfig(this.table).inputSchema;
			try {
				await validateAndTransformInput(this.data, inputSchema);
			} catch (error) {
				return {
					data: void 0,
					error: error instanceof Error ? error : new BuilderInvariantError("InsertBuilder.processResponse", String(error))
				};
			}
		}
		const shouldUseIds = options?.useEntityIds ?? this.config.useEntityIds;
		const includeSpecialColumns = options?.includeSpecialColumns ?? this.config.includeSpecialColumns;
		let transformedResponse = rawResponse;
		if (this.table && shouldUseIds) transformedResponse = transformResponseFields(rawResponse, this.table, void 0);
		let schema;
		if (this.table) {
			const baseTableConfig = getBaseTableConfig(this.table);
			const containerFields = baseTableConfig.containerFields || [];
			schema = { ...baseTableConfig.schema };
			for (const containerField of containerFields) delete schema[containerField];
		}
		const validation = await validateSingleResponse(transformedResponse, schema, void 0, void 0, "exact", includeSpecialColumns);
		if (!validation.valid) return {
			data: void 0,
			error: validation.error
		};
		if (validation.data === null) return {
			data: void 0,
			error: new BuilderInvariantError("InsertBuilder.processResponse", "insert operation returned null response")
		};
		return {
			data: validation.data,
			error: void 0
		};
	}
};
//#endregion
//#region src/client/record-builder.ts
var RecordBuilder = class RecordBuilder {
	table;
	recordLocator;
	operation;
	operationParam;
	operationColumn;
	isNavigateFromEntitySet;
	navigateRelation;
	navigateRelationEntityId;
	navigateSourceTableName;
	navigateSourceTableEntityId;
	readState = createInitialRecordReadBuilderState();
	layer;
	config;
	logger;
	get selectedFields() {
		return this.readState.selectedFields;
	}
	set selectedFields(selectedFields) {
		this.readState = cloneRecordReadBuilderState(this.readState, { selectedFields });
	}
	get expandConfigs() {
		return this.readState.expandConfigs;
	}
	set expandConfigs(expandConfigs) {
		this.readState = cloneRecordReadBuilderState(this.readState, { expandConfigs });
	}
	get fieldMapping() {
		return this.readState.fieldMapping;
	}
	set fieldMapping(fieldMapping) {
		this.readState = cloneRecordReadBuilderState(this.readState, { fieldMapping });
	}
	get systemColumns() {
		return this.readState.systemColumns;
	}
	set systemColumns(systemColumns) {
		this.readState = cloneRecordReadBuilderState(this.readState, { systemColumns });
	}
	constructor(config) {
		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
	*/
	mergeExecuteOptions(options) {
		return {
			...mergeExecuteOptions(options, this.config.useEntityIds),
			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
	*/
	getTableId(useEntityIds) {
		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.
	*/
	cloneWithChanges(changes) {
		const newBuilder = new RecordBuilder({
			occurrence: this.table,
			layer: this.layer,
			recordLocator: this.recordLocator
		});
		const mutableBuilder = newBuilder;
		mutableBuilder.readState = cloneRecordReadBuilderState(this.readState, {
			selectedFields: "selectedFields" in changes ? changes.selectedFields : this.selectedFields,
			fieldMapping: "fieldMapping" in changes ? changes.fieldMapping : this.fieldMapping,
			systemColumns: changes.systemColumns !== void 0 ? changes.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;
		return newBuilder;
	}
	getSingleField(column) {
		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({
			occurrence: this.table,
			layer: this.layer,
			recordLocator: this.recordLocator
		});
		const mutableBuilder = newBuilder;
		mutableBuilder.operation = "getSingleField";
		mutableBuilder.operationColumn = column;
		mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
		mutableBuilder.navigateRelation = this.navigateRelation;
		mutableBuilder.navigateSourceTableName = this.navigateSourceTableName;
		return newBuilder;
	}
	select(fields, systemColumns) {
		if (fields === "all") return this.cloneWithChanges({
			selectedFields: void 0,
			fieldMapping: void 0,
			systemColumns: void 0
		});
		const { selectedFields, fieldMapping } = processSelectWithRenames(fields, getTableName(this.table), this.logger);
		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 : void 0,
			systemColumns
		});
	}
	/**
	* 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(targetTable, callback) {
		const newBuilder = new RecordBuilder({
			occurrence: this.table,
			layer: this.layer,
			recordLocator: this.recordLocator
		});
		const mutableBuilder = newBuilder;
		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;
		const expandConfig = new ExpandBuilder(this.config.useEntityIds, this.logger).processExpand(targetTable, this.table ?? void 0, callback, () => new QueryBuilder({
			occurrence: targetTable,
			layer: this.layer
		}));
		mutableBuilder.readState = cloneRecordReadBuilderState(mutableBuilder.readState, { expandConfigs: [...this.expandConfigs, expandConfig] });
		return newBuilder;
	}
	navigate(targetTable) {
		const relationName = getTableName(targetTable);
		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"}`);
		}
		const builder = new QueryBuilder({
			occurrence: targetTable,
			layer: this.layer
		});
		const relationEntityId = isUsingEntityIds(targetTable) ? resolveTableId(targetTable, relationName, true) : relationName;
		let sourceTableName;
		let sourceTableEntityId;
		let baseRelation;
		let baseRelationEntityId;
		if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) {
			sourceTableName = this.navigateSourceTableName;
			sourceTableEntityId = this.navigateSourceTableEntityId ?? sourceTableName;
			baseRelation = this.navigateRelation;
			baseRelationEntityId = this.navigateRelationEntityId ?? baseRelation;
		} else {
			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;
		}
		builder.navigation = {
			recordLocator: this.recordLocator,
			relation: relationName,
			relationEntityId,
			sourceTableName,
			sourceTableEntityId,
			baseRelation,
			baseRelationEntityId
		};
		return builder;
	}
	/**
	* Builds the complete query string including $select and $expand parameters.
	*/
	buildQueryString(includeSpecialColumns, useEntityIds) {
		const finalIncludeSpecialColumns = includeSpecialColumns ?? this.config.includeSpecialColumns;
		const finalUseEntityIds = useEntityIds ?? this.config.useEntityIds;
		return buildSelectExpandQueryString({
			selectedFields: this.selectedFields,
			expandConfigs: this.expandConfigs,
			table: this.table,
			useEntityIds: finalUseEntityIds,
			logger: this.logger,
			includeSpecialColumns: finalIncludeSpecialColumns
		});
	}
	buildRecordResourcePath(useEntityIds) {
		if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) return `/${buildRecordPath(`${useEntityIds ? this.navigateSourceTableEntityId ?? this.navigateSourceTableName : this.navigateSourceTableName}/${useEntityIds ? this.navigateRelationEntityId ?? this.navigateRelation : this.navigateRelation}`, this.recordLocator)}`;
		return `/${buildRecordPath(this.getTableId(useEntityIds), this.recordLocator)}`;
	}
	execute(options) {
		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 {
			const queryString = this.buildQueryString(mergedOptions.includeSpecialColumns, mergedOptions.useEntityIds);
			url += queryString;
		}
		const pipeline = Effect.gen(this, function* () {
			const response = yield* requestFromService(url, {
				method: "GET",
				...mergedOptions
			});
			if (this.operation === "getSingleField") return response.value;
			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;
		});
		return runLayerResult(this.layer, pipeline, "fmodata.record.get", { "fmodata.table": getTableName(this.table) });
	}
	getRequestConfig() {
		let url = `/${this.config.databaseName}${this.buildRecordResourcePath(this.config.useEntityIds)}`;
		if (this.operation === "getSingleField" && this.operationColumn) url += `/${this.operationColumn.getFieldIdentifier(this.config.useEntityIds)}`;
		else if (this.operation === "getSingleField" && this.operationParam) url += `/${this.operationParam}`;
		else {
			const queryString = this.buildQueryString();
			url += queryString;
		}
		return {
			method: "GET",
			url
		};
	}
	/**
	* Returns the query string for this record builder (for testing purposes).
	*/
	getQueryString(options) {
		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) return `${path}/${this.operationParam}`;
		return `${path}${this.buildQueryString(void 0, useEntityIds)}`;
	}
	toRequest(baseUrl, options) {
		return createODataRequest(baseUrl, this.getRequestConfig(), {
			...options,
			normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName
		});
	}
	async processResponse(response, options) {
		if (!response.ok) {
			const tableName = this.table ? getTableName(this.table) : "unknown";
			return {
				data: void 0,
				error: await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${tableName}`)
			};
		}
		const rawResponse = await safeJsonParse(response);
		if (this.operation === "getSingleField") return {
			data: rawResponse.value,
			error: void 0
		};
		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
		});
	}
};
//#endregion
//#region src/client/update-builder.ts
/**
* Initial update builder returned from EntitySet.update(data)
* Requires calling .byId() or .where() before .execute() is available
*/
var UpdateBuilder = class {
	table;
	data;
	returnPreference;
	layer;
	config;
	constructor(config) {
		this.table = config.occurrence;
		const runtime = createClientRuntime(config.layer);
		this.layer = runtime.layer;
		this.data = config.data;
		this.returnPreference = config.returnPreference;
		this.config = runtime.config;
	}
	/**
	* Update a single record by ID
	* Returns updated count by default, or full record if returnFullRecord was set to true
	*/
	byId(id) {
		return new ExecutableUpdateBuilder({
			occurrence: this.table,
			layer: this.layer,
			data: this.data,
			mode: "byId",
			recordLocator: id,
			returnPreference: this.returnPreference
		});
	}
	/**
	* Update a single record by ROWID
	*/
	byRowId(rowId) {
		return new ExecutableUpdateBuilder({
			occurrence: this.table,
			layer: this.layer,
			data: this.data,
			mode: "byId",
			recordLocator: { ROWID: rowId },
			returnPreference: this.returnPreference
		});
	}
	/**
	* Update records matching a filter query
	* Returns updated count by default, or full record if returnFullRecord was set to true
	* @param fn Callback that receives a QueryBuilder for building the filter
	*/
	where(fn) {
		const configuredBuilder = fn(new QueryBuilder({
			occurrence: this.table,
			layer: this.layer
		}));
		return new ExecutableUpdateBuilder({
			occurrence: this.table,
			layer: this.layer,
			data: this.data,
			mode: "byFilter",
			queryBuilder: configuredBuilder,
			returnPreference: this.returnPreference
		});
	}
};
/**
* Executable update builder - has execute() method
* Returned after calling .byId() or .where()
* Can return either updated count or full record based on returnFullRecord option
*/
var ExecutableUpdateBuilder = class {
	table;
	data;
	mode;
	recordLocator;
	queryBuilder;
	returnPreference;
	layer;
	config;
	constructor(config) {
		this.table = config.occurrence;
		this.layer = config.layer;
		this.data = config.data;
		this.mode = config.mode;
		this.recordLocator = config.recordLocator;
		this.queryBuilder = config.queryBuilder;
		this.returnPreference = config.returnPreference;
		const runtime = createClientRuntime(this.layer);
		this.config = runtime.config;
	}
	execute(options) {
		const mergedOptions = mergeMutationExecuteOptions(options, this.config.useEntityIds, this.config.includeSpecialColumns);
		const { method: _method, body: _body, headers: callerHeaders, ...requestOptions } = mergedOptions;
		const shouldUseIds = mergedOptions.useEntityIds ?? this.config.useEntityIds;
		const includeSpecialColumns = mergedOptions.includeSpecialColumns ?? this.config.includeSpecialColumns;
		const tableId = resolveMutationTableId(this.table, shouldUseIds, "ExecutableUpdateBuilder");
		const url = buildMutationUrl({
			databaseName: this.config.databaseName,
			tableId,
			tableName: getTableName(this.table),
			mode: this.mode,
			recordLocator: this.recordLocator,
			queryBuilder: this.queryBuilder,
			useEntityIds: shouldUseIds,
			builderName: "ExecutableUpdateBuilder"
		});
		const requestHeaders = new Headers(callerHeaders || {});
		const preferHeader = mergePreferHeaderValues(this.returnPreference === "representation" ? "return=representation" : void 0, shouldUseIds ? "fmodata.entity-ids" : void 0, includeSpecialColumns ? "fmodata.include-specialcolumns" : void 0, requestHeaders.get("Prefer") ?? void 0);
		requestHeaders.set("Content-Type", "application/json");
		if (preferHeader) requestHeaders.set("Prefer", preferHeader);
		else requestHeaders.delete("Prefer");
		const pipeline = Effect.gen(this, function* () {
			let validatedData = this.data;
			if (this.table) {
				const baseTableConfig = getBaseTableConfig(this.table);
				validatedData = yield* tryEffect(() => validateAndTransformInput(this.data, baseTableConfig.inputSchema), (e) => e instanceof Error ? e : new BuilderInvariantError("ExecutableUpdateBuilder.execute", String(e)));
			}
			const transformedData = this.table && shouldUseIds ? transformFieldNamesToIds(validatedData, this.table) : validatedData;
			const response = yield* requestFromService(url, {
				...requestOptions,
				method: "PATCH",
				headers: requestHeaders,
				body: JSON.stringify(transformedData)
			});
			if (this.returnPreference === "representation") return response;
			return { updatedCount: extractAffectedRows(response, void 0, 0, "updatedCount") };
		});
		return runLayerResult(this.layer, pipeline, "fmodata.update", { "fmodata.table": getTableName(this.table) });
	}
	getRequestConfig() {
		const tableId = resolveMutationTableId(this.table, this.config.useEntityIds, "ExecutableUpdateBuilder");
		const transformedData = this.table && this.config.useEntityIds ? transformFieldNamesToIds(this.data, this.table) : this.data;
		return {
			method: "PATCH",
			url: buildMutationUrl({
				databaseName: this.config.databaseName,
				tableId,
				tableName: getTableName(this.table),
				mode: this.mode,
				recordLocator: this.recordLocator,
				queryBuilder: this.queryBuilder,
				useEntityIds: this.config.useEntityIds,
				builderName: "ExecutableUpdateBuilder"
			}),
			body: JSON.stringify(transformedData)
		};
	}
	toRequest(baseUrl, options) {
		const config = this.getRequestConfig();
		const fullUrl = `${baseUrl}${normalizeDatabasePath(config.url, { normalizeDatabaseName: options?.normalizeDatabaseName ?? this.config.normalizeDatabaseName })}`;
		const preferHeader = mergePreferHeaderValues(this.returnPreference === "representation" ? "return=representation" : void 0, options?.useEntityIds ?? this.config.useEntityIds ? "fmodata.entity-ids" : void 0, options?.includeSpecialColumns ?? this.config.includeSpecialColumns ? "fmodata.include-specialcolumns" : void 0);
		return new Request(fullUrl, {
			method: config.method,
			headers: {
				"Content-Type": "application/json",
				Accept: getAcceptHeader(options?.includeODataAnnotations),
				...preferHeader ? { Prefer: preferHeader } : {}
			},
			body: config.body
		});
	}
	async processResponse(response, options) {
		if (!response.ok) {
			const tableName = getTableName(this.table);
			return {
				data: void 0,
				error: await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${tableName}`)
			};
		}
		const text = await response.text();
		if (!text || text.trim() === "") return {
			data: { updatedCount: extractAffectedRows(void 0, response.headers, 1, "updatedCount") },
			error: void 0
		};
		const rawResponse = JSON.parse(text);
		this.data;
		if (this.table) {
			const inputSchema = getBaseTableConfig(this.table).inputSchema;
			try {
				await validateAndTransformInput(this.data, inputSchema);
			} catch (error) {
				return {
					data: void 0,
					error: error instanceof Error ? error : new BuilderInvariantError("ExecutableUpdateBuilder.processResponse", String(error))
				};
			}
		}
		if (this.returnPreference === "representation") {
			const shouldUseIds = options?.useEntityIds ?? this.config.useEntityIds;
			const includeSpecialColumns = options?.includeSpecialColumns ?? this.config.includeSpecialColumns;
			let transformedResponse = rawResponse;
			if (this.table && shouldUseIds) transformedResponse = transformResponseFields(rawResponse, this.table, void 0);
			const validation = await validateSingleResponse(transformedResponse, getBaseTableConfig(this.table).schema, void 0, void 0, "exact", includeSpecialColumns);
			if (!validation.valid) return {
				data: void 0,
				error: validation.error
			};
			if (validation.data === null) return {
				data: void 0,
				error: new BuilderInvariantError("ExecutableUpdateBuilder.processResponse", "update operation returned null response")
			};
			return {
				data: validation.data,
				error: void 0
			};
		}
		return {
			data: { updatedCount: extractAffectedRows(rawResponse, response.headers, 0, "updatedCount") },
			error: void 0
		};
	}
};
//#endregion
//#region src/client/entity-set.ts
var EntitySet = class EntitySet {
	occurrence;
	layer;
	config;
	logger;
	database;
	isNavigateFromEntitySet;
	navigateRelation;
	navigateRelationEntityId;
	navigateSourceTableName;
	navigateSourceTableEntityId;
	navigateBasePath;
	navigateBasePathEntityId;
	constructor(config) {
		this.occurrence = config.occurrence;
		this.database = config.database;
		const runtime = createClientRuntime(config.layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
		this.logger = runtime.logger;
	}
	static create(config) {
		return new EntitySet({
			occurrence: config.occurrence,
			layer: config.layer,
			database: config.database
		});
	}
	applyNavigationContext(builder) {
		if (this.isNavigateFromEntitySet && this.navigateRelation && this.navigateSourceTableName) builder.navigation = {
			relation: this.navigateRelation,
			relationEntityId: this.navigateRelationEntityId,
			sourceTableName: this.navigateSourceTableName,
			sourceTableEntityId: this.navigateSourceTableEntityId,
			basePath: this.navigateBasePath,
			basePathEntityId: this.navigateBasePathEntityId
		};
		return builder;
	}
	list() {
		const builder = new QueryBuilder({
			occurrence: this.occurrence,
			layer: this.layer
		});
		if (this.occurrence) {
			const defaultSelectValue = getDefaultSelect(this.occurrence);
			getTableSchema(this.occurrence);
			if (defaultSelectValue === "schema") {
				const allColumns = getTableColumns(this.occurrence);
				return this.applyNavigationContext(this.config.includeSpecialColumns ? builder.select(allColumns, {
					ROWID: true,
					ROWMODID: true
				}) : builder.select(allColumns)).top(1e3);
			}
			if (typeof defaultSelectValue === "object") return this.applyNavigationContext(builder.select(defaultSelectValue)).top(1e3);
		}
		return this.applyNavigationContext(builder).top(1e3);
	}
	count() {
		const builder = new CountBuilder({
			occurrence: this.occurrence,
			layer: this.layer
		});
		return this.applyNavigationContext(builder);
	}
	get(locator) {
		const builder = new RecordBuilder({
			occurrence: this.occurrence,
			layer: this.layer,
			recordLocator: locator
		});
		if (this.occurrence) {
			const defaultSelectValue = getDefaultSelect(this.occurrence);
			getTableSchema(this.occurrence);
			if (defaultSelectValue === "schema") {
				const allColumns = getTableColumns(this.occurrence);
				return this.applyNavigationContext(this.config.includeSpecialColumns ? builder.select(allColumns, {
					ROWID: true,
					ROWMODID: true
				}) : builder.select(allColumns));
			}
			if (typeof defaultSelectValue === "object" && defaultSelectValue !== null && !Array.isArray(defaultSelectValue)) return this.applyNavigationContext(builder.select(defaultSelectValue));
		}
		return this.applyNavigationContext(builder);
	}
	insert(data, options) {
		const returnPreference = options?.returnFullRecord === false ? "minimal" : "representation";
		return new InsertBuilder({
			occurrence: this.occurrence,
			layer: this.layer,
			data,
			returnPreference
		});
	}
	update(data, options) {
		const returnPreference = options?.returnFullRecord === true ? "representation" : "minimal";
		return new UpdateBuilder({
			occurrence: this.occurrence,
			layer: this.layer,
			data,
			returnPreference
		});
	}
	delete() {
		return new DeleteBuilder({
			occurrence: this.occurrence,
			layer: this.layer
		});
	}
	navigate(targetTable) {
		let relationName;
		relationName = getTableName(targetTable);
		if (this.occurrence && FMTable.Symbol.NavigationPaths in this.occurrence) {
			const navigationPaths = this.occurrence[FMTable.Symbol.NavigationPaths];
			if (navigationPaths && !navigationPaths.includes(relationName)) this.logger.warn(`Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`);
		}
		const entitySet = new EntitySet({
			occurrence: targetTable,
			layer: this.layer,
			database: this.database
		});
		const relationEntityId = isUsingEntityIds(targetTable) ? resolveTableId(targetTable, relationName, true) : relationName;
		const sourceTableName = getTableName(this.occurrence);
		const sourceTableEntityId = isUsingEntityIds(this.occurrence) ? resolveTableId(this.occurrence, sourceTableName, true) : sourceTableName;
		entitySet.isNavigateFromEntitySet = true;
		entitySet.navigateRelation = relationName;
		entitySet.navigateRelationEntityId = relationEntityId;
		if (this.isNavigateFromEntitySet && this.navigateBasePath) {
			entitySet.navigateBasePath = `${this.navigateBasePath}/${this.navigateRelation}`;
			entitySet.navigateBasePathEntityId = `${this.navigateBasePathEntityId ?? this.navigateBasePath}/${this.navigateRelationEntityId ?? this.navigateRelation}`;
			entitySet.navigateSourceTableName = this.navigateSourceTableName;
			entitySet.navigateSourceTableEntityId = this.navigateSourceTableEntityId;
		} else if (this.isNavigateFromEntitySet && this.navigateRelation) {
			entitySet.navigateBasePath = `${this.navigateSourceTableName}/${this.navigateRelation}`;
			entitySet.navigateBasePathEntityId = `${this.navigateSourceTableEntityId ?? this.navigateSourceTableName}/${this.navigateRelationEntityId ?? this.navigateRelation}`;
			entitySet.navigateSourceTableName = this.navigateSourceTableName;
			entitySet.navigateSourceTableEntityId = this.navigateSourceTableEntityId;
		} else {
			entitySet.navigateSourceTableName = sourceTableName;
			entitySet.navigateSourceTableEntityId = sourceTableEntityId;
		}
		return entitySet;
	}
};
//#endregion
//#region src/client/schema-manager.ts
var SchemaManager = class SchemaManager {
	layer;
	config;
	constructor(layer) {
		const runtime = createClientRuntime(layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
	}
	createTable(tableName, fields, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/FileMaker_Tables`, {
				method: "POST",
				body: JSON.stringify({
					tableName,
					fields: fields.map(SchemaManager.compileFieldDefinition)
				}),
				...options
			});
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.schema.createTable");
	}
	addFields(tableName, fields, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/FileMaker_Tables/${tableName}`, {
				method: "PATCH",
				body: JSON.stringify({ fields: fields.map(SchemaManager.compileFieldDefinition) }),
				...options
			});
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.schema.addFields");
	}
	async deleteTable(tableName, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/FileMaker_Tables/${tableName}`, {
				method: "DELETE",
				...options
			});
		});
		await runLayerOrThrow(this.layer, pipeline, "fmodata.schema.deleteTable");
	}
	async deleteField(tableName, fieldName, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/FileMaker_Tables/${tableName}/${fieldName}`, {
				method: "DELETE",
				...options
			});
		});
		await runLayerOrThrow(this.layer, pipeline, "fmodata.schema.deleteField");
	}
	createIndex(tableName, fieldName, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/FileMaker_Indexes/${tableName}`, {
				method: "POST",
				body: JSON.stringify({ indexName: fieldName }),
				...options
			});
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.schema.createIndex");
	}
	async deleteIndex(tableName, fieldName, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/FileMaker_Indexes/${tableName}/${fieldName}`, {
				method: "DELETE",
				...options
			});
		});
		await runLayerOrThrow(this.layer, pipeline, "fmodata.schema.deleteIndex");
	}
	static compileFieldDefinition(field) {
		let type = field.type;
		const repetitions = field.repetitions;
		if (field.type === "string") {
			type = "varchar";
			const stringField = field;
			if (stringField.maxLength !== void 0) type += `(${stringField.maxLength})`;
		}
		if (repetitions !== void 0) type += `[${repetitions}]`;
		const result = {
			name: field.name,
			type
		};
		if (field.nullable !== void 0) result.nullable = field.nullable;
		if (field.primary !== void 0) result.primary = field.primary;
		if (field.unique !== void 0) result.unique = field.unique;
		if (field.global !== void 0) result.global = field.global;
		if (field.type === "string") {
			const stringField = field;
			if (stringField.default !== void 0) result.default = stringField.default;
		} else if (field.type === "date") {
			const dateField = field;
			if (dateField.default !== void 0) result.default = dateField.default;
		} else if (field.type === "time") {
			const timeField = field;
			if (timeField.default !== void 0) result.default = timeField.default;
		} else if (field.type === "timestamp") {
			const timestampField = field;
			if (timestampField.default !== void 0) result.default = timestampField.default;
		} else if (field.type === "container") {
			const containerField = field;
			if (containerField.externalSecurePath !== void 0) result.externalSecurePath = containerField.externalSecurePath;
		}
		return result;
	}
};
//#endregion
//#region src/client/webhook-builder.ts
var WebhookManager = class {
	layer;
	config;
	constructor(layer) {
		const runtime = createClientRuntime(layer);
		this.layer = runtime.layer;
		this.config = runtime.config;
	}
	/**
	* Adds a new webhook to the database.
	* @param webhook - The webhook configuration object
	* @param webhook.webhook - The webhook URL to call
	* @param webhook.tableName - The FMTable instance for the table to monitor
	* @param webhook.headers - Optional custom headers to include in webhook requests
	* @param webhook.notifySchemaChanges - Whether to notify on schema changes
	* @param webhook.select - Optional field selection (string or array of Column references)
	* @param webhook.filter - Optional filter (string or FilterExpression)
	* @returns Promise resolving to the created webhook data with ID
	* @example
	* ```ts
	* const result = await db.webhook.add({
	*   webhook: "https://example.com/webhook",
	*   tableName: contactsTable,
	*   headers: { "X-Custom-Header": "value" },
	* });
	* // result.webhookResult.webhookID contains the new webhook ID
	* ```
	* @example
	* ```ts
	* // Using filter expressions and column arrays (same DX as query builder)
	* const result = await db.webhook.add({
	*   webhook: "https://example.com/webhook",
	*   tableName: contacts,
	*   filter: eq(contacts.name, "John"),
	*   select: [contacts.name, contacts.PrimaryKey],
	* });
	* ```
	*/
	add(webhook, options) {
		const tableName = getTableName(webhook.tableName);
		const useEntityIds = options?.useEntityIds ?? this.config.useEntityIds ?? false;
		let filter;
		if (webhook.filter !== void 0) if (webhook.filter instanceof FilterExpression) filter = webhook.filter.toODataFilter(useEntityIds);
		else filter = webhook.filter;
		let select;
		if (webhook.select !== void 0) if (Array.isArray(webhook.select)) select = formatSelectFields(webhook.select.map((item) => {
			if (isColumn(item)) return item.getFieldIdentifier(useEntityIds);
			return String(item);
		}), webhook.tableName, useEntityIds);
		else select = webhook.select;
		const requestBody = {
			webhook: webhook.webhook,
			tableName
		};
		if (webhook.headers !== void 0) requestBody.headers = webhook.headers;
		if (webhook.notifySchemaChanges !== void 0) requestBody.notifySchemaChanges = webhook.notifySchemaChanges;
		if (select !== void 0) requestBody.select = select;
		if (filter !== void 0) requestBody.filter = filter;
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/Webhook.Add`, {
				...options,
				method: "POST",
				body: JSON.stringify(requestBody),
				databaseNameNormalizationMode: "ensureExtension"
			});
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.add");
	}
	/**
	* Deletes a webhook by ID.
	* @param webhookId - The ID of the webhook to delete
	* @returns Promise that resolves when the webhook is deleted
	* @example
	* ```ts
	* await db.webhook.remove(1);
	* ```
	*/
	async remove(webhookId, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/Webhook.Delete(${webhookId})`, {
				...options,
				method: "POST",
				databaseNameNormalizationMode: "ensureExtension"
			});
		});
		await runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.remove");
	}
	/**
	* Gets a webhook by ID.
	* @param webhookId - The ID of the webhook to retrieve
	* @returns Promise resolving to the webhook data
	* @example
	* ```ts
	* const webhook = await db.webhook.get(1);
	* // webhook.webhookID, webhook.tableName, webhook.webhook, etc.
	* ```
	*/
	get(webhookId, options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/Webhook.Get(${webhookId})`, options);
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.get");
	}
	/**
	* Lists all webhooks.
	* @returns Promise resolving to webhook list response with status and webhooks array
	* @example
	* ```ts
	* const result = await db.webhook.list();
	* // result.status contains the status
	* // result.webhooks contains the array of webhooks
	* ```
	*/
	list(options) {
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/Webhook.GetAll`, {
				...options,
				databaseNameNormalizationMode: "ensureExtension"
			});
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.list");
	}
	/**
	* Invokes a webhook by ID, optionally for specific row IDs.
	* @param webhookId - The ID of the webhook to invoke
	* @param options - Optional configuration
	* @param options.rowIDs - Array of row IDs to trigger the webhook for
	* @returns Promise resolving to the invocation result (type unknown until API behavior is confirmed)
	* @example
	* ```ts
	* // Invoke for all rows
	* await db.webhook.invoke(1);
	*
	* // Invoke for specific rows
	* await db.webhook.invoke(1, { rowIDs: [63, 61] });
	* ```
	*/
	invoke(webhookId, options, executeOptions) {
		const body = {};
		if (options?.rowIDs !== void 0) body.rowIDs = options.rowIDs;
		const pipeline = Effect.gen(this, function* () {
			return yield* requestFromService(`/${this.config.databaseName}/Webhook.Invoke(${webhookId})`, {
				method: "POST",
				body: Object.keys(body).length > 0 ? JSON.stringify(body) : void 0,
				...executeOptions
			});
		});
		return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.invoke");
	}
};
//#endregion
//#region src/client/database.ts
var Database = class {
	schema;
	webhook;
	databaseName;
	_normalizeDatabaseName;
	_useEntityIds;
	_includeSpecialColumns;
	/** @internal Database-scoped Effect Layer for dependency injection */
	_layer;
	constructor(databaseName, context, config) {
		this.databaseName = databaseName;
		this._normalizeDatabaseName = config?.normalizeDatabaseName ?? true;
		this._useEntityIds = config?.useEntityIds ?? false;
		this._includeSpecialColumns = config?.includeSpecialColumns ?? false;
		const baseLayer = context._getLayer?.();
		if (baseLayer) this._layer = createDatabaseLayer(baseLayer, {
			databaseName: this.databaseName,
			normalizeDatabaseName: this._normalizeDatabaseName,
			useEntityIds: this._useEntityIds,
			includeSpecialColumns: this._includeSpecialColumns
		});
		else throw new BuilderInvariantError("Database", "ExecutionContext must implement _getLayer() for dependency injection");
		this.schema = new SchemaManager(this._layer);
		this.webhook = new WebhookManager(this._layer);
	}
	/**
	* @internal Used by adapter packages to access the database filename.
	*/
	get _getDatabaseName() {
		return this.databaseName;
	}
	/**
	* @internal Used by EntitySet to access database configuration
	*/
	get _getUseEntityIds() {
		return this._useEntityIds;
	}
	/**
	* @internal Used by EntitySet to access database configuration
	*/
	get _getNormalizeDatabaseName() {
		return this._normalizeDatabaseName;
	}
	/**
	* @internal Used by EntitySet to access database configuration
	*/
	get _getIncludeSpecialColumns() {
		return this._includeSpecialColumns;
	}
	/**
	* @internal Used by adapter packages for raw OData requests.
	* Makes requests through the Effect DI layer.
	*/
	_makeRequest(path, options) {
		const pipeline = requestFromService(`/${this.databaseName}${path}`, options);
		return runLayerResult(this._layer, pipeline);
	}
	from(table) {
		let useEntityIds = this._useEntityIds;
		if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {
			const tableUseEntityIds = table[FMTable.Symbol.UseEntityIds];
			if (typeof tableUseEntityIds === "boolean") useEntityIds = tableUseEntityIds;
		}
		return new EntitySet({
			occurrence: table,
			layer: useEntityIds !== this._useEntityIds ? createDatabaseLayer(this._layer, {
				databaseName: this.databaseName,
				normalizeDatabaseName: this._normalizeDatabaseName,
				useEntityIds,
				includeSpecialColumns: this._includeSpecialColumns
			}) : this._layer,
			database: this
		});
	}
	async getMetadata(args) {
		let url = `/${this.databaseName}/$metadata`;
		if (args?.tableName) url = `/${this.databaseName}/$metadata%23${args.tableName}`;
		const headers = { Accept: args?.format === "xml" ? "application/xml" : "application/json" };
		if (args?.reduceAnnotations) headers.Prefer = "include-annotations=\"-*\"";
		const pipeline = requestFromService(url, { headers });
		const data = await runLayerOrThrow(this._layer, pipeline, "fmodata.metadata");
		if (args?.format === "xml") return data;
		const metadataMap = data;
		const metadata = metadataMap[this.databaseName] ?? metadataMap[stripFmp12Extension(this.databaseName)];
		if (!metadata) throw new MetadataNotFoundError(this.databaseName);
		return metadata;
	}
	/**
	* Lists all available tables (entity sets) in this database.
	* @returns Promise resolving to an array of table names
	*/
	async listTableNames() {
		const pipeline = requestFromService(`/${this.databaseName}`);
		const data = await runLayerOrThrow(this._layer, pipeline, "fmodata.listTableNames");
		if (data.value && Array.isArray(data.value)) return data.value.map((item) => item.name);
		return [];
	}
	/**
	* Executes a FileMaker script.
	* @param scriptName - The name of the script to execute (must be valid according to OData rules)
	* @param options - Optional script parameter and result schema
	* @returns Promise resolving to script execution result
	*/
	async runScript(scriptName, options) {
		const body = {};
		if (options?.scriptParam !== void 0) body.scriptParameterValue = options.scriptParam;
		const pipeline = requestFromService(`/${this.databaseName}/Script.${scriptName}`, {
			method: "POST",
			body: Object.keys(body).length > 0 ? JSON.stringify(body) : void 0
		});
		const response = await runLayerOrThrow(this._layer, pipeline, "fmodata.runScript");
		if (options?.resultSchema && response.scriptResult !== void 0) {
			const validationResult = options.resultSchema["~standard"].validate(response.scriptResult.resultParameter);
			const validated = validationResult instanceof Promise ? await validationResult : validationResult;
			if (validated.issues) throw new SchemaValidationFailedError("Database.runScript", JSON.stringify(validated.issues), { issues: validated.issues });
			return {
				resultCode: response.scriptResult.code,
				result: validated.value
			};
		}
		return {
			resultCode: response.scriptResult.code,
			result: response.scriptResult.resultParameter
		};
	}
	/**
	* Create a batch operation builder that allows multiple queries to be executed together
	* in a single atomic request. All operations succeed or fail together (transactional).
	*
	* @param builders - Array of executable query builders to batch
	* @returns A BatchBuilder that can be executed
	* @example
	* ```ts
	* const result = await db.batch([
	*   db.from('contacts').list().top(5),
	*   db.from('users').list().top(5),
	*   db.from('contacts').insert({ name: 'John' })
	* ]).execute();
	*
	* if (result.data) {
	*   const [contacts, users, insertResult] = result.data;
	* }
	* ```
	*/
	batch(builders) {
		return new BatchBuilder(builders, this._layer);
	}
};
//#endregion
//#region src/client/filemaker-odata.ts
const TRAILING_SLASH_REGEX = /\/+$/;
var FMServerConnection = class {
	fetchClient;
	serverUrl;
	auth;
	normalizeDatabaseName = true;
	useEntityIds = false;
	includeSpecialColumns = false;
	logger;
	clarisIdAuthManager;
	hasWarnedAboutOttoDatabaseNormalization = false;
	/** @internal Stored so credential-override flows can inherit non-auth config. */
	_fetchClientOptions;
	constructor(config) {
		this.logger = createLogger(config.logger);
		this._fetchClientOptions = config.fetchClientOptions;
		this.fetchClient = createClient({
			retries: 0,
			...config.fetchClientOptions
		});
		const url = new URL(config.serverUrl);
		if (url.protocol !== "https:") url.protocol = "https:";
		url.pathname = url.pathname.replace(TRAILING_SLASH_REGEX, "");
		this.serverUrl = url.toString().replace(TRAILING_SLASH_REGEX, "");
		this.auth = config.auth;
		this.clarisIdAuthManager = "clarisId" in config.auth ? new ClarisIdAuthManager({
			username: config.auth.clarisId.username,
			password: config.auth.clarisId.password
		}) : null;
	}
	/**
	* @internal
	* Sets whether to use FileMaker entity IDs (FMFID/FMTID) in requests
	*/
	_setUseEntityIds(useEntityIds) {
		this.useEntityIds = useEntityIds;
	}
	/**
	* @internal
	* Gets whether to use FileMaker entity IDs (FMFID/FMTID) in requests
	*/
	_getUseEntityIds() {
		return this.useEntityIds;
	}
	/**
	* @internal
	* Sets whether to include special columns (ROWID and ROWMODID) in requests
	*/
	_setIncludeSpecialColumns(includeSpecialColumns) {
		this.includeSpecialColumns = includeSpecialColumns;
	}
	/**
	* @internal
	* Gets whether to include special columns (ROWID and ROWMODID) in requests
	*/
	_getIncludeSpecialColumns() {
		return this.includeSpecialColumns;
	}
	/**
	* @internal
	* Gets the base URL for OData requests
	*/
	_getBaseUrl() {
		return `${this.serverUrl}${"apiKey" in this.auth ? "/otto" : ""}/fmi/odata/v4`;
	}
	_getAuthorizationHeader(fetchHandler) {
		if ("apiKey" in this.auth) return Promise.resolve(`Bearer ${this.auth.apiKey}`);
		if ("clarisId" in this.auth) {
			if (!this.clarisIdAuthManager) throw new Error("Claris ID auth manager was not initialized");
			return this.clarisIdAuthManager.getAuthorizationHeader(fetchHandler);
		}
		return Promise.resolve(`Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`);
	}
	/**
	* @internal
	* Gets the logger instance
	*/
	_getLogger() {
		return this.logger;
	}
	/**
	* @internal
	* Returns the Effect Layer for this connection, composing HttpClient, ODataConfig, and ODataLogger services.
	*/
	_getLayer() {
		const httpLayer = Layer.succeed(HttpClient, { request: (url, options) => this._makeRequestEffect(url, options) });
		const configLayer = Layer.succeed(ODataConfig, {
			baseUrl: this._getBaseUrl(),
			databaseName: "",
			normalizeDatabaseName: this.normalizeDatabaseName,
			useEntityIds: this.useEntityIds,
			includeSpecialColumns: this.includeSpecialColumns
		});
		const loggerLayer = Layer.succeed(ODataLogger, { logger: this.logger });
		return Layer.mergeAll(httpLayer, configLayer, loggerLayer);
	}
	/**
	* @internal
	* Classifies a caught error into a typed FMODataErrorType.
	*/
	_classifyError(err, fullUrl) {
		if (err instanceof TimeoutError || err instanceof AbortError || err instanceof NetworkError || err instanceof RetryLimitError || err instanceof CircuitOpenError) return err;
		if (err instanceof ResponseParseError) return err;
		return new NetworkError(fullUrl, err);
	}
	/**
	* @internal
	* Parses an HTTP error response into a typed FMODataErrorType.
	*/
	_parseHttpError(resp, fullUrl, errorBody) {
		if (errorBody?.error) {
			const errorCode = errorBody.error.code;
			const errorMessage = errorBody.error.message || resp.statusText;
			if (errorCode === "303" || errorCode === 303) return new SchemaLockedError(fullUrl, errorMessage, errorBody.error);
			return new ODataError(fullUrl, errorMessage, String(errorCode), errorBody.error);
		}
		return new HTTPError(fullUrl, resp.status, resp.statusText, errorBody);
	}
	/**
	* @internal
	* Checks parsed JSON data for embedded OData errors.
	*/
	_checkEmbeddedODataError(data, fullUrl) {
		if (get(data, "error", null)) {
			const errorCode = get(data, "error.code", null);
			const errorMessage = String(get(data, "error.message", "Unknown OData error"));
			if (errorCode === "303" || errorCode === 303) return new SchemaLockedError(fullUrl, errorMessage, data.error);
			return new ODataError(fullUrl, errorMessage, String(errorCode), data.error);
		}
	}
	/**
	* @internal
	* Builds the Effect pipeline for an HTTP request.
	* Each step in the pipeline is a discrete Effect, enabling composable error handling.
	*/
	_makeRequestEffect(url, options) {
		const logger = this._getLogger();
		const baseUrl = `${this.serverUrl}${"apiKey" in this.auth ? "/otto" : ""}/fmi/odata/v4`;
		const normalizeDatabaseName = options?.normalizeDatabaseName ?? this.normalizeDatabaseName;
		if ("apiKey" in this.auth && normalizeDatabaseName === false && !this.hasWarnedAboutOttoDatabaseNormalization) {
			logger.warn("normalizeDatabaseName=false cannot disable filename normalization with Otto auth; FileMaker Server normalizes it automatically.");
			this.hasWarnedAboutOttoDatabaseNormalization = true;
		}
		const normalizedUrl = normalizeDatabasePath(url, {
			normalizeDatabaseName,
			mode: options?.databaseNameNormalizationMode
		});
		const fullUrl = baseUrl + normalizedUrl;
		const useEntityIds = options?.useEntityIds ?? this.useEntityIds;
		const includeSpecialColumns = options?.includeSpecialColumns ?? this.includeSpecialColumns;
		const includeODataAnnotations = options?.includeODataAnnotations;
		const preferValues = [];
		if (useEntityIds) preferValues.push("fmodata.entity-ids");
		if (includeSpecialColumns) preferValues.push("fmodata.include-specialcolumns");
		const fetchHandler = options?.fetchHandler ?? this._fetchClientOptions?.fetchHandler;
		const { headers: _headers, fetchHandler: _fetchHandler, ...restOptions } = options || {};
		const buildHeaders = async () => {
			const headers = new Headers(options?.headers);
			headers.set("Authorization", await this._getAuthorizationHeader(fetchHandler));
			if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
			if (!headers.has("Accept")) headers.set("Accept", getAcceptHeader(includeODataAnnotations));
			const mergedPrefer = mergePreferHeaderValues(preferValues.length > 0 ? preferValues.join(", ") : void 0, headers.get("Prefer") ?? void 0);
			if (mergedPrefer) headers.set("Prefer", mergedPrefer);
			else headers.delete("Prefer");
			return headers;
		};
		const clientToUse = fetchHandler ? createClient({
			retries: 0,
			fetchHandler
		}) : this.fetchClient;
		const pipeline = Effect.tryPromise({
			try: async () => {
				const headers = await buildHeaders();
				const { authorization: _authorization, ...loggableHeaders } = Object.fromEntries(headers.entries());
				logger.debug("Request headers:", loggableHeaders);
				const finalOptions = {
					...restOptions,
					headers
				};
				return clientToUse(fullUrl, finalOptions);
			},
			catch: (err) => this._classifyError(err, fullUrl)
		}).pipe(Effect.tap((resp) => Effect.sync(() => logger.debug(`${restOptions.method ?? "GET"} ${resp.status} ${fullUrl}`))), Effect.flatMap((resp) => {
			if (!resp.ok) return Effect.tryPromise({
				try: async () => {
					let errorBody;
					try {
						if (resp.headers.get("content-type")?.includes("application/json")) errorBody = await safeJsonParse(resp);
					} catch {}
					return errorBody;
				},
				catch: () => new HTTPError(fullUrl, resp.status, resp.statusText)
			}).pipe(Effect.flatMap((errorBody) => Effect.fail(this._parseHttpError(resp, fullUrl, errorBody))));
			const affectedRows = resp.headers.get("fmodata.affected_rows");
			if (affectedRows !== null) return Effect.succeed(Number.parseInt(affectedRows, 10));
			if (resp.status === 204) {
				const locationHeader = resp.headers?.get?.("Location") || resp.headers?.get?.("location");
				if (locationHeader) return Effect.succeed({ _location: locationHeader });
				return Effect.succeed(0);
			}
			if (resp.headers.get("content-type")?.includes("application/json")) return Effect.tryPromise({
				try: () => safeJsonParse(resp),
				catch: (err) => this._classifyError(err, fullUrl)
			}).pipe(Effect.flatMap((data) => {
				const embeddedError = this._checkEmbeddedODataError(data, fullUrl);
				if (embeddedError) return Effect.fail(embeddedError);
				return Effect.succeed(data);
			}));
			return Effect.tryPromise({
				try: () => resp.text(),
				catch: (err) => this._classifyError(err, fullUrl)
			}).pipe(Effect.map((text) => text));
		}));
		const retryPolicy = options?.retryPolicy;
		const method = (restOptions.method ?? "GET").toUpperCase();
		return withSpan(retryPolicy && (method === "GET" || method === "HEAD" || method === "OPTIONS" || method === "PUT") ? withRetryPolicy(pipeline, retryPolicy) : pipeline, "fmodata.request", {
			"fmodata.url": normalizedUrl,
			"fmodata.method": method
		});
	}
	/**
	* @internal
	*/
	_makeRequest(url, options) {
		return runAsResult(this._makeRequestEffect(url, options));
	}
	database(name, config) {
		return new Database(name, this, config);
	}
	/**
	* Lists all available databases from the FileMaker OData service.
	* @returns Promise resolving to an array of database names
	*/
	async listDatabaseNames() {
		const result = await this._makeRequest("/$metadata", { headers: { Accept: "application/json" } });
		if (result.error) throw result.error;
		if (result.data.value && Array.isArray(result.data.value)) return result.data.value.map((item) => item.name);
		return [];
	}
};
//#endregion
//#region src/cli/utils/connection.ts
const ENV_NAMES = {
	server: "FM_SERVER",
	db: "FM_DATABASE",
	username: "FM_USERNAME",
	password: "FM_PASSWORD",
	clarisIdUsername: "CLARIS_ID_USERNAME",
	clarisIdPassword: "CLARIS_ID_PASSWORD",
	apiKey: "OTTO_API_KEY"
};
function buildConnection(opts) {
	const server = opts.server ?? process.env[ENV_NAMES.server];
	const database = opts.database ?? process.env[ENV_NAMES.db];
	const apiKey = opts.apiKey ?? process.env[ENV_NAMES.apiKey];
	const username = opts.username ?? process.env[ENV_NAMES.username];
	const password = opts.password ?? process.env[ENV_NAMES.password];
	const clarisIdUsername = opts.clarisIdUsername ?? process.env[ENV_NAMES.clarisIdUsername];
	const clarisIdPassword = opts.clarisIdPassword ?? process.env[ENV_NAMES.clarisIdPassword];
	if (!server) throw new Error(`Missing required: --server or ${ENV_NAMES.server} environment variable`);
	if (!database) throw new Error(`Missing required: --database or ${ENV_NAMES.db} environment variable`);
	if (!(apiKey || clarisIdUsername || username)) throw new Error(`Missing required auth: --api-key (${ENV_NAMES.apiKey}), --claris-id-username (${ENV_NAMES.clarisIdUsername}), or --username (${ENV_NAMES.username})`);
	if (!apiKey && clarisIdUsername && !clarisIdPassword) throw new Error(`Missing required: --claris-id-password (${ENV_NAMES.clarisIdPassword}) when using Claris ID auth`);
	if (!apiKey && username && !password) throw new Error(`Missing required: --password (${ENV_NAMES.password}) when using username auth`);
	let auth;
	if (apiKey) auth = { apiKey };
	else if (clarisIdUsername) auth = { clarisId: {
		username: clarisIdUsername,
		password: clarisIdPassword
	} };
	else auth = {
		username,
		password
	};
	const connection = new FMServerConnection({
		serverUrl: server,
		auth
	});
	return {
		connection,
		db: connection.database(database)
	};
}
//#endregion
//#region src/cli/utils/errors.ts
function handleCliError(err) {
	const message = err instanceof Error ? err.message : String(err);
	process.stderr.write(`Error: ${message}\n`);
	process.exit(1);
}
//#endregion
//#region src/cli/utils/output.ts
function printResult(data, opts) {
	if (opts.pretty) printTable(data);
	else console.log(JSON.stringify(data, null, 2));
}
function printTable(data) {
	if (Array.isArray(data) && data.length > 0 && typeof data[0] === "object" && data[0] !== null) {
		const keys = Object.keys(data[0]);
		const table = new Table({ head: keys });
		for (const row of data) table.push(keys.map((k) => String(row[k] ?? "")));
		console.log(table.toString());
		return;
	}
	if (Array.isArray(data) && data.length > 0) {
		const table = new Table({ head: ["Value"] });
		for (const value of data) table.push([String(value ?? "")]);
		console.log(table.toString());
		return;
	}
	if (typeof data === "object" && data !== null && !Array.isArray(data)) {
		const table = new Table({ head: ["Key", "Value"] });
		for (const [key, value] of Object.entries(data)) table.push([key, typeof value === "object" ? JSON.stringify(value) : String(value ?? "")]);
		console.log(table.toString());
		return;
	}
	console.log(JSON.stringify(data, null, 2));
}
//#endregion
//#region src/cli/commands/metadata.ts
function isEntityType(value) {
	return value.$Kind === "EntityType";
}
function isFieldMetadata(value) {
	return value !== null && typeof value === "object" && "$Type" in value;
}
function getEntityFieldEntries(entityType) {
	return Object.entries(entityType).filter((entry) => !entry[0].startsWith("$") && isFieldMetadata(entry[1]));
}
function makeMetadataCommand() {
	const metadata = new Command("metadata").description("FileMaker OData metadata operations");
	metadata.command("get").description("Get OData metadata for the database").option("--format <format>", "Output format: json or xml", "json").option("--table <table>", "Filter metadata to a specific table").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			let result;
			if (opts.format === "xml") result = await db.getMetadata({
				format: "xml",
				tableName: opts.table
			});
			else result = await db.getMetadata({
				format: "json",
				tableName: opts.table
			});
			printResult(result, { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	metadata.command("tables").description("List all table names in the database").action(async (_opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			printResult(await db.listTableNames(), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	metadata.command("fields").description("List field names for a specific table").requiredOption("--table <table>", "Table name").option("--details", "Include field metadata details", false).action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			const metadataResult = await db.getMetadata({ tableName: opts.table });
			const entityType = Object.values(metadataResult).find(isEntityType);
			if (!entityType) throw new Error(`No entity metadata found for table: ${opts.table}`);
			const fields = getEntityFieldEntries(entityType);
			if (opts.details) {
				printResult(fields.map(([fieldName, fieldMeta]) => {
					const { $Type, $Nullable, ...rest } = fieldMeta;
					return {
						name: fieldName,
						type: $Type,
						nullable: $Nullable,
						...rest
					};
				}), { pretty: globalOpts.pretty ?? false });
				return;
			}
			printResult(fields.map(([fieldName]) => fieldName), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	return metadata;
}
//#endregion
//#region src/cli/commands/query.ts
function buildQueryString(params) {
	const parts = [];
	if (params.top !== void 0) parts.push(`$top=${encodeURIComponent(String(params.top))}`);
	if (params.skip !== void 0) parts.push(`$skip=${encodeURIComponent(String(params.skip))}`);
	if (params.select) parts.push(`$select=${encodeURIComponent(params.select)}`);
	if (params.where) parts.push(`$filter=${encodeURIComponent(params.where)}`);
	if (params.orderBy) {
		const orderStr = params.orderBy.split(",").map((part) => {
			const [field, dir] = part.trim().split(":");
			return dir ? `${field} ${dir}` : field;
		}).join(",");
		parts.push(`$orderby=${encodeURIComponent(orderStr)}`);
	}
	return parts.length > 0 ? `?${parts.join("&")}` : "";
}
function makeRecordsCommand() {
	const query = new Command("records").description("FileMaker record operations (list, insert, update, delete)");
	query.command("list").description("List records from a table").requiredOption("--table <name>", "Table name").option("--top <n>", "Max records to return", Number).option("--skip <n>", "Records to skip", Number).option("--select <fields>", "Comma-separated field names").option("--where <expr>", "OData filter expression").option("--order-by <field>", "Order by field (format: field:asc|desc, or comma-separated)").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			const qs = buildQueryString({
				top: opts.top,
				skip: opts.skip,
				select: opts.select,
				where: opts.where,
				orderBy: opts.orderBy
			});
			const table = encodeURIComponent(opts.table);
			const result = await db._makeRequest(`/${table}${qs}`);
			if (result.error) throw result.error;
			printResult(result.data.value ?? result.data, { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	query.command("insert").description("Insert a record into a table").requiredOption("--table <name>", "Table name").requiredOption("--data <json>", "Record data as JSON object").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			let data;
			try {
				const parsed = JSON.parse(opts.data);
				if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("invalid");
				data = parsed;
			} catch {
				throw new Error("--data must be a valid JSON object");
			}
			const table = encodeURIComponent(opts.table);
			const result = await db._makeRequest(`/${table}`, {
				method: "POST",
				body: JSON.stringify(data)
			});
			if (result.error) throw result.error;
			printResult(result.data, { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	query.command("update").description("Update records in a table").requiredOption("--table <name>", "Table name").requiredOption("--data <json>", "Update data as JSON object").option("--where <expr>", "OData filter expression").option("--confirm", "Execute without --where (affects all records)").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			if (!(opts.where || opts.confirm)) {
				printResult({
					dryRun: true,
					action: "update",
					table: opts.table,
					affectsAllRows: true,
					hint: "Add --where to filter or --confirm to update all records"
				}, { pretty: globalOpts.pretty ?? false });
				return;
			}
			const { db } = buildConnection(globalOpts);
			let data;
			try {
				const parsed = JSON.parse(opts.data);
				if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("invalid");
				data = parsed;
			} catch {
				throw new Error("--data must be a valid JSON object");
			}
			const qs = buildQueryString({ where: opts.where });
			const table = encodeURIComponent(opts.table);
			const result = await db._makeRequest(`/${table}${qs}`, {
				method: "PATCH",
				body: JSON.stringify(data)
			});
			if (result.error) throw result.error;
			printResult(result.data, { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	query.command("delete").description("Delete records from a table").requiredOption("--table <name>", "Table name").option("--where <expr>", "OData filter expression").option("--confirm", "Execute without --where (affects all records)").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			if (!(opts.where || opts.confirm)) {
				printResult({
					dryRun: true,
					action: "delete",
					table: opts.table,
					affectsAllRows: true,
					hint: "Add --where to filter or --confirm to delete all records"
				}, { pretty: globalOpts.pretty ?? false });
				return;
			}
			const { db } = buildConnection(globalOpts);
			const qs = buildQueryString({ where: opts.where });
			const table = encodeURIComponent(opts.table);
			const result = await db._makeRequest(`/${table}${qs}`, { method: "DELETE" });
			if (result.error) throw result.error;
			printResult(result.data, { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	return query;
}
//#endregion
//#region src/cli/commands/schema.ts
function makeSchemaCommand() {
	const schema = new Command("schema").description("FileMaker schema modification operations");
	schema.command("list-tables").description("List all tables in the database").action(async (_opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			printResult(await db.listTableNames(), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	schema.command("create-table").description("Create a new table (requires --confirm to execute; dry-run by default)").requiredOption("--name <name>", "Table name").requiredOption("--fields <json>", "Fields definition as JSON array").option("--confirm", "Execute the operation (without this flag, shows what would be created)").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			let fields;
			try {
				const parsed = JSON.parse(opts.fields);
				if (!Array.isArray(parsed)) throw new Error("invalid");
				fields = parsed;
			} catch {
				throw new Error("--fields must be a valid JSON array");
			}
			if (!opts.confirm) {
				printResult({
					dryRun: true,
					action: "create-table",
					tableName: opts.name,
					fields
				}, { pretty: globalOpts.pretty ?? false });
				return;
			}
			const { db } = buildConnection(globalOpts);
			printResult(await db.schema.createTable(opts.name, fields), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	schema.command("add-fields").description("Add fields to an existing table (requires --confirm to execute; dry-run by default)").requiredOption("--table <name>", "Table name").requiredOption("--fields <json>", "Fields to add as JSON array").option("--confirm", "Execute the operation (without this flag, shows what would be added)").action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			let fields;
			try {
				const parsed = JSON.parse(opts.fields);
				if (!Array.isArray(parsed)) throw new Error("invalid");
				fields = parsed;
			} catch {
				throw new Error("--fields must be a valid JSON array");
			}
			if (!opts.confirm) {
				printResult({
					dryRun: true,
					action: "add-fields",
					tableName: opts.table,
					fields
				}, { pretty: globalOpts.pretty ?? false });
				return;
			}
			const { db } = buildConnection(globalOpts);
			printResult(await db.schema.addFields(opts.table, fields), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	return schema;
}
//#endregion
//#region src/cli/commands/script.ts
function makeScriptCommand() {
	const script = new Command("script").description("FileMaker script operations");
	script.command("run <scriptName>").description("Run a FileMaker script").option("--param <json>", "Script parameter as JSON string or plain value").action(async (scriptName, opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			let scriptParam;
			if (opts.param !== void 0) try {
				scriptParam = JSON.parse(opts.param);
			} catch {
				scriptParam = opts.param;
			}
			printResult(await db.runScript(scriptName, { scriptParam }), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	return script;
}
//#endregion
//#region src/cli/commands/webhook.ts
const WEBHOOK_ID_RE = /^\d+$/;
function parseWebhookId(id) {
	if (!WEBHOOK_ID_RE.test(id)) throw new Error(`Invalid webhook ID: "${id}" — must be a positive integer`);
	return Number(id);
}
function makeWebhookCommand() {
	const webhook = new Command("webhook").description("FileMaker webhook operations");
	webhook.command("list").description("List all webhooks").action(async (_opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			printResult(await db.webhook.list(), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	webhook.command("get <id>").description("Get a webhook by ID").action(async (id, _opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const parsedId = parseWebhookId(id);
			const { db } = buildConnection(globalOpts);
			printResult(await db.webhook.get(parsedId), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	webhook.command("add").description("Add a new webhook").requiredOption("--table <name>", "Table to monitor").requiredOption("--url <url>", "Webhook URL to call").option("--select <fields>", "Comma-separated field names to include").option("--header <kv>", "Header in key=value format (repeatable)", (val, acc) => {
		acc.push(val);
		return acc;
	}, []).action(async (opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const { db } = buildConnection(globalOpts);
			const headers = {};
			for (const h of opts.header) {
				const eqIdx = h.indexOf("=");
				if (eqIdx === -1) throw new Error(`Invalid header format (expected key=value): ${h}`);
				headers[h.slice(0, eqIdx)] = h.slice(eqIdx + 1);
			}
			const tableProxy = { [FMTable.Symbol.Name]: opts.table };
			const webhookPayload = {
				webhook: opts.url,
				tableName: tableProxy
			};
			if (Object.keys(headers).length > 0) webhookPayload.headers = headers;
			if (opts.select) webhookPayload.select = opts.select;
			printResult(await db.webhook.add(webhookPayload), { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	webhook.command("remove <id>").description("Remove a webhook by ID").action(async (id, _opts, cmd) => {
		const globalOpts = cmd.parent?.parent?.opts();
		try {
			const parsedId = parseWebhookId(id);
			const { db } = buildConnection(globalOpts);
			await db.webhook.remove(parsedId);
			printResult({
				removed: true,
				id: parsedId
			}, { pretty: globalOpts.pretty ?? false });
		} catch (err) {
			handleCliError(err);
		}
	});
	return webhook;
}
//#endregion
//#region src/cli/index.ts
const { version } = createRequire(import.meta.url)("../../package.json");
const program = new Command();
program.name("fmodata").description("FileMaker OData CLI — query, script, webhook, metadata, and schema operations").version(version).option("--server <url>", `FM server URL [env: ${ENV_NAMES.server}]`).option("--database <name>", `FM database name [env: ${ENV_NAMES.db}]`).option("--username <user>", `FM username [env: ${ENV_NAMES.username}]`).option("--password <pass>", `FM password [env: ${ENV_NAMES.password}]`).option("--claris-id-username <user>", `Claris ID username [env: ${ENV_NAMES.clarisIdUsername}]`).option("--claris-id-password <pass>", `Claris ID password [env: ${ENV_NAMES.clarisIdPassword}]`).option("--api-key <key>", `OttoFMS API key [env: ${ENV_NAMES.apiKey}]`).option("--pretty", "Output as table (default: JSON)", false);
program.addCommand(makeRecordsCommand());
program.addCommand(makeScriptCommand());
program.addCommand(makeWebhookCommand());
program.addCommand(makeMetadataCommand());
program.addCommand(makeSchemaCommand());
program.exitOverride();
try {
	await program.parseAsync(process.argv);
} catch (err) {
	if (err && typeof err === "object" && "code" in err) {
		const code = err.code;
		if (code === "commander.helpDisplayed" || code === "commander.version") process.exit(0);
	}
	handleCliError(err);
}
//#endregion
export {};

//# sourceMappingURL=index.js.map