WP Manifestindependent plugin directory
manifest / integrations / wp-graph-directory-sync

Graph Directory Sync

Scheduled employee directory synchronization for WordPress using Microsoft Graph (users, photos, delta queries) with retry/backoff, failure logging, and a custom read-only table

by redrofigt · github.com/redrofigt/wp-graph-directory-sync · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/redrofigt/wp-graph-directory-sync/archive/refs/heads/main.zip

Readme

Graph Directory Sync for WordPress

Scheduled employee-directory synchronization from Microsoft Graph into a custom read-only WordPress table — with delta queries, pagination, exponential-backoff retries, and structured failure logging. Bonus requirement demonstrated: what happens when the API is unavailable.

Scheduler setup

Two WP-Cron events registered on activation (and triggerable manually):

Event Schedule What it does
gds_delta_sync Hourly /users/delta pass using the stored deltaLink — only changed users cross the wire
gds_full_sync Weekly Fresh sweep with @odata.nextLink pagination + soft-delete reconciliation

On production, WP-Cron is disabled and both events are triggered by a system cron every 5 minutes:

*/5 * * * * /usr/local/bin/wp --path=/var/www/html cron event run --due-now --quiet

The engine is idempotent (upserts keyed on the Entra object ID) so an overlap between runs is harmless.

Sync execution flow

wp-cron / WP-CLI
   │
   ├─ token: client-credentials grant (cached, expires 5 min early)
   │
   ├─ GET /v1/users/delta?$top=100        ◄── starts from stored deltaLink if present
   │     └─ for each page: upsert rows with batch UUID
   │     └─ @removed annotations → soft-delete (account_enabled = 0)
   │
   └─ sweep finished cleanly?
         ├─ yes → store @odata.deltaLink  (atomic resume point)
         └─ no  → deltaLink NOT advanced → next run resumes where it failed

Failure handling (the bonus demo)

Failure modes and their behavior:

Failure Behavior
Network timeout / DNS Retried 4× with jittered exponential backoff (1s→8s ±30%), then logged, pass aborted without advancing the deltaLink
HTTP 429 / 503 Honors Retry-After header, then backoff; logged if exhausted
HTTP 401/403/404 No retry (permanent) — logged with response body, surfaced in wp gds failures
Malformed JSON Logged as gds_bad_json, pass aborted safely
Missing credentials Logged as config_missing, no crash

All failures funnel through a structured logger (JSON entries, 200-line rotation, error_log() mirror) and fire the gds_sync_failed action for Teams/Slack alerting.

Demo recording script (for the screening answer):

  1. wp gds sync --mode=full → show the success output and rows appearing on a frontend directory page (shortcode [gds_directory] or a block reading the table)
  2. Break connectivity (e.g. block graph.microsoft.com at the firewall / invalid tenant ID)
  3. wp gds sync --mode=delta → show the retry/backoff messages, then wp gds failures showing network_error entries with timestamps
  4. Restore connectivity → wp gds sync --mode=delta → success; show that only the failed pass's window needed re-syncing

Schema

CREATE TABLE wp_gds_employees (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  tenant_oid VARCHAR(64) NOT NULL,          -- Entra object ID (stable key)
  display_name VARCHAR(191) NOT NULL,
  given_name VARCHAR(100) NULL,
  surname VARCHAR(100) NULL,
  job_title VARCHAR(191) NULL,
  department VARCHAR(191) NULL,
  office_location VARCHAR(191) NULL,
  mail VARCHAR(191) NULL,
  business_phones TEXT NULL,
  mobile_phone VARCHAR(64) NULL,
  photo_url VARCHAR(255) NULL,              -- CDN/Azure Blob URL after caching
  photo_blob LONGBLOB NULL,                 -- optional local cache
  presentity_type VARCHAR(32) NOT NULL DEFAULT 'employee',
  account_enabled TINYINT(1) NOT NULL DEFAULT 1,  -- soft delete
  graph_created DATETIME NULL,
  last_synced_at DATETIME NOT NULL,
  sync_batch_id VARCHAR(36) NOT NULL,       -- per-batch reconciliation key
  PRIMARY KEY (id),
  UNIQUE KEY tenant_oid (tenant_oid),
  KEY dept_name (department, display_name),
  KEY sync_batch (sync_batch_id),
  KEY enabled_name (account_enabled, display_name)
);

WP-CLI

wp gds sync --mode=delta      # run the incremental sync now
wp gds sync --mode=full       # run a full reconciliation sweep
wp gds failures --limit=20    # tail the structured failure log

Security / least privilege

  • App-only token via client credentials; secret injected from environment (Key Vault in production), never stored in the DB
  • Graph application permission required: User.Read.All (nothing broader)
  • The custom table is read-only by convention: no admin UI writes to it; only the sync engine writes
  • Soft deletes only — no destructive SQL against directory history

Read the full README on GitHub →