@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.
Files changed (67) hide show
  1. package/dist/ajv-formats.d.js +0 -0
  2. package/dist/capabilities/ai_connector.d.ts +97 -0
  3. package/dist/capabilities/ai_connector.d.ts.map +1 -0
  4. package/dist/capabilities/ai_connector.js +54 -0
  5. package/dist/capabilities/ai_connector.test.d.ts +2 -0
  6. package/dist/capabilities/ai_connector.test.d.ts.map +1 -0
  7. package/dist/capabilities/automation.d.ts.map +1 -1
  8. package/dist/capabilities/automation.js +10 -10
  9. package/dist/capabilities/output.d.ts +5 -0
  10. package/dist/capabilities/output.d.ts.map +1 -0
  11. package/dist/capabilities/output.js +10 -0
  12. package/dist/capabilities/stateful-capability.d.ts +30 -0
  13. package/dist/capabilities/stateful-capability.d.ts.map +1 -0
  14. package/dist/capabilities/stateful-capability.js +50 -0
  15. package/dist/capabilities/stateful-capability.test.d.ts +2 -0
  16. package/dist/capabilities/stateful-capability.test.d.ts.map +1 -0
  17. package/dist/capabilities/sync.d.ts +77 -30
  18. package/dist/capabilities/sync.d.ts.map +1 -1
  19. package/dist/capabilities/sync.js +26 -64
  20. package/dist/capabilities/tool.d.ts.map +1 -1
  21. package/dist/capabilities/tool.js +7 -14
  22. package/dist/capabilities/webhook.d.ts +101 -0
  23. package/dist/capabilities/webhook.d.ts.map +1 -0
  24. package/dist/capabilities/webhook.js +47 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/pacer.d.ts +24 -0
  29. package/dist/pacer.d.ts.map +1 -0
  30. package/dist/pacer.js +14 -0
  31. package/dist/pacer.test.d.ts +2 -0
  32. package/dist/pacer.test.d.ts.map +1 -0
  33. package/dist/pacer_internal.d.ts +13 -0
  34. package/dist/pacer_internal.d.ts.map +1 -0
  35. package/dist/pacer_internal.js +49 -0
  36. package/dist/schema.d.ts +28 -18
  37. package/dist/schema.d.ts.map +1 -1
  38. package/dist/schema.js +2 -2
  39. package/dist/types.d.ts +5 -2
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/worker.d.ts +222 -7
  42. package/dist/worker.d.ts.map +1 -1
  43. package/dist/worker.js +184 -8
  44. package/dist/worker.test.d.ts +2 -0
  45. package/dist/worker.test.d.ts.map +1 -0
  46. package/package.json +7 -2
  47. package/src/ajv-formats.d.ts +7 -0
  48. package/src/capabilities/ai_connector.test.ts +341 -0
  49. package/src/capabilities/ai_connector.ts +194 -0
  50. package/src/capabilities/automation.test.ts +2 -2
  51. package/src/capabilities/automation.ts +10 -6
  52. package/src/capabilities/output.ts +8 -0
  53. package/src/capabilities/stateful-capability.test.ts +25 -0
  54. package/src/capabilities/stateful-capability.ts +87 -0
  55. package/src/capabilities/sync.test.ts +178 -35
  56. package/src/capabilities/sync.ts +63 -100
  57. package/src/capabilities/tool.test.ts +47 -3
  58. package/src/capabilities/tool.ts +7 -6
  59. package/src/capabilities/webhook.ts +148 -0
  60. package/src/index.ts +31 -0
  61. package/src/pacer.test.ts +110 -0
  62. package/src/pacer.ts +25 -0
  63. package/src/pacer_internal.ts +94 -0
  64. package/src/schema.ts +29 -19
  65. package/src/types.ts +4 -2
  66. package/src/worker.test.ts +167 -0
  67. package/src/worker.ts +335 -10
package/src/worker.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import type {
2
+ AiConnectorArchetype,
3
+ AiConnectorCapability,
4
+ AiConnectorConfiguration,
5
+ } from "./capabilities/ai_connector.js";
6
+ import { createAiConnectorCapability } from "./capabilities/ai_connector.js";
1
7
  import type {
2
8
  AutomationCapability,
3
9
  AutomationConfiguration,
@@ -16,6 +22,17 @@ import type { SyncCapability, SyncConfiguration } from "./capabilities/sync.js";
16
22
  import { createSyncCapability } from "./capabilities/sync.js";
17
23
  import type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
18
24
  import { createToolCapability } from "./capabilities/tool.js";
25
+ import type {
26
+ WebhookCapability,
27
+ WebhookConfiguration,
28
+ WebhookEvent,
29
+ } from "./capabilities/webhook.js";
30
+ import {
31
+ createWebhookCapability,
32
+ WebhookVerificationError,
33
+ } from "./capabilities/webhook.js";
34
+ import type { PacerDeclaration } from "./pacer_internal.js";
35
+ import { pacerWait } from "./pacer_internal.js";
19
36
  import type { Schema } from "./schema.js";
20
37
  import type { HandlerOptions, JSONValue } from "./types.js";
21
38
 
@@ -23,13 +40,17 @@ import type { HandlerOptions, JSONValue } from "./types.js";
23
40
  export type {
24
41
  AutomationConfiguration,
25
42
  AutomationEvent,
43
+ AiConnectorConfiguration,
26
44
  CapabilityContext,
27
45
  OAuthConfiguration,
28
46
  NotionManagedOAuthConfiguration,
29
47
  UserManagedOAuthConfiguration,
30
48
  SyncConfiguration,
31
49
  ToolConfiguration,
50
+ WebhookConfiguration,
51
+ WebhookEvent,
32
52
  };
53
+ export { WebhookVerificationError };
33
54
 
34
55
  // ============================================================================
35
56
  // Capability types
@@ -37,10 +58,138 @@ export type {
37
58
 
38
59
  type Capability =
39
60
  | SyncCapability
40
- // biome-ignore lint/suspicious/noExplicitAny: any is used to allow any input and output types
61
+ | AiConnectorCapability
62
+ // biome-ignore lint/suspicious/noExplicitAny: tool capabilities are generic over arbitrary input/output payloads.
41
63
  | ToolCapability<any, any>
42
64
  | AutomationCapability
43
- | OAuthCapability;
65
+ | OAuthCapability
66
+ | WebhookCapability;
67
+
68
+ /**
69
+ * Configuration for a database that Notion creates and manages on behalf
70
+ * of the worker. The database is created on first deploy and its schema
71
+ * is migrated on subsequent deploys.
72
+ */
73
+ export type ManagedDatabaseConfig<PK extends string, S extends Schema<PK>> = {
74
+ type: "managed";
75
+ /**
76
+ * The title for the database when it is first created in Notion.
77
+ *
78
+ * Updating this after the database has been created will not change the
79
+ * title of the database — users may rename it freely.
80
+ */
81
+ initialTitle: string;
82
+ /**
83
+ * The property that serves as the primary key for matching records.
84
+ * Must be a property defined in the schema. The value of this property
85
+ * should match the `key` field on each sync change.
86
+ */
87
+ primaryKeyProperty: PK;
88
+ /**
89
+ * The database schema. Notion will migrate the database to match
90
+ * the given schema on worker deploy.
91
+ */
92
+ schema: S;
93
+ };
94
+
95
+ /**
96
+ * Configuration union for all database types.
97
+ * Currently only `"managed"` is supported.
98
+ */
99
+ export type DatabaseConfig<
100
+ PK extends string,
101
+ S extends Schema<PK>,
102
+ > = ManagedDatabaseConfig<PK, S>;
103
+
104
+ /**
105
+ * An opaque handle returned by `worker.database()`. Pass this to
106
+ * `worker.sync()` to associate a sync capability with the database.
107
+ */
108
+ export type DatabaseHandle<PK extends string, S extends Schema<PK>> = {
109
+ readonly key: string;
110
+ readonly config: DatabaseConfig<PK, S>;
111
+ };
112
+
113
+ /** Internal type-erased alias for storing heterogeneous databases. */
114
+ type RegisteredDatabase = DatabaseHandle<string, Schema<string>>;
115
+
116
+ /**
117
+ * Configuration for a pacer that rate-limits requests to an external API.
118
+ * If multiple syncs share a pacer, the server apportions the budget evenly.
119
+ */
120
+ export type PacerConfig = {
121
+ /** Maximum number of requests allowed per interval. */
122
+ allowedRequests: number;
123
+ /** The interval window in milliseconds. */
124
+ intervalMs: number;
125
+ };
126
+
127
+ /**
128
+ * An opaque handle returned by `worker.pacer()`. Call `handle.wait()`
129
+ * before each API request to stay under the rate limit.
130
+ */
131
+ export type PacerHandle = {
132
+ readonly key: string;
133
+ readonly config: PacerConfig;
134
+ /**
135
+ * Wait until a request can proceed under this pacer's rate limit.
136
+ *
137
+ * Call this before **every** API request inside your sync's `execute` function.
138
+ * The pacer ensures requests are evenly spaced over the interval window.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const apiPacer = worker.pacer("myApi", { allowedRequests: 10, intervalMs: 1000 });
143
+ *
144
+ * worker.sync("mySync", {
145
+ * database: myDb,
146
+ * execute: async () => {
147
+ * await apiPacer.wait();
148
+ * const data = await fetchFromApi();
149
+ * // ...
150
+ * },
151
+ * });
152
+ * ```
153
+ */
154
+ wait(): Promise<void>;
155
+ };
156
+
157
+ // Re-exported from pacer_internal.js (the canonical definition).
158
+ export type { PacerDeclaration };
159
+
160
+ /** A capability entry stripped of its handler, used in the manifest. */
161
+ type CapabilityManifestEntry = Pick<Capability, "_tag" | "key" | "config">;
162
+
163
+ /**
164
+ * The full worker manifest, read by the server at deploy time to discover
165
+ * databases, pacers, capabilities, and the SDK version.
166
+ */
167
+ export type WorkerManifest = {
168
+ readonly sdkVersion: string;
169
+ readonly databases: readonly RegisteredDatabase[];
170
+ readonly pacers: readonly PacerDeclaration[];
171
+ readonly capabilities: readonly CapabilityManifestEntry[];
172
+ };
173
+
174
+ // Read SDK version from package.json at module load time.
175
+ // createRequire bridges ESM → CJS so we can require() a JSON file.
176
+ import { createRequire } from "node:module";
177
+
178
+ const SDK_VERSION = (() => {
179
+ const require = createRequire(import.meta.url);
180
+ const packageJson: unknown = require("../package.json");
181
+
182
+ if (
183
+ typeof packageJson !== "object" ||
184
+ packageJson === null ||
185
+ !("version" in packageJson) ||
186
+ typeof packageJson.version !== "string"
187
+ ) {
188
+ throw new Error("Failed to read SDK version from package.json");
189
+ }
190
+
191
+ return packageJson.version;
192
+ })();
44
193
 
45
194
  // ============================================================================
46
195
  // Worker class
@@ -48,6 +197,56 @@ type Capability =
48
197
 
49
198
  export class Worker {
50
199
  #capabilities: Map<string, Capability> = new Map();
200
+ #databases: Map<string, RegisteredDatabase> = new Map();
201
+ #pacers: Map<string, PacerDeclaration> = new Map();
202
+
203
+ database<PK extends string, S extends Schema<PK>>(
204
+ key: string,
205
+ config: DatabaseConfig<PK, S>,
206
+ ): DatabaseHandle<PK, S> {
207
+ this.#validateUniqueKey(key);
208
+ const database: DatabaseHandle<PK, S> = { key, config };
209
+ this.#databases.set(key, { key, config });
210
+ return database;
211
+ }
212
+
213
+ /**
214
+ * Register a pacer for rate-limiting requests to an external API.
215
+ *
216
+ * Multiple syncs can share a pacer — the server will apportion the
217
+ * budget evenly across all syncs that reference it.
218
+ *
219
+ * Example:
220
+ *
221
+ * ```ts
222
+ * const jiraPacer = worker.pacer("jira", {
223
+ * allowedRequests: 10,
224
+ * intervalMs: 1000,
225
+ * });
226
+ *
227
+ * worker.sync("jiraSync", {
228
+ * database: tasks,
229
+ * execute: async (state, { notion }) => {
230
+ * await jiraPacer.wait();
231
+ * const data = await fetchFromJira();
232
+ * // ...
233
+ * },
234
+ * });
235
+ * ```
236
+ *
237
+ * @param key - The unique key for this pacer.
238
+ * @param config - The rate limit configuration.
239
+ * @returns A pacer handle. Call `handle.wait()` before each API request.
240
+ */
241
+ pacer(key: string, config: PacerConfig): PacerHandle {
242
+ this.#validateUniqueKey(key);
243
+ this.#pacers.set(key, { key, config });
244
+ return {
245
+ key,
246
+ config,
247
+ wait: () => pacerWait(key),
248
+ };
249
+ }
51
250
 
52
251
  /**
53
252
  * Register a sync capability.
@@ -62,26 +261,29 @@ export class Worker {
62
261
  * const worker = new Worker();
63
262
  * export default worker;
64
263
  *
65
- * worker.sync("tasksSync", {
66
- * primaryKeyProperty: "Task ID",
264
+ * const tasks = worker.database("tasks", {
265
+ * type: "managed",
266
+ * initialTitle: "Tasks",
67
267
  * schema: {
68
- * defaultName: "Tasks",
69
268
  * properties: {
70
269
  * "Task Name": Schema.title(),
71
- * "Task ID": Schema.richText(),
72
270
  * Status: Schema.select([
73
271
  * { name: "Open", color: "default" },
74
272
  * { name: "Done", color: "green" },
75
273
  * ]),
76
274
  * },
77
275
  * },
276
+ * });
277
+ *
278
+ * worker.sync("tasksSync", {
279
+ * database: tasks,
78
280
  * execute: async () => {
79
281
  * const changes = [
80
282
  * {
81
283
  * key: "task-1",
284
+ * type: "upsert",
82
285
  * properties: {
83
286
  * "Task Name": Builder.title("Write docs"),
84
- * "Task ID": Builder.richText("task-1"),
85
287
  * Status: Builder.select("Open"),
86
288
  * },
87
289
  * },
@@ -101,7 +303,78 @@ export class Worker {
101
303
  config: SyncConfiguration<PK, S, Context>,
102
304
  ): SyncCapability {
103
305
  this.#validateUniqueKey(key);
104
- const capability = createSyncCapability(key, config);
306
+ const capability = createSyncCapability(
307
+ key,
308
+ config,
309
+ Array.from(this.#pacers.values()),
310
+ ) as SyncCapability;
311
+ this.#capabilities.set(key, capability);
312
+ return capability;
313
+ }
314
+
315
+ /**
316
+ * Register an AI connector capability.
317
+ *
318
+ * Example:
319
+ *
320
+ * ```ts
321
+ * const worker = new Worker();
322
+ * export default worker;
323
+ *
324
+ * worker.aiConnector("supportChats", {
325
+ * aiConnectorId: "connector-record-id",
326
+ * archetype: "chat",
327
+ * mode: "incremental",
328
+ * schedule: "15m",
329
+ * execute: async (state) => ({
330
+ * changes: [
331
+ * {
332
+ * type: "upsert",
333
+ * key: "thread-1",
334
+ * record: {
335
+ * createdAt: "2026-01-01T00:00:00Z",
336
+ * updatedAt: "2026-01-01T00:00:00Z",
337
+ * channel: {
338
+ * id: "channel-1",
339
+ * name: "Support",
340
+ * },
341
+ * threadId: "thread-1",
342
+ * messages: [
343
+ * {
344
+ * id: "message-1",
345
+ * content: "Hello from support",
346
+ * timestamp: "2026-01-01T00:00:00Z",
347
+ * author: {
348
+ * id: "user-1",
349
+ * username: "alice",
350
+ * },
351
+ * },
352
+ * ],
353
+ * },
354
+ * },
355
+ * ],
356
+ * hasMore: false,
357
+ * }),
358
+ * });
359
+ * ```
360
+ *
361
+ * `aiConnectorId` must be the Notion record ID of an existing custom
362
+ * connector in the workspace where you deploy this worker.
363
+ *
364
+ * @param key - The unique key for this capability.
365
+ * @param config - The AI connector configuration.
366
+ * @returns The capability object.
367
+ */
368
+ aiConnector<A extends AiConnectorArchetype, Context = unknown>(
369
+ key: string,
370
+ config: AiConnectorConfiguration<A, Context>,
371
+ ): AiConnectorCapability {
372
+ this.#validateUniqueKey(key);
373
+ const capability = createAiConnectorCapability(
374
+ key,
375
+ config,
376
+ Array.from(this.#pacers.values()),
377
+ ) as AiConnectorCapability;
105
378
  this.#capabilities.set(key, capability);
106
379
  return capability;
107
380
  }
@@ -181,6 +454,41 @@ export class Worker {
181
454
  return capability;
182
455
  }
183
456
 
457
+ /**
458
+ * Register a webhook capability.
459
+ *
460
+ * Webhooks expose an HTTP endpoint that external services can call.
461
+ * When hit, the request is processed by the worker's execute function.
462
+ *
463
+ * Example:
464
+ *
465
+ * ```ts
466
+ * const worker = new Worker();
467
+ * export default worker;
468
+ *
469
+ * worker.webhook("onGithubPush", {
470
+ * title: "GitHub Push Webhook",
471
+ * description: "Handles push events from GitHub",
472
+ * execute: async (events, { notion }) => {
473
+ * for (const event of events) {
474
+ * console.log("Received webhook:", event.body);
475
+ * console.log("Headers:", event.headers);
476
+ * }
477
+ * },
478
+ * })
479
+ * ```
480
+ *
481
+ * @param key - The unique key for this capability. Used as the webhook name in the URL.
482
+ * @param config - The webhook configuration.
483
+ * @returns The capability object.
484
+ */
485
+ webhook(key: string, config: WebhookConfiguration): WebhookCapability {
486
+ this.#validateUniqueKey(key);
487
+ const capability = createWebhookCapability(key, config);
488
+ this.#capabilities.set(key, capability);
489
+ return capability;
490
+ }
491
+
184
492
  /**
185
493
  * Register an OAuth capability.
186
494
  *
@@ -240,6 +548,15 @@ export class Worker {
240
548
  }));
241
549
  }
242
550
 
551
+ get manifest(): WorkerManifest {
552
+ return {
553
+ sdkVersion: SDK_VERSION,
554
+ databases: Array.from(this.#databases.values()),
555
+ pacers: Array.from(this.#pacers.values()),
556
+ capabilities: this.capabilities,
557
+ };
558
+ }
559
+
243
560
  /**
244
561
  * Execute a capability by key.
245
562
  *
@@ -265,6 +582,10 @@ export class Worker {
265
582
  );
266
583
  }
267
584
 
585
+ if (!capability.handler) {
586
+ throw new Error(`Capability "${key}" cannot be executed`);
587
+ }
588
+
268
589
  // biome-ignore lint/suspicious/noExplicitAny: context is unknown, passed by external non-typed code.
269
590
  return capability.handler(context as any, options);
270
591
  }
@@ -273,8 +594,12 @@ export class Worker {
273
594
  if (!key || typeof key !== "string") {
274
595
  throw new Error("Capability key must be a non-empty string");
275
596
  }
276
- if (this.#capabilities.has(key)) {
277
- throw new Error(`Capability with key "${key}" already registered`);
597
+ if (
598
+ this.#capabilities.has(key) ||
599
+ this.#databases.has(key) ||
600
+ this.#pacers.has(key)
601
+ ) {
602
+ throw new Error(`Worker item with key "${key}" already registered`);
278
603
  }
279
604
  }
280
605
  }