@notionhq/apps 0.0.30 → 0.0.32

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 (53) hide show
  1. package/dist/cli/codegen.d.ts.map +1 -1
  2. package/dist/cli/codegen.js +6 -1
  3. package/dist/custom-block.d.ts +9 -2
  4. package/dist/custom-block.d.ts.map +1 -1
  5. package/dist/custom-block.js +3 -0
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/notion-as-code/custom-agent.d.ts +6 -5
  9. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  10. package/dist/notion-as-code/custom-agent.js +15 -4
  11. package/dist/notion-as-code/database.d.ts +11 -0
  12. package/dist/notion-as-code/database.d.ts.map +1 -1
  13. package/dist/notion-as-code/database.js +61 -2
  14. package/dist/notion-as-code/index.d.ts +3 -3
  15. package/dist/notion-as-code/index.d.ts.map +1 -1
  16. package/dist/notion-as-code/intents.d.ts +7 -1
  17. package/dist/notion-as-code/intents.d.ts.map +1 -1
  18. package/dist/notion-as-code/provisioning.d.ts.map +1 -1
  19. package/dist/notion-as-code/provisioning.js +25 -0
  20. package/dist/notion-as-code/provisioning.test.d.ts +2 -0
  21. package/dist/notion-as-code/provisioning.test.d.ts.map +1 -0
  22. package/dist/notion-as-code/recorder.d.ts +6 -0
  23. package/dist/notion-as-code/recorder.d.ts.map +1 -1
  24. package/dist/notion-as-code/recorder.js +22 -1
  25. package/dist/notion-as-code/views.d.ts +11 -2
  26. package/dist/notion-as-code/views.d.ts.map +1 -1
  27. package/dist/workflow-access.d.ts +8 -3
  28. package/dist/workflow-access.d.ts.map +1 -1
  29. package/dist/workflow-access.js +33 -34
  30. package/dist/workflow.d.ts +1 -1
  31. package/dist/workflow.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/skills/notion-as-code/SKILL.md +39 -8
  34. package/src/cli/build.test.ts +11 -0
  35. package/src/cli/codegen.test.ts +26 -1
  36. package/src/cli/codegen.ts +10 -2
  37. package/src/custom-block.test.ts +14 -0
  38. package/src/custom-block.ts +10 -0
  39. package/src/index.ts +2 -2
  40. package/src/notion-as-code/custom-agent.test.ts +62 -3
  41. package/src/notion-as-code/custom-agent.ts +19 -10
  42. package/src/notion-as-code/database.test.ts +105 -1
  43. package/src/notion-as-code/database.ts +77 -1
  44. package/src/notion-as-code/index.ts +2 -1
  45. package/src/notion-as-code/intents.ts +4 -1
  46. package/src/notion-as-code/notion-as-code-types.test.ts +21 -11
  47. package/src/notion-as-code/provisioning.test.ts +69 -0
  48. package/src/notion-as-code/provisioning.ts +27 -0
  49. package/src/notion-as-code/recorder.ts +27 -0
  50. package/src/notion-as-code/views-types.test.ts +24 -0
  51. package/src/notion-as-code/views.ts +24 -2
  52. package/src/workflow-access.ts +56 -51
  53. package/src/workflow.ts +1 -1
@@ -1,13 +1,117 @@
1
1
  import { describe, expect, expectTypeOf, it } from "vitest";
2
2
 
3
3
  import { Builder } from "../builder.js";
4
+ import { customBlock } from "../custom-block.js";
4
5
  import { ExecutionError } from "../error.js";
5
6
  import { sync } from "../sync.js";
6
7
  import { notion, type DataSourceHandle, type SingleSourceDatabaseHandle } from "./index.js";
7
8
  import { APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
8
- import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
9
+ import {
10
+ finishMetadataRecording,
11
+ startMetadataRecording,
12
+ withMetadataCapabilityKey,
13
+ } from "./recorder.js";
9
14
 
10
15
  describe("Notion-as-Code database declarations", () => {
16
+ it("infers a custom view data source from the block's default bindings", async () => {
17
+ const recorder = startMetadataRecording();
18
+ const tasks = notion.database("tasks-db", {
19
+ dataSourceResourceId: "tasks-source",
20
+ name: "Tasks",
21
+ schema: { Name: { resourceId: "task-name", type: "title" } },
22
+ });
23
+
24
+ await withMetadataCapabilityKey("board", async () => {
25
+ const board = customBlock({
26
+ path: "./blocks/board",
27
+ dataSources: { tasks },
28
+ });
29
+ tasks.addCustomView({
30
+ resourceId: "tasks-board-view",
31
+ name: "Board",
32
+ customBlock: board,
33
+ });
34
+ });
35
+
36
+ expect(finishMetadataRecording(recorder).intents.at(-1)).toEqual({
37
+ type: "view",
38
+ databaseResourceId: "tasks-db",
39
+ view: {
40
+ type: "custom",
41
+ resourceId: "tasks-board-view",
42
+ name: "Board",
43
+ dataSourceResourceId: "tasks-source",
44
+ customBlockKey: "board",
45
+ },
46
+ });
47
+ });
48
+
49
+ it("requires an explicit bound source when a block binds multiple database sources", async () => {
50
+ const recorder = startMetadataRecording();
51
+ const tasks = notion.database("tasks-db", {
52
+ name: "Tasks",
53
+ datasources: {
54
+ Active: {
55
+ resourceId: "active-source",
56
+ schema: { Name: { resourceId: "active-name", type: "title" } },
57
+ },
58
+ Archive: {
59
+ resourceId: "archive-source",
60
+ schema: { Name: { resourceId: "archive-name", type: "title" } },
61
+ },
62
+ },
63
+ });
64
+ const projects = notion.database("projects-db", {
65
+ dataSourceResourceId: "projects-source",
66
+ name: "Projects",
67
+ schema: { Name: { resourceId: "project-name", type: "title" } },
68
+ });
69
+
70
+ await withMetadataCapabilityKey("board", async () => {
71
+ const board = customBlock({
72
+ path: "./blocks/board",
73
+ dataSources: {
74
+ active: tasks.datasources.Active,
75
+ archive: tasks.datasources.Archive,
76
+ },
77
+ });
78
+ expect(() =>
79
+ tasks.addCustomView({
80
+ resourceId: "ambiguous-board-view",
81
+ customBlock: board,
82
+ }),
83
+ ).toThrow("must bind exactly one data source");
84
+ tasks.addCustomView({
85
+ resourceId: "active-board-view",
86
+ customBlock: board,
87
+ dataSource: tasks.datasources.Active,
88
+ });
89
+ expect(() =>
90
+ tasks.addCustomView({
91
+ resourceId: "foreign-board-view",
92
+ customBlock: board,
93
+ dataSource: projects.dataSource,
94
+ }),
95
+ ).toThrow('dataSource must belong to database "tasks-db"');
96
+ const activeOnlyBoard = customBlock({
97
+ path: "./blocks/active-board",
98
+ dataSources: { active: tasks.datasources.Active },
99
+ });
100
+ expect(() =>
101
+ tasks.addCustomView({
102
+ resourceId: "unbound-board-view",
103
+ customBlock: activeOnlyBoard,
104
+ dataSource: tasks.datasources.Archive,
105
+ }),
106
+ ).toThrow('dataSource must be bound by custom block "board"');
107
+ });
108
+
109
+ expect(finishMetadataRecording(recorder).intents.at(-1)).toEqual(
110
+ expect.objectContaining({
111
+ view: expect.objectContaining({ dataSourceResourceId: "active-source" }),
112
+ }),
113
+ );
114
+ });
11
115
  it("records a single-source page layout on the data source", () => {
12
116
  const recorder = startMetadataRecording();
13
117
 
@@ -1,4 +1,5 @@
1
1
  import type { Database } from "../database.js";
2
+ import type { CustomBlockHandle } from "../custom-block.js";
2
3
  import {
3
4
  APPS_WORKSPACE_RESOURCE_ID,
4
5
  type DatabaseIntent,
@@ -11,7 +12,7 @@ import {
11
12
  type ResourceId,
12
13
  } from "./intents.js";
13
14
  import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
14
- import { recordIntent } from "./recorder.js";
15
+ import { isMetadataRecording, recordIntent } from "./recorder.js";
15
16
  import { assertParent, assertUserResourceId } from "./resource.js";
16
17
  import { createAppsDatabase, type AppsSchemaForSchema } from "./schema.js";
17
18
  import type { ViewSchema } from "./views.js";
@@ -132,6 +133,15 @@ type DatabaseHandleBase = {
132
133
  readonly resourceType: "database";
133
134
  readonly resourceId: string;
134
135
  addView: (view: ViewSchema) => void;
136
+ addCustomView: (view: AddCustomViewArgs) => void;
137
+ };
138
+
139
+ export type AddCustomViewArgs = {
140
+ resourceId: ResourceId;
141
+ name?: string;
142
+ icon?: Extract<NotionAsCodeIcon, { type: "notion_icon" }>;
143
+ customBlock: CustomBlockHandle;
144
+ dataSource?: DataSourceHandle;
135
145
  };
136
146
 
137
147
  /** Map each authoring source key to a handle backed by that source's own schema. */
@@ -278,6 +288,7 @@ function buildDatabaseHandle(
278
288
  resourceId: databaseResourceId,
279
289
  dataSource,
280
290
  addView: handle.addView,
291
+ addCustomView: handle.addCustomView,
281
292
  };
282
293
  }
283
294
 
@@ -416,9 +427,74 @@ function createDatabase(
416
427
  addView(view) {
417
428
  recordIntent({ type: "view", databaseResourceId: intent.resourceId, view });
418
429
  },
430
+ addCustomView(view) {
431
+ recordCustomView(intent.resourceId, view, handles);
432
+ },
419
433
  };
420
434
  }
421
435
 
436
+ function recordCustomView(
437
+ databaseResourceId: ResourceId,
438
+ view: AddCustomViewArgs,
439
+ handles: Readonly<Record<string, DataSourceHandle>>,
440
+ ): void {
441
+ assertUserResourceId(view.resourceId);
442
+ const customBlockKey = view.customBlock.key;
443
+ if (typeof customBlockKey !== "string" || customBlockKey.trim() === "") {
444
+ if (!isMetadataRecording()) {
445
+ return;
446
+ }
447
+ throw new Error(
448
+ "addCustomView must be called from the custom block capability file so its key is available during build",
449
+ );
450
+ }
451
+
452
+ const dataSources = Object.values(handles);
453
+ const boundDataSourceIds = new Set(
454
+ Object.values(view.customBlock.config.defaultBindings ?? {}).map(
455
+ (binding) => binding.dataSourceResourceId,
456
+ ),
457
+ );
458
+ let dataSourceResourceId: string;
459
+ if (view.dataSource !== undefined) {
460
+ if (!dataSources.includes(view.dataSource)) {
461
+ throw new Error(
462
+ `Custom view "${view.resourceId}" dataSource must belong to database "${databaseResourceId}"`,
463
+ );
464
+ }
465
+ if (!boundDataSourceIds.has(view.dataSource.resourceId)) {
466
+ throw new Error(
467
+ `Custom view "${view.resourceId}" dataSource must be bound by custom block "${customBlockKey}"`,
468
+ );
469
+ }
470
+ dataSourceResourceId = view.dataSource.resourceId;
471
+ } else {
472
+ const matchingSources = dataSources.filter((source) =>
473
+ boundDataSourceIds.has(source.resourceId),
474
+ );
475
+ const matchingSource = matchingSources[0];
476
+ if (matchingSources.length !== 1 || matchingSource === undefined) {
477
+ throw new Error(
478
+ `Custom view "${view.resourceId}" must bind exactly one data source from database "${databaseResourceId}" or specify dataSource`,
479
+ );
480
+ }
481
+ dataSourceResourceId = matchingSource.resourceId;
482
+ }
483
+
484
+ recordIntent({
485
+ type: "view",
486
+ databaseResourceId,
487
+ view: {
488
+ type: "custom",
489
+ resourceId: view.resourceId,
490
+ ...(view.name === undefined ? {} : { name: view.name }),
491
+ ...(view.icon === undefined ? {} : { icon: view.icon }),
492
+ dataSourceResourceId,
493
+ customBlockKey,
494
+ },
495
+ });
496
+ }
497
+
422
498
  /** Create a data-source handle backed by the Apps database used by syncs. */
423
499
  function createDataSourceHandle(
424
500
  dataSource: DataSourceDefinition,
@@ -34,7 +34,6 @@ export type {
34
34
  } from "./intents.js";
35
35
  export type {
36
36
  CustomAgentAccess,
37
- CustomAgentAccessRole,
38
37
  CustomAgentArgs,
39
38
  CustomAgentHandle,
40
39
  CustomAgentPageAddedTrigger,
@@ -56,6 +55,7 @@ export type {
56
55
  YearlyRecurrenceSchedule,
57
56
  } from "./custom-agent.js";
58
57
  export type {
58
+ AddCustomViewArgs,
59
59
  ChildDatabaseArgs,
60
60
  ChildDatabaseFactory,
61
61
  ChildMultiSourceDatabaseArgs,
@@ -93,6 +93,7 @@ export type {
93
93
  CoverAspectFormat,
94
94
  CoverFormat,
95
95
  CoverSizeFormat,
96
+ CustomViewSchema,
96
97
  DatabaseViewSortDirection,
97
98
  DateFilterMode,
98
99
  DateGroupByFormat,
@@ -1,3 +1,4 @@
1
+ import type { ResourceAccessLevel } from "../workflow-access.js";
1
2
  import type { CustomAgentArgs } from "./custom-agent.js";
2
3
  import type { PageArgs } from "./page.js";
3
4
  import type { TeamspaceArgs } from "./teamspace.js";
@@ -346,7 +347,9 @@ export type DatabaseIntent = {
346
347
  };
347
348
 
348
349
  export type PageIntent = PageArgs & { parent: Parent };
349
- export type CustomAgentIntent = CustomAgentArgs;
350
+ export type CustomAgentIntent = Omit<CustomAgentArgs, "access"> & {
351
+ access?: readonly { resourceId: ResourceId; level: ResourceAccessLevel }[];
352
+ };
350
353
 
351
354
  export type ViewIntent = {
352
355
  databaseResourceId: ResourceId;
@@ -1,8 +1,9 @@
1
1
  import { expectTypeOf, it } from "vitest";
2
+ import { access } from "../workflow-access.js";
3
+ import { page, database, customAgent } from "../index.js";
2
4
 
3
5
  import type {
4
6
  CustomAgentAccess,
5
- CustomAgentAccessRole,
6
7
  CustomAgentArgs,
7
8
  CustomAgentPropertyUpdatedTrigger,
8
9
  CustomAgentTrustedUrls,
@@ -16,17 +17,26 @@ import type {
16
17
  } from "./index.js";
17
18
 
18
19
  it("types custom agent resource access and URL trust", () => {
19
- expectTypeOf<
20
- "full_access" | "can_edit_content" | "can_comment" | "can_view"
21
- >().toEqualTypeOf<CustomAgentAccessRole>();
22
- expectTypeOf<{
23
- resourceId: "tasks";
24
- role: "can_view";
25
- }>().toExtend<CustomAgentAccess>();
26
- expectTypeOf<{
27
- resourceId: "tasks";
28
- role: "read_and_write";
20
+ const home = page({ resourceId: "home" });
21
+ const tasks = database("tasks", {
22
+ dataSourceResourceId: "source",
23
+ name: "Tasks",
24
+ schema: { Title: { resourceId: "title", type: "title" } },
25
+ });
26
+ expectTypeOf(access.view(home)).toExtend<CustomAgentAccess>();
27
+ expectTypeOf(access.edit(tasks)).toExtend<CustomAgentAccess>();
28
+ expectTypeOf(access.view(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
29
+ expectTypeOf(access.comment(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
30
+ expectTypeOf(access.edit(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
31
+ expectTypeOf(access.fullAccess(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
32
+ expectTypeOf<{
33
+ resource: typeof tasks.dataSource;
34
+ level: "view";
29
35
  }>().not.toExtend<CustomAgentAccess>();
36
+ expectTypeOf<{ resourceId: "tasks"; role: "can_view" }>().not.toExtend<CustomAgentAccess>();
37
+ expectTypeOf(
38
+ access.call(customAgent({ resourceId: "agent", name: "Agent" })),
39
+ ).not.toExtend<CustomAgentAccess>();
30
40
  expectTypeOf<{ type: "all" }>().toEqualTypeOf<CustomAgentTrustedUrls>();
31
41
  expectTypeOf<CustomAgentArgs["webAccess"]>().toEqualTypeOf<boolean | undefined>();
32
42
  expectTypeOf<CustomAgentArgs["trustedUrls"]>().toEqualTypeOf<
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { notion } from "./index.js";
4
+ import { createProvisioningArtifact } from "./provisioning.js";
5
+ import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
6
+ import type { CustomViewSchema } from "./views.js";
7
+
8
+ const customView: CustomViewSchema = {
9
+ resourceId: "tasks-board-view",
10
+ dataSourceResourceId: "tasks-source",
11
+ type: "custom",
12
+ customBlockKey: "board",
13
+ };
14
+ const databaseArgs = {
15
+ name: "Tasks",
16
+ dataSourceResourceId: "tasks-source",
17
+ schema: { Name: { resourceId: "task-name", type: "title" } },
18
+ } as const;
19
+
20
+ describe.each([
21
+ {
22
+ name: "standalone views",
23
+ declare: (view: CustomViewSchema) =>
24
+ notion.view({ ...view, databaseResourceId: "tasks-db" }),
25
+ },
26
+ {
27
+ name: "database views",
28
+ declare: (view: CustomViewSchema) =>
29
+ notion.database("tasks-db", { ...databaseArgs, views: [view] }),
30
+ },
31
+ {
32
+ name: "addView",
33
+ declare: (view: CustomViewSchema) =>
34
+ notion.database("tasks-db", databaseArgs).addView(view),
35
+ },
36
+ ])("provisioning $name", ({ declare }) => {
37
+ it.each([
38
+ ["sorts", []],
39
+ ["filters", []],
40
+ ["properties", []],
41
+ ["showPageIcon", true],
42
+ ["defaultTemplate", "template"],
43
+ ] as const)("rejects %s on custom views when creating the artifact", (field, value) => {
44
+ const recorder = startMetadataRecording();
45
+ // Extra fields can arrive from JavaScript callers despite the TypeScript contract.
46
+ declare({ ...customView, [field]: value });
47
+ const { intents } = finishMetadataRecording(recorder);
48
+
49
+ expect(() => createProvisioningArtifact(intents)).toThrow(
50
+ `Custom views do not support ${field}`,
51
+ );
52
+ });
53
+
54
+ it("rejects a blank custom block key when creating the artifact", () => {
55
+ const recorder = startMetadataRecording();
56
+ declare({ ...customView, customBlockKey: " " });
57
+ const { intents } = finishMetadataRecording(recorder);
58
+
59
+ expect(() => createProvisioningArtifact(intents)).toThrow("non-empty customBlockKey");
60
+ });
61
+
62
+ it("preserves the custom block key for server-side capability resolution", () => {
63
+ const recorder = startMetadataRecording();
64
+ declare(customView);
65
+ const { intents } = finishMetadataRecording(recorder);
66
+
67
+ expect(createProvisioningArtifact(intents).intents).toEqual(intents);
68
+ });
69
+ });
@@ -1,4 +1,5 @@
1
1
  import type { InfraAsCodeIntent } from "./intents.js";
2
+ import type { ViewSchema } from "./views.js";
2
3
 
3
4
  export const APPS_PROVISIONING_SCHEMA = "notion:apps-provisioning:v1";
4
5
 
@@ -24,6 +25,13 @@ export function createProvisioningArtifact(
24
25
  ): ProvisioningArtifact {
25
26
  const provisioningIntents: ProvisioningIntent[] = [];
26
27
  for (const intent of intents) {
28
+ if (intent.type === "view") {
29
+ validateCustomView(intent.view);
30
+ } else if (intent.type === "database") {
31
+ for (const view of intent.views ?? []) {
32
+ validateCustomView(view);
33
+ }
34
+ }
27
35
  if (intent.type === "teamspace") {
28
36
  const { parent: _parent, ...teamspace } = intent;
29
37
  provisioningIntents.push(teamspace);
@@ -48,3 +56,22 @@ export function isRecordedProvisioning(value: unknown): value is RecordedProvisi
48
56
  Array.isArray(value.intents)
49
57
  );
50
58
  }
59
+
60
+ function validateCustomView(view: ViewSchema): void {
61
+ if (view.type === "custom") {
62
+ if (typeof view.customBlockKey !== "string" || view.customBlockKey.trim() === "") {
63
+ throw new Error("Custom views require a non-empty customBlockKey");
64
+ }
65
+ for (const field of [
66
+ "sorts",
67
+ "filters",
68
+ "properties",
69
+ "showPageIcon",
70
+ "defaultTemplate",
71
+ ] as const) {
72
+ if (field in view) {
73
+ throw new Error(`Custom views do not support ${field}`);
74
+ }
75
+ }
76
+ }
77
+ }
@@ -3,11 +3,13 @@ import type { InfraAsCodeIntent } from "./intents.js";
3
3
  import type { RecordedProvisioning } from "./provisioning.js";
4
4
 
5
5
  let activeRecorder: RecordedProvisioning | undefined;
6
+ let currentCapabilityKey: string | undefined;
6
7
 
7
8
  /** Start collecting declarations from the metadata bundle. */
8
9
  export function startMetadataRecording(): RecordedProvisioning {
9
10
  const recorder: RecordedProvisioning = { intents: [] };
10
11
  activeRecorder = recorder;
12
+ currentCapabilityKey = undefined;
11
13
  return recorder;
12
14
  }
13
15
 
@@ -17,6 +19,7 @@ export function finishMetadataRecording(recorder: RecordedProvisioning): Recorde
17
19
  throw new Error("Notion-as-Code metadata recorder was replaced before build completed");
18
20
  }
19
21
  activeRecorder = undefined;
22
+ currentCapabilityKey = undefined;
20
23
  return recorder;
21
24
  }
22
25
 
@@ -24,3 +27,27 @@ export function finishMetadataRecording(recorder: RecordedProvisioning): Recorde
24
27
  export function recordIntent(intent: InfraAsCodeIntent): void {
25
28
  activeRecorder?.intents.push(intent);
26
29
  }
30
+
31
+ /** Evaluate one discovered capability with its build-time identity available to declarations. */
32
+ export async function withMetadataCapabilityKey<T>(
33
+ capabilityKey: string | undefined,
34
+ evaluate: () => Promise<T>,
35
+ ): Promise<T> {
36
+ const previousCapabilityKey = currentCapabilityKey;
37
+ currentCapabilityKey = capabilityKey;
38
+ try {
39
+ return await evaluate();
40
+ } finally {
41
+ currentCapabilityKey = previousCapabilityKey;
42
+ }
43
+ }
44
+
45
+ /** Return the capability key only while a metadata bundle is recording. */
46
+ export function getCurrentMetadataCapabilityKey(): string | undefined {
47
+ return activeRecorder === undefined ? undefined : currentCapabilityKey;
48
+ }
49
+
50
+ /** Whether declarations are currently being collected by the build metadata bundle. */
51
+ export function isMetadataRecording(): boolean {
52
+ return activeRecorder !== undefined;
53
+ }
@@ -1,6 +1,7 @@
1
1
  import { expectTypeOf, it } from "vitest";
2
2
 
3
3
  import type {
4
+ CustomViewSchema,
4
5
  DatabaseArgs,
5
6
  DatabaseHandle,
6
7
  DatePropertyFilter,
@@ -8,6 +9,29 @@ import type {
8
9
  ViewSchema,
9
10
  } from "./index.js";
10
11
 
12
+ it("keeps custom views free of standard view query and property configuration", () => {
13
+ expectTypeOf<{
14
+ resourceId: "custom-view";
15
+ dataSourceResourceId: "source";
16
+ type: "custom";
17
+ customBlockKey: "board";
18
+ }>().toExtend<CustomViewSchema>();
19
+ expectTypeOf<{
20
+ resourceId: "custom-view";
21
+ dataSourceResourceId: "source";
22
+ type: "custom";
23
+ customBlockKey: "board";
24
+ sorts: [];
25
+ }>().not.toExtend<ViewSchema>();
26
+ expectTypeOf<{
27
+ resourceId: "custom-view";
28
+ dataSourceResourceId: "source";
29
+ type: "custom";
30
+ customBlockKey: "board";
31
+ properties: [];
32
+ }>().not.toExtend<ViewSchema>();
33
+ });
34
+
11
35
  it("requires date properties for calendar and timeline views at both entrypoints", () => {
12
36
  type MissingDateProperty = {
13
37
  resourceId: "view";
@@ -1,7 +1,15 @@
1
1
  import type { NotionAsCodeIcon, NotionAsCodeProperty, ResourceId } from "./intents.js";
2
2
 
3
3
  // Ported from notion-next/src/server/helpers/infraAsCode/types.ts.
4
- export type ViewType = "table" | "board" | "calendar" | "list" | "gallery" | "feed" | "timeline";
4
+ export type ViewType =
5
+ | "table"
6
+ | "board"
7
+ | "calendar"
8
+ | "list"
9
+ | "gallery"
10
+ | "feed"
11
+ | "timeline"
12
+ | "custom";
5
13
 
6
14
  export type PropertyVisibility = "show" | "hide" | "hide_if_empty";
7
15
 
@@ -563,6 +571,19 @@ export type TimelineViewSchema = BaseViewSchema & {
563
571
  showTable?: boolean;
564
572
  };
565
573
 
574
+ export type CustomViewSchema = Omit<
575
+ BaseViewSchema,
576
+ "type" | "sorts" | "filters" | "showPageIcon" | "defaultTemplate"
577
+ > & {
578
+ type: "custom";
579
+ customBlockKey: string;
580
+ sorts?: never;
581
+ filters?: never;
582
+ properties?: never;
583
+ showPageIcon?: never;
584
+ defaultTemplate?: never;
585
+ };
586
+
566
587
  export type ViewSchema =
567
588
  | TableViewSchema
568
589
  | BoardViewSchema
@@ -570,4 +591,5 @@ export type ViewSchema =
570
591
  | ListViewSchema
571
592
  | GalleryViewSchema
572
593
  | FeedViewSchema
573
- | TimelineViewSchema;
594
+ | TimelineViewSchema
595
+ | CustomViewSchema;