@notionhq/apps 0.0.17 → 0.0.19

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.
Files changed (67) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +77 -35
  3. package/dist/cli/build.d.ts.map +1 -1
  4. package/dist/cli/build.js +12 -7
  5. package/dist/cli/emit-manifest.d.ts +1 -1
  6. package/dist/cli/emit-manifest.d.ts.map +1 -1
  7. package/dist/cli/emit-manifest.js +66 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/notion-as-code/custom-agent.d.ts +1 -0
  12. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  13. package/dist/notion-as-code/custom-agent.js +1 -1
  14. package/dist/notion-as-code/database.d.ts +90 -48
  15. package/dist/notion-as-code/database.d.ts.map +1 -1
  16. package/dist/notion-as-code/database.js +135 -45
  17. package/dist/notion-as-code/database.test.d.ts +2 -0
  18. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  19. package/dist/notion-as-code/index.d.ts +2 -2
  20. package/dist/notion-as-code/index.d.ts.map +1 -1
  21. package/dist/notion-as-code/intents.d.ts +34 -2
  22. package/dist/notion-as-code/intents.d.ts.map +1 -1
  23. package/dist/notion-as-code/page.d.ts +1 -0
  24. package/dist/notion-as-code/page.d.ts.map +1 -1
  25. package/dist/notion-as-code/page.js +1 -0
  26. package/dist/notion-as-code/schema.d.ts +8 -4
  27. package/dist/notion-as-code/schema.d.ts.map +1 -1
  28. package/dist/notion-as-code/views.d.ts +29 -10
  29. package/dist/notion-as-code/views.d.ts.map +1 -1
  30. package/dist/sync.d.ts +33 -15
  31. package/dist/sync.d.ts.map +1 -1
  32. package/dist/sync.js +12 -0
  33. package/dist/workflow-access-types.test.d.ts +2 -0
  34. package/dist/workflow-access-types.test.d.ts.map +1 -0
  35. package/dist/workflow-access.d.ts +68 -0
  36. package/dist/workflow-access.d.ts.map +1 -0
  37. package/dist/workflow-access.js +135 -0
  38. package/dist/workflow-state.d.ts +13 -0
  39. package/dist/workflow-state.d.ts.map +1 -0
  40. package/dist/workflow-state.js +107 -0
  41. package/dist/workflow.d.ts +65 -9
  42. package/dist/workflow.d.ts.map +1 -1
  43. package/dist/workflow.js +119 -5
  44. package/docs/BUILD.md +134 -4
  45. package/package.json +1 -1
  46. package/skills/notion-as-code/SKILL.md +89 -49
  47. package/skills/sync/SKILL.md +21 -18
  48. package/skills/workflow/SKILL.md +101 -17
  49. package/src/cli/build.test.ts +148 -64
  50. package/src/cli/build.ts +13 -8
  51. package/src/cli/emit-manifest.ts +107 -0
  52. package/src/index.ts +21 -1
  53. package/src/notion-as-code/custom-agent.ts +2 -1
  54. package/src/notion-as-code/database.test.ts +661 -0
  55. package/src/notion-as-code/database.ts +349 -127
  56. package/src/notion-as-code/index.ts +16 -1
  57. package/src/notion-as-code/intents.ts +39 -2
  58. package/src/notion-as-code/page.ts +2 -0
  59. package/src/notion-as-code/schema.ts +11 -3
  60. package/src/notion-as-code/views.ts +29 -10
  61. package/src/sync.test.ts +295 -0
  62. package/src/sync.ts +85 -21
  63. package/src/workflow-access-types.test.ts +69 -0
  64. package/src/workflow-access.ts +299 -0
  65. package/src/workflow-state.ts +152 -0
  66. package/src/workflow.test.ts +441 -1
  67. package/src/workflow.ts +230 -13
package/src/workflow.ts CHANGED
@@ -6,6 +6,13 @@ import {
6
6
  type WorkflowConnectionRequirement,
7
7
  type WorkflowConnections,
8
8
  } from "./connections.js";
9
+ import {
10
+ hydrateWorkflowAccess,
11
+ normalizeWorkflowAccess,
12
+ type WorkflowAccess,
13
+ type WorkflowAccessDeclarations,
14
+ type WorkflowAccessRequirements,
15
+ } from "./workflow-access.js";
9
16
  import { createHash } from "node:crypto";
10
17
  import { readFile } from "node:fs/promises";
11
18
  import { join } from "node:path";
@@ -17,6 +24,7 @@ import { writeOutput } from "./output.js";
17
24
  import { resolveRuntimeInput } from "./runtime-input.js";
18
25
  import { readRunMetadata, type RunMetadata } from "./runtime-metadata.js";
19
26
  import { createWorkflowTriggers } from "./triggers.generated.js";
27
+ import { createWorkflowStepState, type WorkflowState } from "./workflow-state.js";
20
28
  import type {
21
29
  WorkflowEventMap,
22
30
  WorkflowTrigger,
@@ -29,6 +37,60 @@ type HandlerOptions = {
29
37
  concreteOutput?: true;
30
38
  };
31
39
 
40
+ type WorkflowWaitDurationValues = {
41
+ milliseconds?: number;
42
+ seconds?: number;
43
+ minutes?: number;
44
+ hours?: number;
45
+ days?: number;
46
+ weeks?: number;
47
+ };
48
+
49
+ export type WorkflowWaitDuration = {
50
+ [Unit in keyof WorkflowWaitDurationValues]-?: Required<Pick<WorkflowWaitDurationValues, Unit>> &
51
+ WorkflowWaitDurationValues;
52
+ }[keyof WorkflowWaitDurationValues];
53
+
54
+ type WorkflowWaitKey = string | string[];
55
+
56
+ export type WorkflowWaitUntilOptions =
57
+ | {
58
+ at: Date;
59
+ key?: WorkflowWaitKey;
60
+ }
61
+ | {
62
+ after: WorkflowWaitDuration;
63
+ key?: WorkflowWaitKey;
64
+ };
65
+
66
+ export type WorkflowWaitUntilResult = {
67
+ status: "waiting";
68
+ wait: {
69
+ type: "until";
70
+ name: string;
71
+ resumeAtMs: number;
72
+ };
73
+ };
74
+
75
+ type WorkflowHandlerResult = { status: "success" } | WorkflowWaitUntilResult;
76
+
77
+ const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1_000;
78
+
79
+ export type {
80
+ WorkflowAccess,
81
+ WorkflowAccessAgentLevel,
82
+ WorkflowAccessBindings,
83
+ WorkflowAccessDeclaration,
84
+ WorkflowAccessDeclarations,
85
+ WorkflowAccessLevel,
86
+ WorkflowAccessLevelFor,
87
+ WorkflowAccessResource,
88
+ WorkflowAccessResourceLevel,
89
+ WorkflowAccessResourceType,
90
+ WorkflowAccessRequirement,
91
+ WorkflowAccessRequirements,
92
+ WorkflowAccessValue,
93
+ } from "./workflow-access.js";
32
94
  export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
33
95
 
34
96
  export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
@@ -52,6 +114,7 @@ export {
52
114
  export type WorkflowConfiguration<
53
115
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
54
116
  TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
117
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
55
118
  > = {
56
119
  /**
57
120
  * A human-readable name for the workflow, shown in the UI when viewing workflows
@@ -70,6 +133,13 @@ export type WorkflowConfiguration<
70
133
  */
71
134
  triggers: TTriggers;
72
135
 
136
+ /**
137
+ * Notion-as-Code resources whose access is configured during cloud deployment.
138
+ * Deployment reconciles grants through the existing Notion-module permissions
139
+ * and supplies named runtime bindings on `context.access`.
140
+ */
141
+ access?: TAccess;
142
+
73
143
  /**
74
144
  * Connections that must be set up before this workflow can run.
75
145
  */
@@ -77,7 +147,7 @@ export type WorkflowConfiguration<
77
147
 
78
148
  handler: (
79
149
  event: WorkflowEventForTriggers<TTriggers>,
80
- context: WorkflowContext<TConnections>,
150
+ context: WorkflowContext<TConnections, TAccess>,
81
151
  ) => Promise<void> | void;
82
152
  };
83
153
 
@@ -85,7 +155,8 @@ export type WorkflowConfiguration<
85
155
  export type WorkflowTriggerConfiguration<
86
156
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
87
157
  TConnections extends WorkflowConnectionDeclarations,
88
- > = Omit<WorkflowConfiguration<TTriggers, TConnections>, "triggers"> & {
158
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
159
+ > = Omit<WorkflowConfiguration<TTriggers, TConnections, TAccess>, "triggers"> & {
89
160
  triggers: (context: { triggers: WorkflowTriggerCreators<NoInfer<TConnections>> }) => TTriggers;
90
161
  };
91
162
 
@@ -107,11 +178,12 @@ export type Workflow<
107
178
  description: string;
108
179
  triggers: TTriggers;
109
180
  connections?: readonly WorkflowConnectionRequirement[];
181
+ access?: WorkflowAccessRequirements;
110
182
  };
111
183
  handler: (
112
184
  event: WorkflowEventForTriggers<TTriggers>,
113
185
  options?: HandlerOptions,
114
- ) => Promise<{ status: "success" } | undefined>;
186
+ ) => Promise<WorkflowHandlerResult | undefined>;
115
187
  };
116
188
 
117
189
  /**
@@ -146,20 +218,26 @@ export function workflow<
146
218
  ...WorkflowTriggerForConnections<NoInfer<TConnections>>[],
147
219
  ],
148
220
  const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
149
- >(configuration: WorkflowTriggerConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
221
+ const TAccess extends WorkflowAccessDeclarations = Record<never, never>,
222
+ >(
223
+ configuration: WorkflowTriggerConfiguration<TTriggers, TConnections, TAccess>,
224
+ ): Workflow<TTriggers>;
150
225
  export function workflow<
151
226
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
152
227
  const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
153
- >(configuration: WorkflowConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
228
+ const TAccess extends WorkflowAccessDeclarations = Record<never, never>,
229
+ >(configuration: WorkflowConfiguration<TTriggers, TConnections, TAccess>): Workflow<TTriggers>;
154
230
  export function workflow<
155
231
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
156
232
  const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
233
+ const TAccess extends WorkflowAccessDeclarations = Record<never, never>,
157
234
  >(
158
235
  configuration:
159
- | WorkflowConfiguration<TTriggers, TConnections>
160
- | WorkflowTriggerConfiguration<TTriggers, TConnections>,
236
+ | WorkflowConfiguration<TTriggers, TConnections, TAccess>
237
+ | WorkflowTriggerConfiguration<TTriggers, TConnections, TAccess>,
161
238
  ): Workflow<TTriggers> {
162
239
  const requirements = normalizeConnectionRequirements(configuration.connections ?? {});
240
+ const accessRequirements = normalizeWorkflowAccess(configuration.access);
163
241
  const connectionDeclarations =
164
242
  configuration.connections === undefined
165
243
  ? undefined
@@ -177,23 +255,27 @@ export function workflow<
177
255
  description: configuration.description,
178
256
  triggers,
179
257
  ...(configuration.connections === undefined ? {} : { connections: requirements }),
258
+ ...(configuration.access === undefined ? {} : { access: accessRequirements }),
180
259
  },
181
260
  async handler(
182
261
  event: WorkflowEventForTriggers<TTriggers>,
183
262
  options?: HandlerOptions,
184
- ): Promise<{ status: "success" } | undefined> {
263
+ ): Promise<WorkflowHandlerResult | undefined> {
185
264
  try {
186
265
  event = await resolveRuntimeInput(event);
187
266
  const runMetadata = readRunMetadata();
188
267
  const baseContext = createCapabilityContext();
189
- const capabilityContext: WorkflowContext<TConnections> = {
268
+ const step = createStep(runMetadata);
269
+ const capabilityContext: WorkflowContext<TConnections, TAccess> = {
190
270
  ...baseContext,
271
+ access: hydrateWorkflowAccess<TAccess>(accessRequirements),
191
272
  connections: createWorkflowConnections(
192
273
  baseContext.notion,
193
274
  connectionDeclarations,
194
275
  ),
195
276
  ...runMetadata,
196
- step: createStep(runMetadata),
277
+ step,
278
+ wait: createWait(step),
197
279
  };
198
280
  await configuration.handler(event, capabilityContext);
199
281
 
@@ -203,6 +285,18 @@ export function workflow<
203
285
 
204
286
  writeOutput({ _tag: "success", value: { status: "success" } });
205
287
  } catch (err) {
288
+ if (err instanceof WorkflowWaitInterrupt) {
289
+ if (options?.concreteOutput) {
290
+ return err.result;
291
+ }
292
+
293
+ writeOutput({
294
+ _tag: "wait",
295
+ wait: err.result.wait,
296
+ });
297
+ return;
298
+ }
299
+
206
300
  const error = new ExecutionError(err);
207
301
 
208
302
  if (!options?.concreteOutput) {
@@ -229,6 +323,13 @@ export type StepContext = {
229
323
  * Use this to make side effects idempotent when the workflow is retried.
230
324
  */
231
325
  id: string;
326
+ /**
327
+ * Durable key-value state shared by every run of this workflow.
328
+ *
329
+ * State is exposed on the step context because every operation participates
330
+ * in the active step's durability and replay boundary.
331
+ */
332
+ state: WorkflowState;
232
333
  };
233
334
 
234
335
  type WorkflowStepResult<T> = T extends void ? null : T;
@@ -252,10 +353,21 @@ type WorkflowStep = {
252
353
  ): Promise<WorkflowStepResult<T>>;
253
354
  };
254
355
 
356
+ type WorkflowWait = {
357
+ /**
358
+ * Suspend the workflow until a relative duration has elapsed or an absolute
359
+ * date has been reached.
360
+ */
361
+ until(name: string, options: WorkflowWaitUntilOptions): Promise<void>;
362
+ };
363
+
255
364
  /** Context passed to a workflow handler. */
256
365
  export type WorkflowContext<
257
366
  TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
367
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
258
368
  > = CapabilityContext & {
369
+ /** Resolved NaC bindings for this invocation; UI-granted resources are not added here. */
370
+ readonly access: WorkflowAccess<TAccess>;
259
371
  connections: WorkflowConnections<TConnections>;
260
372
  } & RunMetadata & {
261
373
  /**
@@ -279,6 +391,8 @@ export type WorkflowContext<
279
391
  * ```
280
392
  */
281
393
  step: WorkflowStep;
394
+ /** Suspend this workflow without consuming compute resources. */
395
+ wait: WorkflowWait;
282
396
  };
283
397
 
284
398
  type StepCompletionType = "success" | "failure";
@@ -314,7 +428,10 @@ function writeStepEvent(
314
428
  );
315
429
  }
316
430
 
317
- function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowContext["step"] {
431
+ function createStep(
432
+ metadata: Pick<RunMetadata, "runGroupId" | "workerId">,
433
+ ): WorkflowContext["step"] {
434
+ const { runGroupId } = metadata;
318
435
  const stepNameByKey = new Map<string, string>();
319
436
 
320
437
  async function step<T>(
@@ -338,7 +455,9 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
338
455
  }
339
456
  stepNameByKey.set(key, name);
340
457
 
341
- const context: StepContext = { id: `${runGroupId}:${key}` };
458
+ const id = `${runGroupId}:${key}`;
459
+ const stepState = createWorkflowStepState(metadata, id);
460
+ const context: StepContext = { id, state: stepState.state };
342
461
  const event = { id: context.id, name, key };
343
462
  const startedAt = performance.now();
344
463
 
@@ -367,7 +486,7 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
367
486
  return checkpoint.value as WorkflowStepResult<T>;
368
487
  }
369
488
 
370
- const value = normalizeWorkflowStepResult(await fn(context));
489
+ const value = normalizeWorkflowStepResult(await stepState.run(() => fn(context)));
371
490
  writeStepEvent("success", { ...event, value }, startedAt);
372
491
  writeStepEvent(
373
492
  "completed",
@@ -407,6 +526,104 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
407
526
  return step;
408
527
  }
409
528
 
529
+ class WorkflowWaitInterrupt extends Error {
530
+ constructor(readonly result: WorkflowWaitUntilResult) {
531
+ super(`Workflow is waiting until ${new Date(result.wait.resumeAtMs).toISOString()}`);
532
+ this.name = "WorkflowWaitInterrupt";
533
+ }
534
+ }
535
+
536
+ function createWait(step: WorkflowStep): WorkflowWait {
537
+ return {
538
+ async until(name, options) {
539
+ const keyInput = options.key ?? name;
540
+ const keySegments = [
541
+ "wait",
542
+ "until",
543
+ ...(typeof keyInput === "string" ? [keyInput] : keyInput),
544
+ ];
545
+ const resumeAtMs = await step(name, { key: keySegments }, () =>
546
+ resolveWaitUntilMs(options),
547
+ );
548
+
549
+ if (Date.now() >= resumeAtMs) {
550
+ return;
551
+ }
552
+
553
+ throw new WorkflowWaitInterrupt({
554
+ status: "waiting",
555
+ wait: {
556
+ type: "until",
557
+ name,
558
+ resumeAtMs,
559
+ },
560
+ });
561
+ },
562
+ };
563
+ }
564
+
565
+ function resolveWaitUntilMs(options: WorkflowWaitUntilOptions): number {
566
+ const now = Date.now();
567
+ if ("at" in options) {
568
+ const resumeAtMs = options.at.getTime();
569
+ if (!Number.isFinite(resumeAtMs)) {
570
+ throw new Error("Workflow wait date must be valid.");
571
+ }
572
+ assertWaitFitsCheckpointRetention(resumeAtMs - now);
573
+ return resumeAtMs;
574
+ }
575
+
576
+ const durationMs = workflowWaitDurationMs(options.after);
577
+ assertWaitFitsCheckpointRetention(durationMs);
578
+ return now + durationMs;
579
+ }
580
+
581
+ function workflowWaitDurationMs(duration: WorkflowWaitDuration): number {
582
+ const entries = Object.entries(duration);
583
+ if (entries.length === 0) {
584
+ throw new Error("Workflow wait duration must specify at least one unit.");
585
+ }
586
+
587
+ let durationMs = 0;
588
+ for (const [unit, value] of entries) {
589
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
590
+ throw new Error("Workflow wait duration values must be positive finite integers.");
591
+ }
592
+
593
+ durationMs += value * waitDurationUnitMultiplier(unit);
594
+ if (!Number.isSafeInteger(durationMs)) {
595
+ throw new Error("Workflow wait duration is too large.");
596
+ }
597
+ }
598
+
599
+ return durationMs;
600
+ }
601
+
602
+ function assertWaitFitsCheckpointRetention(durationMs: number): void {
603
+ if (durationMs >= MAX_WORKFLOW_WAIT_MS) {
604
+ throw new Error("Workflow waits must be shorter than 7 days.");
605
+ }
606
+ }
607
+
608
+ function waitDurationUnitMultiplier(unit: string): number {
609
+ switch (unit) {
610
+ case "milliseconds":
611
+ return 1;
612
+ case "seconds":
613
+ return 1_000;
614
+ case "minutes":
615
+ return 60_000;
616
+ case "hours":
617
+ return 3_600_000;
618
+ case "days":
619
+ return 86_400_000;
620
+ case "weeks":
621
+ return 604_800_000;
622
+ default:
623
+ throw new Error(`Unsupported workflow wait duration unit: ${unit}`);
624
+ }
625
+ }
626
+
410
627
  function createWorkflowStepKey(segments: readonly unknown[]): string {
411
628
  if (segments.length === 0) {
412
629
  throw new Error("Workflow step keys must contain at least one segment.");