@notionhq/apps 0.0.20 → 0.0.22

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 (45) hide show
  1. package/README.md +2 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/notion-as-code/custom-agent.d.ts +75 -1
  6. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  7. package/dist/notion-as-code/custom-agent.js +11 -0
  8. package/dist/notion-as-code/custom-agent.test.d.ts +2 -0
  9. package/dist/notion-as-code/custom-agent.test.d.ts.map +1 -0
  10. package/dist/notion-as-code/database.d.ts +5 -1
  11. package/dist/notion-as-code/database.d.ts.map +1 -1
  12. package/dist/notion-as-code/database.js +3 -1
  13. package/dist/notion-as-code/index.d.ts +2 -2
  14. package/dist/notion-as-code/index.d.ts.map +1 -1
  15. package/dist/notion-as-code/intents.d.ts +133 -11
  16. package/dist/notion-as-code/intents.d.ts.map +1 -1
  17. package/dist/notion-as-code/notion-as-code-types.test.d.ts +2 -0
  18. package/dist/notion-as-code/notion-as-code-types.test.d.ts.map +1 -0
  19. package/dist/notion-as-code/page.d.ts +2 -2
  20. package/dist/notion-as-code/page.d.ts.map +1 -1
  21. package/dist/workflow-access.d.ts +29 -0
  22. package/dist/workflow-access.d.ts.map +1 -1
  23. package/dist/workflow-access.js +23 -0
  24. package/dist/workflow.d.ts +1 -0
  25. package/dist/workflow.d.ts.map +1 -1
  26. package/dist/workflow.js +2 -0
  27. package/docs/BUILD.md +1 -1
  28. package/package.json +1 -1
  29. package/skills/notion-as-code/SKILL.md +13 -7
  30. package/skills/workflow/SKILL.md +5 -5
  31. package/src/cli/build.test.ts +5 -0
  32. package/src/index.ts +1 -0
  33. package/src/notion-as-code/custom-agent.test.ts +79 -0
  34. package/src/notion-as-code/custom-agent.ts +111 -2
  35. package/src/notion-as-code/database.test.ts +38 -0
  36. package/src/notion-as-code/database.ts +7 -0
  37. package/src/notion-as-code/index.ts +39 -1
  38. package/src/notion-as-code/intents.ts +219 -24
  39. package/src/notion-as-code/notion-as-code-types.test.ts +167 -0
  40. package/src/notion-as-code/page.test.ts +24 -0
  41. package/src/notion-as-code/page.ts +2 -2
  42. package/src/workflow-access-types.test.ts +8 -4
  43. package/src/workflow-access.ts +41 -0
  44. package/src/workflow.test.ts +5 -4
  45. package/src/workflow.ts +1 -0
@@ -21,6 +21,14 @@ export type NotionAsCodeIcon =
21
21
  | { type: "notion_icon"; description: string; color?: string }
22
22
  | { type: "file"; resourceId: ResourceId };
23
23
 
24
+ /** A URL reference used as a page or database cover. */
25
+ export type PageCoverReference = {
26
+ type: "url";
27
+ url: string;
28
+ /** Vertical position from the top, where 0 is the top and 1 is the bottom. */
29
+ position?: number;
30
+ };
31
+
24
32
  export type NotionAsCodeSelectColor =
25
33
  | "default"
26
34
  | "gray"
@@ -49,20 +57,102 @@ type PropertyBase = {
49
57
  description?: string;
50
58
  };
51
59
 
52
- type UnsupportedProperty = PropertyBase & {
53
- type:
54
- | "relation"
55
- | "formula"
56
- | "rollup"
57
- | "created_time"
58
- | "last_edited_time"
59
- | "created_by"
60
- | "last_edited_by"
61
- | "auto_increment_id"
62
- | "person"
63
- | "verification";
64
- // TODO: Port configuration types for unsupported property kinds.
65
- [key: string]: unknown;
60
+ /** Rollup aggregations supported for every target property type. */
61
+ export type RollupBaseAggregationType =
62
+ | "count"
63
+ | "count_values"
64
+ | "unique"
65
+ | "empty"
66
+ | "not_empty"
67
+ | "percent_empty"
68
+ | "percent_not_empty"
69
+ | "show_unique";
70
+
71
+ /** Numeric-only rollup aggregations. */
72
+ export type RollupNumberAggregationType =
73
+ | RollupBaseAggregationType
74
+ | "sum"
75
+ | "average"
76
+ | "median"
77
+ | "min"
78
+ | "max"
79
+ | "range";
80
+
81
+ /** Date-only rollup aggregations. */
82
+ export type RollupDateAggregationType =
83
+ | RollupBaseAggregationType
84
+ | "earliest_date"
85
+ | "latest_date"
86
+ | "date_range";
87
+
88
+ export type RollupTargetPropertyType =
89
+ | "title"
90
+ | "text"
91
+ | "number"
92
+ | "select"
93
+ | "multi_select"
94
+ | "status"
95
+ | "date"
96
+ | "checkbox"
97
+ | "url"
98
+ | "email"
99
+ | "phone_number"
100
+ | "relation"
101
+ | "person"
102
+ | "created_time"
103
+ | "last_edited_time"
104
+ | "created_by"
105
+ | "last_edited_by";
106
+
107
+ type RollupTarget =
108
+ | {
109
+ targetPropertyType: "number";
110
+ aggregation?: RollupNumberAggregationType;
111
+ }
112
+ | {
113
+ targetPropertyType: "date" | "created_time" | "last_edited_time";
114
+ aggregation?: RollupDateAggregationType;
115
+ }
116
+ | {
117
+ targetPropertyType: Exclude<
118
+ RollupTargetPropertyType,
119
+ "number" | "date" | "created_time" | "last_edited_time"
120
+ >;
121
+ aggregation?: RollupBaseAggregationType;
122
+ };
123
+
124
+ type RelationProperty = PropertyBase & {
125
+ type: "relation";
126
+ targetDataSourceResourceId: ResourceId;
127
+ targetDataSourcePropertyResourceId?: ResourceId;
128
+ limit?: 1;
129
+ };
130
+
131
+ type FormulaProperty = PropertyBase & {
132
+ type: "formula";
133
+ expression?: string;
134
+ };
135
+
136
+ type RollupProperty = PropertyBase &
137
+ RollupTarget & {
138
+ type: "rollup";
139
+ relationPropertyResourceId: ResourceId;
140
+ targetPropertyResourceId: ResourceId;
141
+ };
142
+
143
+ type AutoIncrementIdProperty = PropertyBase & {
144
+ type: "auto_increment_id";
145
+ prefix?: string;
146
+ };
147
+
148
+ type PersonProperty = PropertyBase & {
149
+ type: "person";
150
+ limit?: 1;
151
+ };
152
+
153
+ type VerificationProperty = PropertyBase & {
154
+ type: "verification";
155
+ verifierPropertyResourceId: ResourceId;
66
156
  };
67
157
 
68
158
  /** A normalized Notion-as-Code property with its resolved display name. */
@@ -90,22 +180,29 @@ export type NotionAsCodeProperty =
90
180
  | (PropertyBase & { type: "email" })
91
181
  | (PropertyBase & { type: "phone_number" })
92
182
  | (PropertyBase & { type: "file" })
93
- | UnsupportedProperty;
183
+ | RelationProperty
184
+ | FormulaProperty
185
+ | RollupProperty
186
+ | (PropertyBase & { type: "created_time" })
187
+ | (PropertyBase & { type: "last_edited_time" })
188
+ | (PropertyBase & { type: "created_by" })
189
+ | (PropertyBase & { type: "last_edited_by" })
190
+ | AutoIncrementIdProperty
191
+ | PersonProperty
192
+ | VerificationProperty;
94
193
 
95
194
  /**
96
195
  * `T extends unknown` applies Omit to each property variant separately, preserving
97
196
  * variant-specific fields such as required status options. Plain Omit on the union
98
- * would retain only shared keys. Pick also restores type/resourceId on the
99
- * unsupported variant, whose string index signature would otherwise erase them.
197
+ * would retain only shared keys.
100
198
  */
101
199
  type DistributivePropertyConfig<T extends NotionAsCodeProperty> = T extends unknown
102
- ? Omit<T, "name"> & Pick<T, "type" | "resourceId">
200
+ ? Omit<T, "name">
103
201
  : never;
104
202
 
105
203
  /**
106
204
  * Authoring properties are keyed by their schema object name. A property-level
107
- * `name` override would make the key and serialized property diverge, so it is
108
- * explicitly rejected even for the string-indexed unsupported-property variant.
205
+ * `name` override would make the key and serialized property diverge.
109
206
  */
110
207
  export type NotionAsCodePropertyConfig = DistributivePropertyConfig<NotionAsCodeProperty> & {
111
208
  name?: never;
@@ -113,6 +210,106 @@ export type NotionAsCodePropertyConfig = DistributivePropertyConfig<NotionAsCode
113
210
  /** A keyed property schema, with object keys supplying serialized property names. */
114
211
  export type NotionAsCodeSchema = Record<string, NotionAsCodePropertyConfig>;
115
212
 
213
+ type SchemaProperty<Schema extends NotionAsCodeSchema> = Schema[keyof Schema & string];
214
+ type PropertyType = NotionAsCodePropertyConfig["type"];
215
+ type PropertyResourceIdFor<
216
+ Schema extends NotionAsCodeSchema,
217
+ Types extends PropertyType = PropertyType,
218
+ > = Extract<SchemaProperty<Schema>, { type: Types }>["resourceId"];
219
+
220
+ export type PageLayoutPropertyConfig<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
221
+ property: PropertyResourceIdFor<Schema>;
222
+ visibility?: "show" | "hide_if_empty" | "hide";
223
+ };
224
+
225
+ export type PageLayoutPropertySection<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
226
+ name: string;
227
+ properties: readonly PageLayoutPropertyConfig<Schema>[];
228
+ };
229
+
230
+ export type PageLayoutPropertiesModule<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
231
+ type: "properties";
232
+ } & (
233
+ | {
234
+ properties?: readonly PageLayoutPropertyConfig<Schema>[];
235
+ sections?: never;
236
+ }
237
+ | {
238
+ sections: readonly PageLayoutPropertySection<Schema>[];
239
+ properties?: never;
240
+ }
241
+ );
242
+
243
+ export type PageLayoutPersonStyle = "compact" | "large";
244
+ export type PageLayoutNumberAndFormulaStyle = "large" | "small";
245
+
246
+ type PageLayoutStyleFor<Property extends NotionAsCodePropertyConfig> = Property["type"] extends
247
+ | "person"
248
+ | "created_by"
249
+ | "last_edited_by"
250
+ ? { style?: PageLayoutPersonStyle }
251
+ : Property["type"] extends "number" | "formula"
252
+ ? { style?: PageLayoutNumberAndFormulaStyle }
253
+ : { style?: never };
254
+
255
+ type PageLayoutPropertyModuleFor<Property extends NotionAsCodePropertyConfig> =
256
+ Property extends unknown
257
+ ? {
258
+ type: "property";
259
+ property: Property["resourceId"];
260
+ } & PageLayoutStyleFor<Property>
261
+ : never;
262
+
263
+ /**
264
+ * A standalone property module. Its available style is inferred from the
265
+ * referenced property's type when the layout is declared with a literal schema.
266
+ */
267
+ export type PageLayoutPropertyModule<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
268
+ PageLayoutPropertyModuleFor<SchemaProperty<Schema>>;
269
+
270
+ export type PageLayoutRelationConfig<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
271
+ property: PropertyResourceIdFor<Schema, "relation">;
272
+ display: "pageSection" | "minimal";
273
+ };
274
+
275
+ export type PageLayoutRelationsGroupModule<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
276
+ {
277
+ type: "relations";
278
+ relations: readonly PageLayoutRelationConfig<Schema>[];
279
+ };
280
+
281
+ export type PageLayoutMainModule<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
282
+ | PageLayoutPropertiesModule<Schema>
283
+ | PageLayoutPropertyModule<Schema>
284
+ | PageLayoutRelationsGroupModule<Schema>;
285
+
286
+ export type PageLayoutSidebarModule<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
287
+ | PageLayoutPropertiesModule<Schema>
288
+ | PageLayoutPropertyModule<Schema>;
289
+
290
+ export type ContentPageLayoutTab = {
291
+ type: "content";
292
+ name?: string;
293
+ };
294
+
295
+ export type ViewPageLayoutTab = {
296
+ type: "view";
297
+ resourceId: ResourceId;
298
+ };
299
+
300
+ export type DatabasePageLayoutTab = ContentPageLayoutTab | ViewPageLayoutTab;
301
+
302
+ export type DatabasePageLayout<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
303
+ main: readonly PageLayoutMainModule<Schema>[];
304
+ sidebar?: readonly PageLayoutSidebarModule<Schema>[];
305
+ pinnedProperties?: readonly PropertyResourceIdFor<Schema>[];
306
+ discussions?: "top" | "bottom" | "hidden";
307
+ tabs?: readonly DatabasePageLayoutTab[];
308
+ fullWidth?: boolean;
309
+ showPropertyIcons?: boolean;
310
+ showTemplates?: boolean;
311
+ };
312
+
116
313
  /** The normalized data-source shape emitted in provisioning artifacts. */
117
314
  export type DataSourceDefinition<
118
315
  Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
@@ -121,8 +318,7 @@ export type DataSourceDefinition<
121
318
  name: string;
122
319
  properties: Properties;
123
320
  icon?: NotionAsCodeIcon;
124
- // TODO: Port page layout types.
125
- pageLayout?: unknown;
321
+ pageLayout?: DatabasePageLayout;
126
322
  };
127
323
 
128
324
  export type TeamspaceIntent = TeamspaceArgs & {
@@ -141,8 +337,7 @@ export type DatabaseIntent = {
141
337
  dataSources: readonly DataSourceDefinition[];
142
338
  views?: readonly ViewSchema[];
143
339
  icon?: NotionAsCodeIcon;
144
- // TODO: Port cover types.
145
- cover?: unknown;
340
+ cover?: PageCoverReference;
146
341
  hideDataSourceTitle?: boolean;
147
342
  hideDatabaseTitleIfEmpty?: boolean;
148
343
  name?: string;
@@ -0,0 +1,167 @@
1
+ import { expectTypeOf, it } from "vitest";
2
+
3
+ import type {
4
+ CustomAgentArgs,
5
+ CustomAgentPropertyUpdatedTrigger,
6
+ CustomAgentTrigger,
7
+ DatabasePageLayout,
8
+ NotionAsCodePropertyConfig,
9
+ PageArgs,
10
+ PageCoverReference,
11
+ PageLayoutPropertyModule,
12
+ SingleSourceDatabaseArgs,
13
+ } from "./index.js";
14
+
15
+ it("types page and database covers as URL references", () => {
16
+ expectTypeOf<{
17
+ type: "url";
18
+ url: "https://example.com/cover.png";
19
+ position: 0.25;
20
+ }>().toExtend<PageCoverReference>();
21
+ expectTypeOf<{
22
+ type: "external";
23
+ url: "https://example.com/cover.png";
24
+ }>().not.toExtend<PageCoverReference>();
25
+ expectTypeOf<PageArgs["cover"]>().toEqualTypeOf<PageCoverReference | undefined>();
26
+ });
27
+
28
+ it("requires configuration for each advanced property kind", () => {
29
+ expectTypeOf<{
30
+ type: "relation";
31
+ resourceId: "project";
32
+ targetDataSourceResourceId: "projects";
33
+ limit: 1;
34
+ }>().toExtend<NotionAsCodePropertyConfig>();
35
+ expectTypeOf<{
36
+ type: "relation";
37
+ resourceId: "project";
38
+ }>().not.toExtend<NotionAsCodePropertyConfig>();
39
+
40
+ expectTypeOf<{
41
+ type: "rollup";
42
+ resourceId: "total";
43
+ relationPropertyResourceId: "project";
44
+ targetPropertyResourceId: "estimate";
45
+ targetPropertyType: "number";
46
+ aggregation: "sum";
47
+ }>().toExtend<NotionAsCodePropertyConfig>();
48
+ expectTypeOf<{
49
+ type: "rollup";
50
+ resourceId: "invalid-total";
51
+ relationPropertyResourceId: "project";
52
+ targetPropertyResourceId: "name";
53
+ targetPropertyType: "title";
54
+ aggregation: "sum";
55
+ }>().not.toExtend<NotionAsCodePropertyConfig>();
56
+
57
+ expectTypeOf<{
58
+ type: "verification";
59
+ resourceId: "verification";
60
+ verifierPropertyResourceId: "owner";
61
+ }>().toExtend<NotionAsCodePropertyConfig>();
62
+ expectTypeOf<{
63
+ type: "verification";
64
+ resourceId: "verification";
65
+ }>().not.toExtend<NotionAsCodePropertyConfig>();
66
+ });
67
+
68
+ it("links page-layout property styles and relation groups to the data source schema", () => {
69
+ type Schema = {
70
+ Name: { type: "title"; resourceId: "name" };
71
+ Estimate: { type: "number"; resourceId: "estimate" };
72
+ Owner: { type: "person"; resourceId: "owner" };
73
+ Project: {
74
+ type: "relation";
75
+ resourceId: "project";
76
+ targetDataSourceResourceId: "projects";
77
+ };
78
+ };
79
+
80
+ expectTypeOf<{
81
+ type: "property";
82
+ property: "estimate";
83
+ style: "large";
84
+ }>().toExtend<PageLayoutPropertyModule<Schema>>();
85
+ expectTypeOf<{
86
+ type: "property";
87
+ property: "estimate";
88
+ style: "landscape";
89
+ }>().not.toExtend<PageLayoutPropertyModule<Schema>>();
90
+ expectTypeOf<{
91
+ type: "property";
92
+ property: "name";
93
+ style: "large";
94
+ }>().not.toExtend<PageLayoutPropertyModule<Schema>>();
95
+
96
+ type ValidLayout = {
97
+ main: [
98
+ { type: "properties"; properties: [{ property: "name" }] },
99
+ {
100
+ type: "relations";
101
+ relations: [{ property: "project"; display: "pageSection" }];
102
+ },
103
+ ];
104
+ pinnedProperties: ["owner"];
105
+ };
106
+ type InvalidRelationLayout = {
107
+ main: [
108
+ { type: "properties"; properties: [{ property: "name" }] },
109
+ {
110
+ type: "relations";
111
+ relations: [{ property: "estimate"; display: "pageSection" }];
112
+ },
113
+ ];
114
+ };
115
+ expectTypeOf<ValidLayout>().toExtend<DatabasePageLayout<Schema>>();
116
+ expectTypeOf<InvalidRelationLayout>().not.toExtend<DatabasePageLayout<Schema>>();
117
+
118
+ expectTypeOf<{
119
+ dataSourceResourceId: "tasks";
120
+ name: "Tasks";
121
+ schema: Schema;
122
+ pageLayout: ValidLayout;
123
+ }>().toExtend<SingleSourceDatabaseArgs<Schema>>();
124
+ expectTypeOf<{
125
+ dataSourceResourceId: "tasks";
126
+ name: "Tasks";
127
+ schema: Schema;
128
+ pageLayout: InvalidRelationLayout;
129
+ }>().not.toExtend<SingleSourceDatabaseArgs<Schema>>();
130
+ });
131
+
132
+ it("requires a meaningful custom agent trigger configuration", () => {
133
+ expectTypeOf<{
134
+ type: "recurrence";
135
+ resourceId: "weekday-trigger";
136
+ frequency: "week";
137
+ interval: 1;
138
+ start: "2026-09-21T09:00:00";
139
+ timeZone: "America/Los_Angeles";
140
+ weekdays: { MO: true; FR: true };
141
+ }>().toExtend<CustomAgentTrigger>();
142
+ expectTypeOf<{
143
+ type: "recurrence";
144
+ resourceId: "empty-weekdays";
145
+ frequency: "week";
146
+ interval: 1;
147
+ start: "2026-09-21T09:00:00";
148
+ timeZone: "America/Los_Angeles";
149
+ weekdays: {};
150
+ }>().not.toExtend<CustomAgentTrigger>();
151
+
152
+ expectTypeOf<{
153
+ type: "property_updated";
154
+ resourceId: "content-trigger";
155
+ dataSourceResourceId: "tasks";
156
+ triggerWhenPageContentEdited: true;
157
+ }>().toExtend<CustomAgentPropertyUpdatedTrigger>();
158
+ expectTypeOf<{
159
+ type: "property_updated";
160
+ resourceId: "empty-trigger";
161
+ dataSourceResourceId: "tasks";
162
+ }>().not.toExtend<CustomAgentPropertyUpdatedTrigger>();
163
+
164
+ expectTypeOf<CustomAgentArgs["triggers"]>().toEqualTypeOf<
165
+ readonly CustomAgentTrigger[] | undefined
166
+ >();
167
+ });
@@ -4,6 +4,30 @@ import { notion, type PageArgs, type PropertyValue } from "./index.js";
4
4
  import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
5
5
 
6
6
  describe("Notion-as-Code page declarations", () => {
7
+ it("records typed URL covers", () => {
8
+ const recorder = startMetadataRecording();
9
+
10
+ notion.page({
11
+ resourceId: "url-cover-page",
12
+ cover: {
13
+ type: "url",
14
+ url: "https://example.com/cover.png",
15
+ position: 0.25,
16
+ },
17
+ });
18
+
19
+ expect(finishMetadataRecording(recorder).intents).toEqual([
20
+ expect.objectContaining({
21
+ resourceId: "url-cover-page",
22
+ cover: {
23
+ type: "url",
24
+ url: "https://example.com/cover.png",
25
+ position: 0.25,
26
+ },
27
+ }),
28
+ ]);
29
+ });
30
+
7
31
  it("records a regular page title separately from Markdown content", () => {
8
32
  const properties = {
9
33
  title: notion.text("Workflow guide"),
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  APPS_WORKSPACE_RESOURCE_ID,
3
3
  type PageIntent,
4
+ type PageCoverReference,
4
5
  type Parent,
5
6
  type ResourceId,
6
7
  } from "./intents.js";
@@ -56,8 +57,7 @@ export type PageArgs = {
56
57
  resourceId: ResourceId;
57
58
  /** Omit to create a private top-level page in the Apps workspace. */
58
59
  parent?: Parent;
59
- // TODO: Port cover types.
60
- cover?: unknown;
60
+ cover?: PageCoverReference;
61
61
  /**
62
62
  * Page properties, including the page title.
63
63
  *
@@ -4,7 +4,7 @@ import { customAgent } from "./notion-as-code/custom-agent.js";
4
4
  import { database } from "./notion-as-code/database.js";
5
5
  import { page } from "./notion-as-code/page.js";
6
6
  import { triggers } from "./triggers.generated.js";
7
- import { workflow } from "./workflow.js";
7
+ import { access, workflow } from "./workflow.js";
8
8
 
9
9
  it("infers access aliases and rejects resource-specific levels", () => {
10
10
  const home = page({ resourceId: "home" });
@@ -28,15 +28,19 @@ it("infers access aliases and rejects resource-specific levels", () => {
28
28
  description: "Checks access types",
29
29
  triggers: [triggers.notionPageCreated()],
30
30
  access: {
31
- home: { resource: home, level: "view" },
32
- singleDatabase: { resource: singleDatabase, level: "edit" },
33
- multiDatabase: { resource: multiDatabase, level: "view" },
31
+ home: access.view(home),
32
+ singleDatabase: access.edit(singleDatabase),
33
+ multiDatabase: access.fullAccess(multiDatabase),
34
+ source: access.comment(singleDatabase.dataSource),
35
+ agent: access.call(agent),
34
36
  },
35
37
  handler: (_event, context) => {
36
38
  expectTypeOf(context.access.home.type).toEqualTypeOf<"page">();
37
39
  expectTypeOf(context.access.home.id).toEqualTypeOf<string>();
38
40
  expectTypeOf(context.access.singleDatabase.type).toEqualTypeOf<"database">();
39
41
  expectTypeOf(context.access.multiDatabase.type).toEqualTypeOf<"database">();
42
+ expectTypeOf(context.access.source.type).toEqualTypeOf<"dataSource">();
43
+ expectTypeOf(context.access.agent.type).toEqualTypeOf<"customAgent">();
40
44
  // @ts-expect-error Access aliases are limited to declarations.
41
45
  void context.access.missing;
42
46
  // @ts-expect-error Access values are readonly.
@@ -14,6 +14,8 @@ export type WorkflowAccessResource =
14
14
  | DataSourceHandle
15
15
  | CustomAgentHandle;
16
16
 
17
+ type WorkflowAccessContentResource = Exclude<WorkflowAccessResource, CustomAgentHandle>;
18
+
17
19
  /** Resource kinds accepted by workflow access declarations. */
18
20
  export type WorkflowAccessResourceType = WorkflowAccessResource["resourceType"];
19
21
 
@@ -82,6 +84,45 @@ export type WorkflowAccess<
82
84
  /** Resolved NaC bindings keyed by public aliases, without declaration metadata. */
83
85
  export type WorkflowAccessBindings = Readonly<Record<string, WorkflowAccessValue>>;
84
86
 
87
+ /** Create type-safe access declarations for Notion-as-Code resource handles. */
88
+ export const access = {
89
+ view,
90
+ comment,
91
+ edit,
92
+ fullAccess,
93
+ call,
94
+ };
95
+
96
+ function view<TResource extends WorkflowAccessContentResource>(
97
+ resource: TResource,
98
+ ): Readonly<{ resource: TResource; level: "view" }> {
99
+ return { resource, level: "view" };
100
+ }
101
+
102
+ function comment<TResource extends WorkflowAccessContentResource>(
103
+ resource: TResource,
104
+ ): Readonly<{ resource: TResource; level: "comment" }> {
105
+ return { resource, level: "comment" };
106
+ }
107
+
108
+ function edit<TResource extends WorkflowAccessContentResource>(
109
+ resource: TResource,
110
+ ): Readonly<{ resource: TResource; level: "edit" }> {
111
+ return { resource, level: "edit" };
112
+ }
113
+
114
+ function fullAccess<TResource extends WorkflowAccessContentResource>(
115
+ resource: TResource,
116
+ ): Readonly<{ resource: TResource; level: "fullAccess" }> {
117
+ return { resource, level: "fullAccess" };
118
+ }
119
+
120
+ function call(
121
+ resource: CustomAgentHandle,
122
+ ): Readonly<{ resource: CustomAgentHandle; level: "call" }> {
123
+ return { resource, level: "call" };
124
+ }
125
+
85
126
  const ACCESS_ALIAS_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;
86
127
  const RESERVED_ACCESS_ALIASES = new Set<string>(["__proto__", "prototype", "constructor"]);
87
128
  const RESOURCE_TYPES: Record<WorkflowAccessResourceType, true> = {
@@ -12,6 +12,7 @@ import { customAgent } from "./notion-as-code/custom-agent.js";
12
12
  import { database } from "./notion-as-code/database.js";
13
13
  import { page } from "./notion-as-code/page.js";
14
14
  import {
15
+ access,
15
16
  connections,
16
17
  workflow,
17
18
  type StepContext,
@@ -1216,10 +1217,10 @@ describe("workflow", () => {
1216
1217
  description: "Hydrates access",
1217
1218
  triggers: [triggers.notionPageCreated()],
1218
1219
  access: {
1219
- home: { resource: home, level: "view" },
1220
- table: { resource: table, level: "edit" },
1221
- source: { resource: table.dataSource, level: "comment" },
1222
- agent: { resource: agent, level: "call" },
1220
+ home: access.view(home),
1221
+ table: access.edit(table),
1222
+ source: access.comment(table.dataSource),
1223
+ agent: access.call(agent),
1223
1224
  },
1224
1225
  handler: (_event, context) => {
1225
1226
  expectTypeOf(context.access.home.type).toEqualTypeOf<"page">();
package/src/workflow.ts CHANGED
@@ -91,6 +91,7 @@ export type {
91
91
  WorkflowAccessRequirements,
92
92
  WorkflowAccessValue,
93
93
  } from "./workflow-access.js";
94
+ export { access } from "./workflow-access.js";
94
95
  export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
95
96
 
96
97
  export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];