@proofkit/fmodata
Version:
FileMaker OData API client
399 lines (398 loc) • 16.9 kB
JavaScript
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