@project-ajax/create 0.0.35 → 0.0.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @project-ajax/create
2
2
 
3
- A CLI for creating new Project Ajax workers projects.
3
+ A CLI for creating new Notion Workers projects.
4
4
 
5
5
  ## Usage
6
6
 
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
3
+ // src/run-create.ts
4
+ import { execSync } from "child_process";
4
5
  import fs from "fs";
5
6
  import path from "path";
6
7
  import { fileURLToPath } from "url";
@@ -10,70 +11,193 @@ import chalk from "chalk";
10
11
  import ora from "ora";
11
12
  var __filename = fileURLToPath(import.meta.url);
12
13
  var __dirname = path.dirname(__filename);
13
- run().then(() => {
14
- process.exit(0);
15
- }).catch((err) => {
16
- console.error(chalk.red(`
17
- ${err.message}`));
18
- process.exit(1);
19
- });
14
+ async function runCreate() {
15
+ try {
16
+ await run();
17
+ process.exit(0);
18
+ } catch (err) {
19
+ const message = err instanceof Error ? err.message : String(err);
20
+ console.error(chalk.red(`
21
+ ${message}`));
22
+ process.exit(1);
23
+ }
24
+ }
20
25
  async function run() {
21
26
  console.log(chalk.bold.cyan("\n\u{1F680} Create a new worker\n"));
22
27
  const { values } = parseArgs({
23
28
  options: {
24
- directory: {
25
- type: "string",
26
- short: "d"
27
- }
29
+ directory: { type: "string", short: "d" },
30
+ yes: { type: "boolean", short: "y" },
31
+ git: { type: "boolean" },
32
+ "no-git": { type: "boolean" },
33
+ bun: { type: "boolean" },
34
+ install: { type: "boolean" },
35
+ "no-install": { type: "boolean" },
36
+ force: { type: "boolean", short: "f" }
28
37
  }
29
38
  });
39
+ validateFlags(values);
40
+ const isTTY = process.stdin.isTTY;
41
+ const useDefaults = values.yes ?? false;
30
42
  let directoryName = values.directory;
31
43
  if (!directoryName) {
32
- directoryName = await safePrompt({
33
- message: "Path to the new worker project:",
44
+ if (!isTTY) {
45
+ console.error(
46
+ chalk.red(
47
+ "Provide the path to the new worker project with --directory"
48
+ )
49
+ );
50
+ process.exit(1);
51
+ }
52
+ directoryName = await safeInput({
53
+ message: "Where would you like to create your worker project?",
34
54
  default: ".",
35
- required: true,
36
- noTTY: "Provide the path to the new worker project with --directory"
55
+ required: true
37
56
  });
38
57
  }
39
58
  if (!directoryName) {
40
59
  console.log(chalk.red("Cancelled."));
41
60
  process.exit(1);
42
61
  }
62
+ const destPath = directoryName === "." ? process.cwd() : path.resolve(process.cwd(), directoryName);
63
+ const templatePath = getTemplatePath();
64
+ const conflicts = analyzeConflicts(destPath, templatePath);
65
+ if (conflicts.hasConflicts) {
66
+ if (values.force) {
67
+ } else if (!isTTY) {
68
+ console.error(
69
+ chalk.red(
70
+ `Directory has conflicting files: ${conflicts.conflicts.join(", ")}
71
+ Use --force to overwrite.`
72
+ )
73
+ );
74
+ process.exit(1);
75
+ } else if (useDefaults) {
76
+ console.error(
77
+ chalk.red(
78
+ "Directory has conflicting files. Use --force with --yes to overwrite."
79
+ )
80
+ );
81
+ process.exit(1);
82
+ } else {
83
+ const action = await safeSelect({
84
+ message: `Directory has existing files (${conflicts.conflicts.length} conflicts). What would you like to do?`,
85
+ choices: [
86
+ {
87
+ value: "overwrite",
88
+ name: `Overwrite conflicting files (${conflicts.conflicts.join(", ")})`
89
+ },
90
+ { value: "cancel", name: "Cancel" }
91
+ ]
92
+ });
93
+ if (action === "cancel") {
94
+ console.log(chalk.yellow("Cancelled."));
95
+ process.exit(0);
96
+ }
97
+ }
98
+ }
99
+ const existingGit = fs.existsSync(path.join(destPath, ".git"));
100
+ let shouldInitGit = false;
101
+ if (existingGit) {
102
+ } else if (values.git) {
103
+ shouldInitGit = true;
104
+ } else if (values["no-git"]) {
105
+ shouldInitGit = false;
106
+ } else if (useDefaults) {
107
+ shouldInitGit = true;
108
+ } else if (isTTY) {
109
+ shouldInitGit = await safeConfirm({
110
+ message: "Initialize a git repository?",
111
+ default: true
112
+ });
113
+ }
114
+ const packageManager = values.bun ? "bun" : "npm";
115
+ let shouldInstall = false;
116
+ if (values.install) {
117
+ shouldInstall = true;
118
+ } else if (values["no-install"]) {
119
+ shouldInstall = false;
120
+ } else if (useDefaults) {
121
+ shouldInstall = true;
122
+ } else if (isTTY) {
123
+ shouldInstall = await safeConfirm({
124
+ message: "Install dependencies now?",
125
+ default: true
126
+ });
127
+ }
43
128
  const spinner = ora("Setting up template...").start();
44
129
  try {
45
- spinner.text = "Preparing destination...";
46
- const destPath = prepareDestination(directoryName);
130
+ if (!fs.existsSync(destPath)) {
131
+ fs.mkdirSync(destPath, { recursive: true });
132
+ }
47
133
  spinner.text = "Copying template files...";
48
- const templatePath = getTemplatePath();
49
134
  copyTemplate(templatePath, destPath);
50
- spinner.succeed(chalk.green("Worker project created successfully!"));
51
- printNextSteps(directoryName);
135
+ spinner.succeed("Template files copied");
136
+ if (shouldInitGit) {
137
+ const gitSpinner = ora("Initializing git repository...").start();
138
+ const gitSuccess = initializeGit(destPath);
139
+ if (gitSuccess) {
140
+ gitSpinner.succeed("Git repository initialized");
141
+ } else {
142
+ gitSpinner.warn(
143
+ "Could not initialize git repository (is git installed?)"
144
+ );
145
+ }
146
+ } else if (existingGit) {
147
+ console.log(chalk.dim(" Git repository already exists"));
148
+ }
149
+ if (shouldInstall) {
150
+ const installSpinner = ora(
151
+ `Installing dependencies with ${packageManager}...`
152
+ ).start();
153
+ const installSuccess = installPackages(destPath, packageManager);
154
+ if (installSuccess) {
155
+ installSpinner.succeed(`Dependencies installed with ${packageManager}`);
156
+ } else {
157
+ installSpinner.fail("Failed to install dependencies");
158
+ const cmd = packageManager === "bun" ? "bun install" : "npm install";
159
+ console.log(
160
+ chalk.yellow(` Run '${cmd}' manually to install dependencies`)
161
+ );
162
+ }
163
+ }
164
+ console.log(chalk.green("\n\u2728 Worker project created successfully!"));
165
+ printNextSteps({
166
+ directoryName,
167
+ gitInitialized: shouldInitGit || existingGit,
168
+ packagesInstalled: shouldInstall,
169
+ packageManager
170
+ });
52
171
  } catch (err) {
53
172
  spinner.fail("Failed to create worker project.");
54
- console.error(
55
- chalk.red(`
56
- ${err instanceof Error ? err.message : String(err)}`)
57
- );
58
- process.exit(1);
173
+ throw err;
59
174
  }
60
175
  }
61
- function getTemplatePath() {
62
- return path.resolve(__dirname, "..", "template");
176
+ function validateFlags(values) {
177
+ if (values.git && values["no-git"]) {
178
+ console.error(chalk.red("Cannot use both --git and --no-git"));
179
+ process.exit(1);
180
+ }
181
+ if (values.install && values["no-install"]) {
182
+ console.error(chalk.red("Cannot use both --install and --no-install"));
183
+ process.exit(1);
184
+ }
63
185
  }
64
- function prepareDestination(repoName) {
65
- const destPath = repoName === "." ? process.cwd() : path.resolve(process.cwd(), repoName);
66
- if (fs.existsSync(destPath)) {
67
- const files = fs.readdirSync(destPath);
68
- if (files.length > 0) {
69
- throw new Error(
70
- `Destination directory "${repoName}" is not empty. Please use an empty directory.`
71
- );
186
+ function analyzeConflicts(destPath, templatePath) {
187
+ if (!fs.existsSync(destPath)) {
188
+ return { conflicts: [], hasConflicts: false };
189
+ }
190
+ const templateFiles = fs.readdirSync(templatePath);
191
+ const conflicts = [];
192
+ for (const file of templateFiles) {
193
+ if (fs.existsSync(path.join(destPath, file))) {
194
+ conflicts.push(file);
72
195
  }
73
- } else if (repoName !== ".") {
74
- fs.mkdirSync(destPath, { recursive: true });
75
196
  }
76
- return destPath;
197
+ return { conflicts, hasConflicts: conflicts.length > 0 };
198
+ }
199
+ function getTemplatePath() {
200
+ return path.resolve(__dirname, "..", "template");
77
201
  }
78
202
  function copyTemplate(templatePath, destPath) {
79
203
  if (!fs.existsSync(templatePath)) {
@@ -88,33 +212,66 @@ function copyTemplate(templatePath, destPath) {
88
212
  fs.cpSync(srcPath, destFilePath, { recursive: true });
89
213
  }
90
214
  }
91
- function printNextSteps(directoryName) {
92
- console.log(chalk.cyan(`
93
- \u2728 Next steps:`));
94
- if (directoryName === ".") {
95
- console.log(`
96
- ${chalk.bold("npm install")}
97
- ${chalk.bold("npx workers deploy")}
98
- `);
99
- } else {
100
- console.log(`
101
- ${chalk.bold(`cd ${directoryName}`)}
102
- ${chalk.bold("npm install")}
103
- ${chalk.bold("npx workers deploy")}
104
- `);
215
+ function initializeGit(destPath) {
216
+ try {
217
+ execSync("git init", { cwd: destPath, stdio: "pipe" });
218
+ return true;
219
+ } catch {
220
+ return false;
105
221
  }
106
222
  }
107
- function safePrompt(config) {
108
- if (!process.stdin.isTTY) {
109
- console.error(chalk.red(config.noTTY));
110
- process.exit(1);
223
+ function installPackages(destPath, pm) {
224
+ const cmd = pm === "bun" ? "bun install" : "npm install";
225
+ try {
226
+ execSync(cmd, {
227
+ cwd: destPath,
228
+ stdio: "pipe",
229
+ env: { ...process.env, FORCE_COLOR: "1" }
230
+ });
231
+ return true;
232
+ } catch {
233
+ return false;
111
234
  }
112
- return prompts.input(config).catch((err) => {
113
- if (err instanceof Error && err.name === "ExitPromptError") {
114
- console.log(chalk.red("\u{1F44B} Goodbye!"));
115
- process.exit(1);
116
- } else {
117
- throw err;
118
- }
119
- });
120
235
  }
236
+ function printNextSteps(state) {
237
+ console.log(chalk.cyan("\nNext steps:\n"));
238
+ const steps = [];
239
+ if (state.directoryName !== ".") {
240
+ steps.push(`cd ${state.directoryName}`);
241
+ }
242
+ if (!state.packagesInstalled) {
243
+ const installCmd = state.packageManager === "bun" ? "bun install" : "npm install";
244
+ steps.push(installCmd);
245
+ }
246
+ const runPrefix = state.packageManager === "bun" ? "bunx" : "npx";
247
+ steps.push(`${runPrefix} workers deploy`);
248
+ for (let i = 0; i < steps.length; i++) {
249
+ console.log(` ${chalk.bold(`${i + 1}.`)} ${chalk.bold(steps[i])}`);
250
+ }
251
+ console.log("");
252
+ console.log(
253
+ chalk.dim(
254
+ ` Tip: Run '${state.packageManager === "bun" ? "bunx" : "npx"} workers auth login' to connect to your Notion workspace.
255
+ `
256
+ )
257
+ );
258
+ }
259
+ function safeInput(config) {
260
+ return prompts.input(config).catch(handlePromptExit);
261
+ }
262
+ function safeConfirm(config) {
263
+ return prompts.confirm(config).catch(handlePromptExit);
264
+ }
265
+ function safeSelect(config) {
266
+ return prompts.select(config).catch(handlePromptExit);
267
+ }
268
+ function handlePromptExit(err) {
269
+ if (err instanceof Error && err.name === "ExitPromptError") {
270
+ console.log(chalk.yellow("\nCancelled."));
271
+ process.exit(0);
272
+ }
273
+ throw err;
274
+ }
275
+
276
+ // src/index.ts
277
+ void runCreate();
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@project-ajax/create",
3
- "version": "0.0.35",
4
- "description": "Initialize a new Notion Project Ajax extensions repo.",
3
+ "version": "0.0.37",
4
+ "description": "Initialize a new Notion Workers extensions repo.",
5
5
  "bin": {
6
6
  "create-ajax": "dist/index.js"
7
7
  },
8
8
  "type": "module",
9
9
  "scripts": {
10
- "build": "tsup"
10
+ "build": "tsup",
11
+ "test": "vitest run"
11
12
  },
12
13
  "tsup": {
13
14
  "entry": [
@@ -34,6 +35,7 @@
34
35
  "devDependencies": {
35
36
  "@types/node": "^22.19.0",
36
37
  "tsup": "^8.5.0",
37
- "typescript": "^5.9.3"
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^4.0.8"
38
40
  }
39
41
  }
@@ -18,25 +18,25 @@ const worker = new Worker();
18
18
  export default worker;
19
19
 
20
20
  worker.sync("tasksSync", {
21
- primaryKeyProperty: "ID",
22
- schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
23
- execute: async (_state, { notion }) => ({
24
- changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
- hasMore: false,
26
- }),
21
+ primaryKeyProperty: "ID",
22
+ schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
23
+ execute: async (_state, { notion }) => ({
24
+ changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
+ hasMore: false,
26
+ }),
27
27
  });
28
28
 
29
29
  worker.tool("sayHello", {
30
- title: "Say Hello",
31
- description: "Return a greeting",
32
- schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
- execute: ({ name }, { notion }) => `Hello, ${name}`,
30
+ title: "Say Hello",
31
+ description: "Return a greeting",
32
+ schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
+ execute: ({ name }, { notion }) => `Hello, ${name}`,
34
34
  });
35
35
 
36
36
  worker.automation("sendWelcomeEmail", {
37
- title: "Send Welcome Email",
38
- description: "Runs from a database automation",
39
- execute: async (event, { notion }) => {},
37
+ title: "Send Welcome Email",
38
+ description: "Runs from a database automation",
39
+ execute: async (event, { notion }) => {},
40
40
  });
41
41
 
42
42
  worker.oauth("googleAuth", { name: "my-google-auth", provider: "google" });
@@ -69,23 +69,23 @@ Syncs run in a "sync cycle": a back-to-back chain of `execute` calls that starts
69
69
 
70
70
  ```ts
71
71
  worker.sync("paginatedSync", {
72
- mode: "replace",
73
- primaryKeyProperty: "ID",
74
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
- execute: async (state, { notion }) => {
76
- const page = state?.page ?? 1;
77
- const pageSize = 100;
78
- const { items, hasMore } = await fetchPage(page, pageSize);
79
- return {
80
- changes: items.map((item) => ({
81
- type: "upsert",
82
- key: item.id,
83
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
- })),
85
- hasMore,
86
- nextState: hasMore ? { page: page + 1 } : undefined,
87
- };
88
- },
72
+ mode: "replace",
73
+ primaryKeyProperty: "ID",
74
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
+ execute: async (state, { notion }) => {
76
+ const page = state?.page ?? 1;
77
+ const pageSize = 100;
78
+ const { items, hasMore } = await fetchPage(page, pageSize);
79
+ return {
80
+ changes: items.map((item) => ({
81
+ type: "upsert",
82
+ key: item.id,
83
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
+ })),
85
+ hasMore,
86
+ nextState: hasMore ? { page: page + 1 } : undefined,
87
+ };
88
+ },
89
89
  });
90
90
  ```
91
91
 
@@ -94,24 +94,24 @@ worker.sync("paginatedSync", {
94
94
  **Incremental example (changes only, with deletes):**
95
95
  ```ts
96
96
  worker.sync("incrementalSync", {
97
- primaryKeyProperty: "ID",
98
- mode: "incremental",
99
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
- execute: async (state, { notion }) => {
101
- const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
- return {
103
- changes: [
104
- ...upserts.map((item) => ({
105
- type: "upsert",
106
- key: item.id,
107
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
- })),
109
- ...deletes.map((id) => ({ type: "delete", key: id })),
110
- ],
111
- hasMore: Boolean(nextCursor),
112
- nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
- };
114
- },
97
+ primaryKeyProperty: "ID",
98
+ mode: "incremental",
99
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
+ execute: async (state, { notion }) => {
101
+ const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
+ return {
103
+ changes: [
104
+ ...upserts.map((item) => ({
105
+ type: "upsert",
106
+ key: item.id,
107
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
+ })),
109
+ ...deletes.map((id) => ({ type: "delete", key: id })),
110
+ ],
111
+ hasMore: Boolean(nextCursor),
112
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
+ };
114
+ },
115
115
  });
116
116
  ```
117
117
 
@@ -122,29 +122,34 @@ Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and
122
122
  ```ts
123
123
  worker.sync("projectsSync", {
124
124
  primaryKeyProperty: "Project ID",
125
- ...
125
+ ...
126
126
  });
127
127
 
128
128
  // Example sync worker that syncs sample tasks to a database
129
129
  worker.sync("tasksSync", {
130
130
  primaryKeyProperty: "Task ID",
131
- ...
131
+ ...
132
132
  schema: {
133
- ...
133
+ ...
134
134
  properties: {
135
- ...
136
- Project: Schema.relation("projectsSync"),
135
+ ...
136
+ Project: Schema.relation("projectsSync", {
137
+ // Optionally configure a two-way relation. This will automatically create the
138
+ // "Tasks" property on the project synced database: there is no need
139
+ // to configure "Tasks" on the projectSync capability.
140
+ twoWay: true, relatedPropertyName: "Tasks"
141
+ }),
137
142
  },
138
143
  },
139
144
 
140
145
  execute: async () => {
141
146
  // Return sample tasks as database entries
142
- const tasks = fetchTasks()
147
+ const tasks = fetchTasks()
143
148
  const changes = tasks.map((task) => ({
144
149
  type: "upsert" as const,
145
150
  key: task.id,
146
151
  properties: {
147
- ...
152
+ ...
148
153
  Project: [Builder.relation(task.projectId)],
149
154
  },
150
155
  }));
@@ -18,25 +18,25 @@ const worker = new Worker();
18
18
  export default worker;
19
19
 
20
20
  worker.sync("tasksSync", {
21
- primaryKeyProperty: "ID",
22
- schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
23
- execute: async (_state, { notion }) => ({
24
- changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
- hasMore: false,
26
- }),
21
+ primaryKeyProperty: "ID",
22
+ schema: { defaultName: "Tasks", properties: { Name: Schema.title(), ID: Schema.richText() } },
23
+ execute: async (_state, { notion }) => ({
24
+ changes: [{ type: "upsert", key: "1", properties: { Name: Builder.title("Write docs"), ID: Builder.richText("1") } }],
25
+ hasMore: false,
26
+ }),
27
27
  });
28
28
 
29
29
  worker.tool("sayHello", {
30
- title: "Say Hello",
31
- description: "Return a greeting",
32
- schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
- execute: ({ name }, { notion }) => `Hello, ${name}`,
30
+ title: "Say Hello",
31
+ description: "Return a greeting",
32
+ schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false },
33
+ execute: ({ name }, { notion }) => `Hello, ${name}`,
34
34
  });
35
35
 
36
36
  worker.automation("sendWelcomeEmail", {
37
- title: "Send Welcome Email",
38
- description: "Runs from a database automation",
39
- execute: async (event, { notion }) => {},
37
+ title: "Send Welcome Email",
38
+ description: "Runs from a database automation",
39
+ execute: async (event, { notion }) => {},
40
40
  });
41
41
 
42
42
  worker.oauth("googleAuth", { name: "my-google-auth", provider: "google" });
@@ -69,23 +69,23 @@ Syncs run in a "sync cycle": a back-to-back chain of `execute` calls that starts
69
69
 
70
70
  ```ts
71
71
  worker.sync("paginatedSync", {
72
- mode: "replace",
73
- primaryKeyProperty: "ID",
74
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
- execute: async (state, { notion }) => {
76
- const page = state?.page ?? 1;
77
- const pageSize = 100;
78
- const { items, hasMore } = await fetchPage(page, pageSize);
79
- return {
80
- changes: items.map((item) => ({
81
- type: "upsert",
82
- key: item.id,
83
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
- })),
85
- hasMore,
86
- nextState: hasMore ? { page: page + 1 } : undefined,
87
- };
88
- },
72
+ mode: "replace",
73
+ primaryKeyProperty: "ID",
74
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
75
+ execute: async (state, { notion }) => {
76
+ const page = state?.page ?? 1;
77
+ const pageSize = 100;
78
+ const { items, hasMore } = await fetchPage(page, pageSize);
79
+ return {
80
+ changes: items.map((item) => ({
81
+ type: "upsert",
82
+ key: item.id,
83
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
84
+ })),
85
+ hasMore,
86
+ nextState: hasMore ? { page: page + 1 } : undefined,
87
+ };
88
+ },
89
89
  });
90
90
  ```
91
91
 
@@ -94,24 +94,24 @@ worker.sync("paginatedSync", {
94
94
  **Incremental example (changes only, with deletes):**
95
95
  ```ts
96
96
  worker.sync("incrementalSync", {
97
- primaryKeyProperty: "ID",
98
- mode: "incremental",
99
- schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
- execute: async (state, { notion }) => {
101
- const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
- return {
103
- changes: [
104
- ...upserts.map((item) => ({
105
- type: "upsert",
106
- key: item.id,
107
- properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
- })),
109
- ...deletes.map((id) => ({ type: "delete", key: id })),
110
- ],
111
- hasMore: Boolean(nextCursor),
112
- nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
- };
114
- },
97
+ primaryKeyProperty: "ID",
98
+ mode: "incremental",
99
+ schema: { defaultName: "Records", properties: { Name: Schema.title(), ID: Schema.richText() } },
100
+ execute: async (state, { notion }) => {
101
+ const { upserts, deletes, nextCursor } = await fetchChanges(state?.cursor);
102
+ return {
103
+ changes: [
104
+ ...upserts.map((item) => ({
105
+ type: "upsert",
106
+ key: item.id,
107
+ properties: { Name: Builder.title(item.name), ID: Builder.richText(item.id) },
108
+ })),
109
+ ...deletes.map((id) => ({ type: "delete", key: id })),
110
+ ],
111
+ hasMore: Boolean(nextCursor),
112
+ nextState: nextCursor ? { cursor: nextCursor } : undefined,
113
+ };
114
+ },
115
115
  });
116
116
  ```
117
117
 
@@ -122,29 +122,34 @@ Two syncs can relate to one another using `Schema.relation(relatedSyncKey)` and
122
122
  ```ts
123
123
  worker.sync("projectsSync", {
124
124
  primaryKeyProperty: "Project ID",
125
- ...
125
+ ...
126
126
  });
127
127
 
128
128
  // Example sync worker that syncs sample tasks to a database
129
129
  worker.sync("tasksSync", {
130
130
  primaryKeyProperty: "Task ID",
131
- ...
131
+ ...
132
132
  schema: {
133
- ...
133
+ ...
134
134
  properties: {
135
- ...
136
- Project: Schema.relation("projectsSync"),
135
+ ...
136
+ Project: Schema.relation("projectsSync", {
137
+ // Optionally configure a two-way relation. This will automatically create the
138
+ // "Tasks" property on the project synced database: there is no need
139
+ // to configure "Tasks" on the projectSync capability.
140
+ twoWay: true, relatedPropertyName: "Tasks"
141
+ }),
137
142
  },
138
143
  },
139
144
 
140
145
  execute: async () => {
141
146
  // Return sample tasks as database entries
142
- const tasks = fetchTasks()
147
+ const tasks = fetchTasks()
143
148
  const changes = tasks.map((task) => ({
144
149
  type: "upsert" as const,
145
150
  key: task.id,
146
151
  properties: {
147
- ...
152
+ ...
148
153
  Project: [Builder.relation(task.projectId)],
149
154
  },
150
155
  }));
@@ -264,6 +264,29 @@ Build and upload your worker bundle:
264
264
  npx workers deploy
265
265
  ```
266
266
 
267
+ ### Output format
268
+ Some commands support alternate output formats. When supported, these flags
269
+ are mutually exclusive:
270
+
271
+ - `--plain` for tab-separated output (default when stdout is piped)
272
+ - `--json` for JSON output
273
+ - `--human` for human-friendly output (default when stdout is a TTY)
274
+
275
+ List commands currently support these flags.
276
+
277
+ ```shell
278
+ npx workers list --plain
279
+ npx workers list --json
280
+ npx workers list --human
281
+ ```
282
+
283
+ ### `npx workers list`
284
+ List workers in the active space:
285
+
286
+ ```shell
287
+ npx workers list
288
+ ```
289
+
267
290
  ### `npx workers exec`
268
291
  Run a sync or tool capability:
269
292
 
@@ -340,12 +363,6 @@ List recent runs:
340
363
  npx workers runs list
341
364
  ```
342
365
 
343
- Use `--plain` for machine-readable tab-separated output:
344
-
345
- ```shell
346
- npx workers runs list --plain
347
- ```
348
-
349
366
  ### `npx workers runs logs`
350
367
  Fetch logs for a run:
351
368
 
@@ -12,7 +12,7 @@ const projectName = "Project 1";
12
12
  const sampleTasks = [
13
13
  {
14
14
  id: "task-1",
15
- title: "Welcome to Project Ajax",
15
+ title: "Welcome to Notion Workers",
16
16
  status: "Completed",
17
17
  description: "This is a simple hello world example",
18
18
  projectId,