@project-ajax/create 0.0.26 → 0.0.28
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/package.json
CHANGED
|
@@ -29,8 +29,8 @@ const myCustomAuth = worker.oauth("myCustomAuth", {
|
|
|
29
29
|
authorizationEndpoint: "https://provider.example.com/oauth/authorize",
|
|
30
30
|
tokenEndpoint: "https://provider.example.com/oauth/token",
|
|
31
31
|
scope: "read write",
|
|
32
|
-
clientId:
|
|
33
|
-
clientSecret:
|
|
32
|
+
clientId: "1234567890",
|
|
33
|
+
clientSecret: process.env.MY_CUSTOM_OAUTH_CLIENT_SECRET ?? "",
|
|
34
34
|
authorizationParams: {
|
|
35
35
|
access_type: "offline",
|
|
36
36
|
prompt: "consent",
|
|
@@ -54,7 +54,7 @@ worker.sync("googleCalendarSync", {
|
|
|
54
54
|
// Use token to fetch from Google Calendar API
|
|
55
55
|
console.log("Using Google token:", `${token.slice(0, 10)}...`);
|
|
56
56
|
|
|
57
|
-
return {
|
|
57
|
+
return { changes: [], hasMore: false };
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
|
|
@@ -73,12 +73,3 @@ worker.tool("customApiTool", {
|
|
|
73
73
|
return { success: true };
|
|
74
74
|
},
|
|
75
75
|
});
|
|
76
|
-
|
|
77
|
-
function requireEnv(key: string): string {
|
|
78
|
-
const value = process.env[key];
|
|
79
|
-
if (value) {
|
|
80
|
-
return value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
throw new Error(`Missing environment variable "${key}"`);
|
|
84
|
-
}
|
|
@@ -21,9 +21,10 @@ worker.sync("mySync", {
|
|
|
21
21
|
execute: async () => {
|
|
22
22
|
// Fetch and return data
|
|
23
23
|
return {
|
|
24
|
-
|
|
25
|
-
// Each
|
|
24
|
+
changes: [
|
|
25
|
+
// Each change must match the shape of `properties` above.
|
|
26
26
|
{
|
|
27
|
+
type: "upsert" as const,
|
|
27
28
|
key: "1",
|
|
28
29
|
properties: {
|
|
29
30
|
Title: Builder.title("Item 1"),
|
|
@@ -31,7 +32,7 @@ worker.sync("mySync", {
|
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
],
|
|
34
|
-
|
|
35
|
+
hasMore: false,
|
|
35
36
|
};
|
|
36
37
|
},
|
|
37
38
|
});
|
package/template/AGENTS.md
CHANGED
|
@@ -21,8 +21,8 @@ worker.sync("tasksSync", {
|
|
|
21
21
|
primaryKeyProperty: "ID",
|
|
22
22
|
schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
|
|
23
23
|
execute: async () => ({
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
|
|
25
|
+
hasMore: false,
|
|
26
26
|
}),
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -44,6 +44,93 @@ worker.oauth("googleAuth", { name: "my-google-auth", provider: "google" });
|
|
|
44
44
|
|
|
45
45
|
- For user-managed OAuth, supply `name`, `authorizationEndpoint`, `tokenEndpoint`, `clientId`, `clientSecret`, and `scope` (optional: `authorizationParams`, `callbackUrl`, `accessTokenExpireMs`).
|
|
46
46
|
|
|
47
|
+
### Sync Strategy and Pagination
|
|
48
|
+
|
|
49
|
+
Syncs run in a "sync cycle": a back-to-back chain of `execute` calls that starts at a scheduled trigger and ends when an execution returns `hasMore: false`.
|
|
50
|
+
|
|
51
|
+
**Sync strategy (`mode`):**
|
|
52
|
+
- `replace`: each sync cycle must return the full dataset. After the final `hasMore: false`, any records not seen during that cycle are deleted.
|
|
53
|
+
- `incremental`: each sync cycle returns only changes since the last run. Deletions must be explicit via `{ type: "delete", key: "..." }`. Records not mentioned are left unchanged.
|
|
54
|
+
|
|
55
|
+
**When to use each:**
|
|
56
|
+
- `replace`: small datasets or cheap full scans. Each cycle returns the full dataset for simplicity.
|
|
57
|
+
- `incremental`: large datasets (e.g., Stripe, Salesforce, Zendesk) where full scans are expensive. First backfill, then return only incremental changes.
|
|
58
|
+
|
|
59
|
+
Implement pagination to avoid exceeding output size limits. Returning too many changes in one execution can cause the output JSON to exceed size limits and fail.
|
|
60
|
+
|
|
61
|
+
**How pagination works:**
|
|
62
|
+
1. Return a batch of changes with `hasMore: true` and a `nextContext` value
|
|
63
|
+
2. The runtime calls `execute` again with that context
|
|
64
|
+
3. Continue until you return `hasMore: false`
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
worker.sync("paginatedSync", {
|
|
68
|
+
primaryKeyProperty: "ID",
|
|
69
|
+
schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
|
|
70
|
+
execute: async (context?: { page: number }) => {
|
|
71
|
+
const page = context?.page ?? 1;
|
|
72
|
+
const pageSize = 100;
|
|
73
|
+
const { items, hasMore } = await fetchPage(page, pageSize);
|
|
74
|
+
return {
|
|
75
|
+
changes: items.map((item) => ({
|
|
76
|
+
type: "upsert",
|
|
77
|
+
key: item.id,
|
|
78
|
+
properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
|
|
79
|
+
})),
|
|
80
|
+
hasMore,
|
|
81
|
+
nextContext: hasMore ? { page: page + 1 } : undefined,
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Context types:** The `nextContext` can be any serializable value—a cursor string, page number, timestamp, or complex object. Type your execute function's context parameter to match.
|
|
88
|
+
|
|
89
|
+
**Replace example (full dataset each cycle, each execute call paginated):**
|
|
90
|
+
```ts
|
|
91
|
+
worker.sync("fullSync", {
|
|
92
|
+
primaryKeyProperty: "ID",
|
|
93
|
+
mode: "replace",
|
|
94
|
+
schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
|
|
95
|
+
execute: async (context?: { cursor?: string }) => {
|
|
96
|
+
const { items, nextCursor } = await fetchPage(context?.cursor);
|
|
97
|
+
return {
|
|
98
|
+
changes: items.map((item) => ({
|
|
99
|
+
type: "upsert",
|
|
100
|
+
key: item.id,
|
|
101
|
+
properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
|
|
102
|
+
})),
|
|
103
|
+
hasMore: Boolean(nextCursor),
|
|
104
|
+
nextContext: nextCursor ? { cursor: nextCursor } : undefined,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Incremental example (changes only, with deletes):**
|
|
111
|
+
```ts
|
|
112
|
+
worker.sync("incrementalSync", {
|
|
113
|
+
primaryKeyProperty: "ID",
|
|
114
|
+
mode: "incremental",
|
|
115
|
+
schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
|
|
116
|
+
execute: async (context?: { cursor?: string }) => {
|
|
117
|
+
const { upserts, deletes, nextCursor } = await fetchChanges(context?.cursor);
|
|
118
|
+
return {
|
|
119
|
+
changes: [
|
|
120
|
+
...upserts.map((item) => ({
|
|
121
|
+
type: "upsert",
|
|
122
|
+
key: item.id,
|
|
123
|
+
properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
|
|
124
|
+
})),
|
|
125
|
+
...deletes.map((id) => ({ type: "delete", key: id })),
|
|
126
|
+
],
|
|
127
|
+
hasMore: Boolean(nextCursor),
|
|
128
|
+
nextContext: nextCursor ? { cursor: nextCursor } : undefined,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
47
134
|
## Build, Test, and Development Commands
|
|
48
135
|
- Node >= 22 and npm >= 10.9.2 (see `package.json` engines).
|
|
49
136
|
- `npm run dev`: run `src/index.ts` with live reload.
|
package/template/README.md
CHANGED
|
@@ -73,8 +73,9 @@ worker.sync("tasksSync", {
|
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
execute: async () => ({
|
|
76
|
-
|
|
76
|
+
changes: [
|
|
77
77
|
{
|
|
78
|
+
type: "upsert",
|
|
78
79
|
key: "1",
|
|
79
80
|
properties: {
|
|
80
81
|
Name: Builder.title("Write docs"),
|
|
@@ -82,11 +83,70 @@ worker.sync("tasksSync", {
|
|
|
82
83
|
},
|
|
83
84
|
},
|
|
84
85
|
],
|
|
85
|
-
|
|
86
|
+
hasMore: false,
|
|
86
87
|
}),
|
|
87
88
|
});
|
|
88
89
|
```
|
|
89
90
|
|
|
91
|
+
#### Sync strategy and pagination
|
|
92
|
+
|
|
93
|
+
A sync runs in a "sync cycle": a back-to-back chain of `execute` calls that ends when an execution returns `hasMore: false`.
|
|
94
|
+
|
|
95
|
+
Choose a strategy with `mode`:
|
|
96
|
+
- `replace` (default): each cycle returns the full dataset. After the final `hasMore: false`, any records not seen in that cycle are deleted.
|
|
97
|
+
- `incremental`: each cycle returns only changes since the last run. Deletions must be explicit via `{ type: "delete", key: "..." }`. Records not mentioned are left unchanged.
|
|
98
|
+
|
|
99
|
+
**When to use each:**
|
|
100
|
+
- `replace`: small datasets or cheap full scans. Each cycle returns the full dataset for simplicity.
|
|
101
|
+
- `incremental`: large datasets (Stripe, Salesforce, Zendesk) where full scans are expensive. First backfill, then return only changes.
|
|
102
|
+
|
|
103
|
+
Paginate large datasets by returning `hasMore: true` with a `nextContext` cursor, and continue until `hasMore: false`.
|
|
104
|
+
|
|
105
|
+
**Replace example (full dataset each cycle, each execute call paginated):**
|
|
106
|
+
```ts
|
|
107
|
+
worker.sync("fullSync", {
|
|
108
|
+
primaryKeyProperty: "ID",
|
|
109
|
+
mode: "replace",
|
|
110
|
+
schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
|
|
111
|
+
execute: async (context?: { cursor?: string }) => {
|
|
112
|
+
const { items, nextCursor } = await fetchPage(context?.cursor);
|
|
113
|
+
return {
|
|
114
|
+
changes: items.map((item) => ({
|
|
115
|
+
type: "upsert",
|
|
116
|
+
key: item.id,
|
|
117
|
+
properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
|
|
118
|
+
})),
|
|
119
|
+
hasMore: Boolean(nextCursor),
|
|
120
|
+
nextContext: nextCursor ? { cursor: nextCursor } : undefined,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Incremental example (changes only, with deletes):**
|
|
127
|
+
```ts
|
|
128
|
+
worker.sync("incrementalSync", {
|
|
129
|
+
primaryKeyProperty: "ID",
|
|
130
|
+
mode: "incremental",
|
|
131
|
+
schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
|
|
132
|
+
execute: async (context?: { cursor?: string }) => {
|
|
133
|
+
const { upserts, deletes, nextCursor } = await fetchChanges(context?.cursor);
|
|
134
|
+
return {
|
|
135
|
+
changes: [
|
|
136
|
+
...upserts.map((item) => ({
|
|
137
|
+
type: "upsert",
|
|
138
|
+
key: item.id,
|
|
139
|
+
properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
|
|
140
|
+
})),
|
|
141
|
+
...deletes.map((id) => ({ type: "delete", key: id })),
|
|
142
|
+
],
|
|
143
|
+
hasMore: Boolean(nextCursor),
|
|
144
|
+
nextContext: nextCursor ? { cursor: nextCursor } : undefined,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
90
150
|
### Tool
|
|
91
151
|
|
|
92
152
|
Tools are callable by Notion custom agents.
|
package/template/src/index.ts
CHANGED
|
@@ -35,9 +35,13 @@ worker.sync("tasksSync", {
|
|
|
35
35
|
// Use intervals like "30m", "1h", "1d" (min: 1m, max: 7d)
|
|
36
36
|
schedule: "continuous",
|
|
37
37
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// Sync mode:
|
|
39
|
+
// - "replace": Each sync returns the complete dataset. After hasMore:false,
|
|
40
|
+
// pages not seen in this sync run are deleted.
|
|
41
|
+
// - "incremental": Sync returns changes only. Use delete markers
|
|
42
|
+
// (e.g., { type: "delete", key: "task-1" }) to remove pages.
|
|
43
|
+
// Defaults to "replace".
|
|
44
|
+
// mode: "replace",
|
|
41
45
|
|
|
42
46
|
schema: {
|
|
43
47
|
defaultName: "Sample Tasks",
|
|
@@ -68,7 +72,8 @@ worker.sync("tasksSync", {
|
|
|
68
72
|
}
|
|
69
73
|
};
|
|
70
74
|
// Return sample tasks as database entries
|
|
71
|
-
const
|
|
75
|
+
const changes = sampleTasks.map((task) => ({
|
|
76
|
+
type: "upsert" as const,
|
|
72
77
|
key: task.id,
|
|
73
78
|
icon: emojiForStatus(task.status),
|
|
74
79
|
properties: {
|
|
@@ -81,8 +86,8 @@ worker.sync("tasksSync", {
|
|
|
81
86
|
}));
|
|
82
87
|
|
|
83
88
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
changes,
|
|
90
|
+
hasMore: false,
|
|
86
91
|
};
|
|
87
92
|
},
|
|
88
93
|
});
|