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