@notionhq/apps 0.0.19 → 0.0.21

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 (38) hide show
  1. package/README.md +5 -1
  2. package/dist/notion-as-code/custom-agent.d.ts +75 -1
  3. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  4. package/dist/notion-as-code/custom-agent.js +11 -0
  5. package/dist/notion-as-code/custom-agent.test.d.ts +2 -0
  6. package/dist/notion-as-code/custom-agent.test.d.ts.map +1 -0
  7. package/dist/notion-as-code/database.d.ts +5 -1
  8. package/dist/notion-as-code/database.d.ts.map +1 -1
  9. package/dist/notion-as-code/database.js +3 -1
  10. package/dist/notion-as-code/date.d.ts +3 -2
  11. package/dist/notion-as-code/date.d.ts.map +1 -1
  12. package/dist/notion-as-code/handles.d.ts +8 -11
  13. package/dist/notion-as-code/handles.d.ts.map +1 -1
  14. package/dist/notion-as-code/handles.js +8 -7
  15. package/dist/notion-as-code/index.d.ts +10 -13
  16. package/dist/notion-as-code/index.d.ts.map +1 -1
  17. package/dist/notion-as-code/intents.d.ts +133 -11
  18. package/dist/notion-as-code/intents.d.ts.map +1 -1
  19. package/dist/notion-as-code/notion-as-code-types.test.d.ts +2 -0
  20. package/dist/notion-as-code/notion-as-code-types.test.d.ts.map +1 -0
  21. package/dist/notion-as-code/page.d.ts +51 -3
  22. package/dist/notion-as-code/page.d.ts.map +1 -1
  23. package/dist/notion-as-code/page.test.d.ts +2 -0
  24. package/dist/notion-as-code/page.test.d.ts.map +1 -0
  25. package/package.json +1 -1
  26. package/skills/notion-as-code/SKILL.md +19 -9
  27. package/skills/workflow/SKILL.md +3 -1
  28. package/src/notion-as-code/custom-agent.test.ts +79 -0
  29. package/src/notion-as-code/custom-agent.ts +111 -2
  30. package/src/notion-as-code/database.test.ts +38 -0
  31. package/src/notion-as-code/database.ts +7 -0
  32. package/src/notion-as-code/date.ts +4 -2
  33. package/src/notion-as-code/handles.ts +16 -12
  34. package/src/notion-as-code/index.ts +49 -2
  35. package/src/notion-as-code/intents.ts +219 -24
  36. package/src/notion-as-code/notion-as-code-types.test.ts +167 -0
  37. package/src/notion-as-code/page.test.ts +59 -0
  38. package/src/notion-as-code/page.ts +62 -4
@@ -1,11 +1,59 @@
1
- import { type Parent, type ResourceId } from "./intents.js";
1
+ import { type PageCoverReference, type Parent, type ResourceId } from "./intents.js";
2
2
  import { type ChildDatabaseFactory } from "./database.js";
3
+ /**
4
+ * A single text token: plain text as `[string]`, or text with annotations as
5
+ * `[string, ...annotations]`.
6
+ */
7
+ export type SimpleTextToken = [string] | [string, ...unknown[]];
8
+ /**
9
+ * The JSON-serializable text format returned by Notion-as-Code text helpers.
10
+ */
11
+ export type TextValue = SimpleTextToken[];
12
+ export type VerificationInput = {
13
+ state: "verified";
14
+ /** Set an expiration with `notion.datetime(...)`; omit it for indefinite verification. */
15
+ datetime?: TextValue;
16
+ } | {
17
+ state: "unverified";
18
+ };
19
+ export type VerificationPropertyValue = VerificationInput & {
20
+ type: "verification";
21
+ };
22
+ /**
23
+ * A page property value accepted by Notion as Code.
24
+ *
25
+ * Use helpers from `@notionhq/apps/notion-as-code` to construct rich values
26
+ * such as text, dates, checkboxes, and URLs.
27
+ */
28
+ export type PropertyValue = string | number | TextValue | string[] | VerificationPropertyValue | undefined;
29
+ /**
30
+ * Arguments for declaring a page.
31
+ *
32
+ * For a regular page, set its title through
33
+ * `properties.title: notion.text("My Page Title")`. For a database page, use
34
+ * the title property's schema name instead of `title`.
35
+ */
3
36
  export type PageArgs = {
4
37
  resourceId: ResourceId;
5
38
  /** Omit to create a private top-level page in the Apps workspace. */
6
39
  parent?: Parent;
7
- cover?: unknown;
8
- properties?: Record<string, unknown>;
40
+ cover?: PageCoverReference;
41
+ /**
42
+ * Page properties, including the page title.
43
+ *
44
+ * @example Regular page
45
+ * properties: { title: notion.text("My Page Title") }
46
+ *
47
+ * @example Database page
48
+ * properties: { Name: notion.text("Task Name"), Status: "In Progress" }
49
+ */
50
+ properties?: Record<string, PropertyValue | undefined>;
51
+ /**
52
+ * Page body in Notion-flavored Markdown.
53
+ *
54
+ * Markdown headings create child blocks. They do not set the page title;
55
+ * set a regular page's title with `properties.title`.
56
+ */
9
57
  content?: string;
10
58
  fullWidth?: boolean;
11
59
  };
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,MAAM,EACX,KAAK,UAAU,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAI/E,MAAM,MAAM,QAAQ,GAAG;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAErD,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,oFAAoF;AACpF,wBAAgB,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAS/C;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAgBrF"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,kBAAkB,EACvB,KAAK,MAAM,EACX,KAAK,UAAU,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAI/E;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,eAAe,EAAE,CAAC;AAE1C,MAAM,MAAM,iBAAiB,GAC1B;IACA,KAAK,EAAE,UAAU,CAAC;IAClB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,SAAS,CAAC;CACpB,GACD;IAAE,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3B,MAAM,MAAM,yBAAyB,GAAG,iBAAiB,GAAG;IAC3D,IAAI,EAAE,cAAc,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACtB,MAAM,GACN,MAAM,GACN,SAAS,GACT,MAAM,EAAE,GACR,yBAAyB,GACzB,SAAS,CAAC;AAEb;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC,CAAC;IACvD;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAErD,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,oBAAoB,CAAC;IAClC,OAAO,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,UAAU,CAAC;CAC7C,CAAC;AAEF,oFAAoF;AACpF,wBAAgB,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAS/C;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAgBrF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=page.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.test.d.ts","sourceRoot":"","sources":["../../src/notion-as-code/page.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/apps",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "An SDK for building workflow apps for Notion",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -75,7 +75,8 @@ root exports support:
75
75
 
76
76
  - `teamspace({ resourceId, name, accessLevel })`, with `addPage` and
77
77
  `teamspace.addDatabase(id, args)` on the returned handle.
78
- - `page({ resourceId, parent?, properties?, content? })`, with
78
+ - `page({ resourceId, parent?, properties?, content? })`; regular page titles use
79
+ `properties: { title: notion.text("My Page Title") }`. It also has
79
80
  `addPage` and `page.addDatabase(id, args)` for children.
80
81
  - `database(resourceId, { dataSourceResourceId, name, schema, parent?, views? })`,
81
82
  returning a single source as `handle.dataSource`, or
@@ -90,6 +91,11 @@ root exports support:
90
91
  Use property resource IDs in view filters, sorts, and layout options; calendar
91
92
  views require `calendarBy`, and timeline views require `timelineBy`.
92
93
 
94
+ For a regular page, set the title with
95
+ `properties: { title: notion.text("My Page Title") }`. A Markdown heading in
96
+ `content` creates a child heading block; it does not set the page title. For a
97
+ database page, use the title property's schema name instead of `title`.
98
+
93
99
  A database declaration must include at least one data source or at least one
94
100
  view. A linked-only database may omit `datasources`, or use `datasources: {}`,
95
101
  only when `views` is nonempty. `{}`, `{ datasources: {} }`, and
@@ -118,12 +124,17 @@ use `{ type: "resourceId", resourceId: parent.resourceId }`; child helpers set
118
124
  this reference for you. Pages and databases without a parent default to private
119
125
  top-level resources in the Apps workspace.
120
126
 
121
- Several fields, including database covers and custom-agent triggers,
122
- still have incomplete types. A field typed as `unknown` is not proof
123
- that any payload is supported. Check implementation and examples before using
124
- it; do not assume parity with other Notion as Code packages. Database views are
125
- typed with `ViewSchema`; standalone declarations accept `ViewArgs`, which adds
126
- the owning `databaseResourceId`.
127
+ Page and database covers accept `{ type: "url", url, position? }`. Custom-agent
128
+ triggers are discriminated unions for recurrence, page-added, and
129
+ property-updated triggers. Weekly recurrences require at least one weekday, and
130
+ property-updated triggers require property conditions or
131
+ `triggerWhenPageContentEdited: true`.
132
+
133
+ Database page layouts are typed against their keyed data-source schema. Reference
134
+ property resource IDs, not schema keys. The SDK restricts relation groups to
135
+ relation properties and narrows standalone property styles by property type.
136
+ Database views are typed with `ViewSchema`; standalone declarations accept
137
+ `ViewArgs`, which adds the owning `databaseResourceId`.
127
138
 
128
139
  The Apps SDK exposes a subset of Notion as Code. Data sources are nested inside
129
140
  `database(resourceId, { datasources: { ... } })`, not declared by a separate
@@ -131,8 +142,7 @@ The Apps SDK exposes a subset of Notion as Code. Data sources are nested inside
131
142
  Apps deployment rejects workspace creation or changes and supplies the App's
132
143
  workspace binding itself. Value helpers and types stay on their existing
133
144
  subpaths. For example, import `{ notion }` from `@notionhq/apps/notion-as-code`
134
- for `notion.text(...)` or `notion.file(resourceId)`. The latter creates a file
135
- reference, not an upload or file declaration. These helpers are not root exports.
145
+ for `notion.text(...)`. These helpers are not root exports.
136
146
  CLI acceptance of a serialized intent envelope alone does not establish server
137
147
  support for its contents.
138
148
 
@@ -120,10 +120,12 @@ guide page in `src/workflows/sayHello/lib/notion.ts`:
120
120
 
121
121
  ```ts
122
122
  import { page } from "@notionhq/apps";
123
+ import { notion } from "@notionhq/apps/notion-as-code";
123
124
 
124
125
  export const guide = page({
125
126
  resourceId: "workflow-guide",
126
- content: "# Workflow guide\nThis App runs a scheduled workflow.",
127
+ properties: { title: notion.text("Workflow guide") },
128
+ content: "This App runs a scheduled workflow.",
127
129
  });
128
130
  ```
129
131
 
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { notion } from "./index.js";
4
+ import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
5
+
6
+ describe("Notion-as-Code custom agent declarations", () => {
7
+ it("records typed recurrence and database triggers", () => {
8
+ const recorder = startMetadataRecording();
9
+
10
+ notion.customAgent({
11
+ resourceId: "triage-agent",
12
+ name: "Triage agent",
13
+ triggers: [
14
+ {
15
+ type: "recurrence",
16
+ resourceId: "weekday-trigger",
17
+ frequency: "week",
18
+ interval: 1,
19
+ start: "2026-09-21T09:00:00",
20
+ timeZone: "America/Los_Angeles",
21
+ weekdays: { MO: true, FR: true },
22
+ },
23
+ {
24
+ type: "property_updated",
25
+ resourceId: "task-update-trigger",
26
+ dataSourceResourceId: "tasks",
27
+ propertyConditions: {
28
+ status: { type: "property_edited" },
29
+ },
30
+ triggerWhenPageContentEdited: true,
31
+ },
32
+ ],
33
+ });
34
+
35
+ expect(finishMetadataRecording(recorder).intents).toEqual([
36
+ {
37
+ type: "custom_agent",
38
+ resourceId: "triage-agent",
39
+ name: "Triage agent",
40
+ triggers: [
41
+ {
42
+ type: "recurrence",
43
+ resourceId: "weekday-trigger",
44
+ frequency: "week",
45
+ interval: 1,
46
+ start: "2026-09-21T09:00:00",
47
+ timeZone: "America/Los_Angeles",
48
+ weekdays: { MO: true, FR: true },
49
+ },
50
+ {
51
+ type: "property_updated",
52
+ resourceId: "task-update-trigger",
53
+ dataSourceResourceId: "tasks",
54
+ propertyConditions: {
55
+ status: { type: "property_edited" },
56
+ },
57
+ triggerWhenPageContentEdited: true,
58
+ },
59
+ ],
60
+ },
61
+ ]);
62
+ });
63
+
64
+ it("rejects reserved resource IDs inside triggers", () => {
65
+ expect(() =>
66
+ notion.customAgent({
67
+ resourceId: "triage-agent",
68
+ name: "Triage agent",
69
+ triggers: [
70
+ {
71
+ type: "page_added",
72
+ resourceId: "__notion_apps_trigger",
73
+ dataSourceResourceId: "tasks",
74
+ },
75
+ ],
76
+ }),
77
+ ).toThrow("reserved");
78
+ });
79
+ });
@@ -2,6 +2,105 @@ import type { CustomAgentIntent, NotionAsCodeIcon, ResourceId } from "./intents.
2
2
  import { recordIntent } from "./recorder.js";
3
3
  import { assertUserResourceId } from "./resource.js";
4
4
 
5
+ type RecurrenceWeekday = "MO" | "TU" | "WE" | "TH" | "FR" | "SA" | "SU";
6
+
7
+ /** A nonempty selection of weekdays for a weekly recurrence. */
8
+ export type RecurrenceWeekdays = {
9
+ [Day in RecurrenceWeekday]?: true;
10
+ } & {
11
+ [Day in RecurrenceWeekday]: Required<Pick<Record<RecurrenceWeekday, true>, Day>>;
12
+ }[RecurrenceWeekday];
13
+
14
+ export type MonthlyWeekdayOccurrence = "1st" | "2nd" | "3rd" | "4th" | "last";
15
+
16
+ export type MonthlyRecurrenceRestriction =
17
+ | { type: "day_of_month"; day: number }
18
+ | {
19
+ type: "weekday_of_month";
20
+ weekday: RecurrenceWeekday;
21
+ occurrence: MonthlyWeekdayOccurrence;
22
+ };
23
+
24
+ export type RecurrenceScheduleEnd =
25
+ | { type: "date"; endsAt: string }
26
+ | { type: "occurrences"; count: number };
27
+
28
+ type RecurrenceScheduleBase = {
29
+ interval: number;
30
+ start: string;
31
+ timeZone: string;
32
+ end?: RecurrenceScheduleEnd;
33
+ };
34
+
35
+ export type HourlyRecurrenceSchedule = RecurrenceScheduleBase & {
36
+ frequency: "hour";
37
+ };
38
+
39
+ export type DailyRecurrenceSchedule = RecurrenceScheduleBase & {
40
+ frequency: "day";
41
+ };
42
+
43
+ export type WeeklyRecurrenceSchedule = RecurrenceScheduleBase & {
44
+ frequency: "week";
45
+ weekdays: RecurrenceWeekdays;
46
+ };
47
+
48
+ export type MonthlyRecurrenceSchedule = RecurrenceScheduleBase & {
49
+ frequency: "month";
50
+ monthlyRestriction: MonthlyRecurrenceRestriction;
51
+ };
52
+
53
+ export type YearlyRecurrenceSchedule = RecurrenceScheduleBase & {
54
+ frequency: "year";
55
+ };
56
+
57
+ export type CustomAgentRecurrenceSchedule = (
58
+ | HourlyRecurrenceSchedule
59
+ | DailyRecurrenceSchedule
60
+ | WeeklyRecurrenceSchedule
61
+ | MonthlyRecurrenceSchedule
62
+ | YearlyRecurrenceSchedule
63
+ ) & {
64
+ type: "recurrence";
65
+ };
66
+
67
+ export type CustomAgentTriggerBase = {
68
+ resourceId: ResourceId;
69
+ enabled?: boolean;
70
+ };
71
+
72
+ export type CustomAgentRecurrenceTrigger = CustomAgentTriggerBase & CustomAgentRecurrenceSchedule;
73
+
74
+ export type PropertyTriggerCondition = {
75
+ type: "property_edited";
76
+ };
77
+
78
+ export type CustomAgentPageAddedTrigger = CustomAgentTriggerBase & {
79
+ type: "page_added";
80
+ dataSourceResourceId: ResourceId;
81
+ };
82
+
83
+ type PropertyUpdatedTriggerTarget =
84
+ | {
85
+ propertyConditions: Readonly<Record<ResourceId, PropertyTriggerCondition>>;
86
+ triggerWhenPageContentEdited?: boolean;
87
+ }
88
+ | {
89
+ propertyConditions?: never;
90
+ triggerWhenPageContentEdited: true;
91
+ };
92
+
93
+ export type CustomAgentPropertyUpdatedTrigger = CustomAgentTriggerBase &
94
+ PropertyUpdatedTriggerTarget & {
95
+ type: "property_updated";
96
+ dataSourceResourceId: ResourceId;
97
+ };
98
+
99
+ export type CustomAgentTrigger =
100
+ | CustomAgentRecurrenceTrigger
101
+ | CustomAgentPageAddedTrigger
102
+ | CustomAgentPropertyUpdatedTrigger;
103
+
5
104
  export type CustomAgentArgs = {
6
105
  resourceId: ResourceId;
7
106
  name: string;
@@ -9,8 +108,7 @@ export type CustomAgentArgs = {
9
108
  instructions?: string;
10
109
  model?: "ambrosia-tart-high" | "opal-quince-medium" | "almond-croissant-low";
11
110
  sharedResources?: readonly ResourceId[];
12
- // TODO: Port custom agent trigger types.
13
- triggers?: readonly unknown[];
111
+ triggers?: readonly CustomAgentTrigger[];
14
112
  };
15
113
 
16
114
  export type CustomAgentHandle = {
@@ -24,6 +122,17 @@ export function customAgent(args: CustomAgentArgs): CustomAgentHandle {
24
122
  for (const resourceId of args.sharedResources ?? []) {
25
123
  assertUserResourceId(resourceId);
26
124
  }
125
+ for (const trigger of args.triggers ?? []) {
126
+ assertUserResourceId(trigger.resourceId);
127
+ if (trigger.type === "page_added" || trigger.type === "property_updated") {
128
+ assertUserResourceId(trigger.dataSourceResourceId);
129
+ }
130
+ if (trigger.type === "property_updated") {
131
+ for (const propertyResourceId of Object.keys(trigger.propertyConditions ?? {})) {
132
+ assertUserResourceId(propertyResourceId);
133
+ }
134
+ }
135
+ }
27
136
  recordIntent({ type: "custom_agent", ...args } satisfies {
28
137
  type: "custom_agent";
29
138
  } & CustomAgentIntent);
@@ -8,6 +8,44 @@ import { APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
8
8
  import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
9
9
 
10
10
  describe("Notion-as-Code database declarations", () => {
11
+ it("records a single-source page layout on the data source", () => {
12
+ const recorder = startMetadataRecording();
13
+
14
+ notion.database("layout-database", {
15
+ dataSourceResourceId: "layout-source",
16
+ name: "Tasks",
17
+ schema: {
18
+ Name: { type: "title", resourceId: "name" },
19
+ Estimate: { type: "number", resourceId: "estimate" },
20
+ },
21
+ pageLayout: {
22
+ main: [
23
+ { type: "properties", properties: [{ property: "name" }] },
24
+ { type: "property", property: "estimate", style: "large" },
25
+ ],
26
+ pinnedProperties: ["estimate"],
27
+ },
28
+ });
29
+
30
+ expect(finishMetadataRecording(recorder).intents).toEqual([
31
+ expect.objectContaining({
32
+ resourceId: "layout-database",
33
+ dataSources: [
34
+ expect.objectContaining({
35
+ resourceId: "layout-source",
36
+ pageLayout: {
37
+ main: [
38
+ { type: "properties", properties: [{ property: "name" }] },
39
+ { type: "property", property: "estimate", style: "large" },
40
+ ],
41
+ pinnedProperties: ["estimate"],
42
+ },
43
+ }),
44
+ ],
45
+ }),
46
+ ]);
47
+ });
48
+
11
49
  it("converts the single-source form with keyed schemas and resolved property names", () => {
12
50
  const recorder = startMetadataRecording();
13
51
  const schema = {
@@ -2,6 +2,7 @@ import type { Database } from "../database.js";
2
2
  import {
3
3
  APPS_WORKSPACE_RESOURCE_ID,
4
4
  type DatabaseIntent,
5
+ type DatabasePageLayout,
5
6
  type DataSourceDefinition,
6
7
  type NotionAsCodeIcon,
7
8
  type NotionAsCodeProperty,
@@ -26,6 +27,7 @@ export type DataSourceArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchem
26
27
  name?: never;
27
28
  schema: Schema;
28
29
  icon?: NotionAsCodeIcon;
30
+ pageLayout?: DatabasePageLayout<Schema>;
29
31
  };
30
32
 
31
33
  /** Data sources keyed by the names used to access their handles. */
@@ -42,6 +44,7 @@ type SingleSourceDatabaseOptions<Schema extends NotionAsCodeSchema = NotionAsCod
42
44
  dataSourceResourceId: ResourceId;
43
45
  name: string;
44
46
  schema: Schema;
47
+ pageLayout?: DatabasePageLayout<Schema>;
45
48
  datasources?: never;
46
49
  };
47
50
 
@@ -61,6 +64,7 @@ type MultiSourceDatabaseOptions<DataSources extends DataSourceMap = DataSourceMa
61
64
  datasources: DataSources;
62
65
  dataSourceResourceId?: never;
63
66
  schema?: never;
67
+ pageLayout?: never;
64
68
  } & (keyof NoInfer<DataSources> extends never
65
69
  ? { views: NonEmptyViewSchemas }
66
70
  : { views?: readonly ViewSchema[] });
@@ -76,6 +80,7 @@ type ViewOnlyDatabaseOptions = DatabaseMetadata & {
76
80
  datasources?: never;
77
81
  dataSourceResourceId?: never;
78
82
  schema?: never;
83
+ pageLayout?: never;
79
84
  views: NonEmptyViewSchemas;
80
85
  };
81
86
 
@@ -255,6 +260,7 @@ function buildDatabaseHandle(
255
260
  resourceId: args.dataSourceResourceId,
256
261
  name: args.name,
257
262
  properties: normalizeProperties(args.schema),
263
+ ...(args.pageLayout === undefined ? {} : { pageLayout: args.pageLayout }),
258
264
  };
259
265
  const handle = recordDatabase(databaseResourceId, args, parent, [
260
266
  { key: "dataSource", schema: args.schema, definition: source },
@@ -295,6 +301,7 @@ function normalizeDatabaseMetadata(args: DatabaseArgs): DatabaseMetadata {
295
301
  schema: _schema,
296
302
  datasources: _datasources,
297
303
  dataSourceResourceId: _dataSourceResourceId,
304
+ pageLayout: _pageLayout,
298
305
  ...metadata
299
306
  } = args;
300
307
  return metadata;
@@ -1,5 +1,7 @@
1
+ import type { TextValue } from "./page.js";
2
+
1
3
  /** Serialize Notion-as-Code date values for Apps property payloads. */
2
- export function date(startDate: string, endDate?: string): unknown[][] {
4
+ export function date(startDate: string, endDate?: string): TextValue {
3
5
  const start_date = parseIsoDate(startDate);
4
6
  const end_date = endDate === undefined ? undefined : parseIsoDate(endDate);
5
7
  if (end_date !== undefined && end_date < start_date) {
@@ -13,7 +15,7 @@ export function date(startDate: string, endDate?: string): unknown[][] {
13
15
  }
14
16
 
15
17
  /** Serialize Notion-as-Code datetime values for Apps property payloads. */
16
- export function datetime(value: { start: string; end?: string; timeZone?: string }): unknown[][] {
18
+ export function datetime(value: { start: string; end?: string; timeZone?: string }): TextValue {
17
19
  const timeZone = value.timeZone ?? "UTC";
18
20
  try {
19
21
  new Intl.DateTimeFormat("en-US", { timeZone }).format();
@@ -1,8 +1,12 @@
1
1
  import { customAgent } from "./custom-agent.js";
2
2
  import { database } from "./database.js";
3
3
  import { date, datetime } from "./date.js";
4
- import { page } from "./page.js";
5
- import { assertUserResourceId } from "./resource.js";
4
+ import {
5
+ page,
6
+ type TextValue,
7
+ type VerificationInput,
8
+ type VerificationPropertyValue,
9
+ } from "./page.js";
6
10
  import { teamspace } from "./teamspace.js";
7
11
  import { view } from "./view.js";
8
12
 
@@ -12,22 +16,22 @@ const notionAsCodeFacade = {
12
16
  page,
13
17
  customAgent,
14
18
  view,
15
- text: (value: string) => [[value]],
16
- number: (value: number) => [[String(value)]],
19
+ text: (value: string): TextValue => [[value]],
20
+ number: (value: number): TextValue => [[String(value)]],
17
21
  date,
18
22
  datetime,
23
+ verification: (value: VerificationInput): VerificationPropertyValue => ({
24
+ ...value,
25
+ type: "verification",
26
+ }),
19
27
  select: (value: string) => value,
20
28
  status: (value: string) => value,
21
29
  multiSelect: (values: readonly string[]) => values.join(","),
22
- checkbox: (value: boolean) => [[value ? "Yes" : "No"]],
23
- url: (value: string) => [[value, [["a", value]]]],
24
- email: (value: string) => [[value, [["a", `mailto:${value}`]]]],
25
- phone: (value: string) => [[value]],
30
+ checkbox: (value: boolean): TextValue => [[value ? "Yes" : "No"]],
31
+ url: (value: string): TextValue => [[value, [["a", value]]]],
32
+ email: (value: string): TextValue => [[value, [["a", `mailto:${value}`]]]],
33
+ phone: (value: string): TextValue => [[value]],
26
34
  relation: (value: string | readonly string[]) => (Array.isArray(value) ? [...value] : value),
27
- file: (resourceId: string) => {
28
- assertUserResourceId(resourceId);
29
- return { type: "file" as const, resourceId };
30
- },
31
35
  };
32
36
 
33
37
  export type NotionAsCodeFacade = typeof notionAsCodeFacade;
@@ -2,6 +2,9 @@ import { createNotion } from "./handles.js";
2
2
 
3
3
  export { APPS_RESERVED_RESOURCE_ID_PREFIX, APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
4
4
  export type {
5
+ ContentPageLayoutTab,
6
+ DatabasePageLayout,
7
+ DatabasePageLayoutTab,
5
8
  DatabaseIntent,
6
9
  DataSourceDefinition,
7
10
  NotionAsCodeIcon,
@@ -10,10 +13,45 @@ export type {
10
13
  NotionAsCodeSchema,
11
14
  NotionAsCodeSelectOption,
12
15
  NotionAsCodeStatusOption,
16
+ PageCoverReference,
17
+ PageLayoutMainModule,
18
+ PageLayoutNumberAndFormulaStyle,
19
+ PageLayoutPersonStyle,
20
+ PageLayoutPropertiesModule,
21
+ PageLayoutPropertyConfig,
22
+ PageLayoutPropertyModule,
23
+ PageLayoutPropertySection,
24
+ PageLayoutRelationConfig,
25
+ PageLayoutRelationsGroupModule,
26
+ PageLayoutSidebarModule,
13
27
  Parent,
14
28
  ResourceId,
29
+ RollupBaseAggregationType,
30
+ RollupDateAggregationType,
31
+ RollupNumberAggregationType,
32
+ RollupTargetPropertyType,
33
+ ViewPageLayoutTab,
15
34
  } from "./intents.js";
16
- export type { CustomAgentArgs, CustomAgentHandle } from "./custom-agent.js";
35
+ export type {
36
+ CustomAgentArgs,
37
+ CustomAgentHandle,
38
+ CustomAgentPageAddedTrigger,
39
+ CustomAgentPropertyUpdatedTrigger,
40
+ CustomAgentRecurrenceSchedule,
41
+ CustomAgentRecurrenceTrigger,
42
+ CustomAgentTrigger,
43
+ CustomAgentTriggerBase,
44
+ DailyRecurrenceSchedule,
45
+ HourlyRecurrenceSchedule,
46
+ MonthlyRecurrenceRestriction,
47
+ MonthlyRecurrenceSchedule,
48
+ MonthlyWeekdayOccurrence,
49
+ PropertyTriggerCondition,
50
+ RecurrenceScheduleEnd,
51
+ RecurrenceWeekdays,
52
+ WeeklyRecurrenceSchedule,
53
+ YearlyRecurrenceSchedule,
54
+ } from "./custom-agent.js";
17
55
  export type {
18
56
  ChildDatabaseArgs,
19
57
  ChildDatabaseFactory,
@@ -28,7 +66,16 @@ export type {
28
66
  SingleSourceDatabaseArgs,
29
67
  SingleSourceDatabaseHandle,
30
68
  } from "./database.js";
31
- export type { ChildPageArgs, PageArgs, PageHandle } from "./page.js";
69
+ export type {
70
+ ChildPageArgs,
71
+ PageArgs,
72
+ PageHandle,
73
+ PropertyValue,
74
+ SimpleTextToken,
75
+ TextValue,
76
+ VerificationInput,
77
+ VerificationPropertyValue,
78
+ } from "./page.js";
32
79
  export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
33
80
  export type { ViewArgs, ViewHandle } from "./view.js";
34
81
  export type {