@project-ajax/sdk 0.0.74 → 0.0.76
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/capabilities/automation.d.ts +7 -5
- package/dist/capabilities/automation.d.ts.map +1 -1
- package/dist/capabilities/automation.js +4 -2
- package/dist/capabilities/context.d.ts +7 -0
- package/dist/capabilities/context.d.ts.map +1 -0
- package/dist/capabilities/context.js +15 -0
- package/dist/capabilities/sync.d.ts +23 -22
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +20 -4
- package/dist/capabilities/tool.d.ts +2 -1
- package/dist/capabilities/tool.d.ts.map +1 -1
- package/dist/capabilities/tool.js +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/pacer.d.ts +50 -0
- package/dist/pacer.d.ts.map +1 -0
- package/dist/pacer.js +48 -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 +11 -0
- package/dist/worker.d.ts +6 -5
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +3 -3
- package/package.json +8 -1
- package/src/capabilities/automation.test.ts +21 -11
- package/src/capabilities/automation.ts +13 -6
- package/src/capabilities/context.ts +23 -0
- package/src/capabilities/sync.ts +53 -27
- package/src/capabilities/tool.ts +5 -2
- package/src/index.ts +4 -1
- package/src/pacer.test.ts +41 -0
- package/src/pacer.ts +80 -0
- package/src/pacer_internal.ts +21 -0
- package/src/worker.ts +7 -5
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { CapabilityContext } from "./context.js";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Event provided to automation execute functions
|
|
3
4
|
*/
|
|
4
|
-
export interface
|
|
5
|
+
export interface AutomationEvent {
|
|
5
6
|
/**
|
|
6
7
|
* The ID of the page that triggered the automation (if applicable)
|
|
7
8
|
*/
|
|
@@ -54,10 +55,11 @@ export interface AutomationConfiguration {
|
|
|
54
55
|
description: string;
|
|
55
56
|
/**
|
|
56
57
|
* The function that executes when the automation is triggered
|
|
57
|
-
* @param
|
|
58
|
+
* @param event - Event data about the automation trigger, including page data if applicable
|
|
59
|
+
* @param context - The capability execution context (Notion client, etc.)
|
|
58
60
|
* @returns A promise that resolves when the automation completes
|
|
59
61
|
*/
|
|
60
|
-
execute: (context:
|
|
62
|
+
execute: (event: AutomationEvent, context: CapabilityContext) => Promise<void> | void;
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* Result returned from automation execution
|
|
@@ -81,6 +83,6 @@ export declare function createAutomationCapability(key: string, config: Automati
|
|
|
81
83
|
title: string;
|
|
82
84
|
description: string;
|
|
83
85
|
};
|
|
84
|
-
handler(
|
|
86
|
+
handler(event: AutomationEvent): Promise<void>;
|
|
85
87
|
};
|
|
86
88
|
//# sourceMappingURL=automation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation.d.ts","sourceRoot":"","sources":["../../src/capabilities/automation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"automation.d.ts","sourceRoot":"","sources":["../../src/capabilities/automation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,cAAc,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,OAAO,EAAE,CACR,KAAK,EAAE,eAAe,EACtB,OAAO,EAAE,iBAAiB,KACtB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,oBAAoB,GAAG,UAAU,CAC5C,OAAO,0BAA0B,CACjC,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,uBAAuB;;;;;;;mBAST,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;EAgBrD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExecutionError } from "../error.js";
|
|
2
|
+
import { createCapabilityContext } from "./context.js";
|
|
2
3
|
function createAutomationCapability(key, config) {
|
|
3
4
|
return {
|
|
4
5
|
_tag: "automation",
|
|
@@ -7,9 +8,10 @@ function createAutomationCapability(key, config) {
|
|
|
7
8
|
title: config.title,
|
|
8
9
|
description: config.description
|
|
9
10
|
},
|
|
10
|
-
async handler(
|
|
11
|
+
async handler(event) {
|
|
11
12
|
try {
|
|
12
|
-
|
|
13
|
+
const capabilityContext = createCapabilityContext();
|
|
14
|
+
await config.execute(event, capabilityContext);
|
|
13
15
|
process.stdout.write(
|
|
14
16
|
`
|
|
15
17
|
<output>${JSON.stringify({ status: "success" })}</output>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/capabilities/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG1C,MAAM,MAAM,iBAAiB,GAAG;IAC/B,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAgB,uBAAuB,IAAI,iBAAiB,CAc3D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Client } from "@notionhq/client";
|
|
2
|
+
function createCapabilityContext() {
|
|
3
|
+
const options = {};
|
|
4
|
+
if (process.env.NOTION_API_BASE_URL) {
|
|
5
|
+
options.baseUrl = process.env.NOTION_API_BASE_URL;
|
|
6
|
+
}
|
|
7
|
+
if (process.env.NOTION_API_TOKEN) {
|
|
8
|
+
options.auth = process.env.NOTION_API_TOKEN;
|
|
9
|
+
}
|
|
10
|
+
const notion = new Client(options);
|
|
11
|
+
return { notion };
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
createCapabilityContext
|
|
15
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type PacerEntry } from "../pacer_internal.js";
|
|
1
2
|
import type { PropertyConfiguration, PropertySchema, Schema } from "../schema.js";
|
|
2
3
|
import type { Icon, PeopleValue, PlaceValue, RelationValue, Schedule, SyncSchedule, TextValue } from "../types.js";
|
|
4
|
+
import type { CapabilityContext } from "./context.js";
|
|
3
5
|
/**
|
|
4
6
|
* Maps a property configuration to its corresponding value type.
|
|
5
7
|
*/
|
|
@@ -69,7 +71,7 @@ export type SyncChange<PK extends string, S extends PropertySchema<PK>> = SyncCh
|
|
|
69
71
|
/**
|
|
70
72
|
* Result returned from the sync execute function.
|
|
71
73
|
*/
|
|
72
|
-
export type SyncExecutionResult<PK extends string,
|
|
74
|
+
export type SyncExecutionResult<PK extends string, State = unknown> = {
|
|
73
75
|
/**
|
|
74
76
|
* The batch of changes to apply in this execution.
|
|
75
77
|
* Can include upserts (create/update) and deletes.
|
|
@@ -77,23 +79,23 @@ export type SyncExecutionResult<PK extends string, Context = unknown> = {
|
|
|
77
79
|
changes: SyncChange<PK, PropertySchema<PK>>[];
|
|
78
80
|
/**
|
|
79
81
|
* Indicates whether there is more data to fetch.
|
|
80
|
-
* - `true`: More data available, will trigger another execution with
|
|
82
|
+
* - `true`: More data available, will trigger another execution with nextState
|
|
81
83
|
* - `false`: No more data to fetch, sync is complete
|
|
82
84
|
*/
|
|
83
85
|
hasMore: boolean;
|
|
84
86
|
/**
|
|
85
|
-
* Optional
|
|
87
|
+
* Optional state data to pass to the next execution.
|
|
86
88
|
* Required if `hasMore` is `true`, ignored if `hasMore` is `false`.
|
|
87
89
|
* This can be any type of data (cursor, page number, timestamp, etc.).
|
|
88
|
-
* The same data will be provided
|
|
90
|
+
* The same data will be provided as `state` in the next execution.
|
|
89
91
|
*/
|
|
90
|
-
|
|
92
|
+
nextState?: State;
|
|
91
93
|
};
|
|
92
94
|
/**
|
|
93
95
|
* A configuration object that enables synchronization between a data
|
|
94
96
|
* source and a third-party source.
|
|
95
97
|
*/
|
|
96
|
-
export type SyncConfiguration<PK extends string, S extends Schema<PK>,
|
|
98
|
+
export type SyncConfiguration<PK extends string, S extends Schema<PK>, State = unknown> = {
|
|
97
99
|
/**
|
|
98
100
|
* The property of the data source that maps to a "primary key" in the
|
|
99
101
|
* third-party data. This is used to match existing pages to
|
|
@@ -117,45 +119,43 @@ export type SyncConfiguration<PK extends string, S extends Schema<PK>, Context =
|
|
|
117
119
|
mode?: SyncMode;
|
|
118
120
|
/**
|
|
119
121
|
* How often the sync should run.
|
|
120
|
-
* - "continuous": Run as frequently as the system allows
|
|
122
|
+
* - "continuous": Run as frequently as the system allows
|
|
121
123
|
* - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
|
|
122
124
|
*
|
|
123
125
|
* Minimum interval: 1 minute ("1m")
|
|
124
126
|
* Maximum interval: 7 days ("7d")
|
|
125
127
|
*
|
|
126
|
-
* @default "
|
|
128
|
+
* @default "30m"
|
|
127
129
|
*/
|
|
128
130
|
schedule?: Schedule;
|
|
129
131
|
/**
|
|
130
132
|
* A function that fetches the data to sync from the third-party service.
|
|
131
133
|
*
|
|
132
134
|
* This function can return all data at once, or implement pagination by:
|
|
133
|
-
* 1. Returning a batch of changes with `hasMore: true` and a `
|
|
134
|
-
* 2. The runtime will call execute again with that
|
|
135
|
+
* 1. Returning a batch of changes with `hasMore: true` and a `nextState`
|
|
136
|
+
* 2. The runtime will call execute again with that state
|
|
135
137
|
* 3. Continue until `hasMore: false` is returned
|
|
136
138
|
*
|
|
137
139
|
* The runtime will handle diffing against the data source and creating,
|
|
138
140
|
* updating, and deleting pages as necessary.
|
|
139
141
|
*
|
|
140
|
-
* @param
|
|
141
|
-
* @
|
|
142
|
+
* @param state - User-defined state from the previous execution (undefined on first call)
|
|
143
|
+
* @param context - Runtime context, including Notion client
|
|
144
|
+
* @returns A result containing changes, hasMore status, and optional nextState
|
|
142
145
|
*/
|
|
143
|
-
execute: (context
|
|
144
|
-
};
|
|
145
|
-
export type SyncHandlerResult<PK extends string, Context = unknown> = {
|
|
146
|
-
primaryKeyProperty: PK;
|
|
147
|
-
changes: SyncChange<PK, PropertySchema<PK>>[];
|
|
148
|
-
hasMore: boolean;
|
|
149
|
-
nextContext?: Context;
|
|
150
|
-
mode?: SyncMode;
|
|
146
|
+
execute: (state: State | undefined, context: CapabilityContext) => Promise<SyncExecutionResult<PK, State>>;
|
|
151
147
|
};
|
|
152
148
|
export type SyncCapability = ReturnType<typeof createSyncCapability>;
|
|
153
149
|
/**
|
|
154
|
-
*
|
|
150
|
+
* Runtime context object passed from the runtime to sync capability handlers.
|
|
155
151
|
*/
|
|
156
152
|
type RuntimeContext<UserContext = unknown> = {
|
|
157
|
-
/** The user-defined/-controlled
|
|
153
|
+
/** The user-defined/-controlled state (cursor, pagination state, etc.) */
|
|
154
|
+
state?: UserContext;
|
|
155
|
+
/** Legacy field for user-defined/-controlled state. */
|
|
158
156
|
userContext?: UserContext;
|
|
157
|
+
/** Pacer state from the server for rate limiting. */
|
|
158
|
+
pacers?: Record<string, PacerEntry>;
|
|
159
159
|
};
|
|
160
160
|
/**
|
|
161
161
|
* Creates a special handler for syncing third-party data to a collection.
|
|
@@ -177,6 +177,7 @@ export declare function createSyncCapability<PK extends string, S extends Schema
|
|
|
177
177
|
changes: SyncChange<PK, PropertySchema<PK>>[];
|
|
178
178
|
hasMore: boolean;
|
|
179
179
|
nextUserContext: Context | undefined;
|
|
180
|
+
nextPacerState: Record<string, PacerEntry>;
|
|
180
181
|
}>;
|
|
181
182
|
};
|
|
182
183
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/capabilities/sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,qBAAqB,EACrB,cAAc,EACd,MAAM,EACN,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACX,IAAI,EACJ,WAAW,EACX,UAAU,EACV,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,SAAS,EAET,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/capabilities/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { ExecutionError, unreachable } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
getPacerState,
|
|
4
|
+
setPacerState
|
|
5
|
+
} from "../pacer_internal.js";
|
|
6
|
+
import { createCapabilityContext } from "./context.js";
|
|
2
7
|
function createSyncCapability(key, syncConfiguration) {
|
|
3
8
|
return {
|
|
4
9
|
_tag: "sync",
|
|
@@ -10,14 +15,21 @@ function createSyncCapability(key, syncConfiguration) {
|
|
|
10
15
|
schedule: parseSchedule(syncConfiguration.schedule)
|
|
11
16
|
},
|
|
12
17
|
async handler(runtimeContext) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
18
|
+
const capabilityContext = createCapabilityContext();
|
|
19
|
+
const state = runtimeContext?.state ?? runtimeContext?.userContext;
|
|
20
|
+
const pacerState = {
|
|
21
|
+
pacers: runtimeContext?.pacers ?? {}
|
|
22
|
+
};
|
|
23
|
+
setPacerState(pacerState);
|
|
24
|
+
const executionResult = await syncConfiguration.execute(state, capabilityContext).catch((err) => {
|
|
15
25
|
throw new ExecutionError(err);
|
|
16
26
|
});
|
|
27
|
+
const updatedPacerState = getPacerState();
|
|
17
28
|
const result = {
|
|
18
29
|
changes: executionResult.changes,
|
|
19
30
|
hasMore: executionResult.hasMore,
|
|
20
|
-
nextUserContext: executionResult.
|
|
31
|
+
nextUserContext: executionResult.nextState,
|
|
32
|
+
nextPacerState: updatedPacerState.pacers
|
|
21
33
|
};
|
|
22
34
|
process.stdout.write(`
|
|
23
35
|
<output>${JSON.stringify(result)}</output>
|
|
@@ -31,10 +43,14 @@ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
|
31
43
|
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
32
44
|
const MIN_INTERVAL_MS = MS_PER_MINUTE;
|
|
33
45
|
const MAX_INTERVAL_MS = 7 * MS_PER_DAY;
|
|
46
|
+
const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE;
|
|
34
47
|
function parseSchedule(schedule) {
|
|
35
|
-
if (
|
|
48
|
+
if (schedule === "continuous") {
|
|
36
49
|
return { type: "continuous" };
|
|
37
50
|
}
|
|
51
|
+
if (!schedule) {
|
|
52
|
+
return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
|
|
53
|
+
}
|
|
38
54
|
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
39
55
|
if (!match || !match[1] || !match[2]) {
|
|
40
56
|
throw new Error(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type JSONSchemaType } from "ajv";
|
|
2
2
|
import type { JSONValue } from "../types.js";
|
|
3
|
+
import type { CapabilityContext } from "./context.js";
|
|
3
4
|
export interface ToolConfiguration<I extends JSONValue, O extends JSONValue = JSONValue> {
|
|
4
5
|
title: string;
|
|
5
6
|
description: string;
|
|
6
7
|
schema: JSONSchemaType<I>;
|
|
7
8
|
outputSchema?: JSONSchemaType<O>;
|
|
8
|
-
execute: (input: I) => O | Promise<O>;
|
|
9
|
+
execute: (input: I, context: CapabilityContext) => O | Promise<O>;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
12
|
* An error returned when the input to a tool doesn't match the input schema.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/capabilities/tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../../src/capabilities/tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,MAAM,WAAW,iBAAiB,CACjC,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,SAAS,GAAG,SAAS;IAE/B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAC1B,YAAY,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,iBAAiB,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAClE;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;IAK3B,MAAM;;;;CAMN;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;IAK3B,MAAM;;;;CAMN;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;IAK3B,MAAM;;;;CAMN;AAED,MAAM,MAAM,cAAc,CACzB,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,SAAS,GAAG,SAAS,IAC5B,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAElD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,SAAS,GAAG,SAAS,EAC9B,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC;;;;;;;;;mBAgBvB,SAAS,GAAG,OAAO,CACrC;QACA,IAAI,EAAE,SAAS,CAAC;QAChB,KAAK,EAAE,CAAC,CAAC;KACR,GACD;QACA,IAAI,EAAE,OAAO,CAAC;QACd,KAAK,EACF,qBAAqB,GACrB,sBAAsB,GACtB,kBAAkB,CAAC;KACrB,CACH;EA2DF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Ajv } from "ajv";
|
|
2
|
+
import { createCapabilityContext } from "./context.js";
|
|
2
3
|
class InvalidToolInputError extends Error {
|
|
3
4
|
constructor(message) {
|
|
4
5
|
super(message);
|
|
@@ -67,7 +68,8 @@ function createToolCapability(key, config) {
|
|
|
67
68
|
return result;
|
|
68
69
|
}
|
|
69
70
|
try {
|
|
70
|
-
const
|
|
71
|
+
const capabilityContext = createCapabilityContext();
|
|
72
|
+
const result = await config.execute(input, capabilityContext);
|
|
71
73
|
if (validateOutput && !validateOutput(result)) {
|
|
72
74
|
const result2 = {
|
|
73
75
|
_tag: "error",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { emojiIcon, imageIcon, notionIcon, place } from "./builder.js";
|
|
2
|
-
export type { AutomationCapability, AutomationConfiguration,
|
|
2
|
+
export type { AutomationCapability, AutomationConfiguration, AutomationEvent, PageObjectResponse, } from "./capabilities/automation.js";
|
|
3
|
+
export type { CapabilityContext } from "./capabilities/context.js";
|
|
3
4
|
export type { NotionManagedOAuthConfiguration, OAuthCapability, OAuthConfiguration, UserManagedOAuthConfiguration, } from "./capabilities/oauth.js";
|
|
4
5
|
export type { SyncCapability, SyncChange, SyncChangeDelete, SyncChangeUpsert, SyncConfiguration, SyncExecutionResult, SyncMode, } from "./capabilities/sync.js";
|
|
5
6
|
export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
7
|
+
export { Pacer } from "./pacer.js";
|
|
6
8
|
export type { Icon, ImageIcon, NoticonColor, NoticonName, PlaceValue, Schedule, } from "./types.js";
|
|
7
9
|
export { Worker } from "./worker.js";
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,YAAY,EACX,oBAAoB,EACpB,uBAAuB,EACvB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,YAAY,EACX,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,GAClB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,YAAY,EACX,+BAA+B,EAC/B,eAAe,EACf,kBAAkB,EAClB,6BAA6B,GAC7B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,QAAQ,GACR,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EACX,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,EACV,QAAQ,GACR,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/pacer.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacer module for rate limiting API requests.
|
|
3
|
+
*
|
|
4
|
+
* The Pacer ensures requests are evenly spaced over time to respect third-party
|
|
5
|
+
* API rate limits. Instead of making all requests immediately until hitting a 429,
|
|
6
|
+
* Pacer paces requests throughout the rate limit window.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Pacer } from "@project-ajax/sdk/pacer"
|
|
11
|
+
*
|
|
12
|
+
* // Rate limit: 10 requests per minute
|
|
13
|
+
* await Pacer.wait("salesforce-api", { requests: 10, intervalMs: 60 * 1000 })
|
|
14
|
+
*
|
|
15
|
+
* // Now make your API call
|
|
16
|
+
* const data = await fetchFromSalesforce()
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
export type PacerLimit = {
|
|
22
|
+
requests: number;
|
|
23
|
+
intervalMs: number;
|
|
24
|
+
};
|
|
25
|
+
export declare const Pacer: {
|
|
26
|
+
/**
|
|
27
|
+
* Wait until a request can proceed under the specified rate limit.
|
|
28
|
+
*
|
|
29
|
+
* This function paces requests evenly across the rate limit interval. For example,
|
|
30
|
+
* with 60 requests allowed per hour, requests are spaced approximately 1 minute apart.
|
|
31
|
+
*
|
|
32
|
+
* @param key - Unique identifier for this rate limit scope (e.g., "salesforce-api", "jira:token123"). If you're not sure, use any string.
|
|
33
|
+
* @param limit - Rate limit configuration: `requests` per `intervalMs`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // Salesforce: 30,000 requests per 24 hours
|
|
38
|
+
* await Pacer.wait("salesforce", { requests: 30_000, intervalMs: 24 * 60 * 60 * 1000 })
|
|
39
|
+
*
|
|
40
|
+
* // Jira: 100 requests per minute
|
|
41
|
+
* await Pacer.wait("jira", { requests: 100, intervalMs: 60 * 1000 })
|
|
42
|
+
*
|
|
43
|
+
* // Multiple rate limits for different endpoints
|
|
44
|
+
* await Pacer.wait("api-read", { requests: 1000, intervalMs: 60 * 1000 })
|
|
45
|
+
* await Pacer.wait("api-write", { requests: 100, intervalMs: 60 * 1000 })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
wait(key: string, limit: PacerLimit): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=pacer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer.d.ts","sourceRoot":"","sources":["../src/pacer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,KAAK;IACjB;;;;;;;;;;;;;;;;;;;;;OAqBG;cACa,MAAM,SAAS,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;CA4BzD,CAAC"}
|
package/dist/pacer.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getPacerState, setPacerState } from "./pacer_internal.js";
|
|
2
|
+
const Pacer = {
|
|
3
|
+
/**
|
|
4
|
+
* Wait until a request can proceed under the specified rate limit.
|
|
5
|
+
*
|
|
6
|
+
* This function paces requests evenly across the rate limit interval. For example,
|
|
7
|
+
* with 60 requests allowed per hour, requests are spaced approximately 1 minute apart.
|
|
8
|
+
*
|
|
9
|
+
* @param key - Unique identifier for this rate limit scope (e.g., "salesforce-api", "jira:token123"). If you're not sure, use any string.
|
|
10
|
+
* @param limit - Rate limit configuration: `requests` per `intervalMs`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Salesforce: 30,000 requests per 24 hours
|
|
15
|
+
* await Pacer.wait("salesforce", { requests: 30_000, intervalMs: 24 * 60 * 60 * 1000 })
|
|
16
|
+
*
|
|
17
|
+
* // Jira: 100 requests per minute
|
|
18
|
+
* await Pacer.wait("jira", { requests: 100, intervalMs: 60 * 1000 })
|
|
19
|
+
*
|
|
20
|
+
* // Multiple rate limits for different endpoints
|
|
21
|
+
* await Pacer.wait("api-read", { requests: 1000, intervalMs: 60 * 1000 })
|
|
22
|
+
* await Pacer.wait("api-write", { requests: 100, intervalMs: 60 * 1000 })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
async wait(key, limit) {
|
|
26
|
+
if (limit.requests <= 0) {
|
|
27
|
+
throw new Error("requests must be greater than 0");
|
|
28
|
+
}
|
|
29
|
+
if (limit.intervalMs <= 0) {
|
|
30
|
+
throw new Error("intervalMs must be greater than 0");
|
|
31
|
+
}
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const paceMs = Math.ceil(limit.intervalMs / limit.requests);
|
|
34
|
+
const state = getPacerState();
|
|
35
|
+
const existing = state.pacers[key];
|
|
36
|
+
const lastScheduledAtMs = existing?.lastScheduledAtMs ?? 0;
|
|
37
|
+
const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
|
|
38
|
+
const delayMs = scheduledAtMs - now;
|
|
39
|
+
state.pacers[key] = { lastScheduledAtMs: scheduledAtMs };
|
|
40
|
+
setPacerState(state);
|
|
41
|
+
if (delayMs > 0) {
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export {
|
|
47
|
+
Pacer
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer.test.d.ts","sourceRoot":"","sources":["../src/pacer.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pacer_internal.d.ts","sourceRoot":"","sources":["../src/pacer_internal.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACnC,CAAC;AAIF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAErD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C"}
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { AutomationCapability, AutomationConfiguration,
|
|
1
|
+
import type { AutomationCapability, AutomationConfiguration, AutomationEvent } from "./capabilities/automation.js";
|
|
2
|
+
import type { CapabilityContext } from "./capabilities/context.js";
|
|
2
3
|
import type { NotionManagedOAuthConfiguration, OAuthCapability, OAuthConfiguration, UserManagedOAuthConfiguration } from "./capabilities/oauth.js";
|
|
3
4
|
import type { SyncCapability, SyncConfiguration } from "./capabilities/sync.js";
|
|
4
5
|
import type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
5
6
|
import type { Schema } from "./schema.js";
|
|
6
7
|
import type { JSONValue } from "./types.js";
|
|
7
|
-
export type { AutomationConfiguration,
|
|
8
|
+
export type { AutomationConfiguration, AutomationEvent, CapabilityContext, OAuthConfiguration, NotionManagedOAuthConfiguration, UserManagedOAuthConfiguration, SyncConfiguration, ToolConfiguration, };
|
|
8
9
|
type Capability = SyncCapability | ToolCapability<any, any> | AutomationCapability | OAuthCapability;
|
|
9
10
|
export declare class Worker {
|
|
10
11
|
#private;
|
|
@@ -72,7 +73,7 @@ export declare class Worker {
|
|
|
72
73
|
* },
|
|
73
74
|
* required: ["name"],
|
|
74
75
|
* },
|
|
75
|
-
* execute: ({ name }) => {
|
|
76
|
+
* execute: ({ name }, { notion }) => {
|
|
76
77
|
* return `Hello, ${name}!`;
|
|
77
78
|
* },
|
|
78
79
|
* })
|
|
@@ -96,8 +97,8 @@ export declare class Worker {
|
|
|
96
97
|
* worker.automation("sendWelcomeEmail", {
|
|
97
98
|
* title: "Send Welcome Email",
|
|
98
99
|
* description: "Sends a welcome email when a new user is added",
|
|
99
|
-
* execute: async (
|
|
100
|
-
* const { pageId, pageData } =
|
|
100
|
+
* execute: async (event, { notion }) => {
|
|
101
|
+
* const { pageId, pageData } = event;
|
|
101
102
|
*
|
|
102
103
|
* // Access page properties from the Public API format
|
|
103
104
|
* if (pageData) {
|
package/dist/worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EACpB,uBAAuB,EACvB,
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EACX,+BAA+B,EAC/B,eAAe,EACf,kBAAkB,EAClB,6BAA6B,EAC7B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,YAAY,EACX,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,+BAA+B,EAC/B,6BAA6B,EAC7B,iBAAiB,EACjB,iBAAiB,GACjB,CAAC;AAMF,KAAK,UAAU,GACZ,cAAc,GAEd,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,GACxB,oBAAoB,GACpB,eAAe,CAAC;AAMnB,qBAAa,MAAM;;IAGlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,EAC9D,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,GACvC,cAAc;IAOjB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,IAAI,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS,EACxD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC7B,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;IAQvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,UAAU,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,uBAAuB,GAC7B,oBAAoB;IAOvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,eAAe;IAO/D;;OAEG;IACH,IAAI,YAAY,IAAI,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC,EAAE,CAMhE;IAED;;;;;;OAMG;IACG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;CAyB1D"}
|
package/dist/worker.js
CHANGED
|
@@ -73,7 +73,7 @@ class Worker {
|
|
|
73
73
|
* },
|
|
74
74
|
* required: ["name"],
|
|
75
75
|
* },
|
|
76
|
-
* execute: ({ name }) => {
|
|
76
|
+
* execute: ({ name }, { notion }) => {
|
|
77
77
|
* return `Hello, ${name}!`;
|
|
78
78
|
* },
|
|
79
79
|
* })
|
|
@@ -102,8 +102,8 @@ class Worker {
|
|
|
102
102
|
* worker.automation("sendWelcomeEmail", {
|
|
103
103
|
* title: "Send Welcome Email",
|
|
104
104
|
* description: "Sends a welcome email when a new user is added",
|
|
105
|
-
* execute: async (
|
|
106
|
-
* const { pageId, pageData } =
|
|
105
|
+
* execute: async (event, { notion }) => {
|
|
106
|
+
* const { pageId, pageData } = event;
|
|
107
107
|
*
|
|
108
108
|
* // Access page properties from the Public API format
|
|
109
109
|
* if (pageData) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@project-ajax/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.76",
|
|
4
4
|
"description": "An SDK for building workers for the Project Ajax platform",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"./types": {
|
|
29
29
|
"types": "./dist/types.d.ts",
|
|
30
30
|
"default": "./dist/types.js"
|
|
31
|
+
},
|
|
32
|
+
"./pacer": {
|
|
33
|
+
"types": "./dist/pacer.d.ts",
|
|
34
|
+
"default": "./dist/pacer.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"engines": {
|
|
@@ -58,6 +62,9 @@
|
|
|
58
62
|
"typescript": "^5.9.3",
|
|
59
63
|
"vitest": "^4.0.8"
|
|
60
64
|
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@notionhq/client": "^2.2.15"
|
|
67
|
+
},
|
|
61
68
|
"dependencies": {
|
|
62
69
|
"ajv": "^8.17.1"
|
|
63
70
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
vi,
|
|
9
9
|
} from "vitest";
|
|
10
10
|
import { ExecutionError } from "../error.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type { PageObjectResponse } from "./automation.js";
|
|
12
12
|
import { createAutomationCapability } from "./automation.js";
|
|
13
13
|
|
|
14
14
|
describe("createAutomationCapability", () => {
|
|
@@ -45,14 +45,19 @@ describe("createAutomationCapability", () => {
|
|
|
45
45
|
execute: executeFn,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
const
|
|
48
|
+
const event = {
|
|
49
49
|
pageId: "page-123",
|
|
50
50
|
actionType: "test_action",
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
await capability.handler(
|
|
53
|
+
await capability.handler(event);
|
|
54
54
|
|
|
55
|
-
expect(executeFn).toHaveBeenCalledWith(
|
|
55
|
+
expect(executeFn).toHaveBeenCalledWith(
|
|
56
|
+
event,
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
notion: expect.any(Object),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
56
61
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
57
62
|
`\n<output>{"status":"success"}</output>\n`,
|
|
58
63
|
);
|
|
@@ -85,15 +90,20 @@ describe("createAutomationCapability", () => {
|
|
|
85
90
|
public_url: null,
|
|
86
91
|
};
|
|
87
92
|
|
|
88
|
-
const
|
|
93
|
+
const event = {
|
|
89
94
|
pageId: "page-789",
|
|
90
95
|
actionType: "process_page",
|
|
91
96
|
pageData,
|
|
92
97
|
};
|
|
93
98
|
|
|
94
|
-
await capability.handler(
|
|
99
|
+
await capability.handler(event);
|
|
95
100
|
|
|
96
|
-
expect(executeFn).toHaveBeenCalledWith(
|
|
101
|
+
expect(executeFn).toHaveBeenCalledWith(
|
|
102
|
+
event,
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
notion: expect.any(Object),
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
97
107
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
98
108
|
`\n<output>{"status":"success"}</output>\n`,
|
|
99
109
|
);
|
|
@@ -108,12 +118,12 @@ describe("createAutomationCapability", () => {
|
|
|
108
118
|
},
|
|
109
119
|
});
|
|
110
120
|
|
|
111
|
-
const
|
|
121
|
+
const event = {
|
|
112
122
|
pageId: "page-error",
|
|
113
123
|
actionType: "error_action",
|
|
114
124
|
};
|
|
115
125
|
|
|
116
|
-
await expect(capability.handler(
|
|
126
|
+
await expect(capability.handler(event)).rejects.toThrow(ExecutionError);
|
|
117
127
|
|
|
118
128
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
119
129
|
`\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: Something went wrong"}}</output>\n`,
|
|
@@ -129,11 +139,11 @@ describe("createAutomationCapability", () => {
|
|
|
129
139
|
},
|
|
130
140
|
});
|
|
131
141
|
|
|
132
|
-
const
|
|
142
|
+
const event = {
|
|
133
143
|
actionType: "string_error_action",
|
|
134
144
|
};
|
|
135
145
|
|
|
136
|
-
await expect(capability.handler(
|
|
146
|
+
await expect(capability.handler(event)).rejects.toThrow(ExecutionError);
|
|
137
147
|
|
|
138
148
|
expect(stdoutSpy).toHaveBeenCalledWith(
|
|
139
149
|
`\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: String error"}}</output>\n`,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ExecutionError } from "../error.js";
|
|
2
|
+
import type { CapabilityContext } from "./context.js";
|
|
3
|
+
import { createCapabilityContext } from "./context.js";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* Event provided to automation execute functions
|
|
5
7
|
*/
|
|
6
|
-
export interface
|
|
8
|
+
export interface AutomationEvent {
|
|
7
9
|
/**
|
|
8
10
|
* The ID of the page that triggered the automation (if applicable)
|
|
9
11
|
*/
|
|
@@ -56,10 +58,14 @@ export interface AutomationConfiguration {
|
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* The function that executes when the automation is triggered
|
|
59
|
-
* @param
|
|
61
|
+
* @param event - Event data about the automation trigger, including page data if applicable
|
|
62
|
+
* @param context - The capability execution context (Notion client, etc.)
|
|
60
63
|
* @returns A promise that resolves when the automation completes
|
|
61
64
|
*/
|
|
62
|
-
execute: (
|
|
65
|
+
execute: (
|
|
66
|
+
event: AutomationEvent,
|
|
67
|
+
context: CapabilityContext,
|
|
68
|
+
) => Promise<void> | void;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
@@ -92,9 +98,10 @@ export function createAutomationCapability(
|
|
|
92
98
|
title: config.title,
|
|
93
99
|
description: config.description,
|
|
94
100
|
},
|
|
95
|
-
async handler(
|
|
101
|
+
async handler(event: AutomationEvent): Promise<void> {
|
|
96
102
|
try {
|
|
97
|
-
|
|
103
|
+
const capabilityContext = createCapabilityContext();
|
|
104
|
+
await config.execute(event, capabilityContext);
|
|
98
105
|
process.stdout.write(
|
|
99
106
|
`\n<output>${JSON.stringify({ status: "success" })}</output>\n`,
|
|
100
107
|
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Client } from "@notionhq/client";
|
|
2
|
+
import type { ClientOptions } from "@notionhq/client/build/src/Client.js";
|
|
3
|
+
|
|
4
|
+
export type CapabilityContext = {
|
|
5
|
+
/** Notion API SDK client for this execution. */
|
|
6
|
+
notion: Client;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createCapabilityContext(): CapabilityContext {
|
|
10
|
+
const options: ClientOptions = {};
|
|
11
|
+
|
|
12
|
+
if (process.env.NOTION_API_BASE_URL) {
|
|
13
|
+
options.baseUrl = process.env.NOTION_API_BASE_URL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (process.env.NOTION_API_TOKEN) {
|
|
17
|
+
options.auth = process.env.NOTION_API_TOKEN;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const notion = new Client(options);
|
|
21
|
+
|
|
22
|
+
return { notion };
|
|
23
|
+
}
|
package/src/capabilities/sync.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { ExecutionError, unreachable } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
getPacerState,
|
|
4
|
+
type PacerEntry,
|
|
5
|
+
type PacerState,
|
|
6
|
+
setPacerState,
|
|
7
|
+
} from "../pacer_internal.js";
|
|
2
8
|
import type {
|
|
3
9
|
PropertyConfiguration,
|
|
4
10
|
PropertySchema,
|
|
@@ -14,6 +20,8 @@ import type {
|
|
|
14
20
|
TextValue,
|
|
15
21
|
TimeUnit,
|
|
16
22
|
} from "../types.js";
|
|
23
|
+
import type { CapabilityContext } from "./context.js";
|
|
24
|
+
import { createCapabilityContext } from "./context.js";
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Maps a property configuration to its corresponding value type.
|
|
@@ -96,7 +104,7 @@ export type SyncChange<PK extends string, S extends PropertySchema<PK>> =
|
|
|
96
104
|
/**
|
|
97
105
|
* Result returned from the sync execute function.
|
|
98
106
|
*/
|
|
99
|
-
export type SyncExecutionResult<PK extends string,
|
|
107
|
+
export type SyncExecutionResult<PK extends string, State = unknown> = {
|
|
100
108
|
/**
|
|
101
109
|
* The batch of changes to apply in this execution.
|
|
102
110
|
* Can include upserts (create/update) and deletes.
|
|
@@ -105,18 +113,18 @@ export type SyncExecutionResult<PK extends string, Context = unknown> = {
|
|
|
105
113
|
|
|
106
114
|
/**
|
|
107
115
|
* Indicates whether there is more data to fetch.
|
|
108
|
-
* - `true`: More data available, will trigger another execution with
|
|
116
|
+
* - `true`: More data available, will trigger another execution with nextState
|
|
109
117
|
* - `false`: No more data to fetch, sync is complete
|
|
110
118
|
*/
|
|
111
119
|
hasMore: boolean;
|
|
112
120
|
|
|
113
121
|
/**
|
|
114
|
-
* Optional
|
|
122
|
+
* Optional state data to pass to the next execution.
|
|
115
123
|
* Required if `hasMore` is `true`, ignored if `hasMore` is `false`.
|
|
116
124
|
* This can be any type of data (cursor, page number, timestamp, etc.).
|
|
117
|
-
* The same data will be provided
|
|
125
|
+
* The same data will be provided as `state` in the next execution.
|
|
118
126
|
*/
|
|
119
|
-
|
|
127
|
+
nextState?: State;
|
|
120
128
|
};
|
|
121
129
|
|
|
122
130
|
/**
|
|
@@ -126,7 +134,7 @@ export type SyncExecutionResult<PK extends string, Context = unknown> = {
|
|
|
126
134
|
export type SyncConfiguration<
|
|
127
135
|
PK extends string,
|
|
128
136
|
S extends Schema<PK>,
|
|
129
|
-
|
|
137
|
+
State = unknown,
|
|
130
138
|
> = {
|
|
131
139
|
/**
|
|
132
140
|
* The property of the data source that maps to a "primary key" in the
|
|
@@ -154,13 +162,13 @@ export type SyncConfiguration<
|
|
|
154
162
|
|
|
155
163
|
/**
|
|
156
164
|
* How often the sync should run.
|
|
157
|
-
* - "continuous": Run as frequently as the system allows
|
|
165
|
+
* - "continuous": Run as frequently as the system allows
|
|
158
166
|
* - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
|
|
159
167
|
*
|
|
160
168
|
* Minimum interval: 1 minute ("1m")
|
|
161
169
|
* Maximum interval: 7 days ("7d")
|
|
162
170
|
*
|
|
163
|
-
* @default "
|
|
171
|
+
* @default "30m"
|
|
164
172
|
*/
|
|
165
173
|
schedule?: Schedule;
|
|
166
174
|
|
|
@@ -168,35 +176,35 @@ export type SyncConfiguration<
|
|
|
168
176
|
* A function that fetches the data to sync from the third-party service.
|
|
169
177
|
*
|
|
170
178
|
* This function can return all data at once, or implement pagination by:
|
|
171
|
-
* 1. Returning a batch of changes with `hasMore: true` and a `
|
|
172
|
-
* 2. The runtime will call execute again with that
|
|
179
|
+
* 1. Returning a batch of changes with `hasMore: true` and a `nextState`
|
|
180
|
+
* 2. The runtime will call execute again with that state
|
|
173
181
|
* 3. Continue until `hasMore: false` is returned
|
|
174
182
|
*
|
|
175
183
|
* The runtime will handle diffing against the data source and creating,
|
|
176
184
|
* updating, and deleting pages as necessary.
|
|
177
185
|
*
|
|
178
|
-
* @param
|
|
179
|
-
* @
|
|
186
|
+
* @param state - User-defined state from the previous execution (undefined on first call)
|
|
187
|
+
* @param context - Runtime context, including Notion client
|
|
188
|
+
* @returns A result containing changes, hasMore status, and optional nextState
|
|
180
189
|
*/
|
|
181
|
-
execute: (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
primaryKeyProperty: PK;
|
|
186
|
-
changes: SyncChange<PK, PropertySchema<PK>>[];
|
|
187
|
-
hasMore: boolean;
|
|
188
|
-
nextContext?: Context;
|
|
189
|
-
mode?: SyncMode;
|
|
190
|
+
execute: (
|
|
191
|
+
state: State | undefined,
|
|
192
|
+
context: CapabilityContext,
|
|
193
|
+
) => Promise<SyncExecutionResult<PK, State>>;
|
|
190
194
|
};
|
|
191
195
|
|
|
192
196
|
export type SyncCapability = ReturnType<typeof createSyncCapability>;
|
|
193
197
|
|
|
194
198
|
/**
|
|
195
|
-
*
|
|
199
|
+
* Runtime context object passed from the runtime to sync capability handlers.
|
|
196
200
|
*/
|
|
197
201
|
type RuntimeContext<UserContext = unknown> = {
|
|
198
|
-
/** The user-defined/-controlled
|
|
202
|
+
/** The user-defined/-controlled state (cursor, pagination state, etc.) */
|
|
203
|
+
state?: UserContext;
|
|
204
|
+
/** Legacy field for user-defined/-controlled state. */
|
|
199
205
|
userContext?: UserContext;
|
|
206
|
+
/** Pacer state from the server for rate limiting. */
|
|
207
|
+
pacers?: Record<string, PacerEntry>;
|
|
200
208
|
};
|
|
201
209
|
|
|
202
210
|
/**
|
|
@@ -221,17 +229,29 @@ export function createSyncCapability<
|
|
|
221
229
|
schedule: parseSchedule(syncConfiguration.schedule),
|
|
222
230
|
},
|
|
223
231
|
async handler(runtimeContext?: RuntimeContext<Context>) {
|
|
224
|
-
const
|
|
232
|
+
const capabilityContext = createCapabilityContext();
|
|
233
|
+
const state = runtimeContext?.state ?? runtimeContext?.userContext;
|
|
234
|
+
|
|
235
|
+
// Initialize pacer state from runtime context
|
|
236
|
+
const pacerState: PacerState = {
|
|
237
|
+
pacers: runtimeContext?.pacers ?? {},
|
|
238
|
+
};
|
|
239
|
+
setPacerState(pacerState);
|
|
240
|
+
|
|
225
241
|
const executionResult = await syncConfiguration
|
|
226
|
-
.execute(
|
|
242
|
+
.execute(state, capabilityContext)
|
|
227
243
|
.catch((err) => {
|
|
228
244
|
throw new ExecutionError(err);
|
|
229
245
|
});
|
|
230
246
|
|
|
247
|
+
// Get updated pacer state after execution
|
|
248
|
+
const updatedPacerState = getPacerState();
|
|
249
|
+
|
|
231
250
|
const result = {
|
|
232
251
|
changes: executionResult.changes,
|
|
233
252
|
hasMore: executionResult.hasMore,
|
|
234
|
-
nextUserContext: executionResult.
|
|
253
|
+
nextUserContext: executionResult.nextState,
|
|
254
|
+
nextPacerState: updatedPacerState.pacers,
|
|
235
255
|
};
|
|
236
256
|
|
|
237
257
|
process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
|
|
@@ -248,14 +268,20 @@ const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
|
248
268
|
const MIN_INTERVAL_MS = MS_PER_MINUTE; // 1m
|
|
249
269
|
const MAX_INTERVAL_MS = 7 * MS_PER_DAY; // 7d
|
|
250
270
|
|
|
271
|
+
const DEFAULT_INTERVAL_MS = 30 * MS_PER_MINUTE; // 30m
|
|
272
|
+
|
|
251
273
|
/**
|
|
252
274
|
* Parses a user-friendly schedule string into the normalized backend format.
|
|
253
275
|
*/
|
|
254
276
|
function parseSchedule(schedule: Schedule | undefined): SyncSchedule {
|
|
255
|
-
if (
|
|
277
|
+
if (schedule === "continuous") {
|
|
256
278
|
return { type: "continuous" };
|
|
257
279
|
}
|
|
258
280
|
|
|
281
|
+
if (!schedule) {
|
|
282
|
+
return { type: "interval", intervalMs: DEFAULT_INTERVAL_MS };
|
|
283
|
+
}
|
|
284
|
+
|
|
259
285
|
const match = schedule.match(/^(\d+)(m|h|d)$/);
|
|
260
286
|
if (!match || !match[1] || !match[2]) {
|
|
261
287
|
throw new Error(
|
package/src/capabilities/tool.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Ajv, type JSONSchemaType } from "ajv";
|
|
2
2
|
import type { JSONValue } from "../types.js";
|
|
3
|
+
import type { CapabilityContext } from "./context.js";
|
|
4
|
+
import { createCapabilityContext } from "./context.js";
|
|
3
5
|
|
|
4
6
|
export interface ToolConfiguration<
|
|
5
7
|
I extends JSONValue,
|
|
@@ -9,7 +11,7 @@ export interface ToolConfiguration<
|
|
|
9
11
|
description: string;
|
|
10
12
|
schema: JSONSchemaType<I>;
|
|
11
13
|
outputSchema?: JSONSchemaType<O>;
|
|
12
|
-
execute: (input: I) => O | Promise<O>;
|
|
14
|
+
execute: (input: I, context: CapabilityContext) => O | Promise<O>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -127,7 +129,8 @@ export function createToolCapability<
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
try {
|
|
130
|
-
const
|
|
132
|
+
const capabilityContext = createCapabilityContext();
|
|
133
|
+
const result = await config.execute(input, capabilityContext);
|
|
131
134
|
if (validateOutput && !validateOutput(result)) {
|
|
132
135
|
const result = {
|
|
133
136
|
_tag: "error" as const,
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,10 @@ export { emojiIcon, imageIcon, notionIcon, place } from "./builder.js";
|
|
|
2
2
|
export type {
|
|
3
3
|
AutomationCapability,
|
|
4
4
|
AutomationConfiguration,
|
|
5
|
-
|
|
5
|
+
AutomationEvent,
|
|
6
6
|
PageObjectResponse,
|
|
7
7
|
} from "./capabilities/automation.js";
|
|
8
|
+
export type { CapabilityContext } from "./capabilities/context.js";
|
|
8
9
|
export type {
|
|
9
10
|
NotionManagedOAuthConfiguration,
|
|
10
11
|
OAuthCapability,
|
|
@@ -21,6 +22,8 @@ export type {
|
|
|
21
22
|
SyncMode,
|
|
22
23
|
} from "./capabilities/sync.js";
|
|
23
24
|
export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
|
|
25
|
+
// Pacer module re-exported for convenience (users can also import from "@project-ajax/sdk/pacer")
|
|
26
|
+
export { Pacer } from "./pacer.js";
|
|
24
27
|
export type {
|
|
25
28
|
Icon,
|
|
26
29
|
ImageIcon,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Pacer } from "./pacer.js";
|
|
4
|
+
import { getPacerState, setPacerState } from "./pacer_internal.js";
|
|
5
|
+
|
|
6
|
+
describe("Pacer.wait", () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
vi.setSystemTime(0);
|
|
10
|
+
setPacerState({ pacers: {} });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("paces requests based on the rate limit", async () => {
|
|
18
|
+
const first = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
19
|
+
|
|
20
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(6000);
|
|
21
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
22
|
+
await first;
|
|
23
|
+
|
|
24
|
+
const second = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
25
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(12_000);
|
|
26
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
27
|
+
await second;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not wait when the next slot is already in the past", async () => {
|
|
31
|
+
const first = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
32
|
+
await vi.advanceTimersByTimeAsync(6000);
|
|
33
|
+
await first;
|
|
34
|
+
|
|
35
|
+
vi.advanceTimersByTime(20_000);
|
|
36
|
+
|
|
37
|
+
const second = Pacer.wait("api", { requests: 10, intervalMs: 60_000 });
|
|
38
|
+
expect(getPacerState().pacers.api?.lastScheduledAtMs).toBe(26_000);
|
|
39
|
+
await second;
|
|
40
|
+
});
|
|
41
|
+
});
|
package/src/pacer.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacer module for rate limiting API requests.
|
|
3
|
+
*
|
|
4
|
+
* The Pacer ensures requests are evenly spaced over time to respect third-party
|
|
5
|
+
* API rate limits. Instead of making all requests immediately until hitting a 429,
|
|
6
|
+
* Pacer paces requests throughout the rate limit window.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { Pacer } from "@project-ajax/sdk/pacer"
|
|
11
|
+
*
|
|
12
|
+
* // Rate limit: 10 requests per minute
|
|
13
|
+
* await Pacer.wait("salesforce-api", { requests: 10, intervalMs: 60 * 1000 })
|
|
14
|
+
*
|
|
15
|
+
* // Now make your API call
|
|
16
|
+
* const data = await fetchFromSalesforce()
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getPacerState, setPacerState } from "./pacer_internal.js";
|
|
23
|
+
|
|
24
|
+
export type PacerLimit = {
|
|
25
|
+
requests: number;
|
|
26
|
+
intervalMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Pacer = {
|
|
30
|
+
/**
|
|
31
|
+
* Wait until a request can proceed under the specified rate limit.
|
|
32
|
+
*
|
|
33
|
+
* This function paces requests evenly across the rate limit interval. For example,
|
|
34
|
+
* with 60 requests allowed per hour, requests are spaced approximately 1 minute apart.
|
|
35
|
+
*
|
|
36
|
+
* @param key - Unique identifier for this rate limit scope (e.g., "salesforce-api", "jira:token123"). If you're not sure, use any string.
|
|
37
|
+
* @param limit - Rate limit configuration: `requests` per `intervalMs`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Salesforce: 30,000 requests per 24 hours
|
|
42
|
+
* await Pacer.wait("salesforce", { requests: 30_000, intervalMs: 24 * 60 * 60 * 1000 })
|
|
43
|
+
*
|
|
44
|
+
* // Jira: 100 requests per minute
|
|
45
|
+
* await Pacer.wait("jira", { requests: 100, intervalMs: 60 * 1000 })
|
|
46
|
+
*
|
|
47
|
+
* // Multiple rate limits for different endpoints
|
|
48
|
+
* await Pacer.wait("api-read", { requests: 1000, intervalMs: 60 * 1000 })
|
|
49
|
+
* await Pacer.wait("api-write", { requests: 100, intervalMs: 60 * 1000 })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
async wait(key: string, limit: PacerLimit): Promise<void> {
|
|
53
|
+
if (limit.requests <= 0) {
|
|
54
|
+
throw new Error("requests must be greater than 0");
|
|
55
|
+
}
|
|
56
|
+
if (limit.intervalMs <= 0) {
|
|
57
|
+
throw new Error("intervalMs must be greater than 0");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const paceMs = Math.ceil(limit.intervalMs / limit.requests);
|
|
62
|
+
|
|
63
|
+
const state = getPacerState();
|
|
64
|
+
const existing = state.pacers[key];
|
|
65
|
+
const lastScheduledAtMs = existing?.lastScheduledAtMs ?? 0;
|
|
66
|
+
|
|
67
|
+
// Schedule at the later of: (last scheduled + pace) or now
|
|
68
|
+
const scheduledAtMs = Math.max(lastScheduledAtMs + paceMs, now);
|
|
69
|
+
const delayMs = scheduledAtMs - now;
|
|
70
|
+
|
|
71
|
+
// Update state with new scheduled timestamp
|
|
72
|
+
state.pacers[key] = { lastScheduledAtMs: scheduledAtMs };
|
|
73
|
+
setPacerState(state);
|
|
74
|
+
|
|
75
|
+
// Sleep if needed
|
|
76
|
+
if (delayMs > 0) {
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal pacer state helpers used by the runtime.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
5
|
+
export type PacerEntry = {
|
|
6
|
+
lastScheduledAtMs: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PacerState = {
|
|
10
|
+
pacers: Record<string, PacerEntry>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let pacerState: PacerState = { pacers: {} };
|
|
14
|
+
|
|
15
|
+
export function setPacerState(state: PacerState): void {
|
|
16
|
+
pacerState = state;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPacerState(): PacerState {
|
|
20
|
+
return pacerState;
|
|
21
|
+
}
|
package/src/worker.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AutomationCapability,
|
|
3
3
|
AutomationConfiguration,
|
|
4
|
-
|
|
4
|
+
AutomationEvent,
|
|
5
5
|
} from "./capabilities/automation.js";
|
|
6
6
|
import { createAutomationCapability } from "./capabilities/automation.js";
|
|
7
|
+
import type { CapabilityContext } from "./capabilities/context.js";
|
|
7
8
|
import type {
|
|
8
9
|
NotionManagedOAuthConfiguration,
|
|
9
10
|
OAuthCapability,
|
|
@@ -21,7 +22,8 @@ import type { JSONValue } from "./types.js";
|
|
|
21
22
|
// Re-export types for convenience
|
|
22
23
|
export type {
|
|
23
24
|
AutomationConfiguration,
|
|
24
|
-
|
|
25
|
+
AutomationEvent,
|
|
26
|
+
CapabilityContext,
|
|
25
27
|
OAuthConfiguration,
|
|
26
28
|
NotionManagedOAuthConfiguration,
|
|
27
29
|
UserManagedOAuthConfiguration,
|
|
@@ -120,7 +122,7 @@ export class Worker {
|
|
|
120
122
|
* },
|
|
121
123
|
* required: ["name"],
|
|
122
124
|
* },
|
|
123
|
-
* execute: ({ name }) => {
|
|
125
|
+
* execute: ({ name }, { notion }) => {
|
|
124
126
|
* return `Hello, ${name}!`;
|
|
125
127
|
* },
|
|
126
128
|
* })
|
|
@@ -154,8 +156,8 @@ export class Worker {
|
|
|
154
156
|
* worker.automation("sendWelcomeEmail", {
|
|
155
157
|
* title: "Send Welcome Email",
|
|
156
158
|
* description: "Sends a welcome email when a new user is added",
|
|
157
|
-
* execute: async (
|
|
158
|
-
* const { pageId, pageData } =
|
|
159
|
+
* execute: async (event, { notion }) => {
|
|
160
|
+
* const { pageId, pageData } = event;
|
|
159
161
|
*
|
|
160
162
|
* // Access page properties from the Public API format
|
|
161
163
|
* if (pageData) {
|