@notionhq/workers 0.0.86 → 0.2.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/automation.js +2 -2
- package/dist/capabilities/sync.d.ts +73 -16
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +19 -10
- package/dist/capabilities/tool.d.ts.map +1 -1
- package/dist/capabilities/tool.js +18 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- 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 +6 -0
- package/dist/pacer_internal.d.ts.map +1 -0
- package/dist/pacer_internal.js +32 -0
- package/dist/schema.d.ts +3 -10
- package/dist/schema.d.ts.map +1 -1
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +138 -5
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +73 -7
- 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/automation.test.ts +2 -2
- package/src/capabilities/automation.ts +2 -2
- package/src/capabilities/sync.test.ts +89 -32
- package/src/capabilities/sync.ts +52 -25
- package/src/capabilities/tool.test.ts +47 -3
- package/src/capabilities/tool.ts +12 -4
- package/src/index.ts +9 -0
- package/src/pacer.test.ts +110 -0
- package/src/pacer.ts +25 -0
- package/src/pacer_internal.ts +60 -0
- package/src/schema.ts +3 -10
- package/src/types.ts +4 -2
- package/src/worker.test.ts +167 -0
- package/src/worker.ts +205 -7
|
@@ -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
|
+
});
|
package/src/worker.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { SyncCapability, SyncConfiguration } from "./capabilities/sync.js";
|
|
|
16
16
|
import { createSyncCapability } from "./capabilities/sync.js";
|
|
17
17
|
import type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
18
18
|
import { createToolCapability } from "./capabilities/tool.js";
|
|
19
|
+
import { pacerWait } from "./pacer_internal.js";
|
|
19
20
|
import type { Schema } from "./schema.js";
|
|
20
21
|
import type { HandlerOptions, JSONValue } from "./types.js";
|
|
21
22
|
|
|
@@ -42,12 +43,193 @@ type Capability =
|
|
|
42
43
|
| AutomationCapability
|
|
43
44
|
| OAuthCapability;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Configuration for a database that Notion creates and manages on behalf
|
|
48
|
+
* of the worker. The database is created on first deploy and its schema
|
|
49
|
+
* is migrated on subsequent deploys.
|
|
50
|
+
*/
|
|
51
|
+
export type ManagedDatabaseConfig<PK extends string, S extends Schema<PK>> = {
|
|
52
|
+
type: "managed";
|
|
53
|
+
/**
|
|
54
|
+
* The title for the database when it is first created in Notion.
|
|
55
|
+
*
|
|
56
|
+
* Updating this after the database has been created will not change the
|
|
57
|
+
* title of the database — users may rename it freely.
|
|
58
|
+
*/
|
|
59
|
+
initialTitle: string;
|
|
60
|
+
/**
|
|
61
|
+
* The property that serves as the primary key for matching records.
|
|
62
|
+
* Must be a property defined in the schema. The value of this property
|
|
63
|
+
* should match the `key` field on each sync change.
|
|
64
|
+
*/
|
|
65
|
+
primaryKeyProperty: PK;
|
|
66
|
+
/**
|
|
67
|
+
* The database schema. Notion will migrate the database to match
|
|
68
|
+
* the given schema on worker deploy.
|
|
69
|
+
*/
|
|
70
|
+
schema: S;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configuration union for all database types.
|
|
75
|
+
* Currently only `"managed"` is supported.
|
|
76
|
+
*/
|
|
77
|
+
export type DatabaseConfig<
|
|
78
|
+
PK extends string,
|
|
79
|
+
S extends Schema<PK>,
|
|
80
|
+
> = ManagedDatabaseConfig<PK, S>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* An opaque handle returned by `worker.database()`. Pass this to
|
|
84
|
+
* `worker.sync()` to associate a sync capability with the database.
|
|
85
|
+
*/
|
|
86
|
+
export type DatabaseHandle<PK extends string, S extends Schema<PK>> = {
|
|
87
|
+
readonly key: string;
|
|
88
|
+
readonly config: DatabaseConfig<PK, S>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Internal type-erased alias for storing heterogeneous databases. */
|
|
92
|
+
type RegisteredDatabase = DatabaseHandle<string, Schema<string>>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Configuration for a pacer that rate-limits requests to an external API.
|
|
96
|
+
* If multiple syncs share a pacer, the server apportions the budget evenly.
|
|
97
|
+
*/
|
|
98
|
+
export type PacerConfig = {
|
|
99
|
+
/** Maximum number of requests allowed per interval. */
|
|
100
|
+
allowedRequests: number;
|
|
101
|
+
/** The interval window in milliseconds. */
|
|
102
|
+
intervalMs: number;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* An opaque handle returned by `worker.pacer()`. Call `handle.wait()`
|
|
107
|
+
* before each API request to stay under the rate limit.
|
|
108
|
+
*/
|
|
109
|
+
export type PacerHandle = {
|
|
110
|
+
readonly key: string;
|
|
111
|
+
readonly config: PacerConfig;
|
|
112
|
+
/**
|
|
113
|
+
* Wait until a request can proceed under this pacer's rate limit.
|
|
114
|
+
*
|
|
115
|
+
* Call this before **every** API request inside your sync's `execute` function.
|
|
116
|
+
* The pacer ensures requests are evenly spaced over the interval window.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const apiPacer = worker.pacer("myApi", { allowedRequests: 10, intervalMs: 1000 });
|
|
121
|
+
*
|
|
122
|
+
* worker.sync("mySync", {
|
|
123
|
+
* database: myDb,
|
|
124
|
+
* execute: async () => {
|
|
125
|
+
* await apiPacer.wait();
|
|
126
|
+
* const data = await fetchFromApi();
|
|
127
|
+
* // ...
|
|
128
|
+
* },
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
wait(): Promise<void>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A pacer declaration as it appears in the manifest.
|
|
137
|
+
*/
|
|
138
|
+
export type PacerDeclaration = {
|
|
139
|
+
readonly key: string;
|
|
140
|
+
readonly config: PacerConfig;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** A capability entry stripped of its handler, used in the manifest. */
|
|
144
|
+
type CapabilityManifestEntry = Pick<Capability, "_tag" | "key" | "config">;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The full worker manifest, read by the server at deploy time to discover
|
|
148
|
+
* databases, pacers, capabilities, and the SDK version.
|
|
149
|
+
*/
|
|
150
|
+
export type WorkerManifest = {
|
|
151
|
+
readonly sdkVersion: string;
|
|
152
|
+
readonly databases: readonly RegisteredDatabase[];
|
|
153
|
+
readonly pacers: readonly PacerDeclaration[];
|
|
154
|
+
readonly capabilities: readonly CapabilityManifestEntry[];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Read SDK version from package.json at module load time.
|
|
158
|
+
// createRequire bridges ESM → CJS so we can require() a JSON file.
|
|
159
|
+
import { createRequire } from "node:module";
|
|
160
|
+
|
|
161
|
+
const SDK_VERSION = (() => {
|
|
162
|
+
const require = createRequire(import.meta.url);
|
|
163
|
+
const packageJson: unknown = require("../package.json");
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
typeof packageJson !== "object" ||
|
|
167
|
+
packageJson === null ||
|
|
168
|
+
!("version" in packageJson) ||
|
|
169
|
+
typeof packageJson.version !== "string"
|
|
170
|
+
) {
|
|
171
|
+
throw new Error("Failed to read SDK version from package.json");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return packageJson.version;
|
|
175
|
+
})();
|
|
176
|
+
|
|
45
177
|
// ============================================================================
|
|
46
178
|
// Worker class
|
|
47
179
|
// ============================================================================
|
|
48
180
|
|
|
49
181
|
export class Worker {
|
|
50
182
|
#capabilities: Map<string, Capability> = new Map();
|
|
183
|
+
#databases: Map<string, RegisteredDatabase> = new Map();
|
|
184
|
+
#pacers: Map<string, PacerDeclaration> = new Map();
|
|
185
|
+
|
|
186
|
+
database<PK extends string, S extends Schema<PK>>(
|
|
187
|
+
key: string,
|
|
188
|
+
config: DatabaseConfig<PK, S>,
|
|
189
|
+
): DatabaseHandle<PK, S> {
|
|
190
|
+
this.#validateUniqueKey(key);
|
|
191
|
+
const database: DatabaseHandle<PK, S> = { key, config };
|
|
192
|
+
this.#databases.set(key, { key, config });
|
|
193
|
+
return database;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Register a pacer for rate-limiting requests to an external API.
|
|
198
|
+
*
|
|
199
|
+
* Multiple syncs can share a pacer — the server will apportion the
|
|
200
|
+
* budget evenly across all syncs that reference it.
|
|
201
|
+
*
|
|
202
|
+
* Example:
|
|
203
|
+
*
|
|
204
|
+
* ```ts
|
|
205
|
+
* const jiraPacer = worker.pacer("jira", {
|
|
206
|
+
* allowedRequests: 10,
|
|
207
|
+
* intervalMs: 1000,
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* worker.sync("jiraSync", {
|
|
211
|
+
* database: tasks,
|
|
212
|
+
* execute: async (state, { notion }) => {
|
|
213
|
+
* await jiraPacer.wait();
|
|
214
|
+
* const data = await fetchFromJira();
|
|
215
|
+
* // ...
|
|
216
|
+
* },
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @param key - The unique key for this pacer.
|
|
221
|
+
* @param config - The rate limit configuration.
|
|
222
|
+
* @returns A pacer handle. Call `handle.wait()` before each API request.
|
|
223
|
+
*/
|
|
224
|
+
pacer(key: string, config: PacerConfig): PacerHandle {
|
|
225
|
+
this.#validateUniqueKey(key);
|
|
226
|
+
this.#pacers.set(key, { key, config });
|
|
227
|
+
return {
|
|
228
|
+
key,
|
|
229
|
+
config,
|
|
230
|
+
wait: () => pacerWait(key),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
51
233
|
|
|
52
234
|
/**
|
|
53
235
|
* Register a sync capability.
|
|
@@ -62,26 +244,29 @@ export class Worker {
|
|
|
62
244
|
* const worker = new Worker();
|
|
63
245
|
* export default worker;
|
|
64
246
|
*
|
|
65
|
-
* worker.
|
|
66
|
-
*
|
|
247
|
+
* const tasks = worker.database("tasks", {
|
|
248
|
+
* type: "managed",
|
|
249
|
+
* initialTitle: "Tasks",
|
|
67
250
|
* schema: {
|
|
68
|
-
* defaultName: "Tasks",
|
|
69
251
|
* properties: {
|
|
70
252
|
* "Task Name": Schema.title(),
|
|
71
|
-
* "Task ID": Schema.richText(),
|
|
72
253
|
* Status: Schema.select([
|
|
73
254
|
* { name: "Open", color: "default" },
|
|
74
255
|
* { name: "Done", color: "green" },
|
|
75
256
|
* ]),
|
|
76
257
|
* },
|
|
77
258
|
* },
|
|
259
|
+
* });
|
|
260
|
+
*
|
|
261
|
+
* worker.sync("tasksSync", {
|
|
262
|
+
* database: tasks,
|
|
78
263
|
* execute: async () => {
|
|
79
264
|
* const changes = [
|
|
80
265
|
* {
|
|
81
266
|
* key: "task-1",
|
|
267
|
+
* type: "upsert",
|
|
82
268
|
* properties: {
|
|
83
269
|
* "Task Name": Builder.title("Write docs"),
|
|
84
|
-
* "Task ID": Builder.richText("task-1"),
|
|
85
270
|
* Status: Builder.select("Open"),
|
|
86
271
|
* },
|
|
87
272
|
* },
|
|
@@ -240,6 +425,15 @@ export class Worker {
|
|
|
240
425
|
}));
|
|
241
426
|
}
|
|
242
427
|
|
|
428
|
+
get manifest(): WorkerManifest {
|
|
429
|
+
return {
|
|
430
|
+
sdkVersion: SDK_VERSION,
|
|
431
|
+
databases: Array.from(this.#databases.values()),
|
|
432
|
+
pacers: Array.from(this.#pacers.values()),
|
|
433
|
+
capabilities: this.capabilities,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
243
437
|
/**
|
|
244
438
|
* Execute a capability by key.
|
|
245
439
|
*
|
|
@@ -273,8 +467,12 @@ export class Worker {
|
|
|
273
467
|
if (!key || typeof key !== "string") {
|
|
274
468
|
throw new Error("Capability key must be a non-empty string");
|
|
275
469
|
}
|
|
276
|
-
if (
|
|
277
|
-
|
|
470
|
+
if (
|
|
471
|
+
this.#capabilities.has(key) ||
|
|
472
|
+
this.#databases.has(key) ||
|
|
473
|
+
this.#pacers.has(key)
|
|
474
|
+
) {
|
|
475
|
+
throw new Error(`Worker item with key "${key}" already registered`);
|
|
278
476
|
}
|
|
279
477
|
}
|
|
280
478
|
}
|