@notionhq/apps 0.0.10 → 0.0.12

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 (67) hide show
  1. package/dist/cli/build.d.ts +2 -2
  2. package/dist/cli/build.d.ts.map +1 -1
  3. package/dist/cli/build.js +44 -2
  4. package/dist/cli/codegen.d.ts +5 -0
  5. package/dist/cli/codegen.d.ts.map +1 -1
  6. package/dist/cli/codegen.js +15 -1
  7. package/dist/cli/emit-manifest.d.ts +3 -0
  8. package/dist/cli/emit-manifest.d.ts.map +1 -1
  9. package/dist/cli/emit-manifest.js +12 -1
  10. package/dist/notion-as-code/custom-agent.d.ts +18 -0
  11. package/dist/notion-as-code/custom-agent.d.ts.map +1 -0
  12. package/dist/notion-as-code/custom-agent.js +13 -0
  13. package/dist/notion-as-code/database.d.ts +44 -0
  14. package/dist/notion-as-code/database.d.ts.map +1 -0
  15. package/dist/notion-as-code/database.js +68 -0
  16. package/dist/notion-as-code/date.d.ts +9 -0
  17. package/dist/notion-as-code/date.d.ts.map +1 -0
  18. package/dist/notion-as-code/date.js +69 -0
  19. package/dist/notion-as-code/handles.d.ts +32 -0
  20. package/dist/notion-as-code/handles.d.ts.map +1 -0
  21. package/dist/notion-as-code/handles.js +34 -0
  22. package/dist/notion-as-code/index.d.ts +30 -0
  23. package/dist/notion-as-code/index.d.ts.map +1 -0
  24. package/dist/notion-as-code/index.js +8 -0
  25. package/dist/notion-as-code/intents.d.ts +115 -0
  26. package/dist/notion-as-code/intents.d.ts.map +1 -0
  27. package/dist/notion-as-code/intents.js +6 -0
  28. package/dist/notion-as-code/page.d.ts +22 -0
  29. package/dist/notion-as-code/page.d.ts.map +1 -0
  30. package/dist/notion-as-code/page.js +31 -0
  31. package/dist/notion-as-code/provisioning.d.ts +22 -0
  32. package/dist/notion-as-code/provisioning.d.ts.map +1 -0
  33. package/dist/notion-as-code/provisioning.js +25 -0
  34. package/dist/notion-as-code/recorder.d.ts +10 -0
  35. package/dist/notion-as-code/recorder.d.ts.map +1 -0
  36. package/dist/notion-as-code/recorder.js +21 -0
  37. package/dist/notion-as-code/resource.d.ts +6 -0
  38. package/dist/notion-as-code/resource.d.ts.map +1 -0
  39. package/dist/notion-as-code/resource.js +19 -0
  40. package/dist/notion-as-code/schema.d.ts +63 -0
  41. package/dist/notion-as-code/schema.d.ts.map +1 -0
  42. package/dist/notion-as-code/schema.js +94 -0
  43. package/dist/notion-as-code/teamspace.d.ts +18 -0
  44. package/dist/notion-as-code/teamspace.d.ts.map +1 -0
  45. package/dist/notion-as-code/teamspace.js +30 -0
  46. package/dist/sync.d.ts +15 -2
  47. package/dist/sync.d.ts.map +1 -1
  48. package/dist/sync.js +14 -3
  49. package/package.json +9 -1
  50. package/src/cli/build.test.ts +91 -1
  51. package/src/cli/build.ts +61 -6
  52. package/src/cli/codegen.ts +21 -0
  53. package/src/cli/discover.test.ts +6 -0
  54. package/src/cli/emit-manifest.ts +18 -0
  55. package/src/notion-as-code/custom-agent.ts +30 -0
  56. package/src/notion-as-code/database.ts +143 -0
  57. package/src/notion-as-code/date.ts +91 -0
  58. package/src/notion-as-code/handles.ts +36 -0
  59. package/src/notion-as-code/index.ts +16 -0
  60. package/src/notion-as-code/intents.ts +127 -0
  61. package/src/notion-as-code/page.ts +61 -0
  62. package/src/notion-as-code/provisioning.ts +50 -0
  63. package/src/notion-as-code/recorder.ts +26 -0
  64. package/src/notion-as-code/resource.ts +19 -0
  65. package/src/notion-as-code/schema.ts +149 -0
  66. package/src/notion-as-code/teamspace.ts +50 -0
  67. package/src/sync.ts +63 -5
package/src/cli/build.ts CHANGED
@@ -4,9 +4,14 @@ import * as path from "node:path";
4
4
  import { rolldown } from "rolldown";
5
5
 
6
6
  import type { AppManifest } from "../manifest.js";
7
- import { generateEntry } from "./codegen.js";
8
- import { discoverCapabilities } from "./discover.js";
9
- import { extractManifest } from "./emit-manifest.js";
7
+ import {
8
+ createProvisioningArtifact,
9
+ type RecordedProvisioning,
10
+ } from "../notion-as-code/provisioning.js";
11
+ import { clearRegisteredPacers } from "../pacer.js";
12
+ import { generateEntry, generateMetadataEntry } from "./codegen.js";
13
+ import { discoverCapabilities, type DiscoveredCapability } from "./discover.js";
14
+ import { extractManifest, extractProvisioning } from "./emit-manifest.js";
10
15
 
11
16
  export type BuildResult = {
12
17
  manifest: AppManifest;
@@ -16,8 +21,8 @@ export type BuildResult = {
16
21
 
17
22
  /**
18
23
  * Build an app project: discover capability files by convention, generate
19
- * the entry module, bundle it into a single `dist/worker.js`, and emit
20
- * `dist/manifest.json` describing every capability.
24
+ * the Worker and build-only metadata entries, bundle the Worker, and emit
25
+ * the Worker manifest plus any recorded Notion-as-Code provisioning artifact.
21
26
  */
22
27
  export async function buildApp(projectRoot: string): Promise<BuildResult> {
23
28
  const capabilities = await discoverCapabilities(projectRoot);
@@ -49,9 +54,59 @@ export async function buildApp(projectRoot: string): Promise<BuildResult> {
49
54
  }
50
55
 
51
56
  const manifest = await extractManifest(bundlePath, capabilities);
52
-
53
57
  const manifestPath = path.join(projectRoot, "dist", "manifest.json");
54
58
  await fs.promises.writeFile(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`);
55
59
 
60
+ await emitProvisioningArtifact(projectRoot, capabilities);
61
+
56
62
  return { manifest, bundlePath, manifestPath };
57
63
  }
64
+
65
+ /** Build Notion-as-Code metadata separately so it does not ship in the Worker bundle. */
66
+ async function emitProvisioningArtifact(
67
+ projectRoot: string,
68
+ capabilities: readonly DiscoveredCapability[],
69
+ ): Promise<void> {
70
+ const entityProvisioning = await buildMetadata(projectRoot, capabilities);
71
+ if (entityProvisioning.intents.length === 0) return;
72
+
73
+ const artifact = createProvisioningArtifact(entityProvisioning.intents);
74
+ await fs.promises.writeFile(
75
+ path.join(projectRoot, "dist", "provisioning.json"),
76
+ `${JSON.stringify(artifact, null, "\t")}\n`,
77
+ );
78
+ }
79
+
80
+ /** Bundle capabilities separately to record their Notion-as-Code declarations without shipping them. */
81
+ async function buildMetadata(
82
+ projectRoot: string,
83
+ capabilities: readonly DiscoveredCapability[],
84
+ ): Promise<RecordedProvisioning> {
85
+ const metadataEntryPath = path.join(projectRoot, ".notion", "metadata-entry.ts");
86
+ const metadataBundlePath = path.join(projectRoot, ".notion", "metadata.js");
87
+ await fs.promises.writeFile(metadataEntryPath, generateMetadataEntry(capabilities));
88
+ // Metadata evaluation imports capability modules; clear their process-global pacer registrations first.
89
+ clearRegisteredPacers();
90
+
91
+ try {
92
+ const metadataBundle = await rolldown({
93
+ input: metadataEntryPath,
94
+ platform: "node",
95
+ // Keep SDK packages external so the metadata bundle resolves the app's installed SDK.
96
+ external: (id) => !id.startsWith(".") && !path.isAbsolute(id),
97
+ });
98
+ try {
99
+ await metadataBundle.write({
100
+ format: "esm",
101
+ file: metadataBundlePath,
102
+ codeSplitting: false,
103
+ });
104
+ } finally {
105
+ await metadataBundle.close();
106
+ }
107
+ return await extractProvisioning(metadataBundlePath);
108
+ } finally {
109
+ clearRegisteredPacers();
110
+ await fs.promises.rm(metadataBundlePath, { force: true });
111
+ }
112
+ }
@@ -49,3 +49,24 @@ export async function run(
49
49
  }
50
50
  `;
51
51
  }
52
+
53
+ /**
54
+ * Generate the build-only metadata entry. Dynamic imports run after the
55
+ * recorder starts and remain isolated from the deployed Worker entry.
56
+ */
57
+ export function generateMetadataEntry(capabilities: readonly DiscoveredCapability[]): string {
58
+ const imports = capabilities
59
+ .map((capability) => `await import(${JSON.stringify(`../${capability.sourcePath}`)});`)
60
+ .join("\n");
61
+
62
+ return `// Generated by \`notion-apps build\`. Do not edit.
63
+ import {
64
+ finishMetadataRecording,
65
+ startMetadataRecording,
66
+ } from "@notionhq/apps/notion-as-code/recorder";
67
+
68
+ const recorder = startMetadataRecording();
69
+ ${imports}
70
+ export default finishMetadataRecording(recorder);
71
+ `;
72
+ }
@@ -28,6 +28,12 @@ describe("discoverCapabilities", () => {
28
28
  key: "issues",
29
29
  sourcePath: path.join("src", "syncs", "issues.ts"),
30
30
  },
31
+ {
32
+ type: "sync",
33
+ tag: "sync",
34
+ key: "notionAsCodeIssues",
35
+ sourcePath: path.join("src", "syncs", "notionAsCodeIssues.ts"),
36
+ },
31
37
  ]);
32
38
  });
33
39
 
@@ -4,6 +4,10 @@ import { pathToFileURL } from "node:url";
4
4
  import { Ajv } from "ajv";
5
5
 
6
6
  import { APPS_MANIFEST_SCHEMA } from "../manifest.js";
7
+ import {
8
+ isRecordedProvisioning,
9
+ type RecordedProvisioning,
10
+ } from "../notion-as-code/provisioning.js";
7
11
  import type { AppManifest, DatabaseManifestEntry, PacerManifestEntry } from "../manifest.js";
8
12
  import { clearRegisteredPacers, registeredPacers } from "../pacer.js";
9
13
  import type { DiscoveredCapability } from "./discover.js";
@@ -291,6 +295,20 @@ export async function extractManifest(
291
295
  return manifest as AppManifest;
292
296
  }
293
297
 
298
+ /** Import the metadata bundle and return the provisioning declarations it recorded. */
299
+ export async function extractProvisioning(bundlePath: string): Promise<RecordedProvisioning> {
300
+ const bundle = await importBundle(bundlePath);
301
+ if (
302
+ typeof bundle !== "object" ||
303
+ bundle === null ||
304
+ !("default" in bundle) ||
305
+ !isRecordedProvisioning(bundle.default)
306
+ ) {
307
+ throw new Error("Metadata bundle did not export a Notion-as-Code provisioning record");
308
+ }
309
+ return bundle.default;
310
+ }
311
+
294
312
  let importNonce = 0;
295
313
 
296
314
  async function importBundle(bundlePath: string): Promise<unknown> {
@@ -0,0 +1,30 @@
1
+ import type { CustomAgentIntent, NotionAsCodeIcon, ResourceId } from "./intents.js";
2
+ import { recordIntent } from "./recorder.js";
3
+ import { assertUserResourceId } from "./resource.js";
4
+
5
+ export type CustomAgentArgs = {
6
+ resourceId: ResourceId;
7
+ name: string;
8
+ icon?: Exclude<NotionAsCodeIcon, { type: "file" }>;
9
+ instructions?: string;
10
+ model?: "ambrosia-tart-high" | "opal-quince-medium" | "almond-croissant-low";
11
+ sharedResources?: readonly ResourceId[];
12
+ // TODO: Port custom agent trigger types.
13
+ triggers?: readonly unknown[];
14
+ };
15
+
16
+ export type CustomAgentHandle = {
17
+ readonly resourceId: string;
18
+ };
19
+
20
+ /** Declare a custom agent and validate every shared resource reference. */
21
+ export function customAgent(args: CustomAgentArgs): CustomAgentHandle {
22
+ assertUserResourceId(args.resourceId);
23
+ for (const resourceId of args.sharedResources ?? []) {
24
+ assertUserResourceId(resourceId);
25
+ }
26
+ recordIntent({ type: "custom_agent", ...args } satisfies {
27
+ type: "custom_agent";
28
+ } & CustomAgentIntent);
29
+ return { resourceId: args.resourceId };
30
+ }
@@ -0,0 +1,143 @@
1
+ import type { Database } from "../database.js";
2
+ import type {
3
+ DataSourceDefinition,
4
+ DatabaseIntent,
5
+ NotionAsCodeIcon,
6
+ NotionAsCodeProperty,
7
+ Parent,
8
+ ResourceId,
9
+ } from "./intents.js";
10
+ import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
11
+ import { recordIntent } from "./recorder.js";
12
+ import { assertParent, assertUserResourceId } from "./resource.js";
13
+ import { createAppsDatabase, type AppsSchemaForProperties } from "./schema.js";
14
+
15
+ /** Input for declaring a database and its optional data sources. */
16
+ export type DatabaseArgs<
17
+ DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
18
+ > = {
19
+ resourceId: ResourceId;
20
+ parent: Parent;
21
+ dataSources?: DataSources;
22
+ // TODO: Port view types.
23
+ views?: readonly unknown[];
24
+ icon?: NotionAsCodeIcon;
25
+ // TODO: Port cover types.
26
+ cover?: unknown;
27
+ hideDataSourceTitle?: boolean;
28
+ hideDatabaseTitleIfEmpty?: boolean;
29
+ name?: string;
30
+ description?: string;
31
+ };
32
+
33
+ /** Input for adding a database beneath an existing resource. */
34
+ export type ChildDatabaseArgs<
35
+ DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
36
+ > = Omit<DatabaseArgs<DataSources>, "parent">;
37
+
38
+ /** Resolves one data source ID to the properties declared for it. */
39
+ type DataSourcePropertiesForResourceId<
40
+ DataSources extends readonly DataSourceDefinition[],
41
+ ResourceId extends DataSources[number]["resourceId"],
42
+ > =
43
+ Extract<DataSources[number], { resourceId: ResourceId }> extends DataSourceDefinition<
44
+ infer Properties
45
+ >
46
+ ? Properties
47
+ : never;
48
+
49
+ /** Handle for a data source and the Apps database it backs. */
50
+ export type DataSourceHandle<
51
+ Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
52
+ > = {
53
+ readonly resourceId: string;
54
+ readonly schema: Properties;
55
+ readonly database: Database<AppsSchemaForProperties<Properties>>;
56
+ addPage: (args: ChildPageArgs) => PageHandle;
57
+ };
58
+
59
+ /** Handle for a database with data sources indexed by resource ID. */
60
+ export type DatabaseHandle<
61
+ DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
62
+ > = {
63
+ readonly resourceId: string;
64
+ readonly dataSources: {
65
+ readonly [ResourceId in DataSources[number]["resourceId"]]: DataSourceHandle<
66
+ DataSourcePropertiesForResourceId<DataSources, ResourceId>
67
+ >;
68
+ };
69
+ addView: (view: unknown) => void;
70
+ };
71
+
72
+ /** Declare a top-level database and its data sources. */
73
+ export function database<const DataSources extends readonly DataSourceDefinition[]>(
74
+ args: DatabaseArgs<DataSources>,
75
+ ): DatabaseHandle<DataSources>;
76
+ export function database(args: DatabaseArgs): DatabaseHandle {
77
+ assertParent(args.parent);
78
+ return createDatabase(args, args.parent);
79
+ }
80
+
81
+ /** Record a database declaration and create handles for each of its data sources. */
82
+ export function createDatabase(args: DatabaseArgs, parent: Parent): DatabaseHandle {
83
+ assertUserResourceId(args.resourceId);
84
+ const dataSources = args.dataSources ?? [];
85
+ const dataSourceIds = new Set<string>();
86
+ const handles: Record<string, DataSourceHandle> = {};
87
+
88
+ for (const dataSource of dataSources) {
89
+ assertUserResourceId(dataSource.resourceId);
90
+ if (dataSourceIds.has(dataSource.resourceId)) {
91
+ throw new Error(
92
+ `Database "${args.resourceId}" has duplicate data source "${dataSource.resourceId}"`,
93
+ );
94
+ }
95
+ dataSourceIds.add(dataSource.resourceId);
96
+ handles[dataSource.resourceId] = createDataSourceHandle(dataSource);
97
+ }
98
+
99
+ recordIntent({
100
+ type: "database",
101
+ ...args,
102
+ parent,
103
+ dataSources: [...dataSources],
104
+ } satisfies { type: "database" } & DatabaseIntent);
105
+
106
+ return {
107
+ resourceId: args.resourceId,
108
+ dataSources: handles,
109
+ addView(view) {
110
+ recordIntent({ type: "view", databaseResourceId: args.resourceId, view });
111
+ },
112
+ };
113
+ }
114
+
115
+ /** Create a data source handle backed by the Apps database used by syncs. */
116
+ function createDataSourceHandle(dataSource: DataSourceDefinition): DataSourceHandle {
117
+ const propertyResourceIds = new Set<string>();
118
+ const propertyNames = new Set<string>();
119
+ for (const property of dataSource.properties) {
120
+ assertUserResourceId(property.resourceId);
121
+ if (propertyResourceIds.has(property.resourceId)) {
122
+ throw new Error(
123
+ `Data source "${dataSource.resourceId}" has duplicate property resourceId "${property.resourceId}"`,
124
+ );
125
+ }
126
+ if (propertyNames.has(property.name)) {
127
+ throw new Error(
128
+ `Data source "${dataSource.resourceId}" has duplicate property name "${property.name}"`,
129
+ );
130
+ }
131
+ propertyResourceIds.add(property.resourceId);
132
+ propertyNames.add(property.name);
133
+ }
134
+
135
+ return {
136
+ resourceId: dataSource.resourceId,
137
+ schema: dataSource.properties,
138
+ database: createAppsDatabase(dataSource.resourceId, dataSource),
139
+ addPage(pageArgs) {
140
+ return createPage(pageArgs, { type: "resourceId", resourceId: dataSource.resourceId });
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,91 @@
1
+ /** Serialize Notion-as-Code date values for Apps property payloads. */
2
+ export function date(startDate: string, endDate?: string): unknown[][] {
3
+ const start_date = parseIsoDate(startDate);
4
+ const end_date = endDate === undefined ? undefined : parseIsoDate(endDate);
5
+ if (end_date !== undefined && end_date < start_date) {
6
+ throw new Error("Date range end must be on or after start.");
7
+ }
8
+ const value =
9
+ end_date === undefined
10
+ ? { type: "date", start_date }
11
+ : { type: "daterange", start_date, end_date };
12
+ return [["‣", [["d", value]]]];
13
+ }
14
+
15
+ /** Serialize Notion-as-Code datetime values for Apps property payloads. */
16
+ export function datetime(value: { start: string; end?: string; timeZone?: string }): unknown[][] {
17
+ const timeZone = value.timeZone ?? "UTC";
18
+ try {
19
+ new Intl.DateTimeFormat("en-US", { timeZone }).format();
20
+ } catch {
21
+ throw new Error("Invalid time zone: " + timeZone);
22
+ }
23
+ const start = parseIsoDatetime(value.start);
24
+ const end = value.end === undefined ? undefined : parseIsoDatetime(value.end);
25
+ if (end !== undefined && end.date + "T" + end.time < start.date + "T" + start.time) {
26
+ throw new Error("Datetime range end must be on or after start.");
27
+ }
28
+ const dateValue =
29
+ end === undefined
30
+ ? {
31
+ type: "datetime",
32
+ start_date: start.date,
33
+ start_time: start.time,
34
+ time_zone: timeZone,
35
+ }
36
+ : {
37
+ type: "datetimerange",
38
+ start_date: start.date,
39
+ start_time: start.time,
40
+ end_date: end.date,
41
+ end_time: end.time,
42
+ time_zone: timeZone,
43
+ };
44
+ return [["‣", [["d", dateValue]]]];
45
+ }
46
+
47
+ /** Validate a calendar date before serializing it. */
48
+ function parseIsoDate(value: string): string {
49
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
50
+ if (match === null) {
51
+ throw new Error("Invalid ISO 8601 date: " + value + ". Expected format like 2024-01-15");
52
+ }
53
+ if (!isValidCalendarDate(match) || Number.isNaN(new Date(value).getTime())) {
54
+ throw new Error("Invalid date: " + value);
55
+ }
56
+ return value;
57
+ }
58
+
59
+ /** Validate and split an ISO datetime before serializing it. */
60
+ function parseIsoDatetime(value: string): { date: string; time: string } {
61
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value);
62
+ if (match === null) {
63
+ throw new Error(
64
+ "Invalid ISO 8601 datetime: " +
65
+ value +
66
+ ". Expected format like 2024-01-15T10:30 or 2024-01-15T10:30:00Z",
67
+ );
68
+ }
69
+ if (
70
+ !isValidCalendarDate(match) ||
71
+ Number(match[4]) > 23 ||
72
+ Number(match[5]) > 59 ||
73
+ Number.isNaN(new Date(value).getTime())
74
+ ) {
75
+ throw new Error("Invalid datetime: " + value);
76
+ }
77
+ return { date: value.slice(0, 10), time: value.slice(11, 16) };
78
+ }
79
+
80
+ /** Verify that parsed date components form a real calendar date. */
81
+ function isValidCalendarDate(match: RegExpExecArray): boolean {
82
+ const year = Number(match[1] ?? "");
83
+ const month = Number(match[2] ?? "");
84
+ const day = Number(match[3] ?? "");
85
+ const date = new Date(Date.UTC(year, month - 1, day));
86
+ return (
87
+ date.getUTCFullYear() === year &&
88
+ date.getUTCMonth() === month - 1 &&
89
+ date.getUTCDate() === day
90
+ );
91
+ }
@@ -0,0 +1,36 @@
1
+ import { customAgent } from "./custom-agent.js";
2
+ import { database } from "./database.js";
3
+ import { date, datetime } from "./date.js";
4
+ import { page } from "./page.js";
5
+ import { assertUserResourceId } from "./resource.js";
6
+ import { teamspace } from "./teamspace.js";
7
+
8
+ const notionAsCodeFacade = {
9
+ teamspace,
10
+ database,
11
+ page,
12
+ customAgent,
13
+ text: (value: string) => [[value]],
14
+ number: (value: number) => [[String(value)]],
15
+ date,
16
+ datetime,
17
+ select: (value: string) => value,
18
+ status: (value: string) => value,
19
+ multiSelect: (values: readonly string[]) => values.join(","),
20
+ checkbox: (value: boolean) => [[value ? "Yes" : "No"]],
21
+ url: (value: string) => [[value, [["a", value]]]],
22
+ email: (value: string) => [[value, [["a", `mailto:${value}`]]]],
23
+ phone: (value: string) => [[value]],
24
+ relation: (value: string | readonly string[]) => (Array.isArray(value) ? [...value] : value),
25
+ file: (resourceId: string) => {
26
+ assertUserResourceId(resourceId);
27
+ return { type: "file" as const, resourceId };
28
+ },
29
+ };
30
+
31
+ export type NotionAsCodeFacade = typeof notionAsCodeFacade;
32
+
33
+ /** Create the runtime facade used to declare Notion-as-Code project metadata. */
34
+ export function createNotion(): NotionAsCodeFacade {
35
+ return notionAsCodeFacade;
36
+ }
@@ -0,0 +1,16 @@
1
+ import { createNotion } from "./handles.js";
2
+
3
+ export { APPS_RESERVED_RESOURCE_ID_PREFIX, APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
4
+ export type { DataSourceDefinition, NotionAsCodeProperty, Parent } from "./intents.js";
5
+ export type { CustomAgentArgs, CustomAgentHandle } from "./custom-agent.js";
6
+ export type {
7
+ ChildDatabaseArgs,
8
+ DataSourceHandle,
9
+ DatabaseArgs,
10
+ DatabaseHandle,
11
+ } from "./database.js";
12
+ export type { ChildPageArgs, PageArgs, PageHandle } from "./page.js";
13
+ export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
14
+
15
+ /** Experimental Notion-as-Code facade for Apps SDK project metadata. */
16
+ export const notion = createNotion();
@@ -0,0 +1,127 @@
1
+ import type { CustomAgentArgs } from "./custom-agent.js";
2
+ import type { DatabaseArgs } from "./database.js";
3
+ import type { PageArgs } from "./page.js";
4
+ import type { TeamspaceArgs } from "./teamspace.js";
5
+
6
+ /** Apps-specific resource IDs used while declaring Notion-as-Code metadata. */
7
+ /** Synthetic workspace parent for teamspaces declared by an Apps project. */
8
+ export const APPS_WORKSPACE_RESOURCE_ID = "__notion_apps_workspace__";
9
+ /** Reserved prefix for resource IDs owned by the Apps runtime. */
10
+ export const APPS_RESERVED_RESOURCE_ID_PREFIX = "__notion_apps_";
11
+
12
+ export type ResourceId = string;
13
+
14
+ export type Parent = {
15
+ type: "resourceId";
16
+ resourceId: ResourceId;
17
+ };
18
+
19
+ export type NotionAsCodeIcon =
20
+ | { type: "emoji"; emoji: string }
21
+ | { type: "notion_icon"; description: string; color?: string }
22
+ | { type: "file"; resourceId: ResourceId };
23
+
24
+ export type NotionAsCodeSelectColor =
25
+ | "default"
26
+ | "gray"
27
+ | "brown"
28
+ | "orange"
29
+ | "yellow"
30
+ | "green"
31
+ | "blue"
32
+ | "purple"
33
+ | "pink"
34
+ | "red";
35
+
36
+ export type NotionAsCodeSelectOption = {
37
+ name: string;
38
+ color?: NotionAsCodeSelectColor;
39
+ };
40
+
41
+ export type NotionAsCodeStatusOption = NotionAsCodeSelectOption & {
42
+ default?: boolean;
43
+ };
44
+
45
+ type PropertyBase = {
46
+ resourceId: ResourceId;
47
+ name: string;
48
+ icon?: Extract<NotionAsCodeIcon, { type: "notion_icon" }>;
49
+ description?: string;
50
+ };
51
+
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;
66
+ };
67
+
68
+ export type NotionAsCodeProperty =
69
+ | (PropertyBase & { type: "title" })
70
+ | (PropertyBase & { type: "text" })
71
+ | (PropertyBase & { type: "number" })
72
+ | (PropertyBase & { type: "select"; options?: readonly NotionAsCodeSelectOption[] })
73
+ | (PropertyBase & { type: "multi_select"; options?: readonly NotionAsCodeSelectOption[] })
74
+ | {
75
+ resourceId: ResourceId;
76
+ name: string;
77
+ type: "status";
78
+ icon?: Extract<NotionAsCodeIcon, { type: "notion_icon" }>;
79
+ description?: string;
80
+ options: {
81
+ todo: readonly NotionAsCodeStatusOption[];
82
+ inProgress: readonly NotionAsCodeStatusOption[];
83
+ complete: readonly NotionAsCodeStatusOption[];
84
+ };
85
+ }
86
+ | (PropertyBase & { type: "date" })
87
+ | (PropertyBase & { type: "checkbox" })
88
+ | (PropertyBase & { type: "url" })
89
+ | (PropertyBase & { type: "email" })
90
+ | (PropertyBase & { type: "phone_number" })
91
+ | (PropertyBase & { type: "file" })
92
+ | UnsupportedProperty;
93
+
94
+ export type DataSourceDefinition<
95
+ Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
96
+ > = {
97
+ resourceId: ResourceId;
98
+ name: string;
99
+ properties: Properties;
100
+ icon?: NotionAsCodeIcon;
101
+ // TODO: Port page layout types.
102
+ pageLayout?: unknown;
103
+ };
104
+
105
+ export type TeamspaceIntent = TeamspaceArgs & {
106
+ parent: Parent;
107
+ };
108
+
109
+ export type DatabaseIntent = DatabaseArgs & {
110
+ dataSources: readonly DataSourceDefinition[];
111
+ };
112
+
113
+ export type PageIntent = PageArgs;
114
+ export type CustomAgentIntent = CustomAgentArgs;
115
+
116
+ export type ViewIntent = {
117
+ databaseResourceId: ResourceId;
118
+ // TODO: Port view types.
119
+ view: unknown;
120
+ };
121
+
122
+ export type InfraAsCodeIntent =
123
+ | ({ type: "teamspace" } & TeamspaceIntent)
124
+ | ({ type: "database" } & DatabaseIntent)
125
+ | ({ type: "page" } & PageIntent)
126
+ | ({ type: "custom_agent" } & CustomAgentIntent)
127
+ | ({ type: "view" } & ViewIntent);
@@ -0,0 +1,61 @@
1
+ import type {
2
+ DataSourceDefinition,
3
+ NotionAsCodeIcon,
4
+ PageIntent,
5
+ Parent,
6
+ ResourceId,
7
+ } from "./intents.js";
8
+ import { database, type ChildDatabaseArgs, type DatabaseHandle } from "./database.js";
9
+ import { recordIntent } from "./recorder.js";
10
+ import { assertParent, assertUserResourceId } from "./resource.js";
11
+
12
+ export type PageArgs = {
13
+ resourceId: ResourceId;
14
+ parent: Parent;
15
+ icon?: NotionAsCodeIcon;
16
+ // TODO: Port cover types.
17
+ cover?: unknown;
18
+ // TODO: Port page property types.
19
+ properties?: Record<string, unknown>;
20
+ content?: string;
21
+ fullWidth?: boolean;
22
+ };
23
+
24
+ export type ChildPageArgs = Omit<PageArgs, "parent">;
25
+
26
+ export type PageHandle = {
27
+ readonly resourceId: string;
28
+ addDatabase: <const DataSources extends readonly DataSourceDefinition[]>(
29
+ args: ChildDatabaseArgs<DataSources>,
30
+ ) => DatabaseHandle<DataSources>;
31
+ addPage: (args: ChildPageArgs) => PageHandle;
32
+ };
33
+
34
+ /** Declare a top-level page. */
35
+ export function page(args: PageArgs): PageHandle {
36
+ assertParent(args.parent);
37
+ return createPage(args, args.parent);
38
+ }
39
+
40
+ /** Record a page declaration beneath its parent resource. */
41
+ export function createPage(args: ChildPageArgs | PageArgs, parent: Parent): PageHandle {
42
+ assertUserResourceId(args.resourceId);
43
+ recordIntent({
44
+ type: "page",
45
+ ...args,
46
+ parent,
47
+ } satisfies { type: "page" } & PageIntent);
48
+
49
+ return {
50
+ resourceId: args.resourceId,
51
+ addDatabase(databaseArgs) {
52
+ return database({
53
+ ...databaseArgs,
54
+ parent: { type: "resourceId", resourceId: args.resourceId },
55
+ });
56
+ },
57
+ addPage(pageArgs) {
58
+ return createPage(pageArgs, { type: "resourceId", resourceId: args.resourceId });
59
+ },
60
+ };
61
+ }