Skip to content

Commit 5824811

Browse files
committed
Refactor webhook management and enhance typegen functionality
- Updated client exports in the typegen output to improve naming consistency. - Enhanced tests for dynamic scanning to include new properties like title and description. - Added webhook management capabilities, including listing, adding, getting, and deleting webhooks with type-safe API. - Improved response handling in capture-responses script and updated mock responses for webhook operations. - Refactored coverage configuration to include relevant files and exclude test files for better reporting.
1 parent 63619c7 commit 5824811

File tree

22 files changed

+1344
-23
lines changed

22 files changed

+1344
-23
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { client as testLayoutClient } from "./testLayout";
2-
export { client as weirdPortalsClient } from "./weirdPortals";
1+
export { client as testLayoutLayout } from "./testLayout";
2+
export { client as weirdPortalsLayout } from "./weirdPortals";

apps/docs/tests/utils.manifest.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ describe("Registry utils (dynamic scanning)", () => {
1212
// Should find the mode-toggle template
1313
expect(index.length).toBeGreaterThan(0);
1414
expect(index[0]).toHaveProperty("name");
15-
expect(index[0]).toHaveProperty("type");
1615
expect(index[0]).toHaveProperty("category");
17-
// RegistryIndexItem only has name, type, and category - not files
16+
expect(index[0]).toHaveProperty("title");
17+
expect(index[0]).toHaveProperty("description");
18+
// RegistryIndexItem has name, category, title, description - not type or files
1819
});
1920

2021
it("reads a known template (mode-toggle)", async () => {

packages/cli/vitest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ export default defineConfig({
1111
environment: "node",
1212
setupFiles: ["./tests/setup.ts"],
1313
include: ["tests/**/*.test.ts"],
14+
testTimeout: 60000, // 60 seconds for CLI tests which can be slow
1415
coverage: {
1516
provider: "v8",
1617
reporter: ["text", "json", "html"],
18+
include: ["src/**/*.ts"],
1719
},
1820
},
1921
});

packages/fmodata/README.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,173 @@ console.log(result.result.recordId);
778778

779779
**Note:** OData doesn't support script names with special characters (e.g., `@`, `&`, `/`) or script names beginning with a number. TypeScript will catch these at compile time.
780780

781+
## Webhooks
782+
783+
Webhooks allow you to receive notifications when data changes in your FileMaker database. The library provides a type-safe API for managing webhooks through the `db.webhook` property.
784+
785+
### Adding a Webhook
786+
787+
Create a new webhook to monitor a table for changes:
788+
789+
```typescript
790+
// Basic webhook
791+
const result = await db.webhook.add({
792+
webhook: "https://example.com/webhook",
793+
tableName: contactsTable,
794+
});
795+
796+
// Access the created webhook ID
797+
console.log(result.webHookResult.webHookID);
798+
```
799+
800+
### Webhook Configuration Options
801+
802+
Webhooks support various configuration options:
803+
804+
```typescript
805+
// With custom headers
806+
const result = await db.webhook.add({
807+
webhook: "https://example.com/webhook",
808+
tableName: contactsTable,
809+
headers: {
810+
"X-Custom-Header": "value",
811+
"Authorization": "Bearer token",
812+
},
813+
notifySchemaChanges: true, // Notify when schema changes
814+
});
815+
816+
// With field selection (using column references)
817+
const result = await db.webhook.add({
818+
webhook: "https://example.com/webhook",
819+
tableName: contacts,
820+
select: [contacts.name, contacts.email, contacts.PrimaryKey],
821+
});
822+
823+
// With filtering (using filter expressions)
824+
import { eq, gt } from "@proofkit/fmodata";
825+
826+
const result = await db.webhook.add({
827+
webhook: "https://example.com/webhook",
828+
tableName: contacts,
829+
filter: eq(contacts.active, true),
830+
select: [contacts.name, contacts.email],
831+
});
832+
833+
// Complex filter example
834+
const result = await db.webhook.add({
835+
webhook: "https://example.com/webhook",
836+
tableName: users,
837+
filter: and(eq(users.active, true), gt(users.age, 18)),
838+
select: [users.username, users.email],
839+
});
840+
```
841+
842+
**Webhook Configuration Properties:**
843+
844+
- `webhook` (required) - The URL to call when the webhook is triggered
845+
- `tableName` (required) - The `FMTable` instance for the table to monitor
846+
- `headers` (optional) - Custom headers to include in webhook requests
847+
- `notifySchemaChanges` (optional) - Whether to notify on schema changes
848+
- `select` (optional) - Field selection as a string or array of `Column` references
849+
- `filter` (optional) - Filter expression (string or `FilterExpression`) to limit which records trigger the webhook
850+
851+
### Listing Webhooks
852+
853+
Get all webhooks configured for the database:
854+
855+
```typescript
856+
const result = await db.webhook.list();
857+
858+
console.log(result.Status); // Status of the operation
859+
console.log(result.WebHook); // Array of webhook configurations
860+
861+
result.WebHook.forEach((webhook) => {
862+
console.log(`Webhook ${webhook.webHookID}:`);
863+
console.log(` Table: ${webhook.tableName}`);
864+
console.log(` URL: ${webhook.url}`);
865+
console.log(` Notify Schema Changes: ${webhook.notifySchemaChanges}`);
866+
console.log(` Select: ${webhook.select}`);
867+
console.log(` Filter: ${webhook.filter}`);
868+
console.log(` Pending Operations: ${webhook.pendingOperations.length}`);
869+
});
870+
```
871+
872+
### Getting a Webhook
873+
874+
Retrieve a specific webhook by ID:
875+
876+
```typescript
877+
const webhook = await db.webhook.get(1);
878+
879+
console.log(webhook.webHookID);
880+
console.log(webhook.tableName);
881+
console.log(webhook.url);
882+
console.log(webhook.headers);
883+
console.log(webhook.notifySchemaChanges);
884+
console.log(webhook.select);
885+
console.log(webhook.filter);
886+
console.log(webhook.pendingOperations);
887+
```
888+
889+
### Removing a Webhook
890+
891+
Delete a webhook by ID:
892+
893+
```typescript
894+
await db.webhook.remove(1);
895+
```
896+
897+
### Invoking a Webhook
898+
899+
Manually trigger a webhook. This is useful for testing or triggering webhooks on-demand:
900+
901+
```typescript
902+
// Invoke for all rows matching the webhook's filter
903+
await db.webhook.invoke(1);
904+
905+
// Invoke for specific row IDs
906+
await db.webhook.invoke(1, { rowIDs: [63, 61] });
907+
```
908+
909+
### Complete Example
910+
911+
Here's a complete example of setting up and managing webhooks:
912+
913+
```typescript
914+
import { eq } from "@proofkit/fmodata";
915+
916+
// Add a webhook to monitor active contacts
917+
const addResult = await db.webhook.add({
918+
webhook: "https://api.example.com/webhooks/contacts",
919+
tableName: contacts,
920+
headers: {
921+
"X-API-Key": "your-api-key",
922+
},
923+
filter: eq(contacts.active, true),
924+
select: [contacts.name, contacts.email, contacts.PrimaryKey],
925+
notifySchemaChanges: false,
926+
});
927+
928+
const webhookId = addResult.webHookResult.webHookID;
929+
console.log(`Created webhook with ID: ${webhookId}`);
930+
931+
// List all webhooks
932+
const listResult = await db.webhook.list();
933+
console.log(`Total webhooks: ${listResult.WebHook.length}`);
934+
935+
// Get the webhook we just created
936+
const webhook = await db.webhook.get(webhookId);
937+
console.log(`Webhook URL: ${webhook.url}`);
938+
939+
// Manually invoke the webhook for specific records
940+
await db.webhook.invoke(webhookId, { rowIDs: [1, 2, 3] });
941+
942+
// Remove the webhook when done
943+
await db.webhook.remove(webhookId);
944+
```
945+
946+
**Note:** Webhooks are triggered automatically by FileMaker when records matching the webhook's filter are created, updated, or deleted. The `invoke()` method allows you to manually trigger webhooks for testing or on-demand processing.
947+
781948
## Batch Operations
782949

783950
Batch operations allow you to execute multiple queries and operations together in a single request. All operations in a batch are executed atomically - they all succeed or all fail together. This is both more efficient (fewer network round-trips) and ensures data consistency across related operations.

packages/fmodata/scripts/capture-responses.ts

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import path from "path";
3535
import { fileURLToPath } from "url";
3636
import { config } from "dotenv";
3737
import { writeFileSync } from "fs";
38+
import * as prettier from "prettier";
3839
import createClient from "@fetchkit/ffetch";
3940
import { MOCK_SERVER_URL } from "../tests/utils/mock-server-url";
4041

@@ -406,6 +407,122 @@ const queriesToCapture: {
406407
return { url, response };
407408
},
408409
},
410+
// Webhook API queries
411+
{
412+
name: "webhook-list",
413+
description: "List all webhooks",
414+
execute: async (client) => {
415+
const path = "/Webhook.GetAll";
416+
const response = await client(path);
417+
const url = response.url;
418+
return { url, response };
419+
},
420+
},
421+
{
422+
name: "webhook-add",
423+
description: "Add a new webhook",
424+
execute: async (client) => {
425+
const path = "/Webhook.Add";
426+
const response = await client(path, {
427+
method: "POST",
428+
body: {
429+
webhook: "https://example.com/webhook",
430+
tableName: "contacts",
431+
headers: {
432+
"X-Custom-Header": "test-value",
433+
},
434+
notifySchemaChanges: false,
435+
select: "",
436+
filter: "",
437+
},
438+
});
439+
const url = response.url;
440+
441+
// Clone the response before extracting the data
442+
const cloned = response.clone();
443+
const newWebhookId = (await cloned.json()).webHookResult.webHookID;
444+
await client(`/Webhook.Delete(${newWebhookId})`);
445+
446+
return { url, response };
447+
},
448+
},
449+
{
450+
name: "webhook-add-with-options",
451+
description: "Add a new webhook",
452+
execute: async (client) => {
453+
const path = "/Webhook.Add";
454+
const response = await client(path, {
455+
method: "POST",
456+
body: {
457+
webhook: "https://example.com/webhook",
458+
tableName: "contacts",
459+
headers: {
460+
"X-Custom-Header": "test-value",
461+
},
462+
notifySchemaChanges: false,
463+
select: "name, age",
464+
filter: "name eq 'John'",
465+
},
466+
});
467+
const url = response.url;
468+
469+
// Clone the response before extracting the data
470+
const cloned = response.clone();
471+
const newWebhookId = (await cloned.json()).webHookResult.webHookID;
472+
await client(`/Webhook.Delete(${newWebhookId})`);
473+
474+
return { url, response };
475+
},
476+
},
477+
{
478+
name: "webhook-get",
479+
description: "Get a webhook by ID",
480+
execute: async (client) => {
481+
const listResponse = await client("/Webhook.GetAll");
482+
const listData = await listResponse.json();
483+
const webhookId = listData.WebHook?.[0]?.webHookID;
484+
if (!webhookId) {
485+
throw new Error("No webhook ID found");
486+
}
487+
488+
// First, try to get webhook ID 1, or use a known ID if available
489+
const path = `/Webhook.Get(${webhookId})`;
490+
const response = await client(path);
491+
const url = response.url;
492+
return { url, response };
493+
},
494+
},
495+
{
496+
name: "webhook-get-not-found",
497+
description: "Error response for non-existent webhook",
498+
expectError: true,
499+
execute: async (client) => {
500+
const path = "/Webhook.Get(99999)";
501+
const response = await client(path);
502+
const url = response.url;
503+
return { url, response };
504+
},
505+
},
506+
{
507+
name: "webhook-delete",
508+
description: "Delete a webhook by ID",
509+
execute: async (client) => {
510+
const listResponse = await client("/Webhook.GetAll");
511+
const listData = await listResponse.json();
512+
const webhookId = listData.WebHook?.[0]?.webHookID;
513+
if (!webhookId) {
514+
throw new Error("No webhook ID found");
515+
}
516+
517+
// Use webhook ID 1, or a known ID if available
518+
const path = `/Webhook.Delete(${webhookId})`;
519+
const response = await client(path, {
520+
method: "POST",
521+
});
522+
const url = response.url;
523+
return { url, response };
524+
},
525+
},
409526
];
410527

411528
/**
@@ -489,7 +606,7 @@ function generateResponsesFile(
489606
* 2. Run: pnpm capture
490607
* 3. The captured response will be added to this file automatically
491608
*
492-
* You can manually edit responses here if you need to modify test data.
609+
* You MUST NOT manually edit this file. Any changes will be overwritten by the capture script.
493610
*/
494611
495612
export type MockResponse = {
@@ -669,7 +786,13 @@ async function main() {
669786
"../tests/fixtures/responses.ts",
670787
);
671788
const fileContent = generateResponsesFile(capturedResponses);
672-
writeFileSync(fixturesPath, fileContent, "utf-8");
789+
790+
// Format the file content with prettier
791+
const formattedContent = await prettier.format(fileContent, {
792+
filepath: fixturesPath,
793+
});
794+
795+
writeFileSync(fixturesPath, formattedContent, "utf-8");
673796

674797
console.log(`\nResponses written to: ${fixturesPath}`);
675798
console.log("\nYou can now use these mocks in your tests!");

0 commit comments

Comments
 (0)