@proofkit/fmodata
Version:
FileMaker OData API client
285 lines (284 loc) • 11.5 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 { 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