@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
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
} from "vitest";
|
|
10
10
|
import * as Builder from "../builder.js";
|
|
11
11
|
import { ExecutionError } from "../error.js";
|
|
12
|
+
import { getPacerState } from "../pacer_internal.js";
|
|
12
13
|
import * as Schema from "../schema.js";
|
|
14
|
+
import { Worker } from "../worker.js";
|
|
13
15
|
import { createSyncCapability } from "./sync.js";
|
|
14
16
|
|
|
15
17
|
describe("Worker.sync", () => {
|
|
@@ -27,34 +29,11 @@ describe("Worker.sync", () => {
|
|
|
27
29
|
|
|
28
30
|
describe("Schema.relation", () => {
|
|
29
31
|
it("creates a relation property configuration", () => {
|
|
30
|
-
const relationProp = Schema.relation("
|
|
32
|
+
const relationProp = Schema.relation("projects");
|
|
31
33
|
|
|
32
34
|
expect(relationProp).toEqual({
|
|
33
35
|
type: "relation",
|
|
34
|
-
|
|
35
|
-
config: { twoWay: false },
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("can be used in a schema definition", () => {
|
|
40
|
-
const capability = createSyncCapability("tasksSync", {
|
|
41
|
-
primaryKeyProperty: "Task ID",
|
|
42
|
-
schema: {
|
|
43
|
-
defaultName: "Tasks",
|
|
44
|
-
properties: {
|
|
45
|
-
"Task ID": Schema.title(),
|
|
46
|
-
Project: Schema.relation("projectsSync"),
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
execute: async () => ({
|
|
50
|
-
changes: [],
|
|
51
|
-
hasMore: false,
|
|
52
|
-
}),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
expect(capability.config.schema.properties.Project).toEqual({
|
|
56
|
-
type: "relation",
|
|
57
|
-
relatedSyncKey: "projectsSync",
|
|
36
|
+
relatedDatabaseKey: "projects",
|
|
58
37
|
config: { twoWay: false },
|
|
59
38
|
});
|
|
60
39
|
});
|
|
@@ -82,15 +61,21 @@ describe("Worker.sync", () => {
|
|
|
82
61
|
|
|
83
62
|
describe("SyncedObject with relation properties", () => {
|
|
84
63
|
it("allows relation values in synced objects", async () => {
|
|
85
|
-
const
|
|
64
|
+
const worker = new Worker();
|
|
65
|
+
const tasks = worker.database("tasks", {
|
|
66
|
+
type: "managed",
|
|
67
|
+
initialTitle: "Tasks",
|
|
86
68
|
primaryKeyProperty: "Task ID",
|
|
87
69
|
schema: {
|
|
88
|
-
defaultName: "Tasks",
|
|
89
70
|
properties: {
|
|
90
71
|
"Task ID": Schema.title(),
|
|
91
|
-
Project: Schema.relation("
|
|
72
|
+
Project: Schema.relation("projects"),
|
|
92
73
|
},
|
|
93
74
|
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const capability = createSyncCapability("tasksSync", {
|
|
78
|
+
database: tasks,
|
|
94
79
|
execute: async () => ({
|
|
95
80
|
changes: [
|
|
96
81
|
{
|
|
@@ -117,22 +102,112 @@ describe("Worker.sync", () => {
|
|
|
117
102
|
}),
|
|
118
103
|
});
|
|
119
104
|
|
|
120
|
-
// Verify the capability is properly configured
|
|
121
105
|
expect(capability._tag).toBe("sync");
|
|
122
|
-
expect(capability.config.
|
|
106
|
+
expect(capability.config.databaseKey).toBe("tasks");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("pacer local fallback", () => {
|
|
111
|
+
it("seeds pacer state from declarations when runtime context has no pacers", async () => {
|
|
112
|
+
const worker = new Worker();
|
|
113
|
+
const db = worker.database("test", {
|
|
114
|
+
type: "managed",
|
|
115
|
+
initialTitle: "Test",
|
|
116
|
+
primaryKeyProperty: "ID",
|
|
117
|
+
schema: { properties: { ID: Schema.title() } },
|
|
118
|
+
});
|
|
119
|
+
const pacer = worker.pacer("myApi", {
|
|
120
|
+
allowedRequests: 10,
|
|
121
|
+
intervalMs: 1000,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let pacerWaitCalled = false;
|
|
125
|
+
const capability = createSyncCapability(
|
|
126
|
+
"testSync",
|
|
127
|
+
{
|
|
128
|
+
database: db,
|
|
129
|
+
execute: async () => {
|
|
130
|
+
await pacer.wait();
|
|
131
|
+
pacerWaitCalled = true;
|
|
132
|
+
return { changes: [], hasMore: false };
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
[{ key: "myApi", config: { allowedRequests: 10, intervalMs: 1000 } }],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// No runtimeContext → no pacers from server → should fall back to declarations
|
|
139
|
+
await capability.handler(undefined, { concreteOutput: true });
|
|
140
|
+
|
|
141
|
+
expect(pacerWaitCalled).toBe(true);
|
|
142
|
+
const state = getPacerState();
|
|
143
|
+
expect(state.pacers.myApi).toBeDefined();
|
|
144
|
+
expect(state.pacers.myApi?.allowedRequests).toBe(10);
|
|
145
|
+
expect(state.pacers.myApi?.intervalMs).toBe(1000);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("uses server-provided pacers when available", async () => {
|
|
149
|
+
const worker = new Worker();
|
|
150
|
+
const db = worker.database("test", {
|
|
151
|
+
type: "managed",
|
|
152
|
+
initialTitle: "Test",
|
|
153
|
+
primaryKeyProperty: "ID",
|
|
154
|
+
schema: { properties: { ID: Schema.title() } },
|
|
155
|
+
});
|
|
156
|
+
const pacer = worker.pacer("myApi", {
|
|
157
|
+
allowedRequests: 10,
|
|
158
|
+
intervalMs: 1000,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
let pacerWaitCalled = false;
|
|
162
|
+
const capability = createSyncCapability(
|
|
163
|
+
"testSync",
|
|
164
|
+
{
|
|
165
|
+
database: db,
|
|
166
|
+
execute: async () => {
|
|
167
|
+
await pacer.wait();
|
|
168
|
+
pacerWaitCalled = true;
|
|
169
|
+
return { changes: [], hasMore: false };
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
[{ key: "myApi", config: { allowedRequests: 10, intervalMs: 1000 } }],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Server provides apportioned budget (e.g., 5 instead of 10)
|
|
176
|
+
await capability.handler(
|
|
177
|
+
{
|
|
178
|
+
pacers: {
|
|
179
|
+
myApi: {
|
|
180
|
+
lastScheduledAtMs: 0,
|
|
181
|
+
allowedRequests: 5,
|
|
182
|
+
intervalMs: 1000,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{ concreteOutput: true },
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(pacerWaitCalled).toBe(true);
|
|
190
|
+
// Should use server-provided value, not the declaration
|
|
191
|
+
expect(getPacerState().pacers.myApi?.allowedRequests).toBe(5);
|
|
123
192
|
});
|
|
124
193
|
});
|
|
125
194
|
|
|
126
195
|
describe("handler output envelope", () => {
|
|
127
196
|
it("wraps successful result in success envelope", async () => {
|
|
128
|
-
const
|
|
197
|
+
const worker = new Worker();
|
|
198
|
+
const db = worker.database("test", {
|
|
199
|
+
type: "managed",
|
|
200
|
+
initialTitle: "Test",
|
|
129
201
|
primaryKeyProperty: "ID",
|
|
130
202
|
schema: {
|
|
131
|
-
defaultName: "Test",
|
|
132
203
|
properties: {
|
|
133
204
|
ID: Schema.title(),
|
|
134
205
|
},
|
|
135
206
|
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const capability = createSyncCapability("testSync", {
|
|
210
|
+
database: db,
|
|
136
211
|
execute: async () => ({
|
|
137
212
|
changes: [],
|
|
138
213
|
hasMore: false,
|
|
@@ -142,19 +217,25 @@ describe("Worker.sync", () => {
|
|
|
142
217
|
await capability.handler();
|
|
143
218
|
|
|
144
219
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
145
|
-
`\n<
|
|
220
|
+
`\n<__notion_output__>${JSON.stringify({ _tag: "success", value: { changes: [], hasMore: false, nextUserContext: undefined, nextPacerStates: {} } })}</__notion_output__>\n`,
|
|
146
221
|
);
|
|
147
222
|
});
|
|
148
223
|
|
|
149
224
|
it("wraps error in error envelope", async () => {
|
|
150
|
-
const
|
|
225
|
+
const worker = new Worker();
|
|
226
|
+
const db = worker.database("test", {
|
|
227
|
+
type: "managed",
|
|
228
|
+
initialTitle: "Test",
|
|
151
229
|
primaryKeyProperty: "ID",
|
|
152
230
|
schema: {
|
|
153
|
-
defaultName: "Test",
|
|
154
231
|
properties: {
|
|
155
232
|
ID: Schema.title(),
|
|
156
233
|
},
|
|
157
234
|
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const capability = createSyncCapability("errorSync", {
|
|
238
|
+
database: db,
|
|
158
239
|
execute: async () => {
|
|
159
240
|
throw new Error("sync failed");
|
|
160
241
|
},
|
|
@@ -169,5 +250,67 @@ describe("Worker.sync", () => {
|
|
|
169
250
|
expect.stringContaining(`"name":"ExecutionError"`),
|
|
170
251
|
);
|
|
171
252
|
});
|
|
253
|
+
|
|
254
|
+
it("emits sync output with key and default targetDatabaseKey", async () => {
|
|
255
|
+
const worker = new Worker();
|
|
256
|
+
const tasks = worker.database("tasks", {
|
|
257
|
+
type: "managed",
|
|
258
|
+
initialTitle: "Tasks",
|
|
259
|
+
primaryKeyProperty: "Name",
|
|
260
|
+
schema: {
|
|
261
|
+
properties: {
|
|
262
|
+
Name: Schema.title(),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const capability = createSyncCapability("tasksSync", {
|
|
268
|
+
database: tasks,
|
|
269
|
+
execute: async () => ({
|
|
270
|
+
changes: [
|
|
271
|
+
{
|
|
272
|
+
type: "upsert" as const,
|
|
273
|
+
key: "task-1",
|
|
274
|
+
properties: {
|
|
275
|
+
Name: Builder.title("Write docs"),
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: "delete" as const,
|
|
280
|
+
key: "task-2",
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
hasMore: false,
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await capability.handler();
|
|
288
|
+
|
|
289
|
+
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
290
|
+
`\n<__notion_output__>${JSON.stringify({
|
|
291
|
+
_tag: "success",
|
|
292
|
+
value: {
|
|
293
|
+
changes: [
|
|
294
|
+
{
|
|
295
|
+
type: "upsert",
|
|
296
|
+
key: "task-1",
|
|
297
|
+
properties: {
|
|
298
|
+
Name: Builder.title("Write docs"),
|
|
299
|
+
},
|
|
300
|
+
targetDatabaseKey: "tasks",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: "delete",
|
|
304
|
+
key: "task-2",
|
|
305
|
+
targetDatabaseKey: "tasks",
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
hasMore: false,
|
|
309
|
+
nextUserContext: undefined,
|
|
310
|
+
nextPacerStates: {},
|
|
311
|
+
},
|
|
312
|
+
})}</__notion_output__>\n`,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
172
315
|
});
|
|
173
316
|
});
|
package/src/capabilities/sync.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { ExecutionError
|
|
1
|
+
import { ExecutionError } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
getPacerState,
|
|
4
|
+
initPacerState,
|
|
5
|
+
type PacerDeclaration,
|
|
6
|
+
} from "../pacer_internal.js";
|
|
2
7
|
import type {
|
|
3
8
|
PropertyConfiguration,
|
|
4
9
|
PropertySchema,
|
|
@@ -11,12 +16,17 @@ import type {
|
|
|
11
16
|
PlaceValue,
|
|
12
17
|
RelationValue,
|
|
13
18
|
Schedule,
|
|
14
|
-
SyncSchedule,
|
|
15
19
|
TextValue,
|
|
16
|
-
TimeUnit,
|
|
17
20
|
} from "../types.js";
|
|
21
|
+
import type { DatabaseHandle } from "../worker.js";
|
|
18
22
|
import type { CapabilityContext } from "./context.js";
|
|
19
23
|
import { createCapabilityContext } from "./context.js";
|
|
24
|
+
import { writeOutput } from "./output.js";
|
|
25
|
+
import {
|
|
26
|
+
parseSchedule,
|
|
27
|
+
type StatefulCapabilityMode,
|
|
28
|
+
type RuntimeContext as StatefulRuntimeContext,
|
|
29
|
+
} from "./stateful-capability.js";
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Maps a property configuration to its corresponding value type.
|
|
@@ -34,7 +44,7 @@ type PropertyValueType<T extends PropertyConfiguration> = T extends {
|
|
|
34
44
|
/**
|
|
35
45
|
* Sync mode determines how the sync handles data lifecycle.
|
|
36
46
|
*/
|
|
37
|
-
export type SyncMode =
|
|
47
|
+
export type SyncMode = StatefulCapabilityMode;
|
|
38
48
|
|
|
39
49
|
/**
|
|
40
50
|
* A change representing a record to be created or updated.
|
|
@@ -49,9 +59,13 @@ export type SyncChangeUpsert<
|
|
|
49
59
|
type: "upsert";
|
|
50
60
|
/**
|
|
51
61
|
* 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
62
|
*/
|
|
54
63
|
key: string;
|
|
64
|
+
/**
|
|
65
|
+
* The key of the database to write to. If omitted, defaults to
|
|
66
|
+
* the database associated with the sync capability.
|
|
67
|
+
*/
|
|
68
|
+
targetDatabaseKey?: string;
|
|
55
69
|
/**
|
|
56
70
|
* The property values for this record.
|
|
57
71
|
* Keys must match the property names defined in the schema.
|
|
@@ -60,6 +74,13 @@ export type SyncChangeUpsert<
|
|
|
60
74
|
properties: {
|
|
61
75
|
[Property in keyof S]: PropertyValueType<S[Property]>;
|
|
62
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* When this record was last updated in the upstream system.
|
|
79
|
+
* ISO 8601 string (e.g. `"2026-03-19T12:00:00Z"`).
|
|
80
|
+
* Used for conflict resolution when multiple syncs write to the same database.
|
|
81
|
+
* Recommended, but optional.
|
|
82
|
+
*/
|
|
83
|
+
upstreamUpdatedAt?: string;
|
|
63
84
|
/**
|
|
64
85
|
* Optional icon to use as the icon for this row's page.
|
|
65
86
|
* Use the `Builder.emojiIcon()`, `Builder.notionIcon()`, or `Builder.imageIcon()` helpers.
|
|
@@ -83,9 +104,13 @@ export type SyncChangeDelete = {
|
|
|
83
104
|
type: "delete";
|
|
84
105
|
/**
|
|
85
106
|
* The unique identifier of the record to delete.
|
|
86
|
-
* Must match the `key` of a previously upserted record.
|
|
87
107
|
*/
|
|
88
108
|
key: string;
|
|
109
|
+
/**
|
|
110
|
+
* The key of the database to delete from. If omitted, defaults to
|
|
111
|
+
* the database associated with the sync capability.
|
|
112
|
+
*/
|
|
113
|
+
targetDatabaseKey?: string;
|
|
89
114
|
};
|
|
90
115
|
|
|
91
116
|
/**
|
|
@@ -123,8 +148,7 @@ export type SyncExecutionResult<PK extends string, State = unknown> = {
|
|
|
123
148
|
};
|
|
124
149
|
|
|
125
150
|
/**
|
|
126
|
-
*
|
|
127
|
-
* source and a third-party source.
|
|
151
|
+
* Configuration for a sync capability that syncs data to a database.
|
|
128
152
|
*/
|
|
129
153
|
export type SyncConfiguration<
|
|
130
154
|
PK extends string,
|
|
@@ -132,16 +156,9 @@ export type SyncConfiguration<
|
|
|
132
156
|
State = unknown,
|
|
133
157
|
> = {
|
|
134
158
|
/**
|
|
135
|
-
* 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.
|
|
159
|
+
* The database to sync data into.
|
|
138
160
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* The schema defining the structure of properties in the collection.
|
|
143
|
-
*/
|
|
144
|
-
schema: S;
|
|
161
|
+
database: DatabaseHandle<PK, S>;
|
|
145
162
|
|
|
146
163
|
/**
|
|
147
164
|
* How the sync handles data lifecycle:
|
|
@@ -158,6 +175,7 @@ export type SyncConfiguration<
|
|
|
158
175
|
/**
|
|
159
176
|
* How often the sync should run.
|
|
160
177
|
* - "continuous": Run as frequently as the system allows
|
|
178
|
+
* - "manual": Only run when explicitly triggered
|
|
161
179
|
* - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
|
|
162
180
|
*
|
|
163
181
|
* Minimum interval: 1 minute ("1m")
|
|
@@ -191,17 +209,7 @@ export type SyncConfiguration<
|
|
|
191
209
|
export type SyncCapability = ReturnType<typeof createSyncCapability>;
|
|
192
210
|
|
|
193
211
|
/**
|
|
194
|
-
*
|
|
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.
|
|
212
|
+
* Creates a special handler for syncing third-party data to a database.
|
|
205
213
|
*
|
|
206
214
|
* @param syncConfiguration - The configuration for the sync.
|
|
207
215
|
* @returns A handler function that executes the sync function, and passes data
|
|
@@ -211,22 +219,31 @@ export function createSyncCapability<
|
|
|
211
219
|
PK extends string,
|
|
212
220
|
S extends Schema<PK>,
|
|
213
221
|
Context = unknown,
|
|
214
|
-
>(
|
|
222
|
+
>(
|
|
223
|
+
key: string,
|
|
224
|
+
syncConfiguration: SyncConfiguration<PK, S, Context>,
|
|
225
|
+
pacerDeclarations?: readonly PacerDeclaration[],
|
|
226
|
+
) {
|
|
215
227
|
return {
|
|
216
228
|
_tag: "sync" as const,
|
|
217
229
|
key,
|
|
218
230
|
config: {
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
databaseKey: syncConfiguration.database.key,
|
|
232
|
+
primaryKeyProperty: syncConfiguration.database.config.primaryKeyProperty,
|
|
221
233
|
mode: syncConfiguration.mode,
|
|
222
|
-
schedule:
|
|
234
|
+
schedule: syncConfiguration.schedule
|
|
235
|
+
? parseSchedule(syncConfiguration.schedule)
|
|
236
|
+
: undefined,
|
|
223
237
|
},
|
|
224
238
|
async handler(
|
|
225
|
-
runtimeContext?:
|
|
239
|
+
runtimeContext?: StatefulRuntimeContext<Context>,
|
|
226
240
|
options?: HandlerOptions,
|
|
227
241
|
) {
|
|
228
242
|
const capabilityContext = createCapabilityContext();
|
|
229
243
|
const state = runtimeContext?.state ?? runtimeContext?.userContext;
|
|
244
|
+
|
|
245
|
+
initPacerState(runtimeContext?.pacers, pacerDeclarations);
|
|
246
|
+
|
|
230
247
|
let executionResult: SyncExecutionResult<PK, Context>;
|
|
231
248
|
try {
|
|
232
249
|
executionResult = await syncConfiguration.execute(
|
|
@@ -236,88 +253,34 @@ export function createSyncCapability<
|
|
|
236
253
|
} catch (err) {
|
|
237
254
|
const error = new ExecutionError(err);
|
|
238
255
|
if (!options?.concreteOutput) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
writeOutput({
|
|
257
|
+
_tag: "error",
|
|
258
|
+
error: { name: error.name, message: error.message },
|
|
259
|
+
});
|
|
242
260
|
}
|
|
243
261
|
throw error;
|
|
244
262
|
}
|
|
245
263
|
|
|
264
|
+
const changes = executionResult.changes.map((change) => ({
|
|
265
|
+
...change,
|
|
266
|
+
targetDatabaseKey:
|
|
267
|
+
change.targetDatabaseKey ?? syncConfiguration.database.key,
|
|
268
|
+
}));
|
|
269
|
+
|
|
246
270
|
const result = {
|
|
247
|
-
changes
|
|
271
|
+
changes,
|
|
248
272
|
hasMore: executionResult.hasMore,
|
|
249
273
|
nextUserContext: executionResult.nextState,
|
|
274
|
+
nextPacerStates: getPacerState().pacers,
|
|
250
275
|
};
|
|
251
276
|
|
|
252
277
|
if (options?.concreteOutput) {
|
|
253
278
|
return result;
|
|
254
279
|
} else {
|
|
255
|
-
|
|
256
|
-
`\n<output>${JSON.stringify({ _tag: "success", value: result })}</output>\n`,
|
|
257
|
-
);
|
|
280
|
+
writeOutput({ _tag: "success", value: result });
|
|
258
281
|
}
|
|
259
282
|
|
|
260
283
|
return result;
|
|
261
284
|
},
|
|
262
285
|
};
|
|
263
286
|
}
|
|
264
|
-
|
|
265
|
-
const MS_PER_MINUTE = 60 * 1000;
|
|
266
|
-
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
267
|
-
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
268
|
-
|
|
269
|
-
const MIN_INTERVAL_MS = MS_PER_MINUTE; // 1m
|
|
270
|
-
const MAX_INTERVAL_MS = 7 * MS_PER_DAY; // 7d
|
|
271
|
-
|
|
272
|
-
const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE; // 30m
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Parses a user-friendly schedule string into the normalized backend format.
|
|
276
|
-
*/
|
|
277
|
-
function parseSchedule(schedule: Schedule | undefined): SyncSchedule {
|
|
278
|
-
if (schedule === "continuous") {
|
|
279
|
-
return { type: "continuous" };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!schedule) {
|
|
283
|
-
return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
287
|
-
if (!match || !match[1] || !match[2]) {
|
|
288
|
-
throw new Error(
|
|
289
|
-
`Invalid schedule format: "${schedule}". Use "continuous" or an interval like "30m", "1h", "1d".`,
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const value = parseInt(match[1], 10);
|
|
294
|
-
const unit = match[2] as TimeUnit;
|
|
295
|
-
|
|
296
|
-
let intervalMs: number;
|
|
297
|
-
switch (unit) {
|
|
298
|
-
case "m":
|
|
299
|
-
intervalMs = value * MS_PER_MINUTE;
|
|
300
|
-
break;
|
|
301
|
-
case "h":
|
|
302
|
-
intervalMs = value * MS_PER_HOUR;
|
|
303
|
-
break;
|
|
304
|
-
case "d":
|
|
305
|
-
intervalMs = value * MS_PER_DAY;
|
|
306
|
-
break;
|
|
307
|
-
default:
|
|
308
|
-
unreachable(unit);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (intervalMs < MIN_INTERVAL_MS) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`Schedule interval must be at least 1 minute. Got: "${schedule}"`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
if (intervalMs > MAX_INTERVAL_MS) {
|
|
317
|
-
throw new Error(
|
|
318
|
-
`Schedule interval must be at most 7 days. Got: "${schedule}"`,
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return { type: "interval", intervalMs };
|
|
323
|
-
}
|
|
@@ -41,7 +41,7 @@ describe("Worker.tool", () => {
|
|
|
41
41
|
expect(result).toEqual({ _tag: "success", value: "Hello, Alice!" });
|
|
42
42
|
|
|
43
43
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
44
|
-
`\n<
|
|
44
|
+
`\n<__notion_output__>{"_tag":"success","value":"Hello, Alice!"}</__notion_output__>\n`,
|
|
45
45
|
);
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -68,7 +68,7 @@ describe("Worker.tool", () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
71
|
-
`\n<
|
|
71
|
+
`\n<__notion_output__>{"_tag":"success","value":{"data":"Data for ID 42"}}</__notion_output__>\n`,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -125,7 +125,7 @@ describe("Worker.tool", () => {
|
|
|
125
125
|
|
|
126
126
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
127
127
|
expect.stringContaining(
|
|
128
|
-
`\n<
|
|
128
|
+
`\n<__notion_output__>{"_tag":"error","error":{"name":"InvalidToolInputError"`,
|
|
129
129
|
),
|
|
130
130
|
);
|
|
131
131
|
});
|
|
@@ -161,6 +161,50 @@ describe("Worker.tool", () => {
|
|
|
161
161
|
);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
it("accepts schema with format string types (date, datetime, etc.)", () => {
|
|
165
|
+
const capability = createToolCapability<
|
|
166
|
+
{ due: string; createdAt: string },
|
|
167
|
+
string
|
|
168
|
+
>("dateFormats", {
|
|
169
|
+
title: "Date Formats",
|
|
170
|
+
description: "Uses date and datetime format strings",
|
|
171
|
+
schema: j.object({
|
|
172
|
+
due: j.date(),
|
|
173
|
+
createdAt: j.datetime(),
|
|
174
|
+
}),
|
|
175
|
+
execute: ({ due, createdAt }) => `${due} ${createdAt}`,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(capability.config.schema).toBeDefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects invalid date-time format at runtime", async () => {
|
|
182
|
+
const capability = createToolCapability<{ ts: string }, string>(
|
|
183
|
+
"datetimeValidation",
|
|
184
|
+
{
|
|
185
|
+
title: "Datetime Validation",
|
|
186
|
+
description: "Validates datetime input",
|
|
187
|
+
schema: j.object({
|
|
188
|
+
ts: j.datetime(),
|
|
189
|
+
}),
|
|
190
|
+
execute: ({ ts }) => ts,
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const valid = await capability.handler({ ts: "2024-01-01T00:00:00Z" });
|
|
195
|
+
expect(valid).toEqual({
|
|
196
|
+
_tag: "success",
|
|
197
|
+
value: "2024-01-01T00:00:00Z",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const invalid = (await capability.handler({ ts: "not-a-date" })) as {
|
|
201
|
+
_tag: "error";
|
|
202
|
+
error: { name: string; message: string };
|
|
203
|
+
};
|
|
204
|
+
expect(invalid._tag).toBe("error");
|
|
205
|
+
expect(invalid.error.name).toBe("InvalidToolInputError");
|
|
206
|
+
});
|
|
207
|
+
|
|
164
208
|
it("accepts schema with nullable (string | null) properties", () => {
|
|
165
209
|
const capability = createToolCapability<
|
|
166
210
|
{ name: string; nickname: string | null },
|
package/src/capabilities/tool.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Ajv } from "ajv";
|
|
2
|
+
import addFormats from "ajv-formats";
|
|
2
3
|
import type { AnyJSONSchema } from "../json-schema.js";
|
|
3
4
|
import type { SchemaBuilder } from "../schema-builder.js";
|
|
4
5
|
import { getSchema } from "../schema-builder.js";
|
|
5
6
|
import type { HandlerOptions, JSONValue } from "../types.js";
|
|
6
7
|
import type { CapabilityContext } from "./context.js";
|
|
7
8
|
import { createCapabilityContext } from "./context.js";
|
|
9
|
+
import { writeOutput } from "./output.js";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Validates that every object schema in the tree has all its `properties`
|
|
@@ -160,6 +162,7 @@ export function createToolCapability<
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
const ajv = new Ajv();
|
|
165
|
+
addFormats(ajv);
|
|
163
166
|
const validateInput = ajv.compile(inputSchema);
|
|
164
167
|
const validateOutput = outputSchema ? ajv.compile(outputSchema) : null;
|
|
165
168
|
|
|
@@ -209,7 +212,7 @@ export function createToolCapability<
|
|
|
209
212
|
error: { name: error.name, message: error.message, trace: error.stack },
|
|
210
213
|
};
|
|
211
214
|
|
|
212
|
-
|
|
215
|
+
writeOutput(result);
|
|
213
216
|
|
|
214
217
|
return result;
|
|
215
218
|
}
|
|
@@ -235,7 +238,7 @@ export function createToolCapability<
|
|
|
235
238
|
},
|
|
236
239
|
};
|
|
237
240
|
|
|
238
|
-
|
|
241
|
+
writeOutput(result);
|
|
239
242
|
|
|
240
243
|
return result;
|
|
241
244
|
}
|
|
@@ -244,9 +247,7 @@ export function createToolCapability<
|
|
|
244
247
|
return result;
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
|
|
248
|
-
`\n<output>${JSON.stringify({ _tag: "success", value: result })}</output>\n`,
|
|
249
|
-
);
|
|
250
|
+
writeOutput({ _tag: "success", value: result });
|
|
250
251
|
|
|
251
252
|
return {
|
|
252
253
|
_tag: "success" as const,
|
|
@@ -266,7 +267,7 @@ export function createToolCapability<
|
|
|
266
267
|
error: { name: error.name, message: error.message, trace: error.stack },
|
|
267
268
|
};
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
writeOutput(result);
|
|
270
271
|
|
|
271
272
|
return result;
|
|
272
273
|
}
|