@tribe-nest/forge 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +4 -2
  2. package/src/server/jobs.ts +42 -2
package/package.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "@tribe-nest/forge",
3
- "version": "1.13.0",
4
- "publishConfig": { "access": "public" },
3
+ "version": "1.14.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
5
7
  "description": "Forge — the headless React SDK for building custom TribeNest creator sites (the Hydrogen of TribeNest). Exposes the backend (memberships, commerce, gated content, ticketing, courses, booking, auth) as data + behavior primitives.",
6
8
  "exports": {
7
9
  ".": "./src/index.ts",
@@ -25,7 +25,14 @@ export interface JobSchedule {
25
25
  intervalMs: number;
26
26
  }
27
27
 
28
- export type JobHandler = (payload: unknown, env: unknown) => Promise<void> | void;
28
+ /** Context passed to a job handler carries the platform token so the handler
29
+ * can write app-owned data back (writeAppCollection). */
30
+ export interface JobContext {
31
+ /** The app-sync token this run was invoked with (forward it to writes). */
32
+ token: string;
33
+ }
34
+
35
+ export type JobHandler = (payload: unknown, env: unknown, ctx: JobContext) => Promise<void> | void;
29
36
 
30
37
  export interface AppJobsConfig {
31
38
  /** The app id (VITE_APP_ID). */
@@ -95,10 +102,43 @@ export async function handleAppJobRun(
95
102
  const handler = body.job ? jobs[body.job] : undefined;
96
103
  if (!handler) return new Response("unknown job", { status: 404 });
97
104
  try {
98
- await handler(body.payload ?? null, env);
105
+ // Pass the verified token so the handler can write app-owned data back
106
+ // (writeAppCollection) without re-minting anything.
107
+ await handler(body.payload ?? null, env, { token });
99
108
  return new Response("ok");
100
109
  } catch (err) {
101
110
  // Non-2xx → the DO retries (one-time) / re-fires next interval (recurring).
102
111
  return new Response(`job failed: ${(err as Error).message}`, { status: 500 });
103
112
  }
104
113
  }
114
+
115
+ /**
116
+ * Persist APP-OWNED rows into one of the app's collections from a job handler —
117
+ * the sanctioned server-side write path (do NOT invent an endpoint). Authenticated
118
+ * by the run's token (from `JobContext`). `replace: true` = full refresh (archive
119
+ * the existing app-owned rows first), so a re-sync doesn't accumulate duplicates.
120
+ *
121
+ * jobs = {
122
+ * "sync-shopify": async (_p, env, ctx) => {
123
+ * const orders = await fetchShopify(env);
124
+ * await writeAppCollection(ctx, cfg, "shopify-orders",
125
+ * orders.map(o => ({ data: { orderId: o.id, total: o.total, utmSource: o.utm } })),
126
+ * { replace: true });
127
+ * },
128
+ * };
129
+ */
130
+ export async function writeAppCollection(
131
+ ctx: JobContext,
132
+ cfg: AppJobsConfig,
133
+ slug: string,
134
+ entries: Array<{ data: Record<string, unknown>; status?: "draft" | "published" }>,
135
+ opts?: { replace?: boolean },
136
+ ): Promise<{ created: number }> {
137
+ const res = await fetch(`${cfg.apiUrl}/public/apps/${cfg.appId}/collections/${encodeURIComponent(slug)}/entries`, {
138
+ method: "POST",
139
+ headers: { "content-type": "application/json", "x-app-sync-token": ctx.token },
140
+ body: JSON.stringify({ entries, replace: opts?.replace === true }),
141
+ });
142
+ if (!res.ok) throw new Error(`writeAppCollection ${slug} failed: ${res.status} ${await res.text().catch(() => "")}`);
143
+ return res.json();
144
+ }