@proofkit/fmodata
Version:
FileMaker OData API client
744 lines (671 loc) • 29.5 kB
text/typescript
import type { StandardSchemaV1 } from "@standard-schema/spec";
import { Column } from "./column";
import type { ContainerDbType, FieldBuilder, FieldBuilder as FieldBuilderType } from "./field-builders";
// import { z } from "zod/v4";
/**
* Extract the output type from a FieldBuilder.
* This is what you get when reading from the database.
*
* This type extracts the TOutput type parameter, which is set by readValidator()
* and represents the transformed/validated output type.
*/
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
export type InferFieldOutput<F> = F extends FieldBuilder<infer TOutput, any, any, any> ? TOutput : never;
/**
* Extract the input type from a FieldBuilder.
* This is what you pass when writing to the database.
*
* This type extracts the TInput type parameter, which is set by writeValidator()
* and represents the transformed/validated input type.
*/
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
type InferFieldInput<F> = F extends FieldBuilder<any, infer TInput, any, any> ? TInput : never;
/**
* Build a schema type from field builders (output/read types).
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
type InferSchemaFromFields<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
[K in keyof TFields]: InferFieldOutput<TFields[K]>;
};
/**
* Build an input schema type from field builders (input/write types).
* Used for insert and update operations.
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
type InferInputSchemaFromFields<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
[K in keyof TFields]: InferFieldInput<TFields[K]>;
};
/**
* Check if a field is a container field by inspecting its TDbType.
* Container fields have a branded TDbType that extends ContainerDbType.
*/
type IsContainerField<F> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
F extends FieldBuilder<any, any, infer TDbType, any>
? NonNullable<TDbType> extends ContainerDbType
? true
: false
: false;
/**
* Extract only selectable (non-container) field keys from a fields record.
* Container fields are excluded because they cannot be selected via $select in FileMaker OData.
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
type SelectableFieldKeys<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
[K in keyof TFields]: IsContainerField<TFields[K]> extends true ? never : K;
}[keyof TFields];
/**
* Build a schema type excluding container fields (for query return types).
* This is used to ensure container fields don't appear in the return type
* when using defaultSelect: "schema" or "all".
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
type _InferSelectableSchemaFromFields<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
[K in SelectableFieldKeys<TFields>]: InferFieldOutput<TFields[K]>;
};
/**
* 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.
*/
export class FMTable<
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration, default allows untyped tables
TFields extends Record<string, FieldBuilder<any, any, any, any>> = any,
TName extends string = string,
TNavigationPaths extends readonly string[] = readonly string[],
> {
/**
* Internal Symbols for accessing table metadata.
* @internal - Not intended for public use. Access table properties via columns instead.
*/
static readonly Symbol = {
Name: FMTableName,
EntityId: FMTableEntityId,
UseEntityIds: FMTableUseEntityIds,
Schema: FMTableSchema,
Fields: FMTableFields,
NavigationPaths: FMTableNavigationPaths,
DefaultSelect: FMTableDefaultSelect,
BaseTableConfig: FMTableBaseTableConfig,
Comment: FMTableComment,
};
/** @internal */
[FMTableName]: TName;
/** @internal */
[FMTableEntityId]?: `FMTID:${string}`;
/** @internal */
[FMTableUseEntityIds]?: boolean;
/** @internal */
[FMTableComment]?: string;
/** @internal */
[FMTableSchema]: Partial<Record<keyof TFields, StandardSchemaV1>>;
/** @internal */
[FMTableFields]: TFields;
/** @internal */
[FMTableNavigationPaths]: TNavigationPaths;
/** @internal */
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
[FMTableDefaultSelect]: "all" | "schema" | Record<string, Column<any, any, TName>>;
/** @internal */
[FMTableBaseTableConfig]: {
schema: Partial<Record<keyof TFields, StandardSchemaV1>>;
inputSchema?: Partial<Record<keyof TFields, StandardSchemaV1>>;
idField?: keyof TFields;
required: readonly (keyof TFields)[];
readOnly: readonly (keyof TFields)[];
containerFields: readonly (keyof TFields)[];
fmfIds?: Record<keyof TFields, `FMFID:${string}`>;
};
constructor(config: {
name: TName;
entityId?: `FMTID:${string}`;
useEntityIds?: boolean;
comment?: string;
schema: Partial<Record<keyof TFields, StandardSchemaV1>>;
fields: TFields;
navigationPaths: TNavigationPaths;
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
defaultSelect: "all" | "schema" | Record<string, Column<any, any, TName>>;
baseTableConfig: {
schema: Partial<Record<keyof TFields, StandardSchemaV1>>;
inputSchema?: Partial<Record<keyof TFields, StandardSchemaV1>>;
idField?: keyof TFields;
required: readonly (keyof TFields)[];
readOnly: readonly (keyof TFields)[];
containerFields: readonly (keyof TFields)[];
fmfIds?: Record<keyof TFields, `FMFID:${string}`>;
};
}) {
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;
}
}
/**
* Type helper to extract the column map from fields.
* Table name is baked into each column type for validation.
* Container fields are marked with IsContainer=true.
* Columns include both output type (for reading) and input type (for writing/filtering).
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
export type ColumnMap<TFields extends Record<string, FieldBuilder<any, any, any, any>>, TName extends string> = {
[K in keyof TFields]: Column<
InferFieldOutput<TFields[K]>,
InferFieldInput<TFields[K]>,
TName,
IsContainerField<TFields[K]>
>;
};
/**
* Extract only selectable (non-container) columns from a table.
* This is used to prevent selecting container fields in queries.
*/
export type SelectableColumnMap<
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
TFields extends Record<string, FieldBuilder<any, any, any, any>>,
TName extends string,
> = {
[K in SelectableFieldKeys<TFields>]: Column<InferFieldOutput<TFields[K]>, InferFieldInput<TFields[K]>, TName, false>;
};
/**
* Validates that a select object doesn't contain container field columns.
* Returns never if any container fields are found, otherwise returns the original type.
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
export type ValidateNoContainerFields<TSelect extends Record<string, Column<any, any, any, any>>> = {
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
[K in keyof TSelect]: TSelect[K] extends Column<any, any, any, true> ? never : TSelect[K];
} extends TSelect
? TSelect
: {
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
[K in keyof TSelect]: TSelect[K] extends Column<any, any, any, true>
? "❌ Container fields cannot be selected. Use .getSingleField() instead."
: TSelect[K];
};
/**
* Extract the keys from a defaultSelect function's return type.
* Used to infer which fields are selected by default for type narrowing.
*/
type _ExtractDefaultSelectKeys<
TDefaultSelect,
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
TFields extends Record<string, FieldBuilder<any, any, any, any>>,
TName extends string,
> = TDefaultSelect extends (columns: ColumnMap<TFields, TName>) => infer R
? keyof R
: TDefaultSelect extends "schema"
? keyof TFields
: keyof TFields; // "all" defaults to all keys
/**
* Complete table type with both metadata (via Symbols) and column accessors.
* This is the return type of fmTableOccurrence - users see columns directly,
* but internal config is hidden via Symbols.
*/
export type FMTableWithColumns<
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
TFields extends Record<string, FieldBuilder<any, any, any, any>>,
TName extends string,
TNavigationPaths extends readonly string[] = readonly string[],
> = FMTable<TFields, TName, TNavigationPaths> & ColumnMap<TFields, TName>;
/**
* Options for fmTableOccurrence function.
* Provides autocomplete-friendly typing while preserving inference for navigationPaths.
*/
export interface FMTableOccurrenceOptions<
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
TFields extends Record<string, FieldBuilder<any, any, any, any>>,
TName extends string,
> {
/** The entity ID (FMTID) for this table occurrence */
entityId?: `FMTID:${string}`;
/** The comment for this table */
comment?: string;
/**
* Default select behavior:
* - "all": Select all fields (including related tables)
* - "schema": Select only schema-defined fields (default)
* - function: Custom selection from columns
*/
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
defaultSelect?: "all" | "schema" | ((columns: ColumnMap<TFields, TName>) => Record<string, Column<any, any, TName>>);
/** Navigation paths available from this table (for expand operations) */
navigationPaths?: readonly string[];
/** Whether to use entity IDs (FMTID/FMFID) instead of names in queries */
useEntityIds?: boolean;
}
/**
* Create a table occurrence with field builders.
* This is the main API for defining tables in the new ORM style.
*
* @example
* const users = fmTableOccurrence("users", {
* id: textField().primaryKey().entityId("FMFID:1"),
* name: textField().notNull().entityId("FMFID:6"),
* active: numberField()
* .outputValidator(z.coerce.boolean())
* .inputValidator(z.boolean().transform(v => v ? 1 : 0))
* .entityId("FMFID:7"),
* }, {
* entityId: "FMTID:100",
* defaultSelect: "schema",
* navigationPaths: ["contacts"],
* });
*
* // Access columns
* users.id // Column<string, "id">
* users.name // Column<string, "name">
*
* // Use in queries
* db.from(users).select("id", "name").where(eq(users.active, true))
*/
export function fmTableOccurrence<
const TName extends string,
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
const TFields extends Record<string, FieldBuilder<any, any, any, any>>,
const TNavPaths extends readonly string[] = readonly [],
>(
name: TName,
fields: TFields,
options?: FMTableOccurrenceOptions<TFields, TName> & {
/** Navigation paths available from this table (for expand operations) */
navigationPaths?: TNavPaths;
},
): FMTableWithColumns<TFields, TName, TNavPaths> {
// Extract configuration from field builders
const fieldConfigs = Object.entries(fields).map(([fieldName, builder]) => ({
fieldName,
// biome-ignore lint/suspicious/noExplicitAny: Internal property access for builder pattern
config: (builder as any)._getConfig(),
}));
// Find primary key field
const primaryKeyField = fieldConfigs.find((f) => f.config.primaryKey);
const idField = primaryKeyField?.fieldName;
// Collect required fields (notNull fields)
const required = fieldConfigs.filter((f) => f.config.notNull).map((f) => f.fieldName);
// Collect read-only fields
const readOnly = fieldConfigs.filter((f) => f.config.readOnly).map((f) => f.fieldName);
// Collect container fields (cannot be selected via $select)
const containerFields = fieldConfigs.filter((f) => f.config.fieldType === "container").map((f) => f.fieldName);
// Collect entity IDs
const fmfIds: Record<string, `FMFID:${string}`> = {};
for (const { fieldName, config } of fieldConfigs) {
if (config.entityId) {
fmfIds[fieldName] = config.entityId;
}
}
// Build Zod schema from field builders (output/read validators)
const outputSchema: Partial<Record<keyof TFields, StandardSchemaV1>> = {};
// Build input schema from field builders (input/write validators)
const inputSchema: Record<string, StandardSchemaV1> = {};
for (const { fieldName, config } of fieldConfigs) {
// Use outputValidator if provided
if (config.outputValidator) {
outputSchema[fieldName as keyof TFields] = config.outputValidator;
}
// Store inputValidator if provided (for write operations)
if (config.inputValidator) {
inputSchema[fieldName] = config.inputValidator;
}
}
// Build BaseTable-compatible config
const baseTableConfig = {
schema: outputSchema as Partial<Record<keyof TFields, StandardSchemaV1>>,
inputSchema:
Object.keys(inputSchema).length > 0
? (inputSchema as Partial<Record<keyof TFields, StandardSchemaV1>>)
: undefined,
idField: idField as keyof TFields | undefined,
required: required as readonly (keyof TFields)[],
readOnly: readOnly as readonly (keyof TFields)[],
containerFields: containerFields as readonly (keyof TFields)[],
fmfIds: (Object.keys(fmfIds).length > 0 ? fmfIds : undefined) as
| Record<keyof TFields, `FMFID:${string}`>
| undefined,
};
// Create column instances
const columns = {} as ColumnMap<TFields, TName>;
for (const [fieldName, builder] of Object.entries(fields)) {
// biome-ignore lint/suspicious/noExplicitAny: Internal property access for builder pattern
const config = (builder as any)._getConfig();
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(columns as any)[fieldName] = new Column({
fieldName: String(fieldName),
entityId: config.entityId,
tableName: name,
tableEntityId: options?.entityId,
inputValidator: config.inputValidator,
fieldType: config.fieldType,
});
}
// Resolve defaultSelect: if it's a function, call it with columns; otherwise use as-is
const defaultSelectOption = options?.defaultSelect ?? "schema";
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
const resolvedDefaultSelect: "all" | "schema" | Record<string, Column<any, any, TName>> =
typeof defaultSelectOption === "function"
? defaultSelectOption(columns as ColumnMap<TFields, TName>)
: defaultSelectOption;
// Create the FMTable instance with Symbol-based internal properties
const navigationPaths = (options?.navigationPaths ?? []) as TNavPaths;
const table = new FMTable<TFields, TName, TNavPaths>({
name,
entityId: options?.entityId,
useEntityIds: options?.useEntityIds,
comment: options?.comment,
schema: outputSchema,
fields,
navigationPaths,
defaultSelect: resolvedDefaultSelect,
baseTableConfig,
});
// Assign columns to the table instance (making them accessible directly)
Object.assign(table, columns);
return table as FMTableWithColumns<TFields, TName, TNavPaths>;
}
// /**
// * Type guard to check if a value is a TableOccurrence or FMTable.
// * Supports both Symbol-based (new) and underscore-prefixed (legacy) formats.
// */
// function isTableOccurrence(value: any): value is TableOccurrence {
// if (!value || typeof value !== "object") {
// return false;
// }
// // Check for Symbol-based format (new FMTable class)
// if (
// FMTableName in value &&
// FMTableSchema in value &&
// FMTableFields in value
// ) {
// return typeof value[FMTableName] === "string";
// }
// // Check for underscore-prefixed format (legacy interface)
// if ("_name" in value && "_schema" in value && "_fields" in value) {
// return typeof value._name === "string";
// }
// return false;
// }
/**
* Helper to extract the schema type from a TableOccurrence or FMTable.
*/
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
export type InferTableSchema<T> = T extends FMTable<infer TFields, any> ? InferSchemaFromFields<TFields> : never;
/**
* Extract the schema type from an FMTable instance.
* This is used to infer the schema from table objects passed to db.from(), expand(), etc.
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export type InferSchemaOutputFromFMTable<T extends FMTable<any, any>> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
T extends FMTable<infer TFields, any> ? InferSchemaFromFields<TFields> : never;
/**
* Extract the input schema type from an FMTable instance.
* This is used for insert and update operations where we need write types.
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export type InferInputSchemaFromFMTable<T extends FMTable<any, any>> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
T extends FMTable<infer TFields, any> ? InferInputSchemaFromFields<TFields> : never;
/**
* Helper type to check if a FieldBuilder's input type excludes null and undefined.
* This checks the TInput type parameter, which preserves nullability from notNull().
*/
type FieldInputExcludesNullish<F> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
F extends FieldBuilder<any, infer TInput, any>
? null extends TInput
? false
: undefined extends TInput
? false
: true
: false;
/**
* Check if a FieldBuilder is readOnly at the type level
*/
type IsFieldReadOnly<F> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
F extends FieldBuilderType<any, any, any, infer ReadOnly> ? (ReadOnly extends true ? true : false) : false;
/**
* Compute insert data type from FMTable, making notNull fields required.
* Fields are required if their FieldBuilder's TInput type excludes null/undefined.
* All other fields are optional (can be omitted).
* readOnly fields are excluded (including primaryKey/idField since they're automatically readOnly).
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export type InsertDataFromFMTable<T extends FMTable<any, any>> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
T extends FMTable<infer TFields, any>
? {
[K in keyof TFields as IsFieldReadOnly<TFields[K]> extends true
? never
: FieldInputExcludesNullish<TFields[K]> extends true
? K
: never]: InferFieldInput<TFields[K]>;
} & {
[K in keyof TFields as IsFieldReadOnly<TFields[K]> extends true
? never
: FieldInputExcludesNullish<TFields[K]> extends true
? never
: K]?: InferFieldInput<TFields[K]>;
}
: never;
/**
* Compute update data type from FMTable.
* All fields are optional, but readOnly fields are excluded (including primaryKey/idField).
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export type UpdateDataFromFMTable<T extends FMTable<any, any>> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
T extends FMTable<infer TFields, any>
? {
[K in keyof TFields as IsFieldReadOnly<TFields[K]> extends true ? never : K]?: InferFieldInput<TFields[K]>;
}
: never;
/**
* Extract the table name type from an FMTable.
* This is a workaround since we can't directly index Symbols in types.
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, required for type inference with infer
export type ExtractTableName<T extends FMTable<any, any>> = T extends FMTable<any, infer Name> ? Name : never;
/**
* Validates that a target table's name matches one of the source table's navigationPaths.
* Used to ensure type-safe expand/navigate operations.
*/
export type ValidExpandTarget<
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
SourceTable extends FMTable<any, any, any> | undefined,
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
TargetTable extends FMTable<any, any, any>,
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
> = SourceTable extends FMTable<any, any, infer SourceNavPaths>
? ExtractTableName<TargetTable> extends SourceNavPaths[number]
? TargetTable
: never
: TargetTable;
// ============================================================================
// Helper Functions for Accessing FMTable Internal Properties
// ============================================================================
/**
* Get the table name from an FMTable instance.
* @param table - FMTable instance
* @returns The table name
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableName<T extends FMTable<any, any>>(table: T): string {
return table[FMTableName];
}
/**
* Get the entity ID (FMTID) from an FMTable instance.
* @param table - FMTable instance
* @returns The entity ID or undefined if not using entity IDs
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableEntityId<T extends FMTable<any, any>>(table: T): string | undefined {
return table[FMTableEntityId];
}
/**
* Get the schema validator from an FMTable instance.
* @param table - FMTable instance
* @returns The StandardSchemaV1 validator record (partial - only fields with validators)
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableSchema<T extends FMTable<any, any>>(
table: T,
): Partial<Record<keyof T[typeof FMTableFields], StandardSchemaV1>> {
return table[FMTableSchema];
}
/**
* Get the fields from an FMTable instance.
* @param table - FMTable instance
* @returns The fields record
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableFields<T extends FMTable<any, any>>(table: T) {
return table[FMTableFields];
}
/**
* Get the navigation paths from an FMTable instance.
* @param table - FMTable instance
* @returns Array of navigation path names
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getNavigationPaths<T extends FMTable<any, any>>(table: T): readonly string[] {
return table[FMTableNavigationPaths];
}
/**
* Get the default select configuration from an FMTable instance.
* @param table - FMTable instance
* @returns Default select configuration
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getDefaultSelect<T extends FMTable<any, any>>(table: T) {
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
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getBaseTableConfig<T extends FMTable<any, any>>(table: T) {
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
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function isUsingEntityIds<T extends FMTable<any, any>>(table: T): boolean {
return table[FMTableEntityId] !== undefined && table[FMTableBaseTableConfig].fmfIds !== undefined;
}
/**
* 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
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getFieldId<T extends FMTable<any, any>>(table: T, fieldName: string): string {
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
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getFieldName<T extends FMTable<any, any>>(table: T, fieldId: string): string {
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
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableId<T extends FMTable<any, any>>(table: T): string {
return table[FMTableEntityId] ?? table[FMTableName];
}
/**
* Get the comment from an FMTable instance.
* @param table - FMTable instance
* @returns The comment string or undefined if not set
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableComment<T extends FMTable<any, any>>(table: T): string | undefined {
return table[FMTableComment];
}
/**
* 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
*/
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export function getTableColumns<T extends FMTable<any, any>>(
table: T,
): ColumnMap<T[typeof FMTableFields], ExtractTableName<T>> {
const fields = table[FMTableFields];
const tableName = table[FMTableName];
const tableEntityId = table[FMTableEntityId];
const baseConfig = table[FMTableBaseTableConfig];
const columns = {} as ColumnMap<T[typeof FMTableFields], ExtractTableName<T>>;
for (const [fieldName, builder] of Object.entries(fields)) {
// biome-ignore lint/suspicious/noExplicitAny: Internal property access for builder pattern
const config = (builder as any)._getConfig();
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(columns as any)[fieldName] = new Column({
fieldName: String(fieldName),
entityId: baseConfig.fmfIds?.[fieldName],
tableName,
tableEntityId,
inputValidator: config.inputValidator,
fieldType: config.fieldType,
});
}
return columns;
}