@project-ajax/create 0.0.28 → 0.0.30

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/index.js CHANGED
@@ -94,14 +94,12 @@ function printNextSteps(directoryName) {
94
94
  if (directoryName === ".") {
95
95
  console.log(`
96
96
  ${chalk.bold("npm install")}
97
- ${chalk.bold("npx workers auth login")}
98
97
  ${chalk.bold("npx workers deploy")}
99
98
  `);
100
99
  } else {
101
100
  console.log(`
102
101
  ${chalk.bold(`cd ${directoryName}`)}
103
102
  ${chalk.bold("npm install")}
104
- ${chalk.bold("npx workers auth login")}
105
103
  ${chalk.bold("npx workers deploy")}
106
104
  `);
107
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/create",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "description": "Initialize a new Notion Project Ajax extensions repo.",
5
5
  "bin": {
6
6
  "create-ajax": "dist/index.js"
@@ -5,6 +5,41 @@ import * as Schema from "@project-ajax/sdk/schema";
5
5
  const worker = new Worker();
6
6
  export default worker;
7
7
 
8
+ const projectId = "project-1";
9
+ const projectName = "Example Project";
10
+
11
+ worker.sync("projectsSync", {
12
+ // Which field to use in each object as the primary key. Must be unique.
13
+ primaryKeyProperty: "Project ID",
14
+ // The schema of the collection to create in Notion.
15
+ schema: {
16
+ // Name of the collection to create in Notion.
17
+ defaultName: "Projects",
18
+ properties: {
19
+ // See `Schema` for the full list of possible column types.
20
+ "Project Name": Schema.title(),
21
+ "Project ID": Schema.richText(),
22
+ },
23
+ },
24
+ execute: async () => {
25
+ // Fetch and return data
26
+ return {
27
+ changes: [
28
+ // Each change must match the shape of `properties` above.
29
+ {
30
+ type: "upsert" as const,
31
+ key: projectId,
32
+ properties: {
33
+ "Project Name": Builder.title(projectName),
34
+ "Project ID": Builder.richText(projectId),
35
+ },
36
+ },
37
+ ],
38
+ hasMore: false,
39
+ };
40
+ },
41
+ });
42
+
8
43
  worker.sync("mySync", {
9
44
  // Which field to use in each object as the primary key. Must be unique.
10
45
  primaryKeyProperty: "ID",
@@ -16,6 +51,7 @@ worker.sync("mySync", {
16
51
  // See `Schema` for the full list of possible column types.
17
52
  Title: Schema.title(),
18
53
  ID: Schema.richText(),
54
+ Project: Schema.relation("projectsSync"),
19
55
  },
20
56
  },
21
57
  execute: async () => {
@@ -29,6 +65,7 @@ worker.sync("mySync", {
29
65
  properties: {
30
66
  Title: Builder.title("Item 1"),
31
67
  ID: Builder.richText("1"),
68
+ Project: [Builder.relation(projectId)],
32
69
  },
33
70
  },
34
71
  ],
@@ -44,27 +44,30 @@ 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
47
+ ### Sync
48
+ #### Strategy and Pagination
48
49
 
49
50
  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
 
52
+ - Always use pagination, when available. Returning too many changes in one execution will fail. Start with batch sizes of ~100 changes.
53
+ - `mode=replace` is simpler, and fine for smaller syncs (<10k)
54
+ - Use `mode=incremental` when the sync could return a lot of data (>10k), eg for SaaS tools like Salesforce or Stripe
55
+ - When using `mode=incremental`, emit delete markers as needed if easy to do (below)
56
+
51
57
  **Sync strategy (`mode`):**
52
58
  - `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.
59
+ - `incremental`: each sync cycle returns a subset of the full dataset (usually the changes since the last run). Deletions must be explicit via `{ type: "delete", key: "..." }`. Records not mentioned are left unchanged.
60
60
 
61
61
  **How pagination works:**
62
62
  1. Return a batch of changes with `hasMore: true` and a `nextContext` value
63
63
  2. The runtime calls `execute` again with that context
64
64
  3. Continue until you return `hasMore: false`
65
65
 
66
+ **Example replace sync:**
67
+
66
68
  ```ts
67
69
  worker.sync("paginatedSync", {
70
+ mode: "replace",
68
71
  primaryKeyProperty: "ID",
69
72
  schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
70
73
  execute: async (context?: { page: number }) => {
@@ -86,27 +89,6 @@ worker.sync("paginatedSync", {
86
89
 
87
90
  **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
91
 
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
92
  **Incremental example (changes only, with deletes):**
111
93
  ```ts
112
94
  worker.sync("incrementalSync", {
@@ -131,6 +113,48 @@ worker.sync("incrementalSync", {
131
113
  });
132
114
  ```
133
115
 
116
+ #### Relations
117
+
118
+ Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and `Builder.relation(primaryKey)` entries inside an array.
119
+
120
+ ```ts
121
+ worker.sync("projectsSync", {
122
+ primaryKeyProperty: "Project ID",
123
+ ...
124
+ });
125
+
126
+ // Example sync worker that syncs sample tasks to a database
127
+ worker.sync("tasksSync", {
128
+ primaryKeyProperty: "Task ID",
129
+ ...
130
+ schema: {
131
+ ...
132
+ properties: {
133
+ ...
134
+ Project: Schema.relation("projectsSync"),
135
+ },
136
+ },
137
+
138
+ execute: async () => {
139
+ // Return sample tasks as database entries
140
+ const tasks = fetchTasks()
141
+ const changes = tasks.map((task) => ({
142
+ type: "upsert" as const,
143
+ key: task.id,
144
+ properties: {
145
+ ...
146
+ Project: [Builder.relation(task.projectId)],
147
+ },
148
+ }));
149
+
150
+ return {
151
+ changes,
152
+ hasMore: false,
153
+ };
154
+ },
155
+ });
156
+ ```
157
+
134
158
  ## Build, Test, and Development Commands
135
159
  - Node >= 22 and npm >= 10.9.2 (see `package.json` engines).
136
160
  - `npm run dev`: run `src/index.ts` with live reload.
@@ -22,10 +22,9 @@ npm install
22
22
  Connect to a Notion workspace and deploy the sample worker:
23
23
 
24
24
  ```shell
25
- npx workers auth login
26
- # or target a specific environment:
27
- npx workers auth login --env=dev
28
25
  npx workers deploy
26
+ # or target a specific environment:
27
+ npx workers deploy --env=dev
29
28
  ```
30
29
 
31
30
  Run the sample sync to create a database:
@@ -56,6 +55,8 @@ export default worker;
56
55
 
57
56
  Syncs create or update a Notion database from your source data.
58
57
 
58
+ The most basic sync returns all data that should be copied to the Notion database on each run:
59
+
59
60
  ```ts
60
61
  import * as Builder from "@project-ajax/sdk/builder";
61
62
  import * as Schema from "@project-ajax/sdk/schema";
@@ -88,42 +89,42 @@ worker.sync("tasksSync", {
88
89
  });
89
90
  ```
90
91
 
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`.
92
+ Notion will delete stale rows after each run. A stale row is a row that was in the database but that your function did not return.
94
93
 
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.
94
+ #### Write a sync that paginates
98
95
 
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.
96
+ When your sync is pulling in many rows of data (>1k), you'll want to use pagination. Breaking down pages to ~100 is a good starting point.
102
97
 
103
- Paginate large datasets by returning `hasMore: true` with a `nextContext` cursor, and continue until `hasMore: false`.
98
+ You can use **context** to persist things like pagination tokens between `execute` runs. Notion passes `context` as the first argument to `execute`. Return `nextContext` to set the `context` for the next run:
104
99
 
105
- **Replace example (full dataset each cycle, each execute call paginated):**
106
100
  ```ts
107
101
  worker.sync("fullSync", {
108
102
  primaryKeyProperty: "ID",
109
103
  mode: "replace",
110
104
  schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
111
- execute: async (context?: { cursor?: string }) => {
112
- const { items, nextCursor } = await fetchPage(context?.cursor);
105
+ execute: async (context?: { page: number }) => {
106
+ const { items , nextCursor } = await fetchPage(context?.page);
113
107
  return {
114
108
  changes: items.map((item) => ({
115
109
  type: "upsert",
116
110
  key: item.id,
117
111
  properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
118
112
  })),
119
- hasMore: Boolean(nextCursor),
120
- nextContext: nextCursor ? { cursor: nextCursor } : undefined,
113
+ hasMore: nextCursor ? true : false,
114
+ nextCursor
121
115
  };
122
116
  },
123
117
  });
124
118
  ```
125
119
 
126
- **Incremental example (changes only, with deletes):**
120
+ Return `hasMore=false` for each run until you reach the end. On the last run, return `hasMore=true`. At the start of the next cycle, Notion will start anew and call `execute` with `context=null`.
121
+
122
+ #### Write a sync that syncs incrementally
123
+
124
+ When your sync is working with a lot of data (10k+), you'll want to use the `incremental` sync mode. With incremental syncs, you can for example backfill all the data from an API into Notion, and then sync only incremental updates from that point forward.
125
+
126
+ Set the sync's `mode` to `incremental` and use pagination as above:
127
+
127
128
  ```ts
128
129
  worker.sync("incrementalSync", {
129
130
  primaryKeyProperty: "ID",
@@ -147,6 +148,28 @@ worker.sync("incrementalSync", {
147
148
  });
148
149
  ```
149
150
 
151
+ Unlike the `replace` sync mode, Notion will not drop "stale" rows and `context` will persist between sync cycles.
152
+
153
+ **Deletes**
154
+
155
+ With incremental syncs, you can delete rows by returning a delete marker, like so:
156
+
157
+ ```ts
158
+ changes: [
159
+ // this is an upsert
160
+ {
161
+ type: "upsert",
162
+ key: item.id,
163
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
164
+ },
165
+ // this is a delete
166
+ {
167
+ type: "delete",
168
+ key: item.id
169
+ }
170
+ ]
171
+ ```
172
+
150
173
  ### Tool
151
174
 
152
175
  Tools are callable by Notion custom agents.
@@ -215,6 +238,9 @@ Log in to Notion (use `--env=dev` for dev):
215
238
  npx workers auth login --env=dev
216
239
  ```
217
240
 
241
+ Login is automatically handled by `npx workers deploy`, so this command is
242
+ typically not needed.
243
+
218
244
  ### `npx workers auth show`
219
245
  Show the active auth token:
220
246
 
@@ -5,6 +5,9 @@ import * as Schema from "@project-ajax/sdk/schema";
5
5
  const worker = new Worker();
6
6
  export default worker;
7
7
 
8
+ const projectId = "project-123";
9
+ const projectName = "Project 1";
10
+
8
11
  // Sample data for demonstration
9
12
  const sampleTasks = [
10
13
  {
@@ -12,21 +15,52 @@ const sampleTasks = [
12
15
  title: "Welcome to Project Ajax",
13
16
  status: "Completed",
14
17
  description: "This is a simple hello world example",
18
+ projectId,
15
19
  },
16
20
  {
17
21
  id: "task-2",
18
22
  title: "Build your first worker",
19
23
  status: "In Progress",
20
24
  description: "Create a sync or tool worker",
25
+ projectId,
21
26
  },
22
27
  {
23
28
  id: "task-3",
24
29
  title: "Deploy to production",
25
30
  status: "Todo",
26
31
  description: "Share your worker with your team",
32
+ projectId,
27
33
  },
28
34
  ];
29
35
 
36
+ worker.sync("projectsSync", {
37
+ primaryKeyProperty: "Project ID",
38
+ schema: {
39
+ defaultName: "Projects",
40
+ databaseIcon: Builder.notionIcon("activity"),
41
+ properties: {
42
+ "Project Name": Schema.title(),
43
+ "Project ID": Schema.richText(),
44
+ },
45
+ },
46
+ execute: async () => {
47
+ return {
48
+ changes: [
49
+ {
50
+ type: "upsert" as const,
51
+ key: projectId,
52
+ icon: Builder.notionIcon("activity"),
53
+ properties: {
54
+ "Project Name": Builder.title(projectName),
55
+ "Project ID": Builder.richText(projectId),
56
+ },
57
+ },
58
+ ],
59
+ hasMore: false,
60
+ };
61
+ },
62
+ });
63
+
30
64
  // Example sync worker that syncs sample tasks to a database
31
65
  worker.sync("tasksSync", {
32
66
  primaryKeyProperty: "Task ID",
@@ -36,9 +70,9 @@ worker.sync("tasksSync", {
36
70
  schedule: "continuous",
37
71
 
38
72
  // Sync mode:
39
- // - "replace": Each sync returns the complete dataset. After hasMore:false,
73
+ // - "replace": Each sync cycle returns the complete dataset. After hasMore:false,
40
74
  // pages not seen in this sync run are deleted.
41
- // - "incremental": Sync returns changes only. Use delete markers
75
+ // - "incremental": Each sync cycle returns a subset of the complete dataset. Use delete markers
42
76
  // (e.g., { type: "delete", key: "task-1" }) to remove pages.
43
77
  // Defaults to "replace".
44
78
  // mode: "replace",
@@ -55,6 +89,7 @@ worker.sync("tasksSync", {
55
89
  { name: "In Progress", color: "blue" },
56
90
  { name: "Todo", color: "default" },
57
91
  ]),
92
+ Project: Schema.relation("projectsSync"),
58
93
  },
59
94
  },
60
95
 
@@ -81,13 +116,19 @@ worker.sync("tasksSync", {
81
116
  "Task ID": Builder.richText(task.id),
82
117
  Description: Builder.richText(task.description),
83
118
  Status: Builder.select(task.status),
119
+ Project: [Builder.relation(projectId)],
84
120
  },
85
121
  pageContentMarkdown: `## ${task.title}\n\n${task.description}`,
86
122
  }));
87
123
 
88
124
  return {
125
+ // List of changes to apply to the Notion database.
89
126
  changes,
127
+ // Indicates whether there is more data to fetch this sync cycle. If true, the runtime will call `execute` again with the nextContext.
90
128
  hasMore: false,
129
+ // Optional context data Notion will pass back in the next execution.
130
+ // This can be any type of data (cursor, page number, timestamp, etc.).
131
+ nextContext: undefined,
91
132
  };
92
133
  },
93
134
  });