@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/create",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Initialize a new Notion Project Ajax extensions repo.",
5
5
  "bin": {
6
6
  "create-ajax": "dist/index.js"
@@ -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: requireEnv("MY_CUSTOM_OAUTH_CLIENT_ID"),
33
- clientSecret: requireEnv("MY_CUSTOM_OAUTH_CLIENT_SECRET"),
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 { objects: [], done: true };
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
- objects: [
25
- // Each object must match the shape of `properties` above.
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
- done: true,
35
+ hasMore: false,
35
36
  };
36
37
  },
37
38
  });
@@ -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
- objects: [{ key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
- done: true,
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.
@@ -73,8 +73,9 @@ worker.sync("tasksSync", {
73
73
  },
74
74
  },
75
75
  execute: async () => ({
76
- objects: [
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
- done: true,
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.
@@ -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
- // Optional: Set to true to delete pages that are not returned from sync executions.
39
- // By default (false), sync only creates and updates pages, never deletes them.
40
- // deleteUnreturnedPages: true,
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 objects = sampleTasks.map((task) => ({
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
- objects,
85
- done: true,
89
+ changes,
90
+ hasMore: false,
86
91
  };
87
92
  },
88
93
  });