UNPKG

@proofkit/fmodata

Version:

FileMaker OData API client

399 lines (398 loc) 16.9 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Effect } from "effect"; import { requestFromService, runLayerResult } from "../effect.js"; import { BuilderInvariantError } from "../errors.js"; import { getTableName, getNavigationPaths, isUsingEntityIds } from "../orm/table.js"; import { ExpandBuilder } from "./builders/expand-builder.js"; import { buildRecordPath } from "./builders/mutation-helpers.js"; import { buildSelectExpandQueryString } from "./builders/query-string-builder.js"; import { createInitialRecordReadBuilderState, cloneRecordReadBuilderState } from "./builders/read-builder-state.js"; import { processRecordResponse } from "./builders/response-processor.js"; import { processSelectWithRenames } from "./builders/select-mixin.js"; import { mergeExecuteOptions, resolveTableId, createODataRequest } from "./builders/table-utils.js"; import { parseErrorResponse } from "./error-parser.js"; import { QueryBuilder } from "./query/query-builder.js"; import { createClientRuntime } from "./runtime.js"; import { safeJsonParse } from "./sanitize-json.js"; class RecordBuilder { constructor(config) { __publicField(this, "table"); __publicField(this, "recordLocator"); __publicField(this, "operation"); __publicField(this, "operationParam"); // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration __publicField(this, "operationColumn"); __publicField(this, "isNavigateFromEntitySet"); __publicField(this, "navigateRelation"); __publicField(this, "navigateRelationEntityId"); __publicField(this, "navigateSourceTableName"); __publicField(this, "navigateSourceTableEntityId"); __publicField(this, "readState", createInitialRecordReadBuilderState()); __publicField(this, "layer"); __publicField(this, "config"); __publicField(this, "logger"); 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; } // Compatibility accessors for internal modules that inspect builder internals via `as any`. 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 }); } /** * Helper to merge database-level useEntityIds and includeSpecialColumns with per-request options */ mergeExecuteOptions(options) { const merged = mergeExecuteOptions(options, this.config.useEntityIds); return { ...merged, includeSpecialColumns: (options == null ? void 0 : 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; } // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration 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; } // biome-ignore lint/suspicious/noExplicitAny: Implementation signature hidden from callers select(fields, systemColumns) { if (fields === "all") { return this.cloneWithChanges({ selectedFields: void 0, fieldMapping: void 0, // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter systemColumns: void 0 }); } const tableName = getTableName(this.table); const { selectedFields, fieldMapping } = processSelectWithRenames(fields, tableName, this.logger); const finalSelectedFields = [...selectedFields]; if (systemColumns == null ? void 0 : systemColumns.ROWID) { finalSelectedFields.push("ROWID"); } if (systemColumns == null ? void 0 : systemColumns.ROWMODID) { finalSelectedFields.push("ROWMODID"); } return this.cloneWithChanges({ selectedFields: finalSelectedFields, fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : void 0, // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter systemColumns // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type }); } /** * 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 expandBuilder = new ExpandBuilder(this.config.useEntityIds, this.logger); const expandConfig = expandBuilder.processExpand( targetTable, this.table ?? void 0, callback, () => ( // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration new QueryBuilder({ occurrence: targetTable, layer: this.layer }) ) ); mutableBuilder.readState = cloneRecordReadBuilderState(mutableBuilder.readState, { expandConfigs: [...this.expandConfigs, expandConfig] }); return newBuilder; } // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration 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) { 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 }); } buildRecordResourcePath(useEntityIds) { if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) { const sourceSegment = useEntityIds ? this.navigateSourceTableEntityId ?? this.navigateSourceTableName : this.navigateSourceTableName; const relationSegment = useEntityIds ? this.navigateRelationEntityId ?? this.navigateRelation : this.navigateRelation; return `/${buildRecordPath(`${sourceSegment}/${relationSegment}`, this.recordLocator)}`; } const tableId = this.getTableId(useEntityIds); return `/${buildRecordPath(tableId, this.recordLocator)}`; } execute(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") { const fieldResponse = response; return fieldResponse.value; } const result2 = yield* Effect.tryPromise({ try: () => processRecordResponse(response, { table: this.table, selectedFields: this.selectedFields, expandConfigs: this.expandConfigs, skipValidation: options == null ? void 0 : 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 (result2.error) { return yield* Effect.fail(result2.error); } return result2.data; }); const result = runLayerResult(this.layer, pipeline, "fmodata.record.get", { "fmodata.table": getTableName(this.table) }); return result; } // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value 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 == null ? void 0 : 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}`; } const queryString = this.buildQueryString(void 0, useEntityIds); return `${path}${queryString}`; } toRequest(baseUrl, options) { const config = this.getRequestConfig(); return createODataRequest(baseUrl, config, { ...options, normalizeDatabaseName: (options == null ? void 0 : options.normalizeDatabaseName) ?? this.config.normalizeDatabaseName }); } async processResponse(response, options) { if (!response.ok) { const tableName = this.table ? getTableName(this.table) : "unknown"; const error = await parseErrorResponse(response, response.url || `/${this.config.databaseName}/${tableName}`); return { data: void 0, error }; } const rawResponse = await safeJsonParse(response); if (this.operation === "getSingleField") { const fieldResponse = rawResponse; return { data: fieldResponse.value, error: void 0 }; } const mergedOptions = this.mergeExecuteOptions(options); return processRecordResponse(rawResponse, { table: this.table, selectedFields: this.selectedFields, expandConfigs: this.expandConfigs, skipValidation: options == null ? void 0 : options.skipValidation, useEntityIds: mergedOptions.useEntityIds, includeSpecialColumns: mergedOptions.includeSpecialColumns, fieldMapping: this.fieldMapping, logger: this.logger }); } } export { RecordBuilder }; //# sourceMappingURL=record-builder.js.map