ZuloOne
English
Sign in
Menu

Poll a REST API from a job

Updated

A nightly or periodic pull from a shop or an operator. The job is a cron tick. The service names a channel, waits for the body, and upserts. Create a job is the trio; this page is the REST half.

Note

Never HttpClient from a script. Name the channel. The host owns URL, credential, signature, and SSRF — What a script can do, Send an outbound call.

How to#

1. The job is thin. Three files in Jobs/<Name>/, inherit TaskScriptBase, executeSingle: true. Resolve a service, call it, log. There is no Chain of Command on a job. On import, jobs arrive disabled even if isActive: true in the file. A test drives the run with Db.RunJobAsync(jobMetaId) — Create a job.

RecalcAbcClassifications is that shape (nightly ABC). It does not talk HTTP. Jobs that do pull: PullUaBankStatements (IUaBank, channel pb-statements) and SyncFromOneC (IOneCSync, channel onec-odata). ApplyFatooraOutcomes drains the local outbox (IOutboundGateway.TakeCompletedAsync); it is not a REST poll.

2. Pick the door. A pull needs the body now so you can upsert. That is IOutboundCall.SendAsync, not the outbox. IOutboundGateway is for a document you must not lose after posting — Enqueue an outbound call when a document posts. The table: Integrate with a third-party REST API.

Example only — job PullShopItems, channel shop-items-pull, and IShopCatalog are invented; no model ships them today.

public class PullShopItemsTask : TaskScriptBase
{
    public override async Task ExecuteAsync(TaskContext context)
    {
        var n = await ScriptServices.Get<IShopCatalog>().PullAsync();
        context.Log("upserted=" + n);
    }
}

The service names the channel.

var result = await ScriptServices.Get<IOutboundCall>().SendAsync(
    "shop-items-pull", payload: "", idempotencyKey: "shop-items:nightly");
if (!result.Ok) return 0;
// result.Body — parse, then upsert

Both doors take a channel name, not a URL. GET / HEAD go with no body. A per-request path belongs in X-ZuloOne-RelativePath, not in HttpClient. OutboundCallResult.Ok is HTTP 2xx; a transport failure returns Error and does not throw.

3. Upsert by business key. A second tick must not insert a second row. Find, then SaveRecordAsync (insert or update) — not NewRecordAsync every run.

var items = ScriptServices.Get<IDictionaryManager<Item>>();
var existing = await items.GetRecordsAsync("Article = 'SKU-1'", take: 1);
var row = existing.Count > 0 ? existing[0] : await items.NewRecordAsync();
row.Article = "SKU-1";
row.Name = nameFromShop;
await items.SaveRecordAsync(row);

Article is a real field on Item. When the shop row is already a public projection, upsert through IDataEntityManager (QueryAsync / InsertAsync / UpdateAsync by collection name) — Create a data entity. The idempotencyKey on IOutboundCall reaches the signer; it is not an outbox unique index. The upsert is the guard.

4. Channel in your model, bump modelVersion, and POST /api/services/rebuild for a new channel — Send an outbound call.

Warning

A successful call is not a receipt. InDoubt is an outbox status: do not retry that submit. A pull that failed to parse just runs again next tick; the upsert keeps it idempotent.

Cover everything before the send and everything after the response. An integration test cannot cover the HTTP exchange. Drive the job with Db.RunJobAsync; feed the service a sample body for the upsert — Write an integration test.

What's not there yet#

The platform does not accept an inbound webhook. CompletionHandler is reserved and unread. A web service you publish is RPC they invoke, not a callback that completes this pull.

Where next#

Was this page helpful?