@notionhq/apps 0.0.17 → 0.0.18

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 (44) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +60 -35
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/notion-as-code/database.d.ts +88 -48
  6. package/dist/notion-as-code/database.d.ts.map +1 -1
  7. package/dist/notion-as-code/database.js +132 -45
  8. package/dist/notion-as-code/database.test.d.ts +2 -0
  9. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  10. package/dist/notion-as-code/index.d.ts +2 -2
  11. package/dist/notion-as-code/index.d.ts.map +1 -1
  12. package/dist/notion-as-code/intents.d.ts +34 -2
  13. package/dist/notion-as-code/intents.d.ts.map +1 -1
  14. package/dist/notion-as-code/schema.d.ts +8 -4
  15. package/dist/notion-as-code/schema.d.ts.map +1 -1
  16. package/dist/notion-as-code/views.d.ts +29 -10
  17. package/dist/notion-as-code/views.d.ts.map +1 -1
  18. package/dist/sync.d.ts +33 -15
  19. package/dist/sync.d.ts.map +1 -1
  20. package/dist/sync.js +12 -0
  21. package/dist/workflow-state.d.ts +13 -0
  22. package/dist/workflow-state.d.ts.map +1 -0
  23. package/dist/workflow-state.js +107 -0
  24. package/dist/workflow.d.ts +48 -3
  25. package/dist/workflow.d.ts.map +1 -1
  26. package/dist/workflow.js +111 -4
  27. package/docs/BUILD.md +96 -0
  28. package/package.json +1 -1
  29. package/skills/notion-as-code/SKILL.md +81 -49
  30. package/skills/sync/SKILL.md +21 -18
  31. package/skills/workflow/SKILL.md +52 -3
  32. package/src/cli/build.test.ts +124 -64
  33. package/src/index.ts +1 -0
  34. package/src/notion-as-code/database.test.ts +661 -0
  35. package/src/notion-as-code/database.ts +344 -127
  36. package/src/notion-as-code/index.ts +16 -1
  37. package/src/notion-as-code/intents.ts +39 -2
  38. package/src/notion-as-code/schema.ts +11 -3
  39. package/src/notion-as-code/views.ts +29 -10
  40. package/src/sync.test.ts +295 -0
  41. package/src/sync.ts +85 -21
  42. package/src/workflow-state.ts +152 -0
  43. package/src/workflow.test.ts +329 -1
  44. package/src/workflow.ts +180 -6
package/src/workflow.ts CHANGED
@@ -17,6 +17,7 @@ import { writeOutput } from "./output.js";
17
17
  import { resolveRuntimeInput } from "./runtime-input.js";
18
18
  import { readRunMetadata, type RunMetadata } from "./runtime-metadata.js";
19
19
  import { createWorkflowTriggers } from "./triggers.generated.js";
20
+ import { createWorkflowStepState, type WorkflowState } from "./workflow-state.js";
20
21
  import type {
21
22
  WorkflowEventMap,
22
23
  WorkflowTrigger,
@@ -29,6 +30,45 @@ type HandlerOptions = {
29
30
  concreteOutput?: true;
30
31
  };
31
32
 
33
+ type WorkflowWaitDurationValues = {
34
+ milliseconds?: number;
35
+ seconds?: number;
36
+ minutes?: number;
37
+ hours?: number;
38
+ days?: number;
39
+ weeks?: number;
40
+ };
41
+
42
+ export type WorkflowWaitDuration = {
43
+ [Unit in keyof WorkflowWaitDurationValues]-?: Required<Pick<WorkflowWaitDurationValues, Unit>> &
44
+ WorkflowWaitDurationValues;
45
+ }[keyof WorkflowWaitDurationValues];
46
+
47
+ type WorkflowWaitKey = string | string[];
48
+
49
+ export type WorkflowWaitUntilOptions =
50
+ | {
51
+ at: Date;
52
+ key?: WorkflowWaitKey;
53
+ }
54
+ | {
55
+ after: WorkflowWaitDuration;
56
+ key?: WorkflowWaitKey;
57
+ };
58
+
59
+ export type WorkflowWaitUntilResult = {
60
+ status: "waiting";
61
+ wait: {
62
+ type: "until";
63
+ name: string;
64
+ resumeAtMs: number;
65
+ };
66
+ };
67
+
68
+ type WorkflowHandlerResult = { status: "success" } | WorkflowWaitUntilResult;
69
+
70
+ const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1_000;
71
+
32
72
  export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
33
73
 
34
74
  export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
@@ -111,7 +151,7 @@ export type Workflow<
111
151
  handler: (
112
152
  event: WorkflowEventForTriggers<TTriggers>,
113
153
  options?: HandlerOptions,
114
- ) => Promise<{ status: "success" } | undefined>;
154
+ ) => Promise<WorkflowHandlerResult | undefined>;
115
155
  };
116
156
 
117
157
  /**
@@ -181,11 +221,12 @@ export function workflow<
181
221
  async handler(
182
222
  event: WorkflowEventForTriggers<TTriggers>,
183
223
  options?: HandlerOptions,
184
- ): Promise<{ status: "success" } | undefined> {
224
+ ): Promise<WorkflowHandlerResult | undefined> {
185
225
  try {
186
226
  event = await resolveRuntimeInput(event);
187
227
  const runMetadata = readRunMetadata();
188
228
  const baseContext = createCapabilityContext();
229
+ const step = createStep(runMetadata);
189
230
  const capabilityContext: WorkflowContext<TConnections> = {
190
231
  ...baseContext,
191
232
  connections: createWorkflowConnections(
@@ -193,7 +234,8 @@ export function workflow<
193
234
  connectionDeclarations,
194
235
  ),
195
236
  ...runMetadata,
196
- step: createStep(runMetadata),
237
+ step,
238
+ wait: createWait(step),
197
239
  };
198
240
  await configuration.handler(event, capabilityContext);
199
241
 
@@ -203,6 +245,18 @@ export function workflow<
203
245
 
204
246
  writeOutput({ _tag: "success", value: { status: "success" } });
205
247
  } catch (err) {
248
+ if (err instanceof WorkflowWaitInterrupt) {
249
+ if (options?.concreteOutput) {
250
+ return err.result;
251
+ }
252
+
253
+ writeOutput({
254
+ _tag: "wait",
255
+ wait: err.result.wait,
256
+ });
257
+ return;
258
+ }
259
+
206
260
  const error = new ExecutionError(err);
207
261
 
208
262
  if (!options?.concreteOutput) {
@@ -229,6 +283,13 @@ export type StepContext = {
229
283
  * Use this to make side effects idempotent when the workflow is retried.
230
284
  */
231
285
  id: string;
286
+ /**
287
+ * Durable key-value state shared by every run of this workflow.
288
+ *
289
+ * State is exposed on the step context because every operation participates
290
+ * in the active step's durability and replay boundary.
291
+ */
292
+ state: WorkflowState;
232
293
  };
233
294
 
234
295
  type WorkflowStepResult<T> = T extends void ? null : T;
@@ -252,6 +313,14 @@ type WorkflowStep = {
252
313
  ): Promise<WorkflowStepResult<T>>;
253
314
  };
254
315
 
316
+ type WorkflowWait = {
317
+ /**
318
+ * Suspend the workflow until a relative duration has elapsed or an absolute
319
+ * date has been reached.
320
+ */
321
+ until(name: string, options: WorkflowWaitUntilOptions): Promise<void>;
322
+ };
323
+
255
324
  /** Context passed to a workflow handler. */
256
325
  export type WorkflowContext<
257
326
  TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
@@ -279,6 +348,8 @@ export type WorkflowContext<
279
348
  * ```
280
349
  */
281
350
  step: WorkflowStep;
351
+ /** Suspend this workflow without consuming compute resources. */
352
+ wait: WorkflowWait;
282
353
  };
283
354
 
284
355
  type StepCompletionType = "success" | "failure";
@@ -314,7 +385,10 @@ function writeStepEvent(
314
385
  );
315
386
  }
316
387
 
317
- function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowContext["step"] {
388
+ function createStep(
389
+ metadata: Pick<RunMetadata, "runGroupId" | "workerId">,
390
+ ): WorkflowContext["step"] {
391
+ const { runGroupId } = metadata;
318
392
  const stepNameByKey = new Map<string, string>();
319
393
 
320
394
  async function step<T>(
@@ -338,7 +412,9 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
338
412
  }
339
413
  stepNameByKey.set(key, name);
340
414
 
341
- const context: StepContext = { id: `${runGroupId}:${key}` };
415
+ const id = `${runGroupId}:${key}`;
416
+ const stepState = createWorkflowStepState(metadata, id);
417
+ const context: StepContext = { id, state: stepState.state };
342
418
  const event = { id: context.id, name, key };
343
419
  const startedAt = performance.now();
344
420
 
@@ -367,7 +443,7 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
367
443
  return checkpoint.value as WorkflowStepResult<T>;
368
444
  }
369
445
 
370
- const value = normalizeWorkflowStepResult(await fn(context));
446
+ const value = normalizeWorkflowStepResult(await stepState.run(() => fn(context)));
371
447
  writeStepEvent("success", { ...event, value }, startedAt);
372
448
  writeStepEvent(
373
449
  "completed",
@@ -407,6 +483,104 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
407
483
  return step;
408
484
  }
409
485
 
486
+ class WorkflowWaitInterrupt extends Error {
487
+ constructor(readonly result: WorkflowWaitUntilResult) {
488
+ super(`Workflow is waiting until ${new Date(result.wait.resumeAtMs).toISOString()}`);
489
+ this.name = "WorkflowWaitInterrupt";
490
+ }
491
+ }
492
+
493
+ function createWait(step: WorkflowStep): WorkflowWait {
494
+ return {
495
+ async until(name, options) {
496
+ const keyInput = options.key ?? name;
497
+ const keySegments = [
498
+ "wait",
499
+ "until",
500
+ ...(typeof keyInput === "string" ? [keyInput] : keyInput),
501
+ ];
502
+ const resumeAtMs = await step(name, { key: keySegments }, () =>
503
+ resolveWaitUntilMs(options),
504
+ );
505
+
506
+ if (Date.now() >= resumeAtMs) {
507
+ return;
508
+ }
509
+
510
+ throw new WorkflowWaitInterrupt({
511
+ status: "waiting",
512
+ wait: {
513
+ type: "until",
514
+ name,
515
+ resumeAtMs,
516
+ },
517
+ });
518
+ },
519
+ };
520
+ }
521
+
522
+ function resolveWaitUntilMs(options: WorkflowWaitUntilOptions): number {
523
+ const now = Date.now();
524
+ if ("at" in options) {
525
+ const resumeAtMs = options.at.getTime();
526
+ if (!Number.isFinite(resumeAtMs)) {
527
+ throw new Error("Workflow wait date must be valid.");
528
+ }
529
+ assertWaitFitsCheckpointRetention(resumeAtMs - now);
530
+ return resumeAtMs;
531
+ }
532
+
533
+ const durationMs = workflowWaitDurationMs(options.after);
534
+ assertWaitFitsCheckpointRetention(durationMs);
535
+ return now + durationMs;
536
+ }
537
+
538
+ function workflowWaitDurationMs(duration: WorkflowWaitDuration): number {
539
+ const entries = Object.entries(duration);
540
+ if (entries.length === 0) {
541
+ throw new Error("Workflow wait duration must specify at least one unit.");
542
+ }
543
+
544
+ let durationMs = 0;
545
+ for (const [unit, value] of entries) {
546
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
547
+ throw new Error("Workflow wait duration values must be positive finite integers.");
548
+ }
549
+
550
+ durationMs += value * waitDurationUnitMultiplier(unit);
551
+ if (!Number.isSafeInteger(durationMs)) {
552
+ throw new Error("Workflow wait duration is too large.");
553
+ }
554
+ }
555
+
556
+ return durationMs;
557
+ }
558
+
559
+ function assertWaitFitsCheckpointRetention(durationMs: number): void {
560
+ if (durationMs >= MAX_WORKFLOW_WAIT_MS) {
561
+ throw new Error("Workflow waits must be shorter than 7 days.");
562
+ }
563
+ }
564
+
565
+ function waitDurationUnitMultiplier(unit: string): number {
566
+ switch (unit) {
567
+ case "milliseconds":
568
+ return 1;
569
+ case "seconds":
570
+ return 1_000;
571
+ case "minutes":
572
+ return 60_000;
573
+ case "hours":
574
+ return 3_600_000;
575
+ case "days":
576
+ return 86_400_000;
577
+ case "weeks":
578
+ return 604_800_000;
579
+ default:
580
+ throw new Error(`Unsupported workflow wait duration unit: ${unit}`);
581
+ }
582
+ }
583
+
410
584
  function createWorkflowStepKey(segments: readonly unknown[]): string {
411
585
  if (segments.length === 0) {
412
586
  throw new Error("Workflow step keys must contain at least one segment.");