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

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/redrofigt/wp-graph-directory-sync/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

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