@project-ajax/sdk 0.0.68 → 0.0.69

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/worker.js ADDED
@@ -0,0 +1,218 @@
1
+ import { createAutomationCapability } from "./capabilities/automation.js";
2
+ import { createOAuthCapability } from "./capabilities/oauth.js";
3
+ import { createSyncCapability } from "./capabilities/sync.js";
4
+ import { createToolCapability } from "./capabilities/tool.js";
5
+ class Worker {
6
+ #capabilities = /* @__PURE__ */ new Map();
7
+ /**
8
+ * Register a sync capability.
9
+ *
10
+ * Example:
11
+ *
12
+ * ```ts
13
+ * import { Worker } from "@project-ajax/sdk";
14
+ * import * as Builder from "@project-ajax/sdk/builder";
15
+ * import * as Schema from "@project-ajax/sdk/schema";
16
+ *
17
+ * const worker = new Worker();
18
+ * export default worker;
19
+ *
20
+ * worker.sync("tasksSync", {
21
+ * primaryKeyProperty: "Task ID",
22
+ * schema: {
23
+ * defaultName: "Tasks",
24
+ * properties: {
25
+ * "Task Name": Schema.title(),
26
+ * "Task ID": Schema.richText(),
27
+ * Status: Schema.select([
28
+ * { name: "Open", color: "default" },
29
+ * { name: "Done", color: "green" },
30
+ * ]),
31
+ * },
32
+ * },
33
+ * execute: async () => {
34
+ * const objects = [
35
+ * {
36
+ * key: "task-1",
37
+ * properties: {
38
+ * "Task Name": Builder.title("Write docs"),
39
+ * "Task ID": Builder.richText("task-1"),
40
+ * Status: Builder.select("Open"),
41
+ * },
42
+ * },
43
+ * ];
44
+ *
45
+ * return { objects, done: true };
46
+ * },
47
+ * });
48
+ * ```
49
+ *
50
+ * @param key - The unique key for this capability.
51
+ * @param config - The sync configuration.
52
+ * @returns The capability object.
53
+ */
54
+ sync(key, config) {
55
+ this.#validateUniqueKey(key);
56
+ const capability = createSyncCapability(key, config);
57
+ this.#capabilities.set(key, capability);
58
+ return capability;
59
+ }
60
+ /**
61
+ * Register a tool capability.
62
+ *
63
+ * Example:
64
+ *
65
+ * ```ts
66
+ * worker.tool<{ name: string }, string>("sayHello", {
67
+ * title: "Say Hello",
68
+ * description: "Say hello to the user",
69
+ * schema: {
70
+ * type: "object",
71
+ * properties: {
72
+ * name: { type: "string" },
73
+ * },
74
+ * required: ["name"],
75
+ * },
76
+ * execute: ({ name }) => {
77
+ * return `Hello, ${name}!`;
78
+ * },
79
+ * })
80
+ * ```
81
+ *
82
+ *
83
+ * @param key - The unique key for this capability.
84
+ * @param config - The tool configuration.
85
+ * @returns The capability object.
86
+ */
87
+ tool(key, config) {
88
+ this.#validateUniqueKey(key);
89
+ const capability = createToolCapability(key, config);
90
+ this.#capabilities.set(key, capability);
91
+ return capability;
92
+ }
93
+ /**
94
+ * Register an automation capability.
95
+ *
96
+ * Example:
97
+ *
98
+ * ```ts
99
+ * const worker = new Worker();
100
+ * export default worker;
101
+ *
102
+ * worker.automation("sendWelcomeEmail", {
103
+ * title: "Send Welcome Email",
104
+ * description: "Sends a welcome email when a new user is added",
105
+ * execute: async (context) => {
106
+ * const { pageId, pageData } = context;
107
+ *
108
+ * // Access page properties from the Public API format
109
+ * if (pageData) {
110
+ * const name = pageData.properties.Name; // Access any property
111
+ * const status = pageData.properties.Status;
112
+ * console.log(`Processing: ${name}`);
113
+ * }
114
+ *
115
+ * // Your automation logic here
116
+ * await sendEmail(pageId);
117
+ * },
118
+ * })
119
+ * ```
120
+ *
121
+ * @param key - The unique key for this capability.
122
+ * @param config - The automation configuration.
123
+ * @returns The capability object.
124
+ */
125
+ automation(key, config) {
126
+ this.#validateUniqueKey(key);
127
+ const capability = createAutomationCapability(key, config);
128
+ this.#capabilities.set(key, capability);
129
+ return capability;
130
+ }
131
+ /**
132
+ * Register an OAuth capability.
133
+ *
134
+ * There are two ways to configure OAuth:
135
+ *
136
+ * 1. Notion-managed providers:
137
+ * ```ts
138
+ * const worker = new Worker();
139
+ * export default worker;
140
+ *
141
+ * worker.oauth("googleAuth", {
142
+ * type: "notion_managed",
143
+ * name: "my-google-auth",
144
+ * provider: "google"
145
+ * })
146
+ * ```
147
+ *
148
+ * 2. User-managed OAuth configuration:
149
+ * ```ts
150
+ * const worker = new Worker();
151
+ * export default worker;
152
+ *
153
+ * worker.oauth("myCustomAuth", {
154
+ * type: "user_managed",
155
+ * name: "my-custom-oauth",
156
+ * authorizationEndpoint: "https://provider.com/oauth/authorize",
157
+ * tokenEndpoint: "https://provider.com/oauth/token",
158
+ * scope: "read write",
159
+ * clientId: process.env.CLIENT_ID,
160
+ * clientSecret: process.env.CLIENT_SECRET,
161
+ * authorizationParams: {
162
+ * access_type: "offline",
163
+ * prompt: "consent"
164
+ * }
165
+ * })
166
+ * ```
167
+ *
168
+ * @param key - The unique key used to register this OAuth capability.
169
+ * @param config - The OAuth configuration (Notion-managed or user-managed) for this capability.
170
+ * @returns The registered OAuth capability.
171
+ */
172
+ oauth(key, config) {
173
+ this.#validateUniqueKey(key);
174
+ const capability = createOAuthCapability(key, config);
175
+ this.#capabilities.set(key, capability);
176
+ return capability;
177
+ }
178
+ /**
179
+ * Get all registered capabilities (for discovery) without their handlers.
180
+ */
181
+ get capabilities() {
182
+ return Array.from(this.#capabilities.values()).map((c) => ({
183
+ _tag: c._tag,
184
+ key: c.key,
185
+ config: c.config
186
+ }));
187
+ }
188
+ /**
189
+ * Execute a capability by key.
190
+ *
191
+ * @param key - The key of the capability to execute.
192
+ * @param context - The context to pass to the capability.
193
+ * @returns The result of the capability execution.
194
+ */
195
+ async run(key, context) {
196
+ const capability = this.#capabilities.get(key);
197
+ if (!capability) {
198
+ throw new Error(`Capability "${key}" not found`);
199
+ }
200
+ if (capability._tag === "oauth") {
201
+ throw new Error(
202
+ `Cannot run OAuth capability "${key}" - OAuth capabilities only provide configuration`
203
+ );
204
+ }
205
+ return capability.handler(context);
206
+ }
207
+ #validateUniqueKey(key) {
208
+ if (!key || typeof key !== "string") {
209
+ throw new Error("Capability key must be a non-empty string");
210
+ }
211
+ if (this.#capabilities.has(key)) {
212
+ throw new Error(`Capability with key "${key}" already registered`);
213
+ }
214
+ }
215
+ }
216
+ export {
217
+ Worker
218
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-ajax/sdk",
3
- "version": "0.0.68",
3
+ "version": "0.0.69",
4
4
  "description": "An SDK for building workers for the Project Ajax platform",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -28,10 +28,6 @@
28
28
  "./types": {
29
29
  "types": "./dist/types.d.ts",
30
30
  "default": "./dist/types.js"
31
- },
32
- "./sync": {
33
- "types": "./dist/capabilities/sync.d.ts",
34
- "default": "./dist/capabilities/sync.js"
35
31
  }
36
32
  },
37
33
  "engines": {
@@ -8,14 +8,10 @@ import {
8
8
  vi,
9
9
  } from "vitest";
10
10
  import { ExecutionError } from "../error.js";
11
- import {
12
- type AutomationConfiguration,
13
- type AutomationContext,
14
- automation,
15
- type PageObjectResponse,
16
- } from "./automation.js";
11
+ import type { AutomationContext, PageObjectResponse } from "./automation.js";
12
+ import { createAutomationCapability } from "./automation.js";
17
13
 
18
- describe("automation", () => {
14
+ describe("createAutomationCapability", () => {
19
15
  let stdoutSpy: Mock<typeof process.stdout.write>;
20
16
 
21
17
  beforeEach(() => {
@@ -29,23 +25,21 @@ describe("automation", () => {
29
25
  });
30
26
 
31
27
  it("creates automation capability with correct structure", () => {
32
- const config: AutomationConfiguration = {
28
+ const capability = createAutomationCapability("testAutomation", {
33
29
  title: "Test Automation",
34
30
  description: "Test automation description",
35
31
  execute: () => {},
36
- };
37
-
38
- const result = automation(config);
32
+ });
39
33
 
40
- expect(result._tag).toBe("automation");
41
- expect(result.config.title).toBe("Test Automation");
42
- expect(result.config.description).toBe("Test automation description");
43
- expect(typeof result.handler).toBe("function");
34
+ expect(capability).toBeDefined();
35
+ expect(capability?._tag).toBe("automation");
36
+ expect(capability.config.title).toBe("Test Automation");
37
+ expect(capability.config.description).toBe("Test automation description");
44
38
  });
45
39
 
46
40
  it("executes automation without page data", async () => {
47
41
  const executeFn = vi.fn();
48
- const myAutomation = automation({
42
+ const capability = createAutomationCapability("syncAutomation", {
49
43
  title: "Sync Automation",
50
44
  description: "Executes synchronously",
51
45
  execute: executeFn,
@@ -56,7 +50,7 @@ describe("automation", () => {
56
50
  actionType: "test_action",
57
51
  };
58
52
 
59
- await myAutomation.handler(context);
53
+ await capability.handler(context);
60
54
 
61
55
  expect(executeFn).toHaveBeenCalledWith(context);
62
56
  expect(stdoutSpy).toHaveBeenCalledWith(
@@ -66,7 +60,7 @@ describe("automation", () => {
66
60
 
67
61
  it("executes automation with page data", async () => {
68
62
  const executeFn = vi.fn();
69
- const myAutomation = automation({
63
+ const capability = createAutomationCapability("pageAutomation", {
70
64
  title: "Page Automation",
71
65
  description: "Processes page data",
72
66
  execute: executeFn,
@@ -97,7 +91,7 @@ describe("automation", () => {
97
91
  pageData,
98
92
  };
99
93
 
100
- await myAutomation.handler(context);
94
+ await capability.handler(context);
101
95
 
102
96
  expect(executeFn).toHaveBeenCalledWith(context);
103
97
  expect(stdoutSpy).toHaveBeenCalledWith(
@@ -106,7 +100,7 @@ describe("automation", () => {
106
100
  });
107
101
 
108
102
  it("handles execution error from function", async () => {
109
- const myAutomation = automation({
103
+ const capability = createAutomationCapability("errorAutomation", {
110
104
  title: "Error Automation",
111
105
  description: "Throws an error",
112
106
  execute: () => {
@@ -119,7 +113,7 @@ describe("automation", () => {
119
113
  actionType: "error_action",
120
114
  };
121
115
 
122
- await expect(myAutomation.handler(context)).rejects.toThrow(ExecutionError);
116
+ await expect(capability.handler(context)).rejects.toThrow(ExecutionError);
123
117
 
124
118
  expect(stdoutSpy).toHaveBeenCalledWith(
125
119
  `\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: Something went wrong"}}</output>\n`,
@@ -127,7 +121,7 @@ describe("automation", () => {
127
121
  });
128
122
 
129
123
  it("handles execution error with non-Error object", async () => {
130
- const myAutomation = automation({
124
+ const capability = createAutomationCapability("nonErrorAutomation", {
131
125
  title: "Non-Error Automation",
132
126
  description: "Throws a non-Error object",
133
127
  execute: () => {
@@ -139,7 +133,7 @@ describe("automation", () => {
139
133
  actionType: "string_error_action",
140
134
  };
141
135
 
142
- await expect(myAutomation.handler(context)).rejects.toThrow(ExecutionError);
136
+ await expect(capability.handler(context)).rejects.toThrow(ExecutionError);
143
137
 
144
138
  expect(stdoutSpy).toHaveBeenCalledWith(
145
139
  `\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: String error"}}</output>\n`,
@@ -70,38 +70,24 @@ export interface AutomationHandlerResult {
70
70
  description: string;
71
71
  }
72
72
 
73
+ export type AutomationCapability = ReturnType<
74
+ typeof createAutomationCapability
75
+ >;
76
+
73
77
  /**
74
- * Creates a capability definition for an automation that can be triggered
75
- * from database automations in Notion.
76
- *
77
- * Example:
78
- *
79
- * ```ts
80
- * automation({
81
- * title: "Send Welcome Email",
82
- * description: "Sends a welcome email when a new user is added",
83
- * execute: async (context) => {
84
- * const { pageId, pageData } = context;
85
- *
86
- * // Access page properties from the Public API format
87
- * if (pageData) {
88
- * const name = pageData.properties.Name; // Access any property
89
- * const status = pageData.properties.Status;
90
- * console.log(`Processing: ${name}`);
91
- * }
92
- *
93
- * // Your automation logic here
94
- * await sendEmail(pageId);
95
- * },
96
- * })
97
- * ```
78
+ * Creates an automation capability from configuration.
98
79
  *
99
- * @param config - The configuration for the automation.
100
- * @returns A capability definition for the automation.
80
+ * @param key - The unique name for this capability.
81
+ * @param config - The automation configuration.
82
+ * @returns The capability object.
101
83
  */
102
- export function automation(config: AutomationConfiguration) {
84
+ export function createAutomationCapability(
85
+ key: string,
86
+ config: AutomationConfiguration,
87
+ ) {
103
88
  return {
104
- _tag: "automation",
89
+ _tag: "automation" as const,
90
+ key,
105
91
  config: {
106
92
  title: config.title,
107
93
  description: config.description,
@@ -109,12 +95,10 @@ export function automation(config: AutomationConfiguration) {
109
95
  async handler(context: AutomationContext): Promise<void> {
110
96
  try {
111
97
  await config.execute(context);
112
- // Write success result
113
98
  process.stdout.write(
114
99
  `\n<output>${JSON.stringify({ status: "success" })}</output>\n`,
115
100
  );
116
101
  } catch (err) {
117
- // Convert error to ExecutionError and write it
118
102
  const error = new ExecutionError(err);
119
103
  process.stdout.write(
120
104
  `\n<output>${JSON.stringify({ status: "error", error: { name: error.name, message: error.message } })}</output>\n`,
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "vitest";
2
- import { oauth } from "./oauth.js";
2
+ import { createOAuthCapability } from "./oauth.js";
3
3
 
4
4
  describe("oauth", () => {
5
5
  afterEach(() => {
@@ -9,42 +9,43 @@ describe("oauth", () => {
9
9
  });
10
10
 
11
11
  it("creates notion-managed oauth capability with accessToken helper", async () => {
12
- const myOauth = oauth({
12
+ const capability = createOAuthCapability("googleAuth", {
13
13
  name: "googleAuth",
14
14
  provider: "google",
15
15
  accessTokenExpireMs: 60_000,
16
16
  });
17
17
 
18
- expect(myOauth._tag).toBe("oauth");
19
- expect(myOauth.config.type).toBe("notion_managed");
20
- expect(myOauth.config.accessTokenExpireMs).toBe(60_000);
21
- expect(myOauth.envKey).toBe("OAUTH_676F6F676C6541757468_ACCESS_TOKEN");
22
- expect(typeof myOauth.accessToken).toBe("function");
18
+ expect(capability).toBeDefined();
19
+ expect(capability?._tag).toBe("oauth");
20
+ expect(capability.config.type).toBe("notion_managed");
21
+ expect(capability.config.accessTokenExpireMs).toBe(60_000);
22
+
23
+ expect(typeof capability.accessToken).toBe("function");
23
24
 
24
25
  process.env.OAUTH_676F6F676C6541757468_ACCESS_TOKEN = "token-123";
25
- await expect(myOauth.accessToken()).resolves.toBe("token-123");
26
+ await expect(capability.accessToken()).resolves.toBe("token-123");
26
27
  });
27
28
 
28
29
  it("normalizes non-alphanumeric characters in the identifier", () => {
29
- const myOauth = oauth({
30
+ const capability = createOAuthCapability("googleCalendar", {
30
31
  name: "google-calendar",
31
32
  provider: "google",
32
33
  accessTokenExpireMs: 3600_000,
33
34
  });
34
35
 
35
- expect(myOauth.envKey).toBe(
36
- "OAUTH_676F6F676C652D63616C656E646172_ACCESS_TOKEN",
37
- );
38
- expect(myOauth.config.accessTokenExpireMs).toBe(3600_000);
36
+ expect(capability).toBeDefined();
37
+ expect(capability?._tag).toBe("oauth");
38
+ expect(capability.config.type).toBe("notion_managed");
39
+ expect(capability.config.accessTokenExpireMs).toBe(3600_000);
39
40
  });
40
41
 
41
42
  it("throws a helpful error when the token env var is missing", async () => {
42
- const myOauth = oauth({
43
+ const capability = createOAuthCapability("googleAuthMissing", {
43
44
  name: "googleAuth",
44
45
  provider: "google",
45
46
  });
46
47
 
47
- await expect(myOauth.accessToken()).rejects.toThrow(
48
+ await expect(capability.accessToken()).rejects.toThrow(
48
49
  /Missing OAuth access token env var "OAUTH_676F6F676C6541757468_ACCESS_TOKEN"/,
49
50
  );
50
51
  });
@@ -88,46 +88,21 @@ export type OAuthConfiguration =
88
88
  | NotionManagedOAuthConfiguration
89
89
  | UserManagedOAuthConfiguration;
90
90
 
91
+ export type OAuthCapability = ReturnType<typeof createOAuthCapability>;
92
+
91
93
  /**
92
94
  * Creates an OAuth provider configuration for authenticating with third-party services.
93
95
  *
94
- * There are two ways to configure OAuth:
95
- *
96
- * 1. Notion-managed providers:
97
- * ```ts
98
- * oauth({
99
- * type: "notion_managed",
100
- * name: "my-google-auth",
101
- * provider: "google"
102
- * })
103
- * ```
104
- *
105
- * 2. User-managed OAuth configuration:
106
- * ```ts
107
- * oauth({
108
- * type: "user_managed",
109
- * name: "my-custom-oauth",
110
- * authorizationEndpoint: "https://provider.com/oauth/authorize",
111
- * tokenEndpoint: "https://provider.com/oauth/token",
112
- * scope: "read write",
113
- * clientId: process.env.CLIENT_ID,
114
- * clientSecret: process.env.CLIENT_SECRET,
115
- * authorizationParams: {
116
- * access_type: "offline",
117
- * prompt: "consent"
118
- * }
119
- * })
120
- * ```
121
- *
122
96
  * @param config - The OAuth configuration (Notion-managed or user-managed).
123
97
  * @returns An OAuth provider definition.
124
98
  */
125
- export function oauth(config: OAuthConfiguration) {
99
+ export function createOAuthCapability(key: string, config: OAuthConfiguration) {
126
100
  const envKey = oauthNameToEnvKey(config.name);
127
101
 
128
102
  if ("provider" in config) {
129
103
  return {
130
- _tag: "oauth",
104
+ _tag: "oauth" as const,
105
+ key,
131
106
  envKey,
132
107
  async accessToken(): Promise<string> {
133
108
  return readRequiredEnvVar(envKey, { name: config.name });
@@ -142,7 +117,8 @@ export function oauth(config: OAuthConfiguration) {
142
117
  }
143
118
 
144
119
  return {
145
- _tag: "oauth",
120
+ _tag: "oauth" as const,
121
+ key,
146
122
  envKey,
147
123
  async accessToken(): Promise<string> {
148
124
  return readRequiredEnvVar(envKey, { name: config.name });
@@ -139,6 +139,8 @@ export type SyncHandlerResult<PK extends string, Context = unknown> = {
139
139
  deleteUnreturnedPages?: boolean;
140
140
  };
141
141
 
142
+ export type SyncCapability = ReturnType<typeof createSyncCapability>;
143
+
142
144
  /**
143
145
  * Creates a special handler for syncing third-party data to a collection.
144
146
  *
@@ -146,13 +148,14 @@ export type SyncHandlerResult<PK extends string, Context = unknown> = {
146
148
  * @returns A handler function that executes the sync function, and passes data
147
149
  * needed to complete the sync back to the platform.
148
150
  */
149
- export function sync<
151
+ export function createSyncCapability<
150
152
  PK extends string,
151
153
  S extends Schema<PK>,
152
154
  Context = unknown,
153
- >(syncConfiguration: SyncConfiguration<PK, S, Context>) {
155
+ >(key: string, syncConfiguration: SyncConfiguration<PK, S, Context>) {
154
156
  return {
155
- _tag: "sync",
157
+ _tag: "sync" as const,
158
+ key,
156
159
  config: {
157
160
  primaryKeyProperty: syncConfiguration.primaryKeyProperty,
158
161
  schema: syncConfiguration.schema,