zitejs 0.10.0-start.5 → 0.10.0-start.6
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.
- package/dist/esm/start/describe.d.ts +8 -1
- package/dist/esm/start/describe.js +28 -1
- package/dist/esm/start/index.d.ts +2 -2
- package/dist/esm/start/index.js +1 -1
- package/dist/esm/start/registration.d.ts +13 -0
- package/dist/esm/start/registration.js +21 -0
- package/dist/esm/start/server.js +20 -2
- package/dist/esm/start/types.d.ts +1 -1
- package/dist/esm/start/workflow.d.ts +16 -2
- package/dist/esm/start/workflow.test-d.js +16 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ZiteTrigger } from "../backend/index.js";
|
|
1
2
|
import type { LoadedWorkflow } from "./workflowRun.js";
|
|
2
3
|
import type { WorkflowConfig } from "./workflow.js";
|
|
3
4
|
export type Access = {
|
|
@@ -26,7 +27,13 @@ export type DescribedWorkflow = {
|
|
|
26
27
|
file: string;
|
|
27
28
|
description?: string;
|
|
28
29
|
schedule?: WorkflowConfig["schedule"];
|
|
29
|
-
trigger
|
|
30
|
+
/** As declared, without a provider trigger's `subscribe` and `unsubscribe`. */
|
|
31
|
+
trigger?: ZiteTrigger;
|
|
32
|
+
/** A provider trigger's: whether it can be taken down, and a hash of both functions' code, so publish registers it again when they change. */
|
|
33
|
+
registration?: {
|
|
34
|
+
unsubscribe: boolean;
|
|
35
|
+
hash: string;
|
|
36
|
+
};
|
|
30
37
|
problems: string[];
|
|
31
38
|
};
|
|
32
39
|
export type Description = {
|
|
@@ -123,6 +123,19 @@ export function describeRoutes(routesById) {
|
|
|
123
123
|
}
|
|
124
124
|
return out;
|
|
125
125
|
}
|
|
126
|
+
// cyrb53: enough to tell when the code changed, which is all publish asks.
|
|
127
|
+
const sourceHash = (text) => {
|
|
128
|
+
let h1 = 0xdeadbeef;
|
|
129
|
+
let h2 = 0x41c6ce57;
|
|
130
|
+
for (let i = 0; i < text.length; i++) {
|
|
131
|
+
const ch = text.charCodeAt(i);
|
|
132
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
133
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
134
|
+
}
|
|
135
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
136
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
137
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
|
|
138
|
+
};
|
|
126
139
|
// A workflow's id is its export name: what the host invokes it by and the editor shows.
|
|
127
140
|
export function describeWorkflows(workflows) {
|
|
128
141
|
return workflows.map(({ id, file, workflow }) => {
|
|
@@ -133,7 +146,21 @@ export function describeWorkflows(workflows) {
|
|
|
133
146
|
if (elsewhere)
|
|
134
147
|
problems.push(`duplicate-workflow: ${id} is also exported from ${elsewhere.file}`);
|
|
135
148
|
const { description, schedule, trigger } = workflow.config;
|
|
136
|
-
|
|
149
|
+
if (schedule)
|
|
150
|
+
return { id, file, description, schedule, problems };
|
|
151
|
+
if (!trigger || trigger.every !== undefined || trigger.events !== undefined) {
|
|
152
|
+
return { id, file, description, trigger, problems };
|
|
153
|
+
}
|
|
154
|
+
const { subscribe, unsubscribe, ...declared } = trigger;
|
|
155
|
+
if (typeof subscribe !== "function") {
|
|
156
|
+
problems.push("workflow-trigger-unregistered: a provider trigger needs `subscribe` to register it with the provider");
|
|
157
|
+
return { id, file, description, trigger: declared, problems };
|
|
158
|
+
}
|
|
159
|
+
const registration = {
|
|
160
|
+
unsubscribe: typeof unsubscribe === "function",
|
|
161
|
+
hash: sourceHash(`${String(subscribe)}\n${typeof unsubscribe === "function" ? String(unsubscribe) : ""}`),
|
|
162
|
+
};
|
|
163
|
+
return { id, file, description, trigger: declared, registration, problems };
|
|
137
164
|
});
|
|
138
165
|
}
|
|
139
166
|
export async function describeApp(opts) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { User } from "./types.js";
|
|
2
2
|
export type { User, ZiteRequestContext } from "./types.js";
|
|
3
3
|
export { createZiteRouter, type ZiteRouterContext } from "./router.js";
|
|
4
|
-
export { createWorkflow, type Workflow, type WorkflowArgs, type WorkflowConfig } from "./workflow.js";
|
|
4
|
+
export { createWorkflow, type StartProviderTrigger, type StartTrigger, type Workflow, type WorkflowArgs, type WorkflowConfig, } from "./workflow.js";
|
|
5
5
|
export type { ZiteSchedule } from "../schedules/index.js";
|
|
6
|
-
export type { ZiteTrigger } from "../backend/index.js";
|
|
6
|
+
export type { ZiteTrigger, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput, } from "../backend/index.js";
|
|
7
7
|
declare module "@tanstack/start-client-core" {
|
|
8
8
|
interface ServerFnOptions {
|
|
9
9
|
/** What this function does, in a sentence. Shown in the editor. */
|
package/dist/esm/start/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { USAGE_TOKEN_QUERY_PARAM } from "../auth/constants.js";
|
|
|
4
4
|
import { currentHost } from "./host.js";
|
|
5
5
|
import { getHost, inScope, runInScope } from "./scope.js";
|
|
6
6
|
export { createZiteRouter } from "./router.js";
|
|
7
|
-
export { createWorkflow } from "./workflow.js";
|
|
7
|
+
export { createWorkflow, } from "./workflow.js";
|
|
8
8
|
const STATUS = {
|
|
9
9
|
BAD_REQUEST: 400,
|
|
10
10
|
INVALID_INPUT: 400,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LoadedWorkflow } from "./workflowRun.js";
|
|
2
|
+
export type RegistrationResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
output: unknown;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
error: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
/** Undefined when the workflow has no such function. */
|
|
13
|
+
export declare function runRegistration(workflows: LoadedWorkflow[], name: string, op: unknown, input: unknown): Promise<RegistrationResult | undefined>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Server-only: a provider trigger's `subscribe` or `unsubscribe`, which the host
|
|
2
|
+
// runs at publish. Not a run: nothing is logged, since what `subscribe` returns
|
|
3
|
+
// carries the provider's signing secret.
|
|
4
|
+
import { errorOf } from "./runLog.js";
|
|
5
|
+
import { runInScope } from "./scope.js";
|
|
6
|
+
/** Undefined when the workflow has no such function. */
|
|
7
|
+
export async function runRegistration(workflows, name, op, input) {
|
|
8
|
+
const trigger = workflows.find((w) => w.id === name)?.workflow.config.trigger;
|
|
9
|
+
if (!trigger || !("subscribe" in trigger))
|
|
10
|
+
return undefined;
|
|
11
|
+
const fn = op === "subscribe" ? trigger.subscribe : op === "unsubscribe" ? trigger.unsubscribe : undefined;
|
|
12
|
+
if (typeof fn !== "function")
|
|
13
|
+
return undefined;
|
|
14
|
+
try {
|
|
15
|
+
const output = await runInScope({ kind: "workflow", user: null }, () => fn(input));
|
|
16
|
+
return { ok: true, output: output ?? null };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return { ok: false, error: errorOf(error) };
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/esm/start/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ZiteError, ZITE_ERROR_STATUS } from "./index.js";
|
|
2
2
|
import { describeApp } from "./describe.js";
|
|
3
|
+
import { runRegistration } from "./registration.js";
|
|
3
4
|
import { preview } from "./runLog.js";
|
|
4
5
|
import { getHost, requireScope, runWithHost } from "./scope.js";
|
|
5
6
|
import { loadWorkflows, loggedInput, runWorkflow } from "./workflowRun.js";
|
|
@@ -52,7 +53,7 @@ const watchOutboundFetches = () => {
|
|
|
52
53
|
};
|
|
53
54
|
};
|
|
54
55
|
const decodeUser = (value) => JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0))));
|
|
55
|
-
const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "warm"]);
|
|
56
|
+
const INVOKE_KINDS = new Set(["webhook", "describe", "workflow", "register", "warm"]);
|
|
56
57
|
/** What the host dispatched this request with. Headers are ignored: props are the only thing it alone can set. */
|
|
57
58
|
export function hostContextFromProps(request, props) {
|
|
58
59
|
return {
|
|
@@ -129,6 +130,19 @@ async function invokeWorkflow(request, host, workflows) {
|
|
|
129
130
|
});
|
|
130
131
|
return Response.json(result, { status: result.ok ? 200 : 500 });
|
|
131
132
|
}
|
|
133
|
+
// The host sends `{ op, input }`: `subscribe` with the platform's notification
|
|
134
|
+
// URL and secret, or `unsubscribe` with what `subscribe` returned.
|
|
135
|
+
async function invokeRegistration(request, host, workflows) {
|
|
136
|
+
if (request.method !== "POST" || new URL(request.url).pathname !== INVOKE_PATH)
|
|
137
|
+
return new Response("Not found", { status: 404 });
|
|
138
|
+
const name = host.invoke?.name ?? "";
|
|
139
|
+
const body = (await request.json().catch(() => null));
|
|
140
|
+
const result = await runRegistration(workflows, name, body?.op, body?.input);
|
|
141
|
+
if (!result) {
|
|
142
|
+
return Response.json({ ok: false, error: { code: "NOT_FOUND", message: `${name} has no ${String(body?.op)}` } }, { status: 404 });
|
|
143
|
+
}
|
|
144
|
+
return Response.json(result, { status: result.ok ? 200 : 500 });
|
|
145
|
+
}
|
|
132
146
|
const isZiteError = (error) => error instanceof Error && error.name === "ZiteError" && error.code in ZITE_ERROR_STATUS;
|
|
133
147
|
// TanStack renders any loader error as a 500. A page refused with a ZiteError
|
|
134
148
|
// (a missing role, a record that isn't there) answers with that error's status.
|
|
@@ -167,7 +181,11 @@ export function createZiteHandlers(options) {
|
|
|
167
181
|
workflows: await loadedWorkflows(),
|
|
168
182
|
}));
|
|
169
183
|
}
|
|
170
|
-
let response = await runWithHost(host, async () => host.invoke?.kind === "workflow"
|
|
184
|
+
let response = await runWithHost(host, async () => host.invoke?.kind === "workflow"
|
|
185
|
+
? invokeWorkflow(request, host, await loadedWorkflows())
|
|
186
|
+
: host.invoke?.kind === "register"
|
|
187
|
+
? invokeRegistration(request, host, await loadedWorkflows())
|
|
188
|
+
: options.handler.fetch(request));
|
|
171
189
|
const status = response.status === 500 ? pageStatus(host) : undefined;
|
|
172
190
|
if (status)
|
|
173
191
|
response = new Response(response.body, { status, headers: response.headers });
|
|
@@ -65,7 +65,7 @@ export type PlatformBinding = {
|
|
|
65
65
|
};
|
|
66
66
|
export type HostMode = "live" | "preview" | "validate";
|
|
67
67
|
export type HostInvoke = {
|
|
68
|
-
kind: "webhook" | "describe" | "workflow" | "warm";
|
|
68
|
+
kind: "webhook" | "describe" | "workflow" | "register" | "warm";
|
|
69
69
|
name: string;
|
|
70
70
|
};
|
|
71
71
|
/** The per-request props the host dispatches with. Only the host can set them. */
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import type { ZiteTrigger, ZiteTriggerContext } from "../backend/index.js";
|
|
1
|
+
import type { ZiteTrigger, ZiteTriggerContext, ZiteTriggerSubscribe, ZiteTriggerUnsubscribe } from "../backend/index.js";
|
|
2
2
|
import type { ZiteSchedule } from "../schedules/index.js";
|
|
3
3
|
declare const WORKFLOW: unique symbol;
|
|
4
|
+
type ProviderTriggerBase = Extract<ZiteTrigger, {
|
|
5
|
+
every?: never;
|
|
6
|
+
events?: never;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* A provider trigger, registered with the provider by its `subscribe` when the
|
|
10
|
+
* app is published and taken down by its `unsubscribe` when it no longer is.
|
|
11
|
+
* Both run on the platform with only the trigger's own connection.
|
|
12
|
+
*/
|
|
13
|
+
export type StartProviderTrigger = ProviderTriggerBase & {
|
|
14
|
+
subscribe: ZiteTriggerSubscribe;
|
|
15
|
+
unsubscribe?: ZiteTriggerUnsubscribe;
|
|
16
|
+
};
|
|
17
|
+
export type StartTrigger = Exclude<ZiteTrigger, ProviderTriggerBase> | StartProviderTrigger;
|
|
4
18
|
export type WorkflowConfig = {
|
|
5
19
|
/** What this workflow does, in a sentence. Shown in the editor. */
|
|
6
20
|
description?: string;
|
|
@@ -8,7 +22,7 @@ export type WorkflowConfig = {
|
|
|
8
22
|
schedule: ZiteSchedule;
|
|
9
23
|
trigger?: never;
|
|
10
24
|
} | {
|
|
11
|
-
trigger:
|
|
25
|
+
trigger: StartTrigger;
|
|
12
26
|
schedule?: never;
|
|
13
27
|
});
|
|
14
28
|
/** What a workflow's handler gets. A trigger adds `trigger`, typed by the kind it declares. */
|
|
@@ -13,10 +13,25 @@ describe("createWorkflow handler args", () => {
|
|
|
13
13
|
createWorkflow({ trigger: { integration: "slack", events: ["message.channels"] } }).handler(({ trigger }) => {
|
|
14
14
|
expectTypeOf(trigger).toEqualTypeOf();
|
|
15
15
|
});
|
|
16
|
-
createWorkflow({
|
|
16
|
+
createWorkflow({
|
|
17
|
+
trigger: { integration: "asana", subscribe: async () => ({ externalId: "w1" }), unsubscribe: async () => { } },
|
|
18
|
+
}).handler(({ trigger }) => {
|
|
17
19
|
expectTypeOf(trigger).toEqualTypeOf();
|
|
18
20
|
});
|
|
19
21
|
});
|
|
22
|
+
it("registers a provider trigger with subscribe", () => {
|
|
23
|
+
// @ts-expect-error nothing would register it with the provider
|
|
24
|
+
createWorkflow({ trigger: { integration: "asana" } });
|
|
25
|
+
createWorkflow({
|
|
26
|
+
trigger: {
|
|
27
|
+
integration: "github",
|
|
28
|
+
subscribe: (input) => {
|
|
29
|
+
expectTypeOf(input.notificationUrl).toEqualTypeOf();
|
|
30
|
+
return { externalId: "1", secret: input.secret };
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
20
35
|
it("takes a schedule or a trigger, not both", () => {
|
|
21
36
|
// @ts-expect-error a workflow runs on one or the other
|
|
22
37
|
createWorkflow({ schedule: { scheduleType: "oneTime", fireAt: "x", timezone: "UTC" }, trigger: { every: "5m" } });
|