@notionhq/workers 0.1.0 → 0.3.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.
- package/dist/ajv-formats.d.js +0 -0
- package/dist/capabilities/ai_connector.d.ts +97 -0
- package/dist/capabilities/ai_connector.d.ts.map +1 -0
- package/dist/capabilities/ai_connector.js +54 -0
- package/dist/capabilities/ai_connector.test.d.ts +2 -0
- package/dist/capabilities/ai_connector.test.d.ts.map +1 -0
- package/dist/capabilities/automation.d.ts.map +1 -1
- package/dist/capabilities/automation.js +10 -10
- package/dist/capabilities/output.d.ts +5 -0
- package/dist/capabilities/output.d.ts.map +1 -0
- package/dist/capabilities/output.js +10 -0
- package/dist/capabilities/stateful-capability.d.ts +30 -0
- package/dist/capabilities/stateful-capability.d.ts.map +1 -0
- package/dist/capabilities/stateful-capability.js +50 -0
- package/dist/capabilities/stateful-capability.test.d.ts +2 -0
- package/dist/capabilities/stateful-capability.test.d.ts.map +1 -0
- package/dist/capabilities/sync.d.ts +77 -30
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +26 -64
- package/dist/capabilities/tool.d.ts.map +1 -1
- package/dist/capabilities/tool.js +7 -14
- package/dist/capabilities/webhook.d.ts +101 -0
- package/dist/capabilities/webhook.d.ts.map +1 -0
- package/dist/capabilities/webhook.js +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/pacer.d.ts +24 -0
- package/dist/pacer.d.ts.map +1 -0
- package/dist/pacer.js +14 -0
- package/dist/pacer.test.d.ts +2 -0
- package/dist/pacer.test.d.ts.map +1 -0
- package/dist/pacer_internal.d.ts +13 -0
- package/dist/pacer_internal.d.ts.map +1 -0
- package/dist/pacer_internal.js +49 -0
- package/dist/schema.d.ts +28 -18
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +222 -7
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +184 -8
- package/dist/worker.test.d.ts +2 -0
- package/dist/worker.test.d.ts.map +1 -0
- package/package.json +7 -2
- package/src/ajv-formats.d.ts +7 -0
- package/src/capabilities/ai_connector.test.ts +341 -0
- package/src/capabilities/ai_connector.ts +194 -0
- package/src/capabilities/automation.test.ts +2 -2
- package/src/capabilities/automation.ts +10 -6
- package/src/capabilities/output.ts +8 -0
- package/src/capabilities/stateful-capability.test.ts +25 -0
- package/src/capabilities/stateful-capability.ts +87 -0
- package/src/capabilities/sync.test.ts +178 -35
- package/src/capabilities/sync.ts +63 -100
- package/src/capabilities/tool.test.ts +47 -3
- package/src/capabilities/tool.ts +7 -6
- package/src/capabilities/webhook.ts +148 -0
- package/src/index.ts +31 -0
- package/src/pacer.test.ts +110 -0
- package/src/pacer.ts +25 -0
- package/src/pacer_internal.ts +94 -0
- package/src/schema.ts +29 -19
- package/src/types.ts +4 -2
- package/src/worker.test.ts +167 -0
- package/src/worker.ts +335 -10
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { ExecutionError } from "../error.js";
|
|
2
|
+
import type { HandlerOptions } from "../types.js";
|
|
3
|
+
import type { CapabilityContext } from "./context.js";
|
|
4
|
+
import { createCapabilityContext } from "./context.js";
|
|
5
|
+
import { writeOutput } from "./output.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single incoming webhook request.
|
|
9
|
+
*/
|
|
10
|
+
export interface WebhookEvent {
|
|
11
|
+
/**
|
|
12
|
+
* Unique ID for this webhook delivery, stable across retries.
|
|
13
|
+
* Best-effort idempotency key — prefer using the webhook provider's
|
|
14
|
+
* own delivery/event ID when available, since providers may redeliver
|
|
15
|
+
* the same event with a new deliveryId.
|
|
16
|
+
*/
|
|
17
|
+
deliveryId: string;
|
|
18
|
+
/**
|
|
19
|
+
* The parsed JSON body of the incoming request, or an empty object if
|
|
20
|
+
* the body is not valid JSON.
|
|
21
|
+
*/
|
|
22
|
+
body: Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* The raw request body as a string, useful for signature verification.
|
|
25
|
+
*/
|
|
26
|
+
rawBody: string;
|
|
27
|
+
/**
|
|
28
|
+
* The HTTP headers from the incoming request
|
|
29
|
+
*/
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* The HTTP method of the incoming request (e.g. "POST")
|
|
33
|
+
*/
|
|
34
|
+
method: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Throw this error from your webhook execute handler to signal that
|
|
39
|
+
* signature verification failed. After 5 consecutive verification
|
|
40
|
+
* failures, the platform will short-circuit and reject all incoming
|
|
41
|
+
* requests for this webhook without executing the handler.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* worker.webhook("onGithubPush", {
|
|
46
|
+
* title: "GitHub Push",
|
|
47
|
+
* description: "Handles GitHub push events",
|
|
48
|
+
* execute: async (events) => {
|
|
49
|
+
* const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
|
50
|
+
* if (!secret) {
|
|
51
|
+
* throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not set");
|
|
52
|
+
* }
|
|
53
|
+
* for (const event of events) {
|
|
54
|
+
* if (!verifyGitHubSignature(event.rawBody, event.headers, secret)) {
|
|
55
|
+
* throw new WebhookVerificationError("Invalid GitHub signature");
|
|
56
|
+
* }
|
|
57
|
+
* // ... handle verified event
|
|
58
|
+
* }
|
|
59
|
+
* },
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export class WebhookVerificationError extends ExecutionError {
|
|
64
|
+
constructor(cause?: unknown) {
|
|
65
|
+
super(cause ?? "Webhook signature verification failed");
|
|
66
|
+
this.name = "WebhookVerificationError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Configuration for a webhook capability
|
|
72
|
+
*/
|
|
73
|
+
export interface WebhookConfiguration {
|
|
74
|
+
/**
|
|
75
|
+
* Title of the webhook - shown in the UI when viewing webhooks
|
|
76
|
+
*/
|
|
77
|
+
title: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Description of what this webhook does - shown in the UI
|
|
81
|
+
*/
|
|
82
|
+
description: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The function that executes when the webhook is triggered.
|
|
86
|
+
* Receives an array of events (currently always a single event,
|
|
87
|
+
* but may be batched in the future).
|
|
88
|
+
*
|
|
89
|
+
* To signal a verification failure, throw a `WebhookVerificationError`.
|
|
90
|
+
* After 5 consecutive verification failures, the platform will
|
|
91
|
+
* short-circuit and stop executing the handler.
|
|
92
|
+
*
|
|
93
|
+
* @param events - The incoming webhook events
|
|
94
|
+
* @param context - The capability execution context (Notion client, etc.)
|
|
95
|
+
* @returns A promise that resolves when the webhook processing completes
|
|
96
|
+
*/
|
|
97
|
+
execute: (
|
|
98
|
+
events: WebhookEvent[],
|
|
99
|
+
context: CapabilityContext,
|
|
100
|
+
) => Promise<void> | void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type WebhookCapability = ReturnType<typeof createWebhookCapability>;
|
|
104
|
+
|
|
105
|
+
export function createWebhookCapability(
|
|
106
|
+
key: string,
|
|
107
|
+
config: WebhookConfiguration,
|
|
108
|
+
) {
|
|
109
|
+
return {
|
|
110
|
+
_tag: "webhook" as const,
|
|
111
|
+
key,
|
|
112
|
+
config: {
|
|
113
|
+
title: config.title,
|
|
114
|
+
description: config.description,
|
|
115
|
+
},
|
|
116
|
+
async handler(
|
|
117
|
+
events: WebhookEvent[],
|
|
118
|
+
options?: HandlerOptions,
|
|
119
|
+
): Promise<{ status: "success" } | undefined> {
|
|
120
|
+
try {
|
|
121
|
+
const capabilityContext = createCapabilityContext();
|
|
122
|
+
await config.execute(events, capabilityContext);
|
|
123
|
+
|
|
124
|
+
if (options?.concreteOutput) {
|
|
125
|
+
return { status: "success" };
|
|
126
|
+
} else {
|
|
127
|
+
writeOutput({ _tag: "success", value: { status: "success" } });
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const error =
|
|
131
|
+
err instanceof ExecutionError ? err : new ExecutionError(err);
|
|
132
|
+
|
|
133
|
+
if (!options?.concreteOutput) {
|
|
134
|
+
writeOutput({
|
|
135
|
+
_tag: "error",
|
|
136
|
+
error: {
|
|
137
|
+
name: error.name,
|
|
138
|
+
message: error.message,
|
|
139
|
+
trace: error.stack,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
export { emojiIcon, imageIcon, notionIcon, place } from "./builder.js";
|
|
2
|
+
export type {
|
|
3
|
+
AiConnectorArchetype,
|
|
4
|
+
AiConnectorCapability,
|
|
5
|
+
AiConnectorChange,
|
|
6
|
+
AiConnectorChangeUpsert,
|
|
7
|
+
AiConnectorChatAuthor,
|
|
8
|
+
AiConnectorChatChannel,
|
|
9
|
+
AiConnectorChatMessage,
|
|
10
|
+
AiConnectorChatRecord,
|
|
11
|
+
AiConnectorConfiguration,
|
|
12
|
+
AiConnectorExecutionResult,
|
|
13
|
+
AiConnectorMode,
|
|
14
|
+
AiConnectorProjectBlock,
|
|
15
|
+
AiConnectorProjectRecord,
|
|
16
|
+
AiConnectorRecordByArchetype,
|
|
17
|
+
} from "./capabilities/ai_connector.js";
|
|
2
18
|
export type {
|
|
3
19
|
AutomationCapability,
|
|
4
20
|
AutomationConfiguration,
|
|
@@ -22,6 +38,12 @@ export type {
|
|
|
22
38
|
SyncMode,
|
|
23
39
|
} from "./capabilities/sync.js";
|
|
24
40
|
export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
41
|
+
export type {
|
|
42
|
+
WebhookCapability,
|
|
43
|
+
WebhookConfiguration,
|
|
44
|
+
WebhookEvent,
|
|
45
|
+
} from "./capabilities/webhook.js";
|
|
46
|
+
export { WebhookVerificationError } from "./capabilities/webhook.js";
|
|
25
47
|
export type { AnyJSONSchema, JSONSchema } from "./json-schema.js";
|
|
26
48
|
export type { Infer, SchemaBuilder } from "./schema-builder.js";
|
|
27
49
|
export { getSchema, j } from "./schema-builder.js";
|
|
@@ -33,4 +55,13 @@ export type {
|
|
|
33
55
|
PlaceValue,
|
|
34
56
|
Schedule,
|
|
35
57
|
} from "./types.js";
|
|
58
|
+
export type {
|
|
59
|
+
DatabaseConfig,
|
|
60
|
+
DatabaseHandle,
|
|
61
|
+
ManagedDatabaseConfig,
|
|
62
|
+
PacerConfig,
|
|
63
|
+
PacerDeclaration,
|
|
64
|
+
PacerHandle,
|
|
65
|
+
WorkerManifest,
|
|
66
|
+
} from "./worker.js";
|
|
36
67
|
export { Worker } from "./worker.js";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getPacerState, pacerWait, setPacerState } from "./pacer_internal.js";
|
|
4
|
+
|
|
5
|
+
describe("pacerWait", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.useFakeTimers();
|
|
8
|
+
vi.setSystemTime(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("paces requests using server-provided config", async () => {
|
|
16
|
+
setPacerState({
|
|
17
|
+
pacers: {
|
|
18
|
+
api: {
|
|
19
|
+
lastScheduledAtMs: 0,
|
|
20
|
+
allowedRequests: 10,
|
|
21
|
+
intervalMs: 60_000,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const first = pacerWait("api");
|
|
27
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(6000);
|
|
28
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
29
|
+
await first;
|
|
30
|
+
|
|
31
|
+
const second = pacerWait("api");
|
|
32
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(12_000);
|
|
33
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
34
|
+
await second;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not wait when the next slot is already in the past", async () => {
|
|
38
|
+
setPacerState({
|
|
39
|
+
pacers: {
|
|
40
|
+
api: {
|
|
41
|
+
lastScheduledAtMs: 0,
|
|
42
|
+
allowedRequests: 10,
|
|
43
|
+
intervalMs: 60_000,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const first = pacerWait("api");
|
|
49
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
50
|
+
await first;
|
|
51
|
+
|
|
52
|
+
vi.advanceTimersByTime(20_000);
|
|
53
|
+
|
|
54
|
+
const second = pacerWait("api");
|
|
55
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(26_000);
|
|
56
|
+
await second;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("works with apportioned (reduced) budget", async () => {
|
|
60
|
+
// Server apportioned: 5 requests per 10s (original was 10/10s, shared by 2 syncs)
|
|
61
|
+
setPacerState({
|
|
62
|
+
pacers: {
|
|
63
|
+
shared: {
|
|
64
|
+
lastScheduledAtMs: 0,
|
|
65
|
+
allowedRequests: 5,
|
|
66
|
+
intervalMs: 10_000,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Pace should be 10_000 / 5 = 2000ms between requests
|
|
72
|
+
const first = pacerWait("shared");
|
|
73
|
+
expect(getPacerState().pacers.shared?.lastScheduledAtMs).toBe(2000);
|
|
74
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
75
|
+
await first;
|
|
76
|
+
|
|
77
|
+
const second = pacerWait("shared");
|
|
78
|
+
expect(getPacerState().pacers.shared?.lastScheduledAtMs).toBe(4000);
|
|
79
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
80
|
+
await second;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws when pacer key not found in runtime context", async () => {
|
|
84
|
+
setPacerState({ pacers: {} });
|
|
85
|
+
|
|
86
|
+
await expect(pacerWait("unknown")).rejects.toThrow(
|
|
87
|
+
'Pacer "unknown" not found',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("preserves allowedRequests and intervalMs across calls", async () => {
|
|
92
|
+
setPacerState({
|
|
93
|
+
pacers: {
|
|
94
|
+
api: {
|
|
95
|
+
lastScheduledAtMs: 0,
|
|
96
|
+
allowedRequests: 10,
|
|
97
|
+
intervalMs: 60_000,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
103
|
+
await pacerWait("api");
|
|
104
|
+
|
|
105
|
+
// Config should still be present after wait
|
|
106
|
+
const entry = getPacerState().pacers.api;
|
|
107
|
+
expect(entry?.allowedRequests).toBe(10);
|
|
108
|
+
expect(entry?.intervalMs).toBe(60_000);
|
|
109
|
+
});
|
|
110
|
+
});
|
package/src/pacer.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacer module for rate limiting API requests.
|
|
3
|
+
*
|
|
4
|
+
* The preferred API is the handle returned by `worker.pacer()`:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const apiPacer = worker.pacer("myApi", { allowedRequests: 10, intervalMs: 1000 });
|
|
8
|
+
* await apiPacer.wait();
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { pacerWait } from "./pacer_internal.js";
|
|
15
|
+
|
|
16
|
+
export const Pacer = {
|
|
17
|
+
/**
|
|
18
|
+
* Wait until a request can proceed under the named pacer's rate limit.
|
|
19
|
+
*
|
|
20
|
+
* Prefer using the handle-based API instead: `await myPacer.wait()`.
|
|
21
|
+
*
|
|
22
|
+
* @param key - The pacer key, matching the key used in `worker.pacer()`.
|
|
23
|
+
*/
|
|
24
|
+
wait: pacerWait,
|
|
25
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal pacer state helpers used by the runtime.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export type PacerEntry = {
|
|
6
|
+
lastScheduledAtMs: number;
|
|
7
|
+
/** Server-provided (apportioned) request budget. */
|
|
8
|
+
allowedRequests: number;
|
|
9
|
+
/** Server-provided interval window in milliseconds. */
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type PacerState = {
|
|
14
|
+
pacers: Record<string, PacerEntry>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PacerDeclaration = {
|
|
18
|
+
readonly key: string;
|
|
19
|
+
readonly config: { allowedRequests: number; intervalMs: number };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let pacerState: PacerState = { pacers: {} };
|
|
23
|
+
|
|
24
|
+
export function setPacerState(state: PacerState): void {
|
|
25
|
+
pacerState = state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getPacerState(): PacerState {
|
|
29
|
+
return pacerState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize pacer state from runtime context, falling back to the worker's
|
|
34
|
+
* own declarations for local execution.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export function initPacerState(
|
|
38
|
+
runtimePacers: Record<string, PacerEntry> | undefined,
|
|
39
|
+
declarations?: readonly PacerDeclaration[],
|
|
40
|
+
): void {
|
|
41
|
+
const pacers = runtimePacers ?? {};
|
|
42
|
+
if (
|
|
43
|
+
Object.keys(pacers).length === 0 &&
|
|
44
|
+
declarations &&
|
|
45
|
+
declarations.length > 0
|
|
46
|
+
) {
|
|
47
|
+
const localPacers: Record<string, PacerEntry> = {};
|
|
48
|
+
for (const decl of declarations) {
|
|
49
|
+
localPacers[decl.key] = {
|
|
50
|
+
lastScheduledAtMs: 0,
|
|
51
|
+
allowedRequests: decl.config.allowedRequests,
|
|
52
|
+
intervalMs: decl.config.intervalMs,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
setPacerState({ pacers: localPacers });
|
|
56
|
+
} else {
|
|
57
|
+
setPacerState({ pacers });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Core wait logic for pacing API requests.
|
|
63
|
+
* Sleeps if needed to stay under the rate limit.
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export async function pacerWait(key: string): Promise<void> {
|
|
67
|
+
const state = getPacerState();
|
|
68
|
+
const entry = state.pacers[key];
|
|
69
|
+
|
|
70
|
+
if (!entry) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Pacer "${key}" not found. Make sure you declared it with worker.pacer("${key}", ...) ` +
|
|
73
|
+
`and are running inside the worker runtime.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { allowedRequests, intervalMs } = entry;
|
|
78
|
+
const paceMs = Math.ceil(intervalMs / allowedRequests);
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const lastScheduledAtMs = entry.lastScheduledAtMs;
|
|
81
|
+
|
|
82
|
+
// Schedule at the later of: (last scheduled + pace) or now
|
|
83
|
+
const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
|
|
84
|
+
const delayMs = scheduledAtMs - now;
|
|
85
|
+
|
|
86
|
+
// Update state with new scheduled timestamp
|
|
87
|
+
state.pacers[key] = { ...entry, lastScheduledAtMs: scheduledAtMs };
|
|
88
|
+
setPacerState(state);
|
|
89
|
+
|
|
90
|
+
// Sleep if needed
|
|
91
|
+
if (delayMs > 0) {
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -68,21 +68,14 @@ export type PropertyConfiguration =
|
|
|
68
68
|
| {
|
|
69
69
|
type: "relation";
|
|
70
70
|
/**
|
|
71
|
-
* The
|
|
72
|
-
* This must match the
|
|
71
|
+
* The key of the related database declaration.
|
|
72
|
+
* This must match the key passed to `worker.database()`.
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
relatedDatabaseKey: string;
|
|
75
75
|
config: { twoWay: false } | { twoWay: true; relatedPropertyName: string };
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
export type Schema<PK extends string> = {
|
|
79
|
-
/**
|
|
80
|
-
* The default name for the database when it is first created.
|
|
81
|
-
*
|
|
82
|
-
* Updating this after the database has been created will not change the
|
|
83
|
-
* name of the database.
|
|
84
|
-
*/
|
|
85
|
-
defaultName: string;
|
|
86
79
|
/**
|
|
87
80
|
* Optional icon to use as the icon for the database page.
|
|
88
81
|
* If not provided, defaults to 📋.
|
|
@@ -210,32 +203,49 @@ export function place(): PropertyConfiguration {
|
|
|
210
203
|
}
|
|
211
204
|
|
|
212
205
|
/**
|
|
213
|
-
* Creates a relation property definition that references another
|
|
214
|
-
* The related
|
|
206
|
+
* Creates a relation property definition that references another database.
|
|
207
|
+
* The related database must be declared in the same worker.
|
|
215
208
|
*
|
|
216
|
-
* @param
|
|
209
|
+
* @param relatedDatabaseKey - The key of the related database declaration.
|
|
217
210
|
* @example
|
|
218
211
|
* ```typescript
|
|
219
|
-
*
|
|
212
|
+
* const projects = worker.database("projects", {
|
|
213
|
+
* type: "managed",
|
|
214
|
+
* initialTitle: "Projects",
|
|
215
|
+
* primaryKeyProperty: "Project ID",
|
|
216
|
+
* schema: {
|
|
217
|
+
* properties: {
|
|
218
|
+
* "Project Name": Schema.title(),
|
|
219
|
+
* "Project ID": Schema.richText(),
|
|
220
|
+
* },
|
|
221
|
+
* },
|
|
222
|
+
* });
|
|
220
223
|
*
|
|
221
|
-
*
|
|
224
|
+
* const tasks = worker.database("tasks", {
|
|
225
|
+
* type: "managed",
|
|
226
|
+
* initialTitle: "Tasks",
|
|
227
|
+
* primaryKeyProperty: "Task ID",
|
|
222
228
|
* schema: {
|
|
223
229
|
* properties: {
|
|
224
230
|
* "Task Title": Schema.title(),
|
|
225
|
-
* "Project": Schema.relation("
|
|
231
|
+
* "Project": Schema.relation("projects"),
|
|
226
232
|
* },
|
|
227
233
|
* },
|
|
228
|
-
*
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* export const tasksSync = worker.sync("tasksSync", {
|
|
237
|
+
* database: tasks,
|
|
238
|
+
* execute: async () => ({ changes: [], hasMore: false }),
|
|
229
239
|
* });
|
|
230
240
|
* ```
|
|
231
241
|
*/
|
|
232
242
|
export function relation(
|
|
233
|
-
|
|
243
|
+
relatedDatabaseKey: string,
|
|
234
244
|
config?: { twoWay: false } | { twoWay: true; relatedPropertyName: string },
|
|
235
245
|
): PropertyConfiguration {
|
|
236
246
|
return {
|
|
237
247
|
type: "relation",
|
|
238
|
-
|
|
248
|
+
relatedDatabaseKey,
|
|
239
249
|
config: config ?? { twoWay: false },
|
|
240
250
|
};
|
|
241
251
|
}
|
package/src/types.ts
CHANGED
|
@@ -236,7 +236,7 @@ export type RelationReference = {
|
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
238
|
* Relation value representing references to related records.
|
|
239
|
-
* Each reference identifies a record in the target
|
|
239
|
+
* Each reference identifies a record in the target database.
|
|
240
240
|
*/
|
|
241
241
|
export type RelationValue = RelationReference[];
|
|
242
242
|
|
|
@@ -256,15 +256,17 @@ export type IntervalString = `${number}${TimeUnit}`;
|
|
|
256
256
|
/**
|
|
257
257
|
* Schedule configuration for sync capabilities.
|
|
258
258
|
* - "continuous": Run as frequently as the system allows
|
|
259
|
+
* - "manual": Only run when explicitly triggered
|
|
259
260
|
* - IntervalString: Run at specified intervals, e.g. "30m", "1h", "1d"
|
|
260
261
|
*/
|
|
261
|
-
export type Schedule = "continuous" | IntervalString;
|
|
262
|
+
export type Schedule = "continuous" | "manual" | IntervalString;
|
|
262
263
|
|
|
263
264
|
/**
|
|
264
265
|
* Normalized schedule representation stored in the backend.
|
|
265
266
|
*/
|
|
266
267
|
export type SyncSchedule =
|
|
267
268
|
| { type: "continuous" }
|
|
269
|
+
| { type: "manual" }
|
|
268
270
|
| { type: "interval"; intervalMs: number };
|
|
269
271
|
|
|
270
272
|
export type HandlerOptions = {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import * as Schema from "./schema.js";
|
|
4
|
+
import { Worker } from "./worker.js";
|
|
5
|
+
|
|
6
|
+
describe("Worker", () => {
|
|
7
|
+
it("includes sdkVersion, databases, and capabilities in manifest", () => {
|
|
8
|
+
const worker = new Worker();
|
|
9
|
+
const tasks = worker.database("tasks", {
|
|
10
|
+
type: "managed",
|
|
11
|
+
initialTitle: "Tasks",
|
|
12
|
+
primaryKeyProperty: "Name",
|
|
13
|
+
schema: {
|
|
14
|
+
properties: {
|
|
15
|
+
Name: Schema.title(),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
worker.sync("tasksSync", {
|
|
21
|
+
database: tasks,
|
|
22
|
+
execute: async () => ({
|
|
23
|
+
changes: [],
|
|
24
|
+
hasMore: false,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(worker.manifest.sdkVersion).toMatch(/^\d+\.\d+\.\d+/);
|
|
29
|
+
expect(worker.manifest.databases).toEqual([
|
|
30
|
+
{
|
|
31
|
+
key: "tasks",
|
|
32
|
+
config: {
|
|
33
|
+
type: "managed",
|
|
34
|
+
initialTitle: "Tasks",
|
|
35
|
+
primaryKeyProperty: "Name",
|
|
36
|
+
schema: {
|
|
37
|
+
properties: {
|
|
38
|
+
Name: { type: "title" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
expect(worker.manifest.pacers).toEqual([]);
|
|
45
|
+
expect(worker.manifest.capabilities).toEqual([
|
|
46
|
+
{
|
|
47
|
+
_tag: "sync",
|
|
48
|
+
key: "tasksSync",
|
|
49
|
+
config: {
|
|
50
|
+
databaseKey: "tasks",
|
|
51
|
+
primaryKeyProperty: "Name",
|
|
52
|
+
mode: undefined,
|
|
53
|
+
schedule: undefined,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("serializes manual schedules in the manifest", () => {
|
|
60
|
+
const worker = new Worker();
|
|
61
|
+
const tasks = worker.database("tasks", {
|
|
62
|
+
type: "managed",
|
|
63
|
+
initialTitle: "Tasks",
|
|
64
|
+
primaryKeyProperty: "Name",
|
|
65
|
+
schema: {
|
|
66
|
+
properties: {
|
|
67
|
+
Name: Schema.title(),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
worker.sync("tasksBackfill", {
|
|
73
|
+
database: tasks,
|
|
74
|
+
schedule: "manual",
|
|
75
|
+
execute: async () => ({
|
|
76
|
+
changes: [],
|
|
77
|
+
hasMore: false,
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(worker.manifest.capabilities).toEqual([
|
|
82
|
+
{
|
|
83
|
+
_tag: "sync",
|
|
84
|
+
key: "tasksBackfill",
|
|
85
|
+
config: {
|
|
86
|
+
databaseKey: "tasks",
|
|
87
|
+
primaryKeyProperty: "Name",
|
|
88
|
+
mode: undefined,
|
|
89
|
+
schedule: {
|
|
90
|
+
type: "manual",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("includes pacers in manifest and links to sync capabilities", () => {
|
|
98
|
+
const worker = new Worker();
|
|
99
|
+
const tasks = worker.database("tasks", {
|
|
100
|
+
type: "managed",
|
|
101
|
+
initialTitle: "Tasks",
|
|
102
|
+
primaryKeyProperty: "Name",
|
|
103
|
+
schema: {
|
|
104
|
+
properties: {
|
|
105
|
+
Name: Schema.title(),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const _jiraPacer = worker.pacer("jira", {
|
|
111
|
+
allowedRequests: 10,
|
|
112
|
+
intervalMs: 1000,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
worker.sync("jiraBackfill", {
|
|
116
|
+
database: tasks,
|
|
117
|
+
schedule: "manual",
|
|
118
|
+
execute: async () => ({
|
|
119
|
+
changes: [],
|
|
120
|
+
hasMore: false,
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
worker.sync("jiraDelta", {
|
|
125
|
+
database: tasks,
|
|
126
|
+
schedule: "5m",
|
|
127
|
+
execute: async () => ({
|
|
128
|
+
changes: [],
|
|
129
|
+
hasMore: false,
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(worker.manifest.pacers).toEqual([
|
|
134
|
+
{
|
|
135
|
+
key: "jira",
|
|
136
|
+
config: {
|
|
137
|
+
allowedRequests: 10,
|
|
138
|
+
intervalMs: 1000,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects duplicate keys across databases, pacers, and capabilities", () => {
|
|
145
|
+
const worker = new Worker();
|
|
146
|
+
const db = worker.database("shared-key", {
|
|
147
|
+
type: "managed",
|
|
148
|
+
initialTitle: "Tasks",
|
|
149
|
+
primaryKeyProperty: "Name",
|
|
150
|
+
schema: {
|
|
151
|
+
properties: {
|
|
152
|
+
Name: Schema.title(),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(() =>
|
|
158
|
+
worker.sync("shared-key", {
|
|
159
|
+
database: db,
|
|
160
|
+
execute: async () => ({
|
|
161
|
+
changes: [],
|
|
162
|
+
hasMore: false,
|
|
163
|
+
}),
|
|
164
|
+
}),
|
|
165
|
+
).toThrow('Worker item with key "shared-key" already registered');
|
|
166
|
+
});
|
|
167
|
+
});
|