@notionhq/workers 0.0.82

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 (73) hide show
  1. package/README.md +22 -0
  2. package/dist/block.d.ts +321 -0
  3. package/dist/block.d.ts.map +1 -0
  4. package/dist/block.js +0 -0
  5. package/dist/builder.d.ts +117 -0
  6. package/dist/builder.d.ts.map +1 -0
  7. package/dist/builder.js +168 -0
  8. package/dist/capabilities/automation.d.ts +91 -0
  9. package/dist/capabilities/automation.d.ts.map +1 -0
  10. package/dist/capabilities/automation.js +40 -0
  11. package/dist/capabilities/automation.test.d.ts +2 -0
  12. package/dist/capabilities/automation.test.d.ts.map +1 -0
  13. package/dist/capabilities/context.d.ts +7 -0
  14. package/dist/capabilities/context.d.ts.map +1 -0
  15. package/dist/capabilities/context.js +15 -0
  16. package/dist/capabilities/oauth.d.ts +120 -0
  17. package/dist/capabilities/oauth.d.ts.map +1 -0
  18. package/dist/capabilities/oauth.js +55 -0
  19. package/dist/capabilities/oauth.test.d.ts +2 -0
  20. package/dist/capabilities/oauth.test.d.ts.map +1 -0
  21. package/dist/capabilities/sync.d.ts +180 -0
  22. package/dist/capabilities/sync.d.ts.map +1 -0
  23. package/dist/capabilities/sync.js +84 -0
  24. package/dist/capabilities/sync.test.d.ts +2 -0
  25. package/dist/capabilities/sync.test.d.ts.map +1 -0
  26. package/dist/capabilities/tool.d.ts +68 -0
  27. package/dist/capabilities/tool.d.ts.map +1 -0
  28. package/dist/capabilities/tool.js +174 -0
  29. package/dist/capabilities/tool.test.d.ts +2 -0
  30. package/dist/capabilities/tool.test.d.ts.map +1 -0
  31. package/dist/error.d.ts +12 -0
  32. package/dist/error.d.ts.map +1 -0
  33. package/dist/error.js +15 -0
  34. package/dist/icon-names.d.ts +6 -0
  35. package/dist/icon-names.d.ts.map +1 -0
  36. package/dist/icon-names.js +0 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +9 -0
  40. package/dist/json-schema.d.ts +117 -0
  41. package/dist/json-schema.d.ts.map +1 -0
  42. package/dist/json-schema.js +0 -0
  43. package/dist/json-schema.test.d.ts +2 -0
  44. package/dist/json-schema.test.d.ts.map +1 -0
  45. package/dist/schema.d.ts +178 -0
  46. package/dist/schema.d.ts.map +1 -0
  47. package/dist/schema.js +66 -0
  48. package/dist/types.d.ts +206 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +0 -0
  51. package/dist/worker.d.ts +177 -0
  52. package/dist/worker.d.ts.map +1 -0
  53. package/dist/worker.js +219 -0
  54. package/package.json +68 -0
  55. package/src/block.ts +529 -0
  56. package/src/builder.ts +299 -0
  57. package/src/capabilities/automation.test.ts +152 -0
  58. package/src/capabilities/automation.ts +130 -0
  59. package/src/capabilities/context.ts +23 -0
  60. package/src/capabilities/oauth.test.ts +52 -0
  61. package/src/capabilities/oauth.ts +157 -0
  62. package/src/capabilities/sync.test.ts +104 -0
  63. package/src/capabilities/sync.ts +311 -0
  64. package/src/capabilities/tool.test.ts +351 -0
  65. package/src/capabilities/tool.ts +279 -0
  66. package/src/error.ts +19 -0
  67. package/src/icon-names.ts +890 -0
  68. package/src/index.ts +34 -0
  69. package/src/json-schema.test.ts +719 -0
  70. package/src/json-schema.ts +179 -0
  71. package/src/schema.ts +241 -0
  72. package/src/types.ts +272 -0
  73. package/src/worker.ts +285 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Configuration for a Notion-managed OAuth provider.
3
+ *
4
+ * Notion owns the OAuth app credentials (client ID/secret) and the backend has
5
+ * pre-configured provider settings.
6
+ */
7
+ export interface NotionManagedOAuthConfiguration {
8
+ /**
9
+ * The unique identifier for this OAuth provider instance.
10
+ */
11
+ name: string;
12
+
13
+ /**
14
+ * The pre-configured provider to use (e.g., "google", "github", "salesforce").
15
+ * The backend will use this to look up the OAuth configuration.
16
+ */
17
+ provider: string;
18
+
19
+ /**
20
+ * Optional default access token expiry (in milliseconds) to use when the OAuth provider
21
+ * does not return `expires_in` in token responses.
22
+ *
23
+ * Some providers (e.g. Salesforce in certain configurations) may omit expiry information.
24
+ */
25
+ accessTokenExpireMs?: number;
26
+ }
27
+
28
+ /**
29
+ * Configuration for a user-managed OAuth provider.
30
+ *
31
+ * You own the OAuth app credentials and must explicitly provide endpoints and
32
+ * other OAuth parameters.
33
+ */
34
+ export interface UserManagedOAuthConfiguration {
35
+ /**
36
+ * The unique identifier for this OAuth provider instance.
37
+ */
38
+ name: string;
39
+
40
+ /**
41
+ * The client ID for the OAuth app.
42
+ */
43
+ clientId: string;
44
+
45
+ /**
46
+ * The client secret for the OAuth app.
47
+ */
48
+ clientSecret: string;
49
+
50
+ /**
51
+ * The OAuth 2.0 authorization endpoint URL.
52
+ */
53
+ authorizationEndpoint: string;
54
+
55
+ /**
56
+ * The OAuth 2.0 token endpoint URL.
57
+ */
58
+ tokenEndpoint: string;
59
+
60
+ /**
61
+ * The OAuth scope(s) to request.
62
+ */
63
+ scope: string;
64
+
65
+ /**
66
+ * Optional additional authorization parameters to include in the authorization request.
67
+ */
68
+ authorizationParams?: Record<string, string>;
69
+
70
+ /**
71
+ * Optional callback URL for OAuth redirect.
72
+ */
73
+ callbackUrl?: string;
74
+
75
+ /**
76
+ * Optional default access token expiry (in milliseconds) to use when the OAuth provider
77
+ * does not return `expires_in` in token responses.
78
+ *
79
+ * Some providers (e.g. Salesforce in certain configurations) may omit expiry information.
80
+ */
81
+ accessTokenExpireMs?: number;
82
+ }
83
+
84
+ /**
85
+ * Union type representing either Notion-managed or user-managed OAuth configuration.
86
+ */
87
+ export type OAuthConfiguration =
88
+ | NotionManagedOAuthConfiguration
89
+ | UserManagedOAuthConfiguration;
90
+
91
+ export type OAuthCapability = ReturnType<typeof createOAuthCapability>;
92
+
93
+ /**
94
+ * Creates an OAuth provider configuration for authenticating with third-party services.
95
+ *
96
+ * @param config - The OAuth configuration (Notion-managed or user-managed).
97
+ * @returns An OAuth provider definition.
98
+ */
99
+ export function createOAuthCapability(key: string, config: OAuthConfiguration) {
100
+ const envKey = oauthNameToEnvKey(config.name);
101
+
102
+ if ("provider" in config) {
103
+ return {
104
+ _tag: "oauth" as const,
105
+ key,
106
+ envKey,
107
+ async accessToken(): Promise<string> {
108
+ return readRequiredEnvVar(envKey, { name: config.name });
109
+ },
110
+ config: {
111
+ type: "notion_managed" as const,
112
+ name: config.name,
113
+ provider: config.provider,
114
+ accessTokenExpireMs: config.accessTokenExpireMs,
115
+ },
116
+ };
117
+ }
118
+
119
+ return {
120
+ _tag: "oauth" as const,
121
+ key,
122
+ envKey,
123
+ async accessToken(): Promise<string> {
124
+ return readRequiredEnvVar(envKey, { name: config.name });
125
+ },
126
+ config: {
127
+ type: "user_managed" as const,
128
+ name: config.name,
129
+ authorizationEndpoint: config.authorizationEndpoint,
130
+ tokenEndpoint: config.tokenEndpoint,
131
+ scope: config.scope,
132
+ clientId: config.clientId,
133
+ clientSecret: config.clientSecret,
134
+ authorizationParams: config.authorizationParams,
135
+ callbackUrl: config.callbackUrl,
136
+ accessTokenExpireMs: config.accessTokenExpireMs,
137
+ },
138
+ };
139
+ }
140
+
141
+ function oauthNameToEnvKey(identifier: string): string {
142
+ const encoded = Buffer.from(identifier).toString("hex").toUpperCase();
143
+
144
+ return `OAUTH_${encoded}_ACCESS_TOKEN`;
145
+ }
146
+
147
+ function readRequiredEnvVar(key: string, context: { name: string }): string {
148
+ const value = process.env[key];
149
+ if (value) {
150
+ return value;
151
+ }
152
+
153
+ throw new Error(
154
+ `Missing OAuth access token env var "${key}" (name: "${context.name}"). ` +
155
+ `Make sure you've completed OAuth for this capability and are running inside the worker runtime.`,
156
+ );
157
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as Builder from "../builder.js";
3
+ import * as Schema from "../schema.js";
4
+ import { createSyncCapability } from "./sync.js";
5
+
6
+ describe("Worker.sync", () => {
7
+ describe("Schema.relation", () => {
8
+ it("creates a relation property configuration", () => {
9
+ const relationProp = Schema.relation("projectsSync");
10
+
11
+ expect(relationProp).toEqual({
12
+ type: "relation",
13
+ relatedSyncKey: "projectsSync",
14
+ config: { twoWay: false },
15
+ });
16
+ });
17
+
18
+ it("can be used in a schema definition", () => {
19
+ const capability = createSyncCapability("tasksSync", {
20
+ primaryKeyProperty: "Task ID",
21
+ schema: {
22
+ defaultName: "Tasks",
23
+ properties: {
24
+ "Task ID": Schema.title(),
25
+ Project: Schema.relation("projectsSync"),
26
+ },
27
+ },
28
+ execute: async () => ({
29
+ changes: [],
30
+ hasMore: false,
31
+ }),
32
+ });
33
+
34
+ expect(capability.config.schema.properties.Project).toEqual({
35
+ type: "relation",
36
+ relatedSyncKey: "projectsSync",
37
+ config: { twoWay: false },
38
+ });
39
+ });
40
+ });
41
+
42
+ describe("Builder.relation", () => {
43
+ it("creates a relation reference from a primary key", () => {
44
+ const value = Builder.relation("project-123");
45
+
46
+ expect(value).toEqual({ type: "primaryKey", value: "project-123" });
47
+ });
48
+
49
+ it("can be used to build relation arrays", () => {
50
+ const value = [
51
+ Builder.relation("project-123"),
52
+ Builder.relation("project-456"),
53
+ ];
54
+
55
+ expect(value).toEqual([
56
+ { type: "primaryKey", value: "project-123" },
57
+ { type: "primaryKey", value: "project-456" },
58
+ ]);
59
+ });
60
+ });
61
+
62
+ describe("SyncedObject with relation properties", () => {
63
+ it("allows relation values in synced objects", async () => {
64
+ const capability = createSyncCapability("tasksSync", {
65
+ primaryKeyProperty: "Task ID",
66
+ schema: {
67
+ defaultName: "Tasks",
68
+ properties: {
69
+ "Task ID": Schema.title(),
70
+ Project: Schema.relation("projectsSync"),
71
+ },
72
+ },
73
+ execute: async () => ({
74
+ changes: [
75
+ {
76
+ type: "upsert",
77
+ key: "task-1",
78
+ properties: {
79
+ "Task ID": Builder.title("TASK-001"),
80
+ Project: [Builder.relation("project-123")],
81
+ },
82
+ },
83
+ {
84
+ type: "upsert",
85
+ key: "task-2",
86
+ properties: {
87
+ "Task ID": Builder.title("TASK-002"),
88
+ Project: [
89
+ Builder.relation("project-123"),
90
+ Builder.relation("project-456"),
91
+ ],
92
+ },
93
+ },
94
+ ],
95
+ hasMore: false,
96
+ }),
97
+ });
98
+
99
+ // Verify the capability is properly configured
100
+ expect(capability._tag).toBe("sync");
101
+ expect(capability.config.primaryKeyProperty).toBe("Task ID");
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,311 @@
1
+ import { ExecutionError, unreachable } from "../error.js";
2
+ import type {
3
+ PropertyConfiguration,
4
+ PropertySchema,
5
+ Schema,
6
+ } from "../schema.js";
7
+ import type {
8
+ HandlerOptions,
9
+ Icon,
10
+ PeopleValue,
11
+ PlaceValue,
12
+ RelationValue,
13
+ Schedule,
14
+ SyncSchedule,
15
+ TextValue,
16
+ TimeUnit,
17
+ } from "../types.js";
18
+ import type { CapabilityContext } from "./context.js";
19
+ import { createCapabilityContext } from "./context.js";
20
+
21
+ /**
22
+ * Maps a property configuration to its corresponding value type.
23
+ */
24
+ type PropertyValueType<T extends PropertyConfiguration> = T extends {
25
+ type: "people";
26
+ }
27
+ ? PeopleValue
28
+ : T extends { type: "place" }
29
+ ? PlaceValue
30
+ : T extends { type: "relation" }
31
+ ? RelationValue
32
+ : TextValue;
33
+
34
+ /**
35
+ * Sync mode determines how the sync handles data lifecycle.
36
+ */
37
+ export type SyncMode = "replace" | "incremental";
38
+
39
+ /**
40
+ * A change representing a record to be created or updated.
41
+ */
42
+ export type SyncChangeUpsert<
43
+ PK extends string,
44
+ S extends PropertySchema<PK>,
45
+ > = {
46
+ /**
47
+ * The type of change. Use `"upsert"` to create or update a record.
48
+ */
49
+ type: "upsert";
50
+ /**
51
+ * A unique identifier for this record, used to match against existing pages.
52
+ * This value will be stored in the property specified by `primaryKeyProperty`.
53
+ */
54
+ key: string;
55
+ /**
56
+ * The property values for this record.
57
+ * Keys must match the property names defined in the schema.
58
+ * Use the Builder helpers (e.g., `Builder.title()`, `Builder.richText()`) to create values.
59
+ */
60
+ properties: {
61
+ [Property in keyof S]: PropertyValueType<S[Property]>;
62
+ };
63
+ /**
64
+ * Optional icon to use as the icon for this row's page.
65
+ * Use the `Builder.emojiIcon()`, `Builder.notionIcon()`, or `Builder.imageIcon()` helpers.
66
+ */
67
+ icon?: Icon;
68
+ /**
69
+ * Optional markdown content to add to the page body.
70
+ * This will be converted to Notion blocks and added as page content.
71
+ */
72
+ pageContentMarkdown?: string;
73
+ };
74
+
75
+ /**
76
+ * A change representing a record to be deleted.
77
+ * Only applicable when using `mode: "incremental"`.
78
+ */
79
+ export type SyncChangeDelete = {
80
+ /**
81
+ * The type of change. Use `"delete"` to remove a record.
82
+ */
83
+ type: "delete";
84
+ /**
85
+ * The unique identifier of the record to delete.
86
+ * Must match the `key` of a previously upserted record.
87
+ */
88
+ key: string;
89
+ };
90
+
91
+ /**
92
+ * A change to be applied to the synced database.
93
+ * Can be either an upsert (create/update) or a delete.
94
+ */
95
+ export type SyncChange<PK extends string, S extends PropertySchema<PK>> =
96
+ | SyncChangeUpsert<PK, S>
97
+ | SyncChangeDelete;
98
+
99
+ /**
100
+ * Result returned from the sync execute function.
101
+ */
102
+ export type SyncExecutionResult<PK extends string, State = unknown> = {
103
+ /**
104
+ * The batch of changes to apply in this execution.
105
+ * Can include upserts (create/update) and deletes.
106
+ */
107
+ changes: SyncChange<PK, PropertySchema<PK>>[];
108
+
109
+ /**
110
+ * Indicates whether there is more data to fetch.
111
+ * - `true`: More data available, will trigger another execution with nextState
112
+ * - `false`: No more data to fetch, sync is complete
113
+ */
114
+ hasMore: boolean;
115
+
116
+ /**
117
+ * Optional state data to pass to the next execution.
118
+ * Required if `hasMore` is `true`, ignored if `hasMore` is `false`.
119
+ * This can be any type of data (cursor, page number, timestamp, etc.).
120
+ * The same data will be provided as `state` in the next execution.
121
+ */
122
+ nextState?: State;
123
+ };
124
+
125
+ /**
126
+ * A configuration object that enables synchronization between a data
127
+ * source and a third-party source.
128
+ */
129
+ export type SyncConfiguration<
130
+ PK extends string,
131
+ S extends Schema<PK>,
132
+ State = unknown,
133
+ > = {
134
+ /**
135
+ * The property of the data source that maps to a "primary key" in the
136
+ * third-party data. This is used to match existing pages to
137
+ * records in the third-party service. Must be a property defined in the schema.
138
+ */
139
+ primaryKeyProperty: PK;
140
+
141
+ /**
142
+ * The schema defining the structure of properties in the collection.
143
+ */
144
+ schema: S;
145
+
146
+ /**
147
+ * How the sync handles data lifecycle:
148
+ * - "replace": Each sync returns the complete dataset. After hasMore:false,
149
+ * pages not seen in this sync run are deleted.
150
+ * - "incremental": Sync returns changes only. After hasMore:false, sync
151
+ * continues from saved cursor. Use delete markers
152
+ * to explicitly remove pages.
153
+ *
154
+ * @default "replace"
155
+ */
156
+ mode?: SyncMode;
157
+
158
+ /**
159
+ * How often the sync should run.
160
+ * - "continuous": Run as frequently as the system allows
161
+ * - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
162
+ *
163
+ * Minimum interval: 1 minute ("1m")
164
+ * Maximum interval: 7 days ("7d")
165
+ *
166
+ * @default "30m"
167
+ */
168
+ schedule?: Schedule;
169
+
170
+ /**
171
+ * A function that fetches the data to sync from the third-party service.
172
+ *
173
+ * This function can return all data at once, or implement pagination by:
174
+ * 1. Returning a batch of changes with `hasMore: true` and a `nextState`
175
+ * 2. The runtime will call execute again with that state
176
+ * 3. Continue until `hasMore: false` is returned
177
+ *
178
+ * The runtime will handle diffing against the data source and creating,
179
+ * updating, and deleting pages as necessary.
180
+ *
181
+ * @param state - User-defined state from the previous execution (undefined on first call)
182
+ * @param context - Runtime context, including Notion client
183
+ * @returns A result containing changes, hasMore status, and optional nextState
184
+ */
185
+ execute: (
186
+ state: State | undefined,
187
+ context: CapabilityContext,
188
+ ) => Promise<SyncExecutionResult<PK, State>>;
189
+ };
190
+
191
+ export type SyncCapability = ReturnType<typeof createSyncCapability>;
192
+
193
+ /**
194
+ * Runtime context object passed from the runtime to sync capability handlers.
195
+ */
196
+ type RuntimeContext<UserContext = unknown> = {
197
+ /** The user-defined/-controlled state (cursor, pagination state, etc.) */
198
+ state?: UserContext;
199
+ /** Legacy field for user-defined/-controlled state. */
200
+ userContext?: UserContext;
201
+ };
202
+
203
+ /**
204
+ * Creates a special handler for syncing third-party data to a collection.
205
+ *
206
+ * @param syncConfiguration - The configuration for the sync.
207
+ * @returns A handler function that executes the sync function, and passes data
208
+ * needed to complete the sync back to the platform.
209
+ */
210
+ export function createSyncCapability<
211
+ PK extends string,
212
+ S extends Schema<PK>,
213
+ Context = unknown,
214
+ >(key: string, syncConfiguration: SyncConfiguration<PK, S, Context>) {
215
+ return {
216
+ _tag: "sync" as const,
217
+ key,
218
+ config: {
219
+ primaryKeyProperty: syncConfiguration.primaryKeyProperty,
220
+ schema: syncConfiguration.schema,
221
+ mode: syncConfiguration.mode,
222
+ schedule: parseSchedule(syncConfiguration.schedule),
223
+ },
224
+ async handler(
225
+ runtimeContext?: RuntimeContext<Context>,
226
+ options?: HandlerOptions,
227
+ ) {
228
+ const capabilityContext = createCapabilityContext();
229
+ const state = runtimeContext?.state ?? runtimeContext?.userContext;
230
+ const executionResult = await syncConfiguration
231
+ .execute(state, capabilityContext)
232
+ .catch((err) => {
233
+ throw new ExecutionError(err);
234
+ });
235
+
236
+ const result = {
237
+ changes: executionResult.changes,
238
+ hasMore: executionResult.hasMore,
239
+ nextUserContext: executionResult.nextState,
240
+ };
241
+
242
+ if (options?.concreteOutput) {
243
+ return result;
244
+ } else {
245
+ process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
246
+ }
247
+
248
+ return result;
249
+ },
250
+ };
251
+ }
252
+
253
+ const MS_PER_MINUTE = 60 * 1000;
254
+ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
255
+ const MS_PER_DAY = 24 * MS_PER_HOUR;
256
+
257
+ const MIN_INTERVAL_MS = MS_PER_MINUTE; // 1m
258
+ const MAX_INTERVAL_MS = 7 * MS_PER_DAY; // 7d
259
+
260
+ const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE; // 30m
261
+
262
+ /**
263
+ * Parses a user-friendly schedule string into the normalized backend format.
264
+ */
265
+ function parseSchedule(schedule: Schedule | undefined): SyncSchedule {
266
+ if (schedule === "continuous") {
267
+ return { type: "continuous" };
268
+ }
269
+
270
+ if (!schedule) {
271
+ return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
272
+ }
273
+
274
+ const match = schedule.match(/^(\d+)(m|h|d)$/);
275
+ if (!match || !match[1] || !match[2]) {
276
+ throw new Error(
277
+ `Invalid schedule format: "${schedule}". Use "continuous" or an interval like "30m", "1h", "1d".`,
278
+ );
279
+ }
280
+
281
+ const value = parseInt(match[1], 10);
282
+ const unit = match[2] as TimeUnit;
283
+
284
+ let intervalMs: number;
285
+ switch (unit) {
286
+ case "m":
287
+ intervalMs = value * MS_PER_MINUTE;
288
+ break;
289
+ case "h":
290
+ intervalMs = value * MS_PER_HOUR;
291
+ break;
292
+ case "d":
293
+ intervalMs = value * MS_PER_DAY;
294
+ break;
295
+ default:
296
+ unreachable(unit);
297
+ }
298
+
299
+ if (intervalMs < MIN_INTERVAL_MS) {
300
+ throw new Error(
301
+ `Schedule interval must be at least 1 minute. Got: "${schedule}"`,
302
+ );
303
+ }
304
+ if (intervalMs > MAX_INTERVAL_MS) {
305
+ throw new Error(
306
+ `Schedule interval must be at most 7 days. Got: "${schedule}"`,
307
+ );
308
+ }
309
+
310
+ return { type: "interval", intervalMs };
311
+ }