UNPKG

@proofkit/fmodata

Version:

FileMaker OData API client

258 lines (234 loc) 8.6 kB
import { Effect } from "effect"; import { requestFromService, runLayerOrThrow } from "../effect"; import { type FMTable, getTableName } from "../orm"; import { type Column, isColumn } from "../orm/column"; import { FilterExpression } from "../orm/operators"; import type { FMODataLayer, ODataConfig } from "../services"; import type { ExecuteMethodOptions } from "../types"; import { formatSelectFields } from "./builders/select-utils"; import { createClientRuntime } from "./runtime"; export interface Webhook<TableName = string> { webhook: string; headers?: Record<string, string>; tableName: TableName; notifySchemaChanges?: boolean; // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration select?: string | Column<any, any, any>[]; filter?: string | FilterExpression; } /** * Webhook information returned by the API */ export interface WebhookInfo { webhookID: number; tableName: string; webhook: string; headers?: Record<string, string>; notifySchemaChanges: boolean; select: string; filter: string; pendingOperations: unknown[]; } /** * Response from listing all webhooks */ export interface WebhookListResponse { status: string; webhooks: WebhookInfo[]; } /** * Response from adding a webhook */ export interface WebhookAddResponse { webhookResult: { webhookID: number; }; } export class WebhookManager { private readonly layer: FMODataLayer; private readonly config: ODataConfig; constructor(layer: FMODataLayer) { const runtime = createClientRuntime(layer); this.layer = runtime.layer; this.config = runtime.config; } /** * Adds a new webhook to the database. * @param webhook - The webhook configuration object * @param webhook.webhook - The webhook URL to call * @param webhook.tableName - The FMTable instance for the table to monitor * @param webhook.headers - Optional custom headers to include in webhook requests * @param webhook.notifySchemaChanges - Whether to notify on schema changes * @param webhook.select - Optional field selection (string or array of Column references) * @param webhook.filter - Optional filter (string or FilterExpression) * @returns Promise resolving to the created webhook data with ID * @example * ```ts * const result = await db.webhook.add({ * webhook: "https://example.com/webhook", * tableName: contactsTable, * headers: { "X-Custom-Header": "value" }, * }); * // result.webhookResult.webhookID contains the new webhook ID * ``` * @example * ```ts * // Using filter expressions and column arrays (same DX as query builder) * const result = await db.webhook.add({ * webhook: "https://example.com/webhook", * tableName: contacts, * filter: eq(contacts.name, "John"), * select: [contacts.name, contacts.PrimaryKey], * }); * ``` */ add(webhook: Webhook<FMTable>, options?: ExecuteMethodOptions): Promise<WebhookAddResponse> { // Extract the string table name from the FMTable instance const tableName = getTableName(webhook.tableName); // Get useEntityIds setting (check options first, then config, default to false) const useEntityIds = options?.useEntityIds ?? this.config.useEntityIds ?? false; // Transform filter if it's a FilterExpression let filter: string | undefined; if (webhook.filter !== undefined) { if (webhook.filter instanceof FilterExpression) { filter = webhook.filter.toODataFilter(useEntityIds); } else { filter = webhook.filter; } } // Transform select if it's an array of Columns let select: string | undefined; if (webhook.select !== undefined) { if (Array.isArray(webhook.select)) { // Extract field identifiers from columns or use strings as-is const fieldNames = webhook.select.map((item) => { if (isColumn(item)) { return item.getFieldIdentifier(useEntityIds); } return String(item); }); // Use formatSelectFields to properly format the select string select = formatSelectFields(fieldNames, webhook.tableName, useEntityIds); } else { // Already a string, use as-is select = webhook.select; } } // Create request body with string table name and transformed filter/select const requestBody: { webhook: string; headers?: Record<string, string>; tableName: string; notifySchemaChanges?: boolean; select?: string; filter?: string; } = { webhook: webhook.webhook, tableName, }; if (webhook.headers !== undefined) { requestBody.headers = webhook.headers; } if (webhook.notifySchemaChanges !== undefined) { requestBody.notifySchemaChanges = webhook.notifySchemaChanges; } if (select !== undefined) { requestBody.select = select; } if (filter !== undefined) { requestBody.filter = filter; } const pipeline = Effect.gen(this, function* () { return yield* requestFromService<WebhookAddResponse>(`/${this.config.databaseName}/Webhook.Add`, { ...options, method: "POST", body: JSON.stringify(requestBody), databaseNameNormalizationMode: "ensureExtension", }); }); return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.add"); } /** * Deletes a webhook by ID. * @param webhookId - The ID of the webhook to delete * @returns Promise that resolves when the webhook is deleted * @example * ```ts * await db.webhook.remove(1); * ``` */ async remove(webhookId: number, options?: ExecuteMethodOptions): Promise<void> { const pipeline = Effect.gen(this, function* () { return yield* requestFromService(`/${this.config.databaseName}/Webhook.Delete(${webhookId})`, { ...options, method: "POST", databaseNameNormalizationMode: "ensureExtension", }); }); await runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.remove"); } /** * Gets a webhook by ID. * @param webhookId - The ID of the webhook to retrieve * @returns Promise resolving to the webhook data * @example * ```ts * const webhook = await db.webhook.get(1); * // webhook.webhookID, webhook.tableName, webhook.webhook, etc. * ``` */ get(webhookId: number, options?: ExecuteMethodOptions): Promise<WebhookInfo> { const pipeline = Effect.gen(this, function* () { return yield* requestFromService<WebhookInfo>(`/${this.config.databaseName}/Webhook.Get(${webhookId})`, options); }); return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.get"); } /** * Lists all webhooks. * @returns Promise resolving to webhook list response with status and webhooks array * @example * ```ts * const result = await db.webhook.list(); * // result.status contains the status * // result.webhooks contains the array of webhooks * ``` */ list(options?: ExecuteMethodOptions): Promise<WebhookListResponse> { const pipeline = Effect.gen(this, function* () { return yield* requestFromService<WebhookListResponse>(`/${this.config.databaseName}/Webhook.GetAll`, { ...options, databaseNameNormalizationMode: "ensureExtension", }); }); return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.list"); } /** * Invokes a webhook by ID, optionally for specific row IDs. * @param webhookId - The ID of the webhook to invoke * @param options - Optional configuration * @param options.rowIDs - Array of row IDs to trigger the webhook for * @returns Promise resolving to the invocation result (type unknown until API behavior is confirmed) * @example * ```ts * // Invoke for all rows * await db.webhook.invoke(1); * * // Invoke for specific rows * await db.webhook.invoke(1, { rowIDs: [63, 61] }); * ``` */ invoke(webhookId: number, options?: { rowIDs?: number[] }, executeOptions?: ExecuteMethodOptions): Promise<unknown> { const body: { rowIDs?: number[] } = {}; if (options?.rowIDs !== undefined) { body.rowIDs = options.rowIDs; } const pipeline = Effect.gen(this, function* () { return yield* requestFromService<unknown>(`/${this.config.databaseName}/Webhook.Invoke(${webhookId})`, { method: "POST", body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined, ...executeOptions, }); }); return runLayerOrThrow(this.layer, pipeline, "fmodata.webhook.invoke"); } }