@notionhq/apps 0.0.16 → 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 (77) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +86 -36
  3. package/dist/connections.d.ts +39 -11
  4. package/dist/connections.d.ts.map +1 -1
  5. package/dist/connections.js +85 -21
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/notion-as-code/database.d.ts +89 -48
  10. package/dist/notion-as-code/database.d.ts.map +1 -1
  11. package/dist/notion-as-code/database.js +132 -45
  12. package/dist/notion-as-code/database.test.d.ts +2 -0
  13. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  14. package/dist/notion-as-code/handles.d.ts +2 -0
  15. package/dist/notion-as-code/handles.d.ts.map +1 -1
  16. package/dist/notion-as-code/handles.js +2 -0
  17. package/dist/notion-as-code/index.d.ts +5 -2
  18. package/dist/notion-as-code/index.d.ts.map +1 -1
  19. package/dist/notion-as-code/intents.d.ts +36 -3
  20. package/dist/notion-as-code/intents.d.ts.map +1 -1
  21. package/dist/notion-as-code/schema.d.ts +8 -4
  22. package/dist/notion-as-code/schema.d.ts.map +1 -1
  23. package/dist/notion-as-code/view.d.ts +12 -0
  24. package/dist/notion-as-code/view.d.ts.map +1 -0
  25. package/dist/notion-as-code/view.js +13 -0
  26. package/dist/notion-as-code/views-types.test.d.ts +2 -0
  27. package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
  28. package/dist/notion-as-code/views.d.ts +488 -0
  29. package/dist/notion-as-code/views.d.ts.map +1 -0
  30. package/dist/notion-as-code/views.js +0 -0
  31. package/dist/oauth.d.ts +11 -0
  32. package/dist/oauth.d.ts.map +1 -0
  33. package/dist/oauth.js +26 -0
  34. package/dist/providers.generated.d.ts +26 -86
  35. package/dist/providers.generated.d.ts.map +1 -1
  36. package/dist/providers.generated.js +54 -66
  37. package/dist/sync.d.ts +33 -15
  38. package/dist/sync.d.ts.map +1 -1
  39. package/dist/sync.js +12 -0
  40. package/dist/triggers.generated.d.ts +3 -3
  41. package/dist/triggers.generated.d.ts.map +1 -1
  42. package/dist/workflow-state.d.ts +13 -0
  43. package/dist/workflow-state.d.ts.map +1 -0
  44. package/dist/workflow-state.js +107 -0
  45. package/dist/workflow.d.ts +56 -11
  46. package/dist/workflow.d.ts.map +1 -1
  47. package/dist/workflow.js +123 -10
  48. package/docs/BUILD.md +96 -0
  49. package/docs/CONNECTIONS.md +70 -22
  50. package/package.json +1 -1
  51. package/skills/connections/SKILL.md +7 -8
  52. package/skills/notion-as-code/SKILL.md +88 -50
  53. package/skills/sync/SKILL.md +21 -18
  54. package/skills/workflow/SKILL.md +52 -3
  55. package/src/cli/build.test.ts +124 -64
  56. package/src/connections.test.ts +205 -41
  57. package/src/connections.ts +144 -38
  58. package/src/index.ts +2 -0
  59. package/src/notion-as-code/database.test.ts +661 -0
  60. package/src/notion-as-code/database.ts +346 -129
  61. package/src/notion-as-code/handles.ts +2 -0
  62. package/src/notion-as-code/index.ts +71 -1
  63. package/src/notion-as-code/intents.ts +41 -4
  64. package/src/notion-as-code/schema.ts +11 -3
  65. package/src/notion-as-code/view.ts +23 -0
  66. package/src/notion-as-code/views-types.test.ts +59 -0
  67. package/src/notion-as-code/views.ts +573 -0
  68. package/src/oauth.ts +40 -0
  69. package/src/providers.generated.ts +68 -163
  70. package/src/sync.test.ts +295 -0
  71. package/src/sync.ts +85 -21
  72. package/src/triggers.generated.ts +4 -4
  73. package/src/workflow-connections-types.test.ts +16 -10
  74. package/src/workflow-state.ts +152 -0
  75. package/src/workflow-types.test.ts +88 -16
  76. package/src/workflow.test.ts +374 -18
  77. package/src/workflow.ts +211 -23
package/src/workflow.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  createWorkflowConnections,
3
- validateConnectionRequirements,
3
+ normalizeConnectionRequirements,
4
4
  validateTriggerConnections,
5
- type WorkflowConnection,
5
+ type WorkflowConnectionDeclarations,
6
+ type WorkflowConnectionRequirement,
6
7
  type WorkflowConnections,
7
8
  } from "./connections.js";
8
9
  import { createHash } from "node:crypto";
@@ -16,6 +17,7 @@ import { writeOutput } from "./output.js";
16
17
  import { resolveRuntimeInput } from "./runtime-input.js";
17
18
  import { readRunMetadata, type RunMetadata } from "./runtime-metadata.js";
18
19
  import { createWorkflowTriggers } from "./triggers.generated.js";
20
+ import { createWorkflowStepState, type WorkflowState } from "./workflow-state.js";
19
21
  import type {
20
22
  WorkflowEventMap,
21
23
  WorkflowTrigger,
@@ -28,6 +30,45 @@ type HandlerOptions = {
28
30
  concreteOutput?: true;
29
31
  };
30
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
+
31
72
  export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
32
73
 
33
74
  export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
@@ -35,14 +76,22 @@ export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMa
35
76
  export type WorkflowEventForTriggers<T extends readonly WorkflowTrigger[]> =
36
77
  WorkflowEventForTrigger<T[number]>;
37
78
 
38
- export { connections, type WorkflowConnection, type WorkflowConnections } from "./connections.js";
79
+ export {
80
+ connections,
81
+ type WorkflowConnection,
82
+ type WorkflowConnectionDeclarations,
83
+ type WorkflowConnectionRequirement,
84
+ type WorkflowConnections,
85
+ type OAuthConfiguration,
86
+ type OAuthConnection,
87
+ } from "./connections.js";
39
88
 
40
89
  /**
41
90
  * Configuration passed to {@link workflow}.
42
91
  */
43
92
  export type WorkflowConfiguration<
44
93
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
45
- TConnections extends readonly WorkflowConnection[] = readonly WorkflowConnection[],
94
+ TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
46
95
  > = {
47
96
  /**
48
97
  * A human-readable name for the workflow, shown in the UI when viewing workflows
@@ -75,7 +124,7 @@ export type WorkflowConfiguration<
75
124
  /** Configuration that checks trigger keys against declared connections. */
76
125
  export type WorkflowTriggerConfiguration<
77
126
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
78
- TConnections extends readonly WorkflowConnection[],
127
+ TConnections extends WorkflowConnectionDeclarations,
79
128
  > = Omit<WorkflowConfiguration<TTriggers, TConnections>, "triggers"> & {
80
129
  triggers: (context: { triggers: WorkflowTriggerCreators<NoInfer<TConnections>> }) => TTriggers;
81
130
  };
@@ -97,12 +146,12 @@ export type Workflow<
97
146
  name: string;
98
147
  description: string;
99
148
  triggers: TTriggers;
100
- connections?: readonly WorkflowConnection[];
149
+ connections?: readonly WorkflowConnectionRequirement[];
101
150
  };
102
151
  handler: (
103
152
  event: WorkflowEventForTriggers<TTriggers>,
104
153
  options?: HandlerOptions,
105
- ) => Promise<{ status: "success" } | undefined>;
154
+ ) => Promise<WorkflowHandlerResult | undefined>;
106
155
  };
107
156
 
108
157
  /**
@@ -136,26 +185,30 @@ export function workflow<
136
185
  WorkflowTriggerForConnections<NoInfer<TConnections>>,
137
186
  ...WorkflowTriggerForConnections<NoInfer<TConnections>>[],
138
187
  ],
139
- const TConnections extends readonly WorkflowConnection[] = readonly [],
188
+ const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
140
189
  >(configuration: WorkflowTriggerConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
141
190
  export function workflow<
142
191
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
143
- const TConnections extends readonly WorkflowConnection[] = readonly [],
192
+ const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
144
193
  >(configuration: WorkflowConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
145
194
  export function workflow<
146
195
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
147
- const TConnections extends readonly WorkflowConnection[] = readonly [],
196
+ const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
148
197
  >(
149
198
  configuration:
150
199
  | WorkflowConfiguration<TTriggers, TConnections>
151
200
  | WorkflowTriggerConfiguration<TTriggers, TConnections>,
152
201
  ): Workflow<TTriggers> {
202
+ const requirements = normalizeConnectionRequirements(configuration.connections ?? {});
203
+ const connectionDeclarations =
204
+ configuration.connections === undefined
205
+ ? undefined
206
+ : structuredClone(configuration.connections);
153
207
  const triggers =
154
208
  typeof configuration.triggers === "function"
155
209
  ? configuration.triggers({ triggers: createWorkflowTriggers<TConnections>() })
156
210
  : configuration.triggers;
157
- validateConnectionRequirements(configuration.connections ?? []);
158
- validateTriggerConnections(triggers, configuration.connections ?? []);
211
+ validateTriggerConnections(triggers, requirements);
159
212
 
160
213
  return {
161
214
  _tag: "workflow",
@@ -163,23 +216,26 @@ export function workflow<
163
216
  name: configuration.name,
164
217
  description: configuration.description,
165
218
  triggers,
166
- ...(configuration.connections === undefined
167
- ? {}
168
- : { connections: configuration.connections }),
219
+ ...(configuration.connections === undefined ? {} : { connections: requirements }),
169
220
  },
170
221
  async handler(
171
222
  event: WorkflowEventForTriggers<TTriggers>,
172
223
  options?: HandlerOptions,
173
- ): Promise<{ status: "success" } | undefined> {
224
+ ): Promise<WorkflowHandlerResult | undefined> {
174
225
  try {
175
226
  event = await resolveRuntimeInput(event);
176
227
  const runMetadata = readRunMetadata();
177
228
  const baseContext = createCapabilityContext();
178
- const capabilityContext: WorkflowContext = {
229
+ const step = createStep(runMetadata);
230
+ const capabilityContext: WorkflowContext<TConnections> = {
179
231
  ...baseContext,
180
- connections: createWorkflowConnections(baseContext.notion),
232
+ connections: createWorkflowConnections(
233
+ baseContext.notion,
234
+ connectionDeclarations,
235
+ ),
181
236
  ...runMetadata,
182
- step: createStep(runMetadata),
237
+ step,
238
+ wait: createWait(step),
183
239
  };
184
240
  await configuration.handler(event, capabilityContext);
185
241
 
@@ -189,6 +245,18 @@ export function workflow<
189
245
 
190
246
  writeOutput({ _tag: "success", value: { status: "success" } });
191
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
+
192
260
  const error = new ExecutionError(err);
193
261
 
194
262
  if (!options?.concreteOutput) {
@@ -215,6 +283,13 @@ export type StepContext = {
215
283
  * Use this to make side effects idempotent when the workflow is retried.
216
284
  */
217
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;
218
293
  };
219
294
 
220
295
  type WorkflowStepResult<T> = T extends void ? null : T;
@@ -238,9 +313,17 @@ type WorkflowStep = {
238
313
  ): Promise<WorkflowStepResult<T>>;
239
314
  };
240
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
+
241
324
  /** Context passed to a workflow handler. */
242
325
  export type WorkflowContext<
243
- TConnections extends readonly WorkflowConnection[] = readonly WorkflowConnection[],
326
+ TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
244
327
  > = CapabilityContext & {
245
328
  connections: WorkflowConnections<TConnections>;
246
329
  } & RunMetadata & {
@@ -265,6 +348,8 @@ export type WorkflowContext<
265
348
  * ```
266
349
  */
267
350
  step: WorkflowStep;
351
+ /** Suspend this workflow without consuming compute resources. */
352
+ wait: WorkflowWait;
268
353
  };
269
354
 
270
355
  type StepCompletionType = "success" | "failure";
@@ -300,7 +385,10 @@ function writeStepEvent(
300
385
  );
301
386
  }
302
387
 
303
- function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowContext["step"] {
388
+ function createStep(
389
+ metadata: Pick<RunMetadata, "runGroupId" | "workerId">,
390
+ ): WorkflowContext["step"] {
391
+ const { runGroupId } = metadata;
304
392
  const stepNameByKey = new Map<string, string>();
305
393
 
306
394
  async function step<T>(
@@ -324,7 +412,9 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
324
412
  }
325
413
  stepNameByKey.set(key, name);
326
414
 
327
- 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 };
328
418
  const event = { id: context.id, name, key };
329
419
  const startedAt = performance.now();
330
420
 
@@ -353,7 +443,7 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
353
443
  return checkpoint.value as WorkflowStepResult<T>;
354
444
  }
355
445
 
356
- const value = normalizeWorkflowStepResult(await fn(context));
446
+ const value = normalizeWorkflowStepResult(await stepState.run(() => fn(context)));
357
447
  writeStepEvent("success", { ...event, value }, startedAt);
358
448
  writeStepEvent(
359
449
  "completed",
@@ -393,6 +483,104 @@ function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowCo
393
483
  return step;
394
484
  }
395
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
+
396
584
  function createWorkflowStepKey(segments: readonly unknown[]): string {
397
585
  if (segments.length === 0) {
398
586
  throw new Error("Workflow step keys must contain at least one segment.");