@notionhq/workers 0.7.0 → 0.8.1
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/alpha/workflow.d.ts +30 -2
- package/dist/alpha/workflow.d.ts.map +1 -1
- package/dist/alpha/workflow.js +51 -1
- package/dist/capabilities/custom-block.d.ts +124 -0
- package/dist/capabilities/custom-block.d.ts.map +1 -0
- package/dist/capabilities/custom-block.js +22 -0
- package/dist/credential.d.ts +26 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +176 -0
- package/dist/credential.test.d.ts +2 -0
- package/dist/credential.test.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/worker.d.ts +48 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +78 -1
- package/package.json +1 -1
- package/src/alpha/workflow.test.ts +124 -2
- package/src/alpha/workflow.ts +92 -3
- package/src/capabilities/custom-block.ts +181 -0
- package/src/credential.test.ts +324 -0
- package/src/credential.ts +238 -0
- package/src/index.ts +15 -0
- package/src/worker.test.ts +300 -2
- package/src/worker.ts +102 -2
- package/dist/block.d.ts +0 -321
- package/dist/block.d.ts.map +0 -1
- package/dist/block.js +0 -0
- package/src/block.ts +0 -525
package/dist/worker.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { createAiConnectorCapability } from "./capabilities/ai_connector.js";
|
|
2
2
|
import { createAutomationCapability } from "./capabilities/automation.js";
|
|
3
|
+
import { createCustomBlockCapability } from "./capabilities/custom-block.js";
|
|
3
4
|
import { createOAuthCapability } from "./capabilities/oauth.js";
|
|
4
5
|
import { createSyncCapability } from "./capabilities/sync.js";
|
|
5
6
|
import { createToolCapability } from "./capabilities/tool.js";
|
|
6
7
|
import { createWebhookCapability, WebhookVerificationError } from "./capabilities/webhook.js";
|
|
7
8
|
import { createWorkflowCapability } from "./capabilities/workflow.js";
|
|
9
|
+
import {
|
|
10
|
+
createCredentialDeclaration,
|
|
11
|
+
MAX_CREDENTIAL_DECLARATIONS_BYTES,
|
|
12
|
+
MAX_CREDENTIALS,
|
|
13
|
+
validateCredentialDeclarationConflicts
|
|
14
|
+
} from "./credential.js";
|
|
8
15
|
import { pacerWait } from "./pacer_internal.js";
|
|
9
16
|
import { createRequire } from "node:module";
|
|
10
17
|
const SDK_VERSION = (() => {
|
|
@@ -17,8 +24,28 @@ const SDK_VERSION = (() => {
|
|
|
17
24
|
})();
|
|
18
25
|
class Worker {
|
|
19
26
|
#capabilities = /* @__PURE__ */ new Map();
|
|
27
|
+
#credentials = /* @__PURE__ */ new Map();
|
|
20
28
|
#databases = /* @__PURE__ */ new Map();
|
|
21
29
|
#pacers = /* @__PURE__ */ new Map();
|
|
30
|
+
/**
|
|
31
|
+
* Declare a credential that is injected into matching outbound HTTPS
|
|
32
|
+
* requests without exposing its value to Worker code.
|
|
33
|
+
*/
|
|
34
|
+
credential(key, config) {
|
|
35
|
+
this.#validateUniqueKey(key);
|
|
36
|
+
if (this.#credentials.size >= MAX_CREDENTIALS) {
|
|
37
|
+
throw new Error(`Worker cannot declare more than ${MAX_CREDENTIALS} credentials`);
|
|
38
|
+
}
|
|
39
|
+
const declaration = createCredentialDeclaration(key, config);
|
|
40
|
+
validateCredentialDeclarationConflicts(this.#credentials.values(), declaration);
|
|
41
|
+
const declarations = [...this.#credentials.values(), declaration];
|
|
42
|
+
if (new TextEncoder().encode(JSON.stringify(declarations)).byteLength > MAX_CREDENTIAL_DECLARATIONS_BYTES) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Worker credential declarations must be at most ${MAX_CREDENTIAL_DECLARATIONS_BYTES} bytes`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
this.#credentials.set(key, declaration);
|
|
48
|
+
}
|
|
22
49
|
database(key, config) {
|
|
23
50
|
this.#validateUniqueKey(key);
|
|
24
51
|
const database = { key, config };
|
|
@@ -365,6 +392,50 @@ class Worker {
|
|
|
365
392
|
this.#capabilities.set(key, capability);
|
|
366
393
|
return capability;
|
|
367
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* Register a custom block capability.
|
|
397
|
+
*
|
|
398
|
+
* A custom block is a front-end view that the deploy pipeline builds from
|
|
399
|
+
* the source entry into a deployable bundle. It can optionally declare a
|
|
400
|
+
* data-source *schema* (`name`, and optional `description`, `icon`, and
|
|
401
|
+
* `properties`) that the block reads.
|
|
402
|
+
*
|
|
403
|
+
* Example:
|
|
404
|
+
*
|
|
405
|
+
* ```ts
|
|
406
|
+
* const worker = new Worker();
|
|
407
|
+
* export default worker;
|
|
408
|
+
*
|
|
409
|
+
* worker.customBlock("issueBoard", {
|
|
410
|
+
* version: 1,
|
|
411
|
+
* path: "./views/issueBoard",
|
|
412
|
+
* dataSources: {
|
|
413
|
+
* issues: {
|
|
414
|
+
* name: "Issues",
|
|
415
|
+
* properties: {
|
|
416
|
+
* title: { name: "Title", type: "title" },
|
|
417
|
+
* status: { name: "Status", type: "status" },
|
|
418
|
+
* },
|
|
419
|
+
* },
|
|
420
|
+
* },
|
|
421
|
+
* });
|
|
422
|
+
* ```
|
|
423
|
+
*
|
|
424
|
+
* The manifest keys (`version`, `dataSources`) are top-level on the config;
|
|
425
|
+
* the SDK assembles the deploy-wire `CustomBlockManifest` from them. Binding
|
|
426
|
+
* each declared data source to a concrete managed database is instance-level
|
|
427
|
+
* and handled at deploy time — the worker does not emit bindings.
|
|
428
|
+
*
|
|
429
|
+
* @param key - The unique key for this block (the block declaration key).
|
|
430
|
+
* @param config - The custom block configuration (a `project`/`static` source folder + optional `version`/`dataSources` schema).
|
|
431
|
+
* @returns The custom block capability.
|
|
432
|
+
*/
|
|
433
|
+
customBlock(key, config) {
|
|
434
|
+
this.#validateUniqueKey(key);
|
|
435
|
+
const capability = createCustomBlockCapability(key, config);
|
|
436
|
+
this.#capabilities.set(key, capability);
|
|
437
|
+
return capability;
|
|
438
|
+
}
|
|
368
439
|
/**
|
|
369
440
|
* Get all registered capabilities (for discovery) without their handlers.
|
|
370
441
|
*/
|
|
@@ -378,6 +449,7 @@ class Worker {
|
|
|
378
449
|
get manifest() {
|
|
379
450
|
return {
|
|
380
451
|
sdkVersion: SDK_VERSION,
|
|
452
|
+
credentials: Array.from(this.#credentials.values()),
|
|
381
453
|
databases: Array.from(this.#databases.values()),
|
|
382
454
|
pacers: Array.from(this.#pacers.values()),
|
|
383
455
|
capabilities: this.capabilities
|
|
@@ -401,6 +473,11 @@ class Worker {
|
|
|
401
473
|
`Cannot run OAuth capability "${key}" - OAuth capabilities only provide configuration`
|
|
402
474
|
);
|
|
403
475
|
}
|
|
476
|
+
if (capability._tag === "custom_block") {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Cannot run custom block capability "${key}" - custom blocks only provide configuration`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
404
481
|
if (!capability.handler) {
|
|
405
482
|
throw new Error(`Capability "${key}" cannot be executed`);
|
|
406
483
|
}
|
|
@@ -410,7 +487,7 @@ class Worker {
|
|
|
410
487
|
if (!key || typeof key !== "string") {
|
|
411
488
|
throw new Error("Capability key must be a non-empty string");
|
|
412
489
|
}
|
|
413
|
-
if (this.#capabilities.has(key) || this.#databases.has(key) || this.#pacers.has(key)) {
|
|
490
|
+
if (this.#capabilities.has(key) || this.#credentials.has(key) || this.#databases.has(key) || this.#pacers.has(key)) {
|
|
414
491
|
throw new Error(`Worker item with key "${key}" already registered`);
|
|
415
492
|
}
|
|
416
493
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi, type Mock } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { ExecutionError } from "./error.js";
|
|
4
4
|
import { triggers, type NotionPageCreatedEvent } from "./triggers.generated.js";
|
|
5
|
-
import { createWorkflow } from "./workflow.js";
|
|
5
|
+
import { createWorkflow, type StepContext, type WorkflowContext } from "./workflow.js";
|
|
6
6
|
|
|
7
7
|
let stdoutSpy: Mock<typeof process.stdout.write>;
|
|
8
8
|
|
|
@@ -16,6 +16,128 @@ afterEach(() => {
|
|
|
16
16
|
|
|
17
17
|
const event = { type: "notion.page.created" } as NotionPageCreatedEvent;
|
|
18
18
|
|
|
19
|
+
async function createWorkflowContext(): Promise<WorkflowContext> {
|
|
20
|
+
let context: WorkflowContext | undefined;
|
|
21
|
+
const workflow = createWorkflow({
|
|
22
|
+
title: "Test workflow",
|
|
23
|
+
description: "Captures its workflow context",
|
|
24
|
+
triggers: [triggers.notionPageCreated()],
|
|
25
|
+
handler: (_event, workflowContext) => {
|
|
26
|
+
context = workflowContext;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await workflow.handler(event, { concreteOutput: true });
|
|
31
|
+
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error("Workflow context was not created");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("step", () => {
|
|
40
|
+
it("runs a synchronous callback and writes success events in order", async () => {
|
|
41
|
+
const { step } = await createWorkflowContext();
|
|
42
|
+
let context: StepContext | undefined;
|
|
43
|
+
const resultPromise = step("Fetch page", (stepContext) => {
|
|
44
|
+
context = stepContext;
|
|
45
|
+
return { pageId: "page-1" } as const;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expectTypeOf(resultPromise).toEqualTypeOf<Promise<{ readonly pageId: "page-1" }>>();
|
|
49
|
+
await expect(resultPromise).resolves.toEqual({ pageId: "page-1" });
|
|
50
|
+
|
|
51
|
+
expect(context).toBeDefined();
|
|
52
|
+
const id = context!.id;
|
|
53
|
+
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
54
|
+
expect(stdoutSpy.mock.calls.map(([output]) => output)).toEqual([
|
|
55
|
+
`\n<__notion_step_started__>{"id":"${id}","name":"Fetch page"}</__notion_step_started__>\n`,
|
|
56
|
+
`\n<__notion_step_success__>{"id":"${id}","name":"Fetch page","value":{"pageId":"page-1"}}</__notion_step_success__>\n`,
|
|
57
|
+
`\n<__notion_step_completed__>{"id":"${id}","name":"Fetch page","type":"success"}</__notion_step_completed__>\n`,
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("embeds the invocation timestamp in the UUID v7", async () => {
|
|
62
|
+
const { step } = await createWorkflowContext();
|
|
63
|
+
const now = 1_721_234_567_890;
|
|
64
|
+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
|
|
65
|
+
let id = "";
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await step("Timestamped", async (context) => {
|
|
69
|
+
id = context.id;
|
|
70
|
+
});
|
|
71
|
+
} finally {
|
|
72
|
+
nowSpy.mockRestore();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expect(Number.parseInt(id.replaceAll("-", "").slice(0, 12), 16)).toBe(now);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("uses a distinct UUID for each invocation", async () => {
|
|
79
|
+
const { step } = await createWorkflowContext();
|
|
80
|
+
const ids: string[] = [];
|
|
81
|
+
|
|
82
|
+
await step("First", ({ id }) => {
|
|
83
|
+
ids.push(id);
|
|
84
|
+
});
|
|
85
|
+
await step("Second", ({ id }) => {
|
|
86
|
+
ids.push(id);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(new Set(ids).size).toBe(2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("writes failure events in order and rethrows the original error", async () => {
|
|
93
|
+
const { step } = await createWorkflowContext();
|
|
94
|
+
const originalError = new Error("boom");
|
|
95
|
+
let id = "";
|
|
96
|
+
const fn = vi.fn((context: StepContext) => {
|
|
97
|
+
id = context.id;
|
|
98
|
+
throw originalError;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(step("Explode", fn)).rejects.toBe(originalError);
|
|
102
|
+
|
|
103
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
104
|
+
expect(stdoutSpy).toHaveBeenCalledTimes(3);
|
|
105
|
+
expect(stdoutSpy.mock.calls[0]?.[0]).toBe(
|
|
106
|
+
`\n<__notion_step_started__>{"id":"${id}","name":"Explode"}</__notion_step_started__>\n`,
|
|
107
|
+
);
|
|
108
|
+
expect(stdoutSpy.mock.calls[1]?.[0]).toEqual(
|
|
109
|
+
expect.stringContaining(
|
|
110
|
+
`<__notion_step_failure__>{"id":"${id}","name":"Explode","error":{"name":"ExecutionError","message":"Error during worker execution: Error: boom","trace":`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
expect(stdoutSpy.mock.calls[2]?.[0]).toBe(
|
|
114
|
+
`\n<__notion_step_completed__>{"id":"${id}","name":"Explode","type":"failure"}</__notion_step_completed__>\n`,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("writes step events under concreteOutput", async () => {
|
|
119
|
+
const workflow = createWorkflow({
|
|
120
|
+
title: "Log Pages",
|
|
121
|
+
description: "Logs created pages",
|
|
122
|
+
triggers: [triggers.notionPageCreated()],
|
|
123
|
+
handler: async (_event, context) => {
|
|
124
|
+
await context.step("Always visible", () => "done");
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await workflow.handler(event, { concreteOutput: true });
|
|
129
|
+
|
|
130
|
+
expect(stdoutSpy).toHaveBeenCalledTimes(3);
|
|
131
|
+
expect(stdoutSpy.mock.calls[0]?.[0]).toEqual(
|
|
132
|
+
expect.stringContaining("<__notion_step_started__>"),
|
|
133
|
+
);
|
|
134
|
+
expect(stdoutSpy.mock.calls[1]?.[0]).toEqual(
|
|
135
|
+
expect.stringContaining("<__notion_step_success__>"),
|
|
136
|
+
);
|
|
137
|
+
expect(stdoutSpy.mock.calls[2]?.[0]).toEqual(expect.stringContaining('"type":"success"'));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
19
141
|
describe("createWorkflow", () => {
|
|
20
142
|
it("is tagged and exposes triggers in config", () => {
|
|
21
143
|
const workflow = createWorkflow({
|
package/src/alpha/workflow.ts
CHANGED
|
@@ -37,7 +37,7 @@ export type WorkflowConfiguration<
|
|
|
37
37
|
|
|
38
38
|
handler: (
|
|
39
39
|
event: WorkflowEventForTriggers<TTriggers>,
|
|
40
|
-
context:
|
|
40
|
+
context: WorkflowContext,
|
|
41
41
|
) => Promise<void> | void;
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -81,8 +81,12 @@ export type Workflow<
|
|
|
81
81
|
* title: "Send Welcome Email",
|
|
82
82
|
* description: "Sends a welcome email when a new page is added",
|
|
83
83
|
* triggers: [triggers.notionPageCreated()],
|
|
84
|
-
* handler: async (event) => {
|
|
84
|
+
* handler: async (event, context) => {
|
|
85
85
|
* console.log(event.page);
|
|
86
|
+
* await context.step("Process page", async ({ id }) => {
|
|
87
|
+
* console.log(`Running step ${id}`);
|
|
88
|
+
* return processPage(event.page);
|
|
89
|
+
* });
|
|
86
90
|
* },
|
|
87
91
|
* });
|
|
88
92
|
* ```
|
|
@@ -102,7 +106,10 @@ export function createWorkflow<
|
|
|
102
106
|
options?: HandlerOptions,
|
|
103
107
|
): Promise<{ status: "success" } | undefined> {
|
|
104
108
|
try {
|
|
105
|
-
const capabilityContext =
|
|
109
|
+
const capabilityContext: WorkflowContext = {
|
|
110
|
+
...createCapabilityContext("workflow"),
|
|
111
|
+
step,
|
|
112
|
+
};
|
|
106
113
|
await configuration.handler(event, capabilityContext);
|
|
107
114
|
|
|
108
115
|
if (options?.concreteOutput) {
|
|
@@ -129,3 +136,85 @@ export function createWorkflow<
|
|
|
129
136
|
},
|
|
130
137
|
};
|
|
131
138
|
}
|
|
139
|
+
|
|
140
|
+
/** Context passed to a workflow step. */
|
|
141
|
+
export type StepContext = {
|
|
142
|
+
/** An identifier for this invocation of the step. May be used as an idempotency key. */
|
|
143
|
+
id: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Context passed to a workflow handler. */
|
|
147
|
+
export type WorkflowContext = CapabilityContext & {
|
|
148
|
+
/**
|
|
149
|
+
* Run an observable unit of work within the workflow.
|
|
150
|
+
*
|
|
151
|
+
* The callback may be synchronous or asynchronous, but `step` always returns
|
|
152
|
+
* a promise. Its return value must be JSON-serializable so it can be included
|
|
153
|
+
* in the success event written to stdout.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const page = await context.step("Fetch page", async ({ id }) => {
|
|
158
|
+
* console.log(`Running step ${id}`);
|
|
159
|
+
* return context.notion.pages.retrieve({ page_id: pageId });
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
step: <T>(name: string, fn: (context: StepContext) => T | Promise<T>) => Promise<T>;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type StepCompletionType = "success" | "failure";
|
|
167
|
+
|
|
168
|
+
function generateUuidV7(): string {
|
|
169
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
170
|
+
let timestamp = Date.now();
|
|
171
|
+
|
|
172
|
+
// UUID v7 stores the Unix timestamp in milliseconds in its first 48 bits.
|
|
173
|
+
for (let index = 5; index >= 0; index--) {
|
|
174
|
+
bytes[index] = timestamp % 256;
|
|
175
|
+
timestamp = Math.floor(timestamp / 256);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
bytes[6] = (bytes[6]! & 0x0f) | 0x70;
|
|
179
|
+
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
180
|
+
|
|
181
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
182
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writeStepEvent(event: "started" | "success" | "failure" | "completed", value: unknown) {
|
|
186
|
+
const tag = `__notion_step_${event}__`;
|
|
187
|
+
process.stdout.write(`\n<${tag}>${JSON.stringify(value)}</${tag}>\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function step<T>(name: string, fn: (context: StepContext) => T | Promise<T>): Promise<T> {
|
|
191
|
+
const context: StepContext = { id: generateUuidV7() };
|
|
192
|
+
const event = { id: context.id, name };
|
|
193
|
+
|
|
194
|
+
writeStepEvent("started", event);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const value = await fn(context);
|
|
198
|
+
writeStepEvent("success", { ...event, value });
|
|
199
|
+
writeStepEvent("completed", {
|
|
200
|
+
...event,
|
|
201
|
+
type: "success" satisfies StepCompletionType,
|
|
202
|
+
});
|
|
203
|
+
return value;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const error = new ExecutionError(err);
|
|
206
|
+
writeStepEvent("failure", {
|
|
207
|
+
...event,
|
|
208
|
+
error: {
|
|
209
|
+
name: error.name,
|
|
210
|
+
message: error.message,
|
|
211
|
+
trace: error.stack,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
writeStepEvent("completed", {
|
|
215
|
+
...event,
|
|
216
|
+
type: "failure" satisfies StepCompletionType,
|
|
217
|
+
});
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Custom Block Authoring API
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Icon for a custom block data source. Mirrors the server/client
|
|
7
|
+
* `CustomBlockManifestIcon` (`@notionhq/shared/customViews/customBlockManifestTypes`).
|
|
8
|
+
*/
|
|
9
|
+
export type CustomBlockManifestIcon =
|
|
10
|
+
| { type: "emoji"; emoji: string }
|
|
11
|
+
| { type: "external"; url: string };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Public-API-shaped property type names usable in a custom block manifest.
|
|
15
|
+
*/
|
|
16
|
+
export type CustomBlockManifestPropertyType =
|
|
17
|
+
| "title"
|
|
18
|
+
| "rich_text"
|
|
19
|
+
| "number"
|
|
20
|
+
| "select"
|
|
21
|
+
| "multi_select"
|
|
22
|
+
| "status"
|
|
23
|
+
| "date"
|
|
24
|
+
| "people"
|
|
25
|
+
| "files"
|
|
26
|
+
| "checkbox"
|
|
27
|
+
| "url"
|
|
28
|
+
| "email"
|
|
29
|
+
| "phone_number"
|
|
30
|
+
| "formula"
|
|
31
|
+
| "relation"
|
|
32
|
+
| "rollup"
|
|
33
|
+
| "created_time"
|
|
34
|
+
| "created_by"
|
|
35
|
+
| "last_edited_time"
|
|
36
|
+
| "last_edited_by"
|
|
37
|
+
| "last_visited_time"
|
|
38
|
+
| "button"
|
|
39
|
+
| "unique_id"
|
|
40
|
+
| "location"
|
|
41
|
+
| "verification"
|
|
42
|
+
| "place";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Describes a single property exposed by a custom block data source.
|
|
46
|
+
*/
|
|
47
|
+
export type CustomBlockManifestProperty = {
|
|
48
|
+
name: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
type: CustomBlockManifestPropertyType;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Rich schema describing one of a custom block's data sources.
|
|
55
|
+
*/
|
|
56
|
+
export type CustomBlockManifestDataSource = {
|
|
57
|
+
/** Display name for the data source. */
|
|
58
|
+
name: string;
|
|
59
|
+
/** Optional human-readable description of the data source. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/** Optional icon for the data source. */
|
|
62
|
+
icon?: CustomBlockManifestIcon;
|
|
63
|
+
/** Optional property schema keyed by property key. */
|
|
64
|
+
properties?: Record<string, CustomBlockManifestProperty>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type CustomBlockManifest = {
|
|
68
|
+
version: 1;
|
|
69
|
+
/**
|
|
70
|
+
* The block's datasources schema, mapping keys you name to {@link CustomBlockManifestDataSource}
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
*
|
|
74
|
+
* {
|
|
75
|
+
* "myDataSource": {
|
|
76
|
+
* "name": "My Data Source",
|
|
77
|
+
* "description": "A description of my data source",
|
|
78
|
+
* "icon": "icon.png",
|
|
79
|
+
* "properties": {
|
|
80
|
+
* "property1": {
|
|
81
|
+
* "name": "Property 1",
|
|
82
|
+
* "type": "string"
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
*/
|
|
88
|
+
dataSources: Record<string, CustomBlockManifestDataSource>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A custom block is a front-end web app served in an iframe in the client with live data access.
|
|
93
|
+
*
|
|
94
|
+
* Available `type`s are `project` (a buildable project dir) and `static` (an already-built dir served as-is).
|
|
95
|
+
*/
|
|
96
|
+
export type CustomBlockConfiguration = (
|
|
97
|
+
| {
|
|
98
|
+
/** Buildable project dir (the default). */
|
|
99
|
+
type?: "project";
|
|
100
|
+
/** Path to the project dir, relative to the worker root (e.g. `"./blocks/foo"`). */
|
|
101
|
+
path: string;
|
|
102
|
+
/** Build command run in `path`. Defaults to `npm run build`. */
|
|
103
|
+
command?: string;
|
|
104
|
+
/** Built-output dir relative to `path`. Defaults to `dist`. */
|
|
105
|
+
output?: string;
|
|
106
|
+
}
|
|
107
|
+
| {
|
|
108
|
+
/** An already-built browser-bundle dir; served as-is, no build step. */
|
|
109
|
+
type: "static";
|
|
110
|
+
/** Path to the prebuilt dir, relative to the worker root. */
|
|
111
|
+
path: string;
|
|
112
|
+
command?: never;
|
|
113
|
+
output?: never;
|
|
114
|
+
}
|
|
115
|
+
) &
|
|
116
|
+
Partial<CustomBlockManifest>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The build source of a custom block, mirroring the server's
|
|
120
|
+
* `BlockSourceDeclaration`. `project` is a buildable dir (`command` defaults to
|
|
121
|
+
* `npm run build`, `output` to `dist`); `static` is an already-built dir served
|
|
122
|
+
* as-is.
|
|
123
|
+
*/
|
|
124
|
+
export type BlockSourceDeclaration =
|
|
125
|
+
| { type: "project"; path: string; command?: string; output?: string }
|
|
126
|
+
| { type: "static"; path: string };
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Config payload carried in the runtime manifest for a `_tag: "custom_block"`
|
|
130
|
+
* capability entry. The server parses this (see `RawBlockCapabilityConfig` in
|
|
131
|
+
* `capabilities-service.ts`): `source` is the build entry and `manifest` is the
|
|
132
|
+
* author-declared {@link CustomBlockManifest} describing the block's data-source
|
|
133
|
+
* schema. Data-source *bindings* are instance-level and are not carried here.
|
|
134
|
+
*/
|
|
135
|
+
export type CustomBlockCapabilityConfig = {
|
|
136
|
+
source: BlockSourceDeclaration;
|
|
137
|
+
manifest: CustomBlockManifest;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A custom-block capability entry. Unlike other capabilities it has no runtime
|
|
142
|
+
* handler — it is a build-time/deploy-time declaration only, carried through
|
|
143
|
+
* the manifest so the server can build and bind the block on deploy.
|
|
144
|
+
*/
|
|
145
|
+
export type CustomBlockCapability = {
|
|
146
|
+
readonly _tag: "custom_block";
|
|
147
|
+
readonly key: string;
|
|
148
|
+
readonly config: CustomBlockCapabilityConfig;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Builds a custom-block capability entry from its authoring configuration. The
|
|
153
|
+
* author-declared `dataSources` schema is carried verbatim into the block
|
|
154
|
+
* {@link CustomBlockManifest}; no bindings are emitted.
|
|
155
|
+
*/
|
|
156
|
+
export function createCustomBlockCapability(
|
|
157
|
+
key: string,
|
|
158
|
+
config: CustomBlockConfiguration,
|
|
159
|
+
): CustomBlockCapability {
|
|
160
|
+
const source: BlockSourceDeclaration =
|
|
161
|
+
config.type === "static"
|
|
162
|
+
? { type: "static", path: config.path }
|
|
163
|
+
: {
|
|
164
|
+
type: "project",
|
|
165
|
+
path: config.path,
|
|
166
|
+
...(config.command !== undefined ? { command: config.command } : {}),
|
|
167
|
+
...(config.output !== undefined ? { output: config.output } : {}),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
_tag: "custom_block",
|
|
172
|
+
key,
|
|
173
|
+
config: {
|
|
174
|
+
source,
|
|
175
|
+
manifest: {
|
|
176
|
+
version: config.version ?? 1,
|
|
177
|
+
dataSources: config.dataSources ?? {},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|