UNPKG

@proofkit/fmodata

Version:

FileMaker OData API client

285 lines (284 loc) 11.5 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 { tryEffect, requestFromService, fromValidation, runLayerResult } from "../effect.js"; import { BuilderInvariantError, InvalidLocationHeaderError } from "../errors.js"; import { getBaseTableConfig, getTableName } from "../orm/table.js"; import { transformFieldNamesToIds, transformResponseFields } from "../transform.js"; import { getAcceptHeader } from "../types.js"; import { validateAndTransformInput, validateSingleResponse } from "../validation.js"; import { mergeMutationExecuteOptions, parseRowIdFromLocationHeader, resolveMutationTableId, mergePreferHeaderValues, getLocationHeader } from "./builders/mutation-helpers.js"; import { normalizeDatabasePath } from "./database-name.js"; import { parseErrorResponse } from "./error-parser.js"; import { createClientRuntime } from "./runtime.js"; import { safeJsonParse } from "./sanitize-json.js"; class InsertBuilder { constructor(config) { __publicField(this, "table"); __publicField(this, "data"); __publicField(this, "returnPreference"); __publicField(this, "layer"); __publicField(this, "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. */ // biome-ignore lint/suspicious/noExplicitAny: Dynamic schema shape from table configuration getValidationSchema() { if (!this.table) { return void 0; } 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 == null ? void 0 : responseData._location)) { return yield* Effect.fail( new InvalidLocationHeaderError( "Location header is required when using return=minimal but was not found in response" ) ); } const rowid = this.parseLocationHeader(responseData._location); return { ROWID: rowid }; } 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 ); } // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value 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 == null ? void 0 : options.normalizeDatabaseName) ?? this.config.normalizeDatabaseName })}`; const preferHeader = mergePreferHeaderValues( this.returnPreference === "minimal" ? "return=minimal" : "return=representation", (options == null ? void 0 : options.useEntityIds) ?? this.config.useEntityIds ? "fmodata.entity-ids" : void 0, (options == null ? 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 == null ? void 0 : options.includeODataAnnotations), ...preferHeader ? { Prefer: preferHeader } : {} }, body: config.body }); } 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 }; } if (response.status === 204) { if (this.returnPreference === "minimal") { const locationHeader = getLocationHeader(response.headers); const rowid = locationHeader ? this.parseLocationHeader(locationHeader) : -1; return { data: { ROWID: rowid }, error: void 0 }; } return { // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type data: {}, error: void 0 }; } if (this.returnPreference === "minimal") { const locationHeader = getLocationHeader(response.headers); const rowid = locationHeader ? this.parseLocationHeader(locationHeader) : -1; return { data: { ROWID: rowid }, error: void 0 }; } let rawResponse; try { rawResponse = await safeJsonParse(response); } catch (err) { if (response.status === 204) { return { // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type 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() // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object } }; } let _validatedData = this.data; if (this.table) { const baseTableConfig = getBaseTableConfig(this.table); const inputSchema = baseTableConfig.inputSchema; try { _validatedData = await validateAndTransformInput(this.data, inputSchema); } catch (error) { return { data: void 0, error: error instanceof Error ? error : new BuilderInvariantError("InsertBuilder.processResponse", String(error)) // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type }; } } const shouldUseIds = (options == null ? void 0 : options.useEntityIds) ?? this.config.useEntityIds; const includeSpecialColumns = (options == null ? void 0 : options.includeSpecialColumns) ?? this.config.includeSpecialColumns; let transformedResponse = rawResponse; if (this.table && shouldUseIds) { transformedResponse = transformResponseFields( rawResponse, this.table, void 0 // No expand configs for insert ); } 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, // No selected fields for insert void 0, // No expand configs "exact", // Expect exactly one record 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 }; } } export { InsertBuilder }; //# sourceMappingURL=insert-builder.js.map