Skip to content

Commit

Permalink
feat: wip extract entity or submission data and pass to webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
spwoodcock committed Jan 22, 2025
1 parent 1d25c99 commit 2ca586d
Show file tree
Hide file tree
Showing 12 changed files with 875 additions and 138 deletions.
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Call a remote API on ODK Central database events:

## Usage

The `odkhook` tool is a service that runs continually, monitoring the
ODK Central database for updates and triggering the webhook as appropriate.

### Binary

Download the binary for your platform from the
Expand All @@ -17,24 +20,24 @@ Then run with:
```bash
./odkhook \
-db 'postgresql://{user}:{password}@{hostname}/{db}?sslmode=disable' \
-webhook 'https://your.domain.com/some/webhook'
-entityUrl 'https://your.domain.com/some/webhook' \
-submissionUrl 'https://your.domain.com/some/webhook'
```

> [!TIP]
> By default both Entity editing and new submissions trigger the webhook.
>
> Use the -trigger flag to modify this behaviour.
> It's possible to specify a webhook for only Entities or Submissions, or both.
### Docker

```bash
docker run -d ghcr.io/hotosm/odk-webhook:latest \
-db 'postgresql://{user}:{password}@{hostname}/{db}?sslmode=disable' \
-webhook 'https://your.domain.com/some/webhook'
-entityUrl 'https://your.domain.com/some/webhook' \
-submissionUrl 'https://your.domain.com/some/webhook'
```

> [!NOTE]
> Alternatively, add to your docker compose stack.
> Alternatively, add the service to your docker compose stack.
### Code

Expand Down Expand Up @@ -64,13 +67,14 @@ err = SetupWebhook(
log,
ctx,
dbPool,
"https://your.domain.com/some/webhook",
map[string]bool{
"entity.update.version": true,
"submission.create": true,
},
"https://your.domain.com/some/entity/webhook",
"https://your.domain.com/some/submission/webhook",
)
if err != nil {
fmt.Fprintf(os.Stderr, "error setting up webhook: %v", err)
}
```

> [!NOTE]
> To not provide a webhook for either entities or submissions,
> pass `nil` instead.
11 changes: 10 additions & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ services:
- ./go.mod:/app/go.mod:ro
- ./go.sum:/app/go.sum:ro
- ./main.go:/app/main.go:ro
- ./main_test.go:/app/main_test.go:ro
- ./db:/app/db:ro
- ./webhook:/app/webhook:ro
- ./parser:/app/parser:ro
# environment:
# # Override to use database on host
# ODK_WEBHOOK_DB_URI: postgresql://odk:[email protected]:5434/odk?sslmode=disable
# ODK_WEBHOOK_WEBHOOK_URL:
depends_on:
db:
condition: service_healthy
networks:
- net
# This allows usage of services running directly on the host machine
extra_hosts:
- host.docker.internal:host-gateway
restart: "no"
entrypoint: go test -v ./...
entrypoint: go test -v .

db:
image: "postgis/postgis:17-3.5-alpine"
Expand Down
16 changes: 10 additions & 6 deletions db/notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@ import (
"github.com/matryer/is"
)

// NB: these tests assume you have a postgres server listening on localhost:5432
// with username postgres and password postgres. You can trivially set this up
// with Docker with the following:
// Note: these tests assume you have a postgres server listening on db:5432
// with username odk and password odk.
//
// docker run --rm --name postgres -p 5432:5432 \
// -e POSTGRES_PASSWORD=postgres postgres
// The easiest way to ensure this is to run the tests with docker compose:
// docker compose run --rm odkhook

func TestNotifier(t *testing.T) {
dbUri := "postgresql://odk:odk@db:5432/odkhook?sslmode=disable"
dbUri := os.Getenv("ODK_WEBHOOK_DB_URI")
if len(dbUri) == 0 {
// Default
dbUri = "postgresql://odk:odk@db:5432/odkhook?sslmode=disable"
}

is := is.New(t)
log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
wg := sync.WaitGroup{}

pool, err := InitPool(ctx, log, dbUri)
is.NoErr(err)

Expand Down
60 changes: 50 additions & 10 deletions db/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,68 @@ import (
)

// Example parsed JSON
// {"action":"entity.update.version","actorId":1,"details":{"var1":"test"},"dml_action":"INSERT"}}
// {"action":"entity.update.version","actorId":1,"details":{"entityDefId":1001,...},"dml_action":"INSERT"}}

func CreateTrigger(ctx context.Context, dbPool *pgxpool.Pool, tableName string) error {
// This trigger runs on the `audits` table by default, and creates a new event
// in the odk-events queue when a new event is created in the table

if tableName == "" {
tableName = "audits" // default table
// default table (this is configurable for easier tests mainly)
tableName = "audits"
}

// SQL for creating the function
createFunctionSQL := `
CREATE OR REPLACE FUNCTION new_audit_log() RETURNS trigger AS
$$
DECLARE
js jsonb;
BEGIN
SELECT to_jsonb(NEW.*) INTO js;
js := jsonb_set(js, '{dml_action}', to_jsonb(TG_OP));
PERFORM pg_notify('odk-events', js::text);
RETURN NEW;
END;
DECLARE
js jsonb;
action_type text;
result_data jsonb;
BEGIN
-- Serialize the NEW row into JSONB
SELECT to_jsonb(NEW.*) INTO js;
-- Add the DML action (INSERT/UPDATE)
js := jsonb_set(js, '{dml_action}', to_jsonb(TG_OP));
-- Extract the action type from the NEW row
action_type := NEW.action;
-- Handle different action types with a CASE statement
CASE action_type
WHEN 'entity.update.version' THEN
SELECT entity_defs.data
INTO result_data
FROM entity_defs
WHERE entity_defs.id = (NEW.details->>'entityDefId')::int;
-- Merge the additional data into the original JSON
js := jsonb_set(js, '{data}', result_data, true);
-- Notify the odk-events queue
PERFORM pg_notify('odk-events', js::text);
WHEN 'submission.create' THEN
SELECT jsonb_build_object('xml', submission_defs.xml)
INTO result_data
FROM submission_defs
WHERE submission_defs.id = (NEW.details->>'submissionDefId')::int;
-- Merge the additional data into the original JSON
js := jsonb_set(js, '{data}', result_data, true);
-- Notify the odk-events queue
PERFORM pg_notify('odk-events', js::text);
ELSE
-- Skip pg_notify for unsupported actions & insert as normal
RETURN NEW;
END CASE;
RETURN NEW;
END;
$$ LANGUAGE 'plpgsql';
`

Expand Down
Loading

0 comments on commit 2ca586d

Please sign in to comment.