@proofkit/fmodata
Version:
FileMaker OData API client
176 lines (163 loc) • 4.72 kB
text/typescript
import { BaseTable } from "./base-table";
// Helper type to extract schema from BaseTable
type ExtractSchema<BT> =
BT extends BaseTable<infer S, any, any, any> ? S : never;
// Helper type to resolve navigation functions to their return types
type ResolveNavigation<T> = {
[K in keyof T]: T[K] extends () => infer R ? R : T[K];
};
// Helper to create a getter-based navigation object
function createNavigationGetters<
Nav extends Record<
string,
| TableOccurrence<any, any, any, any>
| (() => TableOccurrence<any, any, any, any>)
>,
>(navConfig: Nav): ResolveNavigation<Nav> {
const result: any = {};
for (const key in navConfig) {
Object.defineProperty(result, key, {
get() {
const navItem = navConfig[key];
return typeof navItem === "function" ? navItem() : navItem;
},
enumerable: true,
configurable: true,
});
}
return result as ResolveNavigation<Nav>;
}
export class TableOccurrence<
BT extends BaseTable<any, any, any, any> = any,
Name extends string = string,
Nav extends Record<
string,
| TableOccurrence<any, any, any, any>
| (() => TableOccurrence<any, any, any, any>)
> = {},
DefSelect extends
| "all"
| "schema"
| readonly (keyof ExtractSchema<BT>)[] = "schema",
> {
public readonly name: Name;
public readonly baseTable: BT;
protected _navigationConfig: Nav;
public readonly navigation: ResolveNavigation<Nav>;
public readonly defaultSelect: DefSelect;
public readonly fmtId?: `FMTID:${string}`;
constructor(config: {
readonly name: Name;
readonly baseTable: BT;
readonly navigation?: Nav;
readonly defaultSelect?: DefSelect;
readonly fmtId?: `FMTID:${string}`;
}) {
this.name = config.name;
this.baseTable = config.baseTable;
this._navigationConfig = (config.navigation ?? {}) as Nav;
this.defaultSelect = (config.defaultSelect ?? "schema") as DefSelect;
this.fmtId = config.fmtId;
// Create navigation getters that lazily resolve functions
this.navigation = createNavigationGetters(this._navigationConfig);
}
addNavigation<
NewNav extends Record<
string,
| TableOccurrence<any, any, any, any>
| (() => TableOccurrence<any, any, any, any>)
>,
>(nav: NewNav): TableOccurrence<BT, Name, Nav & NewNav, DefSelect> {
return new TableOccurrence({
name: this.name,
baseTable: this.baseTable,
navigation: { ...this._navigationConfig, ...nav } as Nav & NewNav,
defaultSelect: this.defaultSelect,
fmtId: this.fmtId,
});
}
/**
* Returns the FileMaker table occurrence ID (FMTID) if available, or the table name.
* @returns The FMTID string or the table name
*/
getTableId(): string {
return this.fmtId ?? this.name;
}
/**
* Returns the table occurrence name.
* @returns The table name
*/
getTableName(): string {
return this.name;
}
/**
* Returns true if this TableOccurrence is using FileMaker table occurrence IDs.
*/
isUsingTableId(): boolean {
return this.fmtId !== undefined;
}
}
// Helper function to create TableOccurrence with proper type inference
export function createTableOccurrence<
const Name extends string,
BT extends BaseTable<any, any, any, any>,
DefSelect extends
| "all"
| "schema"
| readonly (keyof ExtractSchema<BT>)[] = "schema",
>(config: {
name: Name;
baseTable: BT;
defaultSelect?: DefSelect;
fmtId?: `FMTID:${string}`;
}): TableOccurrence<BT, Name, {}, DefSelect> {
return new TableOccurrence(config);
}
/**
* Creates a TableOccurrence with proper TypeScript type inference.
*
* Use this function instead of `new TableOccurrence()` to ensure
* field names are properly typed.
*
* @example Without entity IDs
* ```ts
* const users = defineTableOccurrence({
* name: "users",
* baseTable: usersBase,
* });
* ```
*
* @example With entity IDs
* ```ts
* const products = defineTableOccurrence({
* name: "products",
* baseTable: productsBase,
* fmtId: "FMTID:12345",
* });
* ```
*/
export function defineTableOccurrence<
const Name extends string,
BT extends BaseTable<any, any, any, any>,
const DefSelect extends
| "all"
| "schema"
| readonly (keyof ExtractSchema<BT>)[] = "schema",
>(config: {
readonly name: Name;
readonly baseTable: BT;
readonly fmtId?: `FMTID:${string}`;
readonly defaultSelect?: DefSelect;
readonly navigation?: Record<
string,
| TableOccurrence<any, any, any, any>
| (() => TableOccurrence<any, any, any, any>)
>;
}): TableOccurrence<BT, Name, {}, DefSelect> {
return new TableOccurrence(config) as TableOccurrence<
BT,
Name,
{},
DefSelect
>;
}