@notionhq/apps 0.0.21 → 0.0.23

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 (46) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/build.d.ts.map +1 -1
  3. package/dist/cli/build.js +1 -4
  4. package/dist/custom-block.d.ts +14 -2
  5. package/dist/custom-block.d.ts.map +1 -1
  6. package/dist/custom-block.js +27 -2
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -0
  10. package/dist/notion-as-code/database.d.ts +3 -0
  11. package/dist/notion-as-code/database.d.ts.map +1 -1
  12. package/dist/notion-as-code/database.js +6 -2
  13. package/dist/notion-as-code/index.d.ts +1 -0
  14. package/dist/notion-as-code/index.d.ts.map +1 -1
  15. package/dist/notion-as-code/index.js +4 -1
  16. package/dist/notion-as-code/intents.d.ts +1 -0
  17. package/dist/notion-as-code/intents.d.ts.map +1 -1
  18. package/dist/translate.d.ts +10 -0
  19. package/dist/translate.d.ts.map +1 -0
  20. package/dist/translate.js +65 -0
  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 +5 -3
  28. package/package.json +1 -1
  29. package/skills/custom-blocks/SKILL.md +4 -4
  30. package/skills/notion-as-code/SKILL.md +8 -5
  31. package/skills/workflow/SKILL.md +5 -5
  32. package/src/cli/build.test.ts +55 -0
  33. package/src/cli/build.ts +1 -4
  34. package/src/cli/codegen.test.ts +17 -1
  35. package/src/custom-block.test.ts +138 -2
  36. package/src/custom-block.ts +49 -2
  37. package/src/index.ts +1 -0
  38. package/src/notion-as-code/database.test.ts +5 -1
  39. package/src/notion-as-code/database.ts +9 -1
  40. package/src/notion-as-code/index.ts +1 -0
  41. package/src/notion-as-code/intents.ts +1 -0
  42. package/src/translate.ts +77 -0
  43. package/src/workflow-access-types.test.ts +8 -4
  44. package/src/workflow-access.ts +41 -0
  45. package/src/workflow.test.ts +5 -4
  46. package/src/workflow.ts +1 -0
@@ -25,7 +25,8 @@ are shared across capabilities or do not have a natural capability owner. Module
25
25
  evaluation must work without credentials or network access.
26
26
 
27
27
  To configure a workflow's access to a declared page, database, data source, or
28
- custom agent, pass its handle in `access: { alias: { resource: handle, level } }`.
28
+ custom agent, use the matching helper, such as
29
+ `access: { guide: access.view(guide), analyst: access.call(analyst) }`.
29
30
  The handler receives the live identity as `context.access.alias.id`; importing
30
31
  a declaration alone does not request access. See the
31
32
  [workflow skill](../workflow/SKILL.md#resources-created-with-the-app) for levels
@@ -34,8 +35,8 @@ rejects it before upload.
34
35
 
35
36
  ## Where declarations are picked up
36
37
 
37
- The Apps build discovers direct `.ts` children of `src/workflows/` and
38
- `src/syncs/`, then evaluates their imports with the Notion as Code recorder active.
38
+ The Apps build discovers direct `.ts` children of `src/workflows/`,
39
+ `src/syncs/`, and `src/customBlocks/`, then evaluates their imports with the Notion as Code recorder active.
39
40
  Pages, databases, data sources, teamspaces, and custom agents all follow this
40
41
  same rule. Their resource type does not determine their file location.
41
42
 
@@ -50,6 +51,8 @@ src/
50
51
  myWorkflow/
51
52
  lib/
52
53
  notion.ts Resources owned by myWorkflow
54
+ customBlocks/
55
+ board.ts May import shared resource declarations
53
56
  ```
54
57
 
55
58
  Choose the location based on ownership, not resource type. Export an app-wide
@@ -64,8 +67,8 @@ either. Helpers under capability-specific `lib/` directories are not discovered
64
67
  on their own.
65
68
  Every direct file in a capability directory must still default-export that
66
69
  capability, so do not put a resource-only file there. Imports reached only
67
- through `src/customBlocks/` or browser code do not enter the Notion as Code recording
68
- phase. An App containing only unimported resource declarations is not a
70
+ through browser code do not enter the Notion as Code recording phase. An App containing
71
+ only unimported resource declarations is not a
69
72
  standalone Notion as Code project and has no discovered capabilities.
70
73
 
71
74
  ## Declare resources
@@ -132,7 +132,7 @@ export const guide = page({
132
132
  Import the handle into the workflow and declare the named binding it needs:
133
133
 
134
134
  ```ts
135
- import { workflow } from "@notionhq/apps";
135
+ import { access, workflow } from "@notionhq/apps";
136
136
  import { triggers } from "@notionhq/apps/triggers";
137
137
 
138
138
  import { guide } from "./sayHello/lib/notion.js";
@@ -141,7 +141,7 @@ export default workflow({
141
141
  name: "Read guide",
142
142
  description: "Reads the App's guide on a recurring schedule.",
143
143
  triggers: [triggers.scheduled()],
144
- access: { guide: { resource: guide, level: "view" } },
144
+ access: { guide: access.view(guide) },
145
145
  handler: async (_event, context) => {
146
146
  await context.step("Read guide", () =>
147
147
  context.notion.pages.retrieve({ page_id: context.access.guide.id }),
@@ -155,9 +155,9 @@ declarations in `access` work too. Importing a declaration alone provisions the
155
155
  resource but does not create a `context.access` alias. An `access` entry declares
156
156
  the needed permission level and a named binding for that NaC resource.
157
157
 
158
- `access` accepts page, database, data-source, and custom-agent handles. Pages,
159
- databases, and data sources support `view`, `comment`, `edit`, and `fullAccess`;
160
- custom agents support only `call`. Keys are workflow-local aliases. The handler's
158
+ `access.view()`, `access.comment()`, `access.edit()`, and `access.fullAccess()`
159
+ accept page, database, and data-source handles. `access.call()` accepts custom-agent
160
+ handles. Keys are workflow-local aliases. The handler's
161
161
  typed `context.access` exposes each declared alias as a readonly `{ type, id }`,
162
162
  not the declaration handle or a permission-management API. UI-granted resources
163
163
  are permissions only: they do not create `context.access` aliases.
@@ -194,6 +194,56 @@ describe("buildApp", () => {
194
194
  expect(worker).not.toMatch(/custom_block|customBlock|react|createRoot|NotionCustomBlock/);
195
195
  expect(fs.existsSync(path.join(root, ".notion/blocks.js"))).toBe(false);
196
196
  });
197
+
198
+ it("records resources imported only by a custom block and emits default bindings", async () => {
199
+ const root = fixture("custom-block-notion-as-code");
200
+ const { manifest } = await buildApp(root);
201
+
202
+ expect(manifest.capabilities).toEqual([
203
+ {
204
+ type: "custom_block",
205
+ key: "board",
206
+ config: expect.objectContaining({
207
+ defaultBindings: {
208
+ tasks: {
209
+ dataSourceResourceId: "tasks-source",
210
+ propertyResourceIdsByKey: {
211
+ Name: "task-name",
212
+ Status: "task-status",
213
+ Due: "task-due",
214
+ },
215
+ },
216
+ },
217
+ manifest: expect.objectContaining({
218
+ dataSources: {
219
+ tasks: expect.objectContaining({
220
+ name: "Tasks",
221
+ description: "Work items shown on the board",
222
+ }),
223
+ },
224
+ }),
225
+ }),
226
+ },
227
+ ]);
228
+ const provisioning = JSON.parse(
229
+ await fs.promises.readFile(path.join(root, "dist", "provisioning.json"), "utf8"),
230
+ );
231
+ expect(provisioning.intents).toEqual([
232
+ expect.objectContaining({
233
+ type: "database",
234
+ resourceId: "tasks-db",
235
+ dataSources: [
236
+ expect.objectContaining({
237
+ resourceId: "tasks-source",
238
+ properties: expect.arrayContaining([
239
+ expect.objectContaining({ resourceId: "task-status" }),
240
+ ]),
241
+ }),
242
+ ],
243
+ }),
244
+ ]);
245
+ });
246
+
197
247
  it("names the block file when path is missing", async () => {
198
248
  await expect(buildApp(fixture("custom-block-missing-path"))).rejects.toThrow(
199
249
  /hello.ts.*non-empty path/,
@@ -217,6 +267,11 @@ describe("buildApp", () => {
217
267
  access: {
218
268
  home: { type: "page", resourceId: "home", level: "view" },
219
269
  records: { type: "database", resourceId: "records", level: "edit" },
270
+ fullRecords: {
271
+ type: "database",
272
+ resourceId: "records",
273
+ level: "fullAccess",
274
+ },
220
275
  source: {
221
276
  type: "dataSource",
222
277
  resourceId: "records-source",
package/src/cli/build.ts CHANGED
@@ -54,10 +54,7 @@ export async function buildApp(projectRoot: string): Promise<BuildResult> {
54
54
  }
55
55
 
56
56
  const blockConfigs = await buildBlockDeclarations(projectRoot, capabilities);
57
- const entityProvisioning = await buildMetadata(
58
- projectRoot,
59
- capabilities.filter((capability) => capability.runtime === "worker"),
60
- );
57
+ const entityProvisioning = await buildMetadata(projectRoot, capabilities);
61
58
  const manifest = await extractManifest(
62
59
  bundlePath,
63
60
  capabilities,
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { generateEntry } from "./codegen.js";
3
+ import { generateEntry, generateMetadataEntry } from "./codegen.js";
4
4
 
5
5
  describe("generateEntry", () => {
6
6
  it("statically imports and registers workflows by key", () => {
@@ -36,3 +36,19 @@ describe("generateEntry", () => {
36
36
  expect(entry).toContain(`"issues": capability_0,`);
37
37
  });
38
38
  });
39
+
40
+ describe("generateMetadataEntry", () => {
41
+ it("records resources imported by custom blocks", () => {
42
+ const entry = generateMetadataEntry([
43
+ {
44
+ type: "custom_block",
45
+ tag: "custom_block",
46
+ runtime: "build",
47
+ key: "board",
48
+ sourcePath: "src/customBlocks/board.ts",
49
+ },
50
+ ]);
51
+
52
+ expect(entry).toContain(`await import("../src/customBlocks/board.ts");`);
53
+ });
54
+ });
@@ -1,5 +1,8 @@
1
- import { describe, expect, it } from "vitest";
2
- import { customBlock } from "./custom-block.js";
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
+
3
+ import { customBlock, type CustomBlockOptions } from "./custom-block.js";
4
+ import { notion, type DatabaseHandle } from "./notion-as-code/index.js";
5
+ import { toCustomBlockPropertyType } from "./translate.js";
3
6
 
4
7
  describe("customBlock", () => {
5
8
  it("supports static assets without a build command", () => {
@@ -16,4 +19,137 @@ describe("customBlock", () => {
16
19
  );
17
20
  },
18
21
  );
22
+
23
+ it("generates mappings and default bindings from a single-source database", () => {
24
+ const tasks = notion.database("tasks-db", {
25
+ dataSourceResourceId: "tasks-source",
26
+ name: "Tasks",
27
+ description: "Work items shown on the board",
28
+ schema: {
29
+ Name: { resourceId: "task-name", type: "title" },
30
+ Notes: {
31
+ resourceId: "task-notes",
32
+ type: "text",
33
+ description: "Extra context",
34
+ },
35
+ Files: { resourceId: "task-files", type: "file" },
36
+ Due: { resourceId: "task-due", type: "date" },
37
+ },
38
+ });
39
+
40
+ expect(
41
+ customBlock({
42
+ path: "./blocks/board",
43
+ name: "Task board",
44
+ description: "Shows tasks by status",
45
+ dataSources: { tasks },
46
+ }).config,
47
+ ).toEqual({
48
+ source: {
49
+ type: "project",
50
+ path: "./blocks/board",
51
+ command: "npx vite build",
52
+ output: "dist",
53
+ },
54
+ manifest: {
55
+ version: 1,
56
+ name: "Task board",
57
+ description: "Shows tasks by status",
58
+ dataSources: {
59
+ tasks: {
60
+ name: "Tasks",
61
+ description: "Work items shown on the board",
62
+ properties: {
63
+ Name: { name: "Name", type: "title" },
64
+ Notes: {
65
+ name: "Notes",
66
+ description: "Extra context",
67
+ type: "rich_text",
68
+ },
69
+ Files: { name: "Files", type: "files" },
70
+ Due: { name: "Due", type: "date" },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ defaultBindings: {
76
+ tasks: {
77
+ dataSourceResourceId: "tasks-source",
78
+ propertyResourceIdsByKey: {
79
+ Name: "task-name",
80
+ Notes: "task-notes",
81
+ Files: "task-files",
82
+ Due: "task-due",
83
+ },
84
+ },
85
+ },
86
+ });
87
+ });
88
+
89
+ it("accepts a data-source handle alongside an inline mapping", () => {
90
+ const tasks = notion.database("tasks-db", {
91
+ name: "Project",
92
+ datasources: {
93
+ Tasks: {
94
+ resourceId: "tasks-source",
95
+ description: "Current tasks",
96
+ schema: { Name: { resourceId: "task-name", type: "title" } },
97
+ },
98
+ },
99
+ });
100
+ const block = customBlock({
101
+ path: "./blocks/board",
102
+ dataSources: {
103
+ tasks: tasks.datasources.Tasks,
104
+ people: { name: "People", properties: {} },
105
+ },
106
+ });
107
+
108
+ expect(block.config.manifest.dataSources).toEqual({
109
+ tasks: {
110
+ name: "Tasks",
111
+ description: "Current tasks",
112
+ properties: { Name: { name: "Name", type: "title" } },
113
+ },
114
+ people: { name: "People", properties: {} },
115
+ });
116
+ expect(block.config.defaultBindings).toEqual({
117
+ tasks: {
118
+ dataSourceResourceId: "tasks-source",
119
+ propertyResourceIdsByKey: { Name: "task-name" },
120
+ },
121
+ });
122
+ });
123
+
124
+ it("rejects multi-source database handles in the dataSources type", () => {
125
+ type DataSourceOption = NonNullable<CustomBlockOptions["dataSources"]>[string];
126
+ expectTypeOf<DatabaseHandle>().not.toExtend<DataSourceOption>();
127
+ });
128
+
129
+ it.each([
130
+ ["title", "title"],
131
+ ["text", "rich_text"],
132
+ ["number", "number"],
133
+ ["select", "select"],
134
+ ["multi_select", "multi_select"],
135
+ ["status", "status"],
136
+ ["date", "date"],
137
+ ["checkbox", "checkbox"],
138
+ ["url", "url"],
139
+ ["email", "email"],
140
+ ["phone_number", "phone_number"],
141
+ ["file", "files"],
142
+ ["person", "people"],
143
+ ["auto_increment_id", "unique_id"],
144
+ ["relation", "relation"],
145
+ ["formula", "formula"],
146
+ ["rollup", "rollup"],
147
+ ["created_time", "created_time"],
148
+ ["last_edited_time", "last_edited_time"],
149
+ ["created_by", "created_by"],
150
+ ["last_edited_by", "last_edited_by"],
151
+ ["verification", "verification"],
152
+ ] as const)("maps Notion-as-Code %s properties to manifest %s properties", (input, output) => {
153
+ expect(toCustomBlockPropertyType(input)).toBe(output);
154
+ });
19
155
  });
@@ -1,9 +1,21 @@
1
1
  import type { ManifestDataSource } from "@notionhq/custom-blocks";
2
2
 
3
+ import type { DataSourceHandle, SingleSourceDatabaseHandle } from "./notion-as-code/database.js";
4
+ import { toCustomBlockDefaultBinding, toCustomBlockDataSource } from "./translate.js";
5
+
6
+ export type CustomBlockDefaultBinding = {
7
+ dataSourceResourceId: string;
8
+ propertyResourceIdsByKey: Record<string, string>;
9
+ };
10
+
11
+ type CustomBlockDataSource = ManifestDataSource | DataSourceHandle | SingleSourceDatabaseHandle;
12
+
3
13
  export type CustomBlockOptions = {
4
14
  path: string;
15
+ name?: string;
16
+ description?: string;
5
17
  slashCommand?: string;
6
- dataSources?: Record<string, ManifestDataSource>;
18
+ dataSources?: Record<string, CustomBlockDataSource>;
7
19
  } & (
8
20
  | { static: true; command?: never; output?: never }
9
21
  | { static?: false; command?: string; output?: string }
@@ -20,6 +32,7 @@ export function customBlock(options: CustomBlockOptions) {
20
32
  "Custom block slashCommand must be 1–64 lowercase letters, numbers, hyphens or underscores, starting with a letter or number",
21
33
  );
22
34
  }
35
+ const { dataSources, defaultBindings } = lowerDataSources(options.dataSources ?? {});
23
36
  return {
24
37
  _tag: "custom_block" as const,
25
38
  config: {
@@ -33,9 +46,43 @@ export function customBlock(options: CustomBlockOptions) {
33
46
  },
34
47
  manifest: {
35
48
  version: 1 as const,
49
+ ...(options.name === undefined ? {} : { name: options.name }),
50
+ ...(options.description === undefined ? {} : { description: options.description }),
36
51
  ...(slashCommand === undefined ? {} : { slashCommand }),
37
- dataSources: options.dataSources ?? {},
52
+ dataSources,
38
53
  },
54
+ ...(Object.keys(defaultBindings).length === 0 ? {} : { defaultBindings }),
39
55
  },
40
56
  };
41
57
  }
58
+
59
+ function lowerDataSources(dataSources: Record<string, CustomBlockDataSource>) {
60
+ const manifestDataSources: Record<string, ManifestDataSource> = {};
61
+ const defaultBindings: Record<string, CustomBlockDefaultBinding> = {};
62
+
63
+ for (const [key, dataSource] of Object.entries(dataSources)) {
64
+ const handle = isSingleSourceDatabaseHandle(dataSource)
65
+ ? dataSource.dataSource
66
+ : dataSource;
67
+ if (isDataSourceHandle(handle)) {
68
+ manifestDataSources[key] = toCustomBlockDataSource(handle);
69
+ defaultBindings[key] = toCustomBlockDefaultBinding(handle);
70
+ } else {
71
+ manifestDataSources[key] = handle;
72
+ }
73
+ }
74
+
75
+ return { dataSources: manifestDataSources, defaultBindings };
76
+ }
77
+
78
+ function isSingleSourceDatabaseHandle(
79
+ value: CustomBlockDataSource,
80
+ ): value is SingleSourceDatabaseHandle {
81
+ return "dataSource" in value;
82
+ }
83
+
84
+ function isDataSourceHandle(
85
+ value: ManifestDataSource | DataSourceHandle,
86
+ ): value is DataSourceHandle {
87
+ return "resourceId" in value && "schema" in value && "database" in value;
88
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { teamspace } from "./notion-as-code/teamspace.js";
6
6
  export { view } from "./notion-as-code/view.js";
7
7
  export { sync } from "./sync.js";
8
8
  export {
9
+ access,
9
10
  workflow,
10
11
  type Workflow,
11
12
  type WorkflowAccess,
@@ -75,6 +75,7 @@ describe("Notion-as-Code database declarations", () => {
75
75
  database.dataSource.schema.Title.resourceId,
76
76
  ).toEqualTypeOf<"single-title-id">();
77
77
  expect(database.dataSource.schema).toEqual(schema);
78
+ expect(database.dataSource.name).toBe("Tasks");
78
79
 
79
80
  const declaration = sync({
80
81
  dataSource: database.dataSource,
@@ -636,6 +637,7 @@ describe("Notion-as-Code database declarations", () => {
636
637
  notion.database("metadata-database", {
637
638
  dataSourceResourceId: "metadata-source",
638
639
  name: "Metadata",
640
+ description: "Metadata source",
639
641
  schema: {
640
642
  Name: { type: "title", resourceId: "metadata-name" },
641
643
  },
@@ -658,11 +660,13 @@ describe("Notion-as-Code database declarations", () => {
658
660
  type: "database",
659
661
  resourceId: "metadata-database",
660
662
  name: "Metadata",
663
+ description: "Metadata source",
661
664
  parent: { type: "resourceId", resourceId: APPS_WORKSPACE_RESOURCE_ID },
662
665
  dataSources: [
663
666
  {
664
667
  resourceId: "metadata-source",
665
668
  name: "Metadata",
669
+ description: "Metadata source",
666
670
  properties: [{ type: "title", resourceId: "metadata-name", name: "Name" }],
667
671
  },
668
672
  ],
@@ -680,7 +684,7 @@ describe("Notion-as-Code database declarations", () => {
680
684
  expect(serialized).not.toHaveProperty("schema");
681
685
  expect(serialized).not.toHaveProperty("dataSourceResourceId");
682
686
  expect(serialized).not.toHaveProperty("datasources");
683
- expect(serialized).not.toHaveProperty("description");
687
+ expect(serialized.description).toBe("Metadata source");
684
688
  });
685
689
 
686
690
  it("requires the status property configuration in authoring types", () => {
@@ -25,6 +25,7 @@ type NonEmptyViewSchemas = readonly [ViewSchema, ...ViewSchema[]];
25
25
  export type DataSourceArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
26
26
  resourceId: ResourceId;
27
27
  name?: never;
28
+ description?: string;
28
29
  schema: Schema;
29
30
  icon?: NotionAsCodeIcon;
30
31
  pageLayout?: DatabasePageLayout<Schema>;
@@ -120,6 +121,8 @@ export type ChildDatabaseArgs<
120
121
  export type DataSourceHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
121
122
  readonly resourceType: "dataSource";
122
123
  readonly resourceId: string;
124
+ readonly name: string;
125
+ readonly description?: string;
123
126
  readonly schema: Schema;
124
127
  readonly database: Database<AppsSchemaForSchema<Schema>>;
125
128
  addPage: (args: ChildPageArgs) => PageHandle;
@@ -259,6 +262,7 @@ function buildDatabaseHandle(
259
262
  const source: DataSourceDefinition = {
260
263
  resourceId: args.dataSourceResourceId,
261
264
  name: args.name,
265
+ ...(args.description === undefined ? {} : { description: args.description }),
262
266
  properties: normalizeProperties(args.schema),
263
267
  ...(args.pageLayout === undefined ? {} : { pageLayout: args.pageLayout }),
264
268
  };
@@ -400,7 +404,7 @@ function createDatabase(
400
404
  );
401
405
  }
402
406
  dataSourceIds.add(definition.resourceId);
403
- handles[key] = createDataSourceHandle(definition, schema);
407
+ handles[key] = createDataSourceHandle(definition, schema, intent.description);
404
408
  }
405
409
 
406
410
  recordIntent({ ...intent, type: "database" });
@@ -419,6 +423,7 @@ function createDatabase(
419
423
  function createDataSourceHandle(
420
424
  dataSource: DataSourceDefinition,
421
425
  schema: NotionAsCodeSchema,
426
+ databaseDescription: string | undefined,
422
427
  ): DataSourceHandle {
423
428
  const propertyResourceIds = new Set<string>();
424
429
  const propertyNames = new Set<string>();
@@ -437,10 +442,13 @@ function createDataSourceHandle(
437
442
  propertyResourceIds.add(property.resourceId);
438
443
  propertyNames.add(property.name);
439
444
  }
445
+ const description = dataSource.description ?? databaseDescription;
440
446
 
441
447
  return {
442
448
  resourceType: "dataSource",
443
449
  resourceId: dataSource.resourceId,
450
+ name: dataSource.name,
451
+ ...(description === undefined ? {} : { description }),
444
452
  schema,
445
453
  database: createAppsDatabase(dataSource.resourceId, dataSource),
446
454
  addPage(pageArgs) {
@@ -66,6 +66,7 @@ export type {
66
66
  SingleSourceDatabaseArgs,
67
67
  SingleSourceDatabaseHandle,
68
68
  } from "./database.js";
69
+ export { toCustomBlockDefaultBinding, toCustomBlockDataSource } from "../translate.js";
69
70
  export type {
70
71
  ChildPageArgs,
71
72
  PageArgs,
@@ -316,6 +316,7 @@ export type DataSourceDefinition<
316
316
  > = {
317
317
  resourceId: ResourceId;
318
318
  name: string;
319
+ description?: string;
319
320
  properties: Properties;
320
321
  icon?: NotionAsCodeIcon;
321
322
  pageLayout?: DatabasePageLayout;
@@ -0,0 +1,77 @@
1
+ import type {
2
+ ManifestDataSource,
3
+ ManifestProperty,
4
+ NotionPropertyType,
5
+ } from "@notionhq/custom-blocks";
6
+
7
+ import type { CustomBlockDefaultBinding } from "./custom-block.js";
8
+ import type { DataSourceHandle } from "./notion-as-code/database.js";
9
+ import type { NotionAsCodeProperty } from "./notion-as-code/intents.js";
10
+
11
+ /** Lower a Notion-as-Code handle into the declaration consumed by custom block hosts. */
12
+ export function toCustomBlockDataSource(handle: DataSourceHandle): ManifestDataSource {
13
+ return {
14
+ name: handle.name,
15
+ ...(handle.description === undefined ? {} : { description: handle.description }),
16
+ properties: Object.fromEntries(
17
+ Object.entries(handle.schema).map(([key, property]) => [
18
+ key,
19
+ {
20
+ name: key,
21
+ ...(typeof property.description !== "string"
22
+ ? {}
23
+ : { description: property.description }),
24
+ type: toCustomBlockPropertyType(property.type),
25
+ } satisfies ManifestProperty,
26
+ ]),
27
+ ),
28
+ };
29
+ }
30
+
31
+ /** Keep resource IDs in the build artifact so deploy can resolve workspace-local IDs later. */
32
+ export function toCustomBlockDefaultBinding(handle: DataSourceHandle): CustomBlockDefaultBinding {
33
+ return {
34
+ dataSourceResourceId: handle.resourceId,
35
+ propertyResourceIdsByKey: Object.fromEntries(
36
+ Object.entries(handle.schema).map(([key, property]) => [key, property.resourceId]),
37
+ ),
38
+ };
39
+ }
40
+
41
+ export function toCustomBlockPropertyType(type: NotionAsCodeProperty["type"]): NotionPropertyType {
42
+ switch (type) {
43
+ case "text":
44
+ return "rich_text";
45
+ case "file":
46
+ return "files";
47
+ case "person":
48
+ return "people";
49
+ case "auto_increment_id":
50
+ return "unique_id";
51
+ case "title":
52
+ case "number":
53
+ case "select":
54
+ case "multi_select":
55
+ case "status":
56
+ case "date":
57
+ case "checkbox":
58
+ case "url":
59
+ case "email":
60
+ case "phone_number":
61
+ case "relation":
62
+ case "formula":
63
+ case "rollup":
64
+ case "created_time":
65
+ case "last_edited_time":
66
+ case "created_by":
67
+ case "last_edited_by":
68
+ case "verification":
69
+ return type;
70
+ default:
71
+ return assertNever(type);
72
+ }
73
+ }
74
+
75
+ function assertNever(value: never): never {
76
+ throw new Error(`Unsupported Notion-as-Code property type: ${String(value)}`);
77
+ }
@@ -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.