@notionhq/apps 0.0.16 → 0.0.18

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 (77) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +86 -36
  3. package/dist/connections.d.ts +39 -11
  4. package/dist/connections.d.ts.map +1 -1
  5. package/dist/connections.js +85 -21
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/notion-as-code/database.d.ts +89 -48
  10. package/dist/notion-as-code/database.d.ts.map +1 -1
  11. package/dist/notion-as-code/database.js +132 -45
  12. package/dist/notion-as-code/database.test.d.ts +2 -0
  13. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  14. package/dist/notion-as-code/handles.d.ts +2 -0
  15. package/dist/notion-as-code/handles.d.ts.map +1 -1
  16. package/dist/notion-as-code/handles.js +2 -0
  17. package/dist/notion-as-code/index.d.ts +5 -2
  18. package/dist/notion-as-code/index.d.ts.map +1 -1
  19. package/dist/notion-as-code/intents.d.ts +36 -3
  20. package/dist/notion-as-code/intents.d.ts.map +1 -1
  21. package/dist/notion-as-code/schema.d.ts +8 -4
  22. package/dist/notion-as-code/schema.d.ts.map +1 -1
  23. package/dist/notion-as-code/view.d.ts +12 -0
  24. package/dist/notion-as-code/view.d.ts.map +1 -0
  25. package/dist/notion-as-code/view.js +13 -0
  26. package/dist/notion-as-code/views-types.test.d.ts +2 -0
  27. package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
  28. package/dist/notion-as-code/views.d.ts +488 -0
  29. package/dist/notion-as-code/views.d.ts.map +1 -0
  30. package/dist/notion-as-code/views.js +0 -0
  31. package/dist/oauth.d.ts +11 -0
  32. package/dist/oauth.d.ts.map +1 -0
  33. package/dist/oauth.js +26 -0
  34. package/dist/providers.generated.d.ts +26 -86
  35. package/dist/providers.generated.d.ts.map +1 -1
  36. package/dist/providers.generated.js +54 -66
  37. package/dist/sync.d.ts +33 -15
  38. package/dist/sync.d.ts.map +1 -1
  39. package/dist/sync.js +12 -0
  40. package/dist/triggers.generated.d.ts +3 -3
  41. package/dist/triggers.generated.d.ts.map +1 -1
  42. package/dist/workflow-state.d.ts +13 -0
  43. package/dist/workflow-state.d.ts.map +1 -0
  44. package/dist/workflow-state.js +107 -0
  45. package/dist/workflow.d.ts +56 -11
  46. package/dist/workflow.d.ts.map +1 -1
  47. package/dist/workflow.js +123 -10
  48. package/docs/BUILD.md +96 -0
  49. package/docs/CONNECTIONS.md +70 -22
  50. package/package.json +1 -1
  51. package/skills/connections/SKILL.md +7 -8
  52. package/skills/notion-as-code/SKILL.md +88 -50
  53. package/skills/sync/SKILL.md +21 -18
  54. package/skills/workflow/SKILL.md +52 -3
  55. package/src/cli/build.test.ts +124 -64
  56. package/src/connections.test.ts +205 -41
  57. package/src/connections.ts +144 -38
  58. package/src/index.ts +2 -0
  59. package/src/notion-as-code/database.test.ts +661 -0
  60. package/src/notion-as-code/database.ts +346 -129
  61. package/src/notion-as-code/handles.ts +2 -0
  62. package/src/notion-as-code/index.ts +71 -1
  63. package/src/notion-as-code/intents.ts +41 -4
  64. package/src/notion-as-code/schema.ts +11 -3
  65. package/src/notion-as-code/view.ts +23 -0
  66. package/src/notion-as-code/views-types.test.ts +59 -0
  67. package/src/notion-as-code/views.ts +573 -0
  68. package/src/oauth.ts +40 -0
  69. package/src/providers.generated.ts +68 -163
  70. package/src/sync.test.ts +295 -0
  71. package/src/sync.ts +85 -21
  72. package/src/triggers.generated.ts +4 -4
  73. package/src/workflow-connections-types.test.ts +16 -10
  74. package/src/workflow-state.ts +152 -0
  75. package/src/workflow-types.test.ts +88 -16
  76. package/src/workflow.test.ts +374 -18
  77. package/src/workflow.ts +211 -23
@@ -1,197 +1,414 @@
1
1
  import type { Database } from "../database.js";
2
2
  import {
3
3
  APPS_WORKSPACE_RESOURCE_ID,
4
- type DataSourceDefinition,
5
4
  type DatabaseIntent,
5
+ type DataSourceDefinition,
6
6
  type NotionAsCodeIcon,
7
7
  type NotionAsCodeProperty,
8
+ type NotionAsCodeSchema,
8
9
  type Parent,
9
10
  type ResourceId,
10
11
  } from "./intents.js";
11
12
  import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
12
13
  import { recordIntent } from "./recorder.js";
13
14
  import { assertParent, assertUserResourceId } from "./resource.js";
14
- import { createAppsDatabase, type AppsSchemaForProperties } from "./schema.js";
15
+ import { createAppsDatabase, type AppsSchemaForSchema } from "./schema.js";
16
+ import type { ViewSchema } from "./views.js";
15
17
 
16
- /** Input for declaring a database and its optional data sources. */
17
- export type DatabaseArgs<
18
- DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
19
- > = {
18
+ /** Reuse serialized database intent metadata, excluding identity, parent, and sources. */
19
+ type DatabaseMetadata = Omit<DatabaseIntent, "resourceId" | "parent" | "dataSources">;
20
+
21
+ type NonEmptyViewSchemas = readonly [ViewSchema, ...ViewSchema[]];
22
+
23
+ /** A keyed data-source declaration; the containing map key supplies the serialized source name. */
24
+ export type DataSourceArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
20
25
  resourceId: ResourceId;
21
- /** Omit to create a private top-level database in the Apps workspace. */
22
- parent?: Parent;
23
- dataSources?: DataSources;
24
- // TODO: Port view types.
25
- views?: readonly unknown[];
26
+ name?: never;
27
+ schema: Schema;
26
28
  icon?: NotionAsCodeIcon;
27
- // TODO: Port cover types.
28
- cover?: unknown;
29
- hideDataSourceTitle?: boolean;
30
- hideDatabaseTitleIfEmpty?: boolean;
31
- name?: string;
32
- description?: string;
33
29
  };
34
30
 
35
- /** Input for adding a database beneath an existing resource. */
36
- export type ChildDatabaseArgs<
37
- DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
38
- > = Omit<DatabaseArgs<DataSources>, "parent">;
31
+ /** Data sources keyed by the names used to access their handles. */
32
+ export type DataSourceMap = Record<string, DataSourceArgs>;
39
33
 
40
- /** Single-source shorthand; the source ID is `${resourceId}-source`. */
41
- export type SingleSourceDatabaseArgs<
42
- Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
43
- > = Omit<DatabaseArgs, "dataSources" | "name"> & {
34
+ /**
35
+ * Single-source inputs require a name for both database and source. `datasources?: never`
36
+ * keeps this form exclusive even when callers pass a predeclared object.
37
+ */
38
+ type SingleSourceDatabaseOptions<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = Omit<
39
+ DatabaseMetadata,
40
+ "name"
41
+ > & {
42
+ dataSourceResourceId: ResourceId;
44
43
  name: string;
45
- properties: Properties;
46
- dataSources?: never;
44
+ schema: Schema;
45
+ datasources?: never;
47
46
  };
48
47
 
49
- export type ChildSingleSourceDatabaseArgs<
50
- Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
51
- > = Omit<SingleSourceDatabaseArgs<Properties>, "parent">;
48
+ /** Top-level single-source inputs may choose a parent; page.addDatabase/teamspace.addDatabase bind it instead. */
49
+ export type SingleSourceDatabaseArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
50
+ SingleSourceDatabaseOptions<Schema> & {
51
+ parent?: Parent;
52
+ };
53
+
54
+ /**
55
+ * Multi-source inputs preserve source-map keys. The NoInfer guard makes the
56
+ * emptiness check inspect the inferred map instead of widening it to the broad
57
+ * DataSourceMap constraint.
58
+ */
59
+ type MultiSourceDatabaseOptions<DataSources extends DataSourceMap = DataSourceMap> =
60
+ DatabaseMetadata & {
61
+ datasources: DataSources;
62
+ dataSourceResourceId?: never;
63
+ schema?: never;
64
+ } & (keyof NoInfer<DataSources> extends never
65
+ ? { views: NonEmptyViewSchemas }
66
+ : { views?: readonly ViewSchema[] });
67
+
68
+ /** Top-level multi-source inputs may choose a parent; page.addDatabase/teamspace.addDatabase bind it instead. */
69
+ export type MultiSourceDatabaseArgs<DataSources extends DataSourceMap = DataSourceMap> =
70
+ MultiSourceDatabaseOptions<DataSources> & {
71
+ parent?: Parent;
72
+ };
52
73
 
53
- /** Resolves one data source ID to the properties declared for it. */
54
- type DataSourcePropertiesForResourceId<
55
- DataSources extends readonly DataSourceDefinition[],
56
- ResourceId extends DataSources[number]["resourceId"],
74
+ /** Linked-only databases use a nonempty typed view tuple and no authored source map. */
75
+ type ViewOnlyDatabaseOptions = DatabaseMetadata & {
76
+ datasources?: never;
77
+ dataSourceResourceId?: never;
78
+ schema?: never;
79
+ views: NonEmptyViewSchemas;
80
+ };
81
+
82
+ type ViewOnlyDatabaseArgs = ViewOnlyDatabaseOptions & {
83
+ parent?: Parent;
84
+ };
85
+
86
+ /** Remove parent because page.addDatabase/teamspace.addDatabase supplies it. */
87
+ export type ChildSingleSourceDatabaseArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
88
+ Omit<SingleSourceDatabaseArgs<Schema>, "parent">;
89
+
90
+ /** Remove parent because page.addDatabase/teamspace.addDatabase supplies it. */
91
+ export type ChildMultiSourceDatabaseArgs<DataSources extends DataSourceMap = DataSourceMap> = Omit<
92
+ MultiSourceDatabaseArgs<DataSources>,
93
+ "parent"
94
+ >;
95
+
96
+ /** Input for declaring a database and its data sources. */
97
+ export type DatabaseArgs<
98
+ Schema extends NotionAsCodeSchema = NotionAsCodeSchema,
99
+ DataSources extends DataSourceMap = DataSourceMap,
100
+ > = SingleSourceDatabaseArgs<Schema> | MultiSourceDatabaseArgs<DataSources> | ViewOnlyDatabaseArgs;
101
+
102
+ /**
103
+ * Child inputs omit parent from each union member separately. `page.addDatabase`
104
+ * and `teamspace.addDatabase` use this shape for their shared child factory.
105
+ */
106
+ export type ChildDatabaseArgs<
107
+ Schema extends NotionAsCodeSchema = NotionAsCodeSchema,
108
+ DataSources extends DataSourceMap = DataSourceMap,
57
109
  > =
58
- Extract<DataSources[number], { resourceId: ResourceId }> extends DataSourceDefinition<
59
- infer Properties
60
- >
61
- ? Properties
62
- : never;
63
-
64
- /** Handle for a data source and the Apps database it backs. */
65
- export type DataSourceHandle<
66
- Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
67
- > = {
110
+ | ChildSingleSourceDatabaseArgs<Schema>
111
+ | ChildMultiSourceDatabaseArgs<DataSources>
112
+ | Omit<ViewOnlyDatabaseArgs, "parent">;
113
+
114
+ /** A source handle retains its keyed authoring schema for sync inference. */
115
+ export type DataSourceHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
68
116
  readonly resourceId: string;
69
- readonly schema: Properties;
70
- readonly database: Database<AppsSchemaForProperties<Properties>>;
117
+ readonly schema: Schema;
118
+ readonly database: Database<AppsSchemaForSchema<Schema>>;
71
119
  addPage: (args: ChildPageArgs) => PageHandle;
72
120
  };
73
121
 
74
- /** Handle for a database with data sources indexed by resource ID. */
75
- export type DatabaseHandle<
76
- DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
77
- > = {
122
+ type DatabaseHandleBase = {
78
123
  readonly resourceId: string;
79
- readonly dataSources: {
80
- readonly [ResourceId in DataSources[number]["resourceId"]]: DataSourceHandle<
81
- DataSourcePropertiesForResourceId<DataSources, ResourceId>
82
- >;
83
- };
84
- addView: (view: unknown) => void;
124
+ addView: (view: ViewSchema) => void;
85
125
  };
86
126
 
87
- /** A database whose sole data source is available without an ID lookup. */
88
- export type SingleSourceDatabaseHandle<
89
- Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
90
- > = DatabaseHandle<readonly DataSourceDefinition<Properties>[]> & {
91
- readonly dataSource: DataSourceHandle<Properties>;
92
- };
127
+ /** Map each authoring source key to a handle backed by that source's own schema. */
128
+ export type DatabaseHandle<DataSources extends DataSourceMap = DataSourceMap> =
129
+ DatabaseHandleBase & {
130
+ readonly datasources: {
131
+ readonly [DataSourceName in keyof DataSources]: DataSourceHandle<
132
+ DataSources[DataSourceName]["schema"]
133
+ >;
134
+ };
135
+ };
93
136
 
137
+ /** A database whose sole data source is available without a key lookup. */
138
+ export type SingleSourceDatabaseHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
139
+ DatabaseHandleBase & {
140
+ readonly dataSource: DataSourceHandle<Schema>;
141
+ };
142
+
143
+ /**
144
+ * Overloaded callable used by page.addDatabase/teamspace.addDatabase. Each form
145
+ * returns its matching handle while preserving literal source and schema inference.
146
+ */
94
147
  export type ChildDatabaseFactory = {
95
- <const Properties extends readonly NotionAsCodeProperty[]>(
96
- args: ChildSingleSourceDatabaseArgs<Properties>,
97
- ): SingleSourceDatabaseHandle<Properties>;
98
- <const DataSources extends readonly DataSourceDefinition[]>(
99
- args: ChildDatabaseArgs<DataSources> & { properties?: never },
148
+ <const Schema extends NotionAsCodeSchema>(
149
+ databaseResourceId: ResourceId,
150
+ args: ChildSingleSourceDatabaseArgs<Schema>,
151
+ ): SingleSourceDatabaseHandle<Schema>;
152
+ <const DataSources extends DataSourceMap>(
153
+ databaseResourceId: ResourceId,
154
+ args: ChildMultiSourceDatabaseArgs<DataSources>,
100
155
  ): DatabaseHandle<DataSources>;
156
+ (
157
+ databaseResourceId: ResourceId,
158
+ args: Omit<ViewOnlyDatabaseArgs, "parent">,
159
+ ): DatabaseHandle<{}>;
101
160
  };
102
161
 
103
- /** Declare a database, defaulting to a private top-level database in the Apps workspace. */
104
- export function database<const Properties extends readonly NotionAsCodeProperty[]>(
105
- args: SingleSourceDatabaseArgs<Properties>,
106
- ): SingleSourceDatabaseHandle<Properties>;
107
- export function database<const DataSources extends readonly DataSourceDefinition[]>(
108
- args: DatabaseArgs<DataSources> & { properties?: never },
162
+ /** Declare a database, defaulting to the Apps workspace. */
163
+ export function database<const Schema extends NotionAsCodeSchema>(
164
+ databaseResourceId: ResourceId,
165
+ args: SingleSourceDatabaseArgs<Schema>,
166
+ ): SingleSourceDatabaseHandle<Schema>;
167
+ export function database<const DataSources extends DataSourceMap>(
168
+ databaseResourceId: ResourceId,
169
+ args: MultiSourceDatabaseArgs<DataSources>,
109
170
  ): DatabaseHandle<DataSources>;
110
171
  export function database(
111
- args: DatabaseArgs | SingleSourceDatabaseArgs,
172
+ databaseResourceId: ResourceId,
173
+ args: ViewOnlyDatabaseArgs,
174
+ ): DatabaseHandle<{}>;
175
+ export function database(
176
+ databaseResourceId: ResourceId,
177
+ args: DatabaseArgs,
112
178
  ): DatabaseHandle | SingleSourceDatabaseHandle {
113
- if (args.parent) {
114
- assertParent(args.parent);
179
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
180
+ throw new Error("Notion-as-Code database arguments must be an object");
115
181
  }
116
- const parent = args.parent ?? {
117
- type: "resourceId",
182
+ const parentValue = args.parent;
183
+ if (parentValue !== undefined) {
184
+ assertParent(parentValue);
185
+ }
186
+ const parent = parentValue ?? {
187
+ type: "resourceId" as const,
118
188
  resourceId: APPS_WORKSPACE_RESOURCE_ID,
119
189
  };
120
- if ("properties" in args) {
121
- if (args.dataSources !== undefined) {
122
- throw new Error("Specify either properties or dataSources, not both");
123
- }
124
- const { properties, ...databaseArgs } = args;
190
+ return buildDatabaseHandle(databaseResourceId, args, parent);
191
+ }
192
+
193
+ /** Bind a parent for page.addDatabase/teamspace.addDatabase. */
194
+ export function createChildDatabase(parent: Parent): ChildDatabaseFactory {
195
+ assertParent(parent);
196
+
197
+ function addDatabase<const Schema extends NotionAsCodeSchema>(
198
+ databaseResourceId: ResourceId,
199
+ args: ChildSingleSourceDatabaseArgs<Schema>,
200
+ ): SingleSourceDatabaseHandle<Schema>;
201
+ function addDatabase<const DataSources extends DataSourceMap>(
202
+ databaseResourceId: ResourceId,
203
+ args: ChildMultiSourceDatabaseArgs<DataSources>,
204
+ ): DatabaseHandle<DataSources>;
205
+ function addDatabase(
206
+ databaseResourceId: ResourceId,
207
+ args: Omit<ViewOnlyDatabaseArgs, "parent">,
208
+ ): DatabaseHandle<{}>;
209
+ function addDatabase(
210
+ databaseResourceId: ResourceId,
211
+ args: ChildDatabaseArgs,
212
+ ): DatabaseHandle | SingleSourceDatabaseHandle {
213
+ return buildDatabaseHandle(databaseResourceId, args, parent);
214
+ }
215
+
216
+ return addDatabase;
217
+ }
218
+ /**
219
+ * Internal bridge pairing an authoring key/schema with its serialized definition
220
+ * during database recording and handle creation; never serialized itself.
221
+ */
222
+ type SourceEntry = {
223
+ key: string;
224
+ schema: NotionAsCodeSchema;
225
+ definition: DataSourceDefinition;
226
+ };
227
+
228
+ /**
229
+ * Build a database handle from one of the authoring forms. Form checks happen
230
+ * before normalization or recording so mixed and malformed declarations are atomic.
231
+ */
232
+ function buildDatabaseHandle(
233
+ databaseResourceId: ResourceId,
234
+ args: DatabaseArgs,
235
+ parent: Parent,
236
+ ): DatabaseHandle | SingleSourceDatabaseHandle {
237
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
238
+ throw new Error("Notion-as-Code database arguments must be an object");
239
+ }
240
+ if (typeof databaseResourceId !== "string") {
241
+ throw new Error("Notion-as-Code database resourceId must be a string");
242
+ }
243
+ assertUserResourceId(databaseResourceId);
244
+
245
+ const hasSchema = Object.prototype.hasOwnProperty.call(args, "schema");
246
+ const hasDataSources = Object.prototype.hasOwnProperty.call(args, "datasources");
247
+ if (hasSchema && hasDataSources) {
248
+ throw new Error("Specify either schema or datasources, not both");
249
+ }
250
+
251
+ if (args.schema !== undefined) {
125
252
  const source: DataSourceDefinition = {
126
- resourceId: `${args.resourceId}-source`,
253
+ resourceId: args.dataSourceResourceId,
127
254
  name: args.name,
128
- properties,
255
+ properties: normalizeProperties(args.schema),
129
256
  };
130
- const handle = createDatabase({ ...databaseArgs, dataSources: [source] }, parent);
131
- const dataSource = handle.dataSources[source.resourceId];
132
- if (!dataSource) {
133
- throw new Error(`Missing data source "${source.resourceId}"`);
257
+ const handle = recordDatabase(databaseResourceId, args, parent, [
258
+ { key: "dataSource", schema: args.schema, definition: source },
259
+ ]);
260
+ const dataSource = handle.datasources.dataSource;
261
+ if (dataSource === undefined) {
262
+ throw new Error(`Database "${databaseResourceId}" is missing its data source`);
134
263
  }
135
- return { ...handle, dataSource };
264
+ return {
265
+ resourceId: databaseResourceId,
266
+ dataSource,
267
+ addView: handle.addView,
268
+ };
136
269
  }
137
- return createDatabase(args, parent);
270
+
271
+ if (args.datasources !== undefined) {
272
+ if (
273
+ typeof args.datasources !== "object" ||
274
+ args.datasources === null ||
275
+ Array.isArray(args.datasources)
276
+ ) {
277
+ throw new Error("Notion-as-Code datasources must be an object");
278
+ }
279
+ const sourceEntries = Object.entries(args.datasources).map(([key, sourceArgs]) =>
280
+ normalizeDataSource(key, sourceArgs),
281
+ );
282
+ return recordDatabase(databaseResourceId, args, parent, sourceEntries);
283
+ }
284
+
285
+ return recordDatabase(databaseResourceId, args, parent, []);
138
286
  }
139
287
 
140
- /** Bind both database declaration forms to an existing parent. */
141
- export function createChildDatabase(parent: Parent): ChildDatabaseFactory {
142
- function addDatabase<const Properties extends readonly NotionAsCodeProperty[]>(
143
- args: ChildSingleSourceDatabaseArgs<Properties>,
144
- ): SingleSourceDatabaseHandle<Properties>;
145
- function addDatabase<const DataSources extends readonly DataSourceDefinition[]>(
146
- args: ChildDatabaseArgs<DataSources> & { properties?: never },
147
- ): DatabaseHandle<DataSources>;
148
- function addDatabase(
149
- args: ChildDatabaseArgs | ChildSingleSourceDatabaseArgs,
150
- ): DatabaseHandle | SingleSourceDatabaseHandle {
151
- if ("properties" in args) {
152
- return database({ ...args, parent });
288
+ /** Strip authoring-only fields from database arguments before serializing metadata. */
289
+ function normalizeDatabaseMetadata(args: DatabaseArgs): DatabaseMetadata {
290
+ const {
291
+ parent: _parent,
292
+ schema: _schema,
293
+ datasources: _datasources,
294
+ dataSourceResourceId: _dataSourceResourceId,
295
+ ...metadata
296
+ } = args;
297
+ return metadata;
298
+ }
299
+
300
+ /** Normalize a keyed declaration into its serialized definition while retaining key/schema for handles. */
301
+ function normalizeDataSource(key: string, sourceArgs: DataSourceArgs): SourceEntry {
302
+ if (typeof sourceArgs !== "object" || sourceArgs === null || Array.isArray(sourceArgs)) {
303
+ throw new Error(`Data source "${key}" must be an object`);
304
+ }
305
+ const { schema, ...sourceMetadata } = sourceArgs;
306
+ return {
307
+ key,
308
+ schema,
309
+ definition: {
310
+ ...sourceMetadata,
311
+ name: key,
312
+ properties: normalizeProperties(schema),
313
+ },
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Normalize keyed property configurations to the serialized property array.
319
+ * Property names always come from schema keys; authoring name fields are ignored.
320
+ */
321
+ function normalizeProperties(schema: NotionAsCodeSchema): NotionAsCodeProperty[] {
322
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
323
+ throw new Error("Notion-as-Code schema must be an object");
324
+ }
325
+ const properties: NotionAsCodeProperty[] = [];
326
+ for (const [key, propertyConfig] of Object.entries(schema)) {
327
+ if (
328
+ typeof propertyConfig !== "object" ||
329
+ propertyConfig === null ||
330
+ Array.isArray(propertyConfig)
331
+ ) {
332
+ throw new Error(`Property "${key}" must be an object`);
153
333
  }
154
- return database({ ...args, parent });
334
+ if (typeof propertyConfig.resourceId !== "string") {
335
+ throw new Error(`Property "${key}" must specify a resourceId`);
336
+ }
337
+ properties.push({
338
+ ...propertyConfig,
339
+ name: key,
340
+ });
155
341
  }
156
- return addDatabase;
342
+ return properties;
157
343
  }
158
344
 
159
- /** Record a database declaration and create handles for each of its data sources. */
160
- export function createDatabase(args: DatabaseArgs, parent: Parent): DatabaseHandle {
161
- assertUserResourceId(args.resourceId);
162
- const dataSources = args.dataSources ?? [];
345
+ /** Validate the views shape and require at least one data source or view. */
346
+ function assertSourceOrView(sourceEntries: readonly SourceEntry[], views: unknown): void {
347
+ if (views !== undefined && !Array.isArray(views)) {
348
+ throw new Error("Notion-as-Code database views must be an array");
349
+ }
350
+ if (sourceEntries.length === 0 && (!Array.isArray(views) || views.length === 0)) {
351
+ throw new Error("Notion-as-Code database must define a data source or view");
352
+ }
353
+ }
354
+
355
+ /** Record a normalized database intent and create handles for its data sources. */
356
+ function recordDatabase(
357
+ databaseResourceId: ResourceId,
358
+ args: DatabaseArgs,
359
+ parent: Parent,
360
+ sourceEntries: readonly SourceEntry[],
361
+ ): DatabaseHandle {
362
+ const metadata = normalizeDatabaseMetadata(args);
363
+ assertSourceOrView(sourceEntries, metadata.views);
364
+ const intent: DatabaseIntent = {
365
+ ...metadata,
366
+ resourceId: databaseResourceId,
367
+ parent,
368
+ dataSources: sourceEntries.map(({ definition }) => definition),
369
+ };
370
+ return createDatabase(intent, sourceEntries);
371
+ }
372
+
373
+ /** Create a normalized database handle while preserving each source's authoring schema. */
374
+ function createDatabase(
375
+ intent: DatabaseIntent,
376
+ sourceEntries: readonly SourceEntry[],
377
+ ): DatabaseHandle {
378
+ assertUserResourceId(intent.resourceId);
163
379
  const dataSourceIds = new Set<string>();
164
- const handles: Record<string, DataSourceHandle> = {};
380
+ const handles: Record<string, DataSourceHandle> = Object.create(null);
165
381
 
166
- for (const dataSource of dataSources) {
167
- assertUserResourceId(dataSource.resourceId);
168
- if (dataSourceIds.has(dataSource.resourceId)) {
382
+ for (const { key, schema, definition } of sourceEntries) {
383
+ if (typeof definition.resourceId !== "string") {
384
+ throw new Error(`Data source "${key}" must specify a resourceId`);
385
+ }
386
+ assertUserResourceId(definition.resourceId);
387
+ if (dataSourceIds.has(definition.resourceId)) {
169
388
  throw new Error(
170
- `Database "${args.resourceId}" has duplicate data source "${dataSource.resourceId}"`,
389
+ `Database "${intent.resourceId}" has duplicate data source "${definition.resourceId}"`,
171
390
  );
172
391
  }
173
- dataSourceIds.add(dataSource.resourceId);
174
- handles[dataSource.resourceId] = createDataSourceHandle(dataSource);
392
+ dataSourceIds.add(definition.resourceId);
393
+ handles[key] = createDataSourceHandle(definition, schema);
175
394
  }
176
395
 
177
- recordIntent({
178
- type: "database",
179
- ...args,
180
- parent,
181
- dataSources: [...dataSources],
182
- } satisfies { type: "database" } & DatabaseIntent);
396
+ recordIntent({ ...intent, type: "database" });
183
397
 
184
398
  return {
185
- resourceId: args.resourceId,
186
- dataSources: handles,
399
+ resourceId: intent.resourceId,
400
+ datasources: handles,
187
401
  addView(view) {
188
- recordIntent({ type: "view", databaseResourceId: args.resourceId, view });
402
+ recordIntent({ type: "view", databaseResourceId: intent.resourceId, view });
189
403
  },
190
404
  };
191
405
  }
192
406
 
193
- /** Create a data source handle backed by the Apps database used by syncs. */
194
- function createDataSourceHandle(dataSource: DataSourceDefinition): DataSourceHandle {
407
+ /** Create a data-source handle backed by the Apps database used by syncs. */
408
+ function createDataSourceHandle(
409
+ dataSource: DataSourceDefinition,
410
+ schema: NotionAsCodeSchema,
411
+ ): DataSourceHandle {
195
412
  const propertyResourceIds = new Set<string>();
196
413
  const propertyNames = new Set<string>();
197
414
  for (const property of dataSource.properties) {
@@ -212,7 +429,7 @@ function createDataSourceHandle(dataSource: DataSourceDefinition): DataSourceHan
212
429
 
213
430
  return {
214
431
  resourceId: dataSource.resourceId,
215
- schema: dataSource.properties,
432
+ schema,
216
433
  database: createAppsDatabase(dataSource.resourceId, dataSource),
217
434
  addPage(pageArgs) {
218
435
  return createPage(pageArgs, { type: "resourceId", resourceId: dataSource.resourceId });
@@ -4,12 +4,14 @@ import { date, datetime } from "./date.js";
4
4
  import { page } from "./page.js";
5
5
  import { assertUserResourceId } from "./resource.js";
6
6
  import { teamspace } from "./teamspace.js";
7
+ import { view } from "./view.js";
7
8
 
8
9
  const notionAsCodeFacade = {
9
10
  teamspace,
10
11
  database,
11
12
  page,
12
13
  customAgent,
14
+ view,
13
15
  text: (value: string) => [[value]],
14
16
  number: (value: number) => [[String(value)]],
15
17
  date,
@@ -1,20 +1,90 @@
1
1
  import { createNotion } from "./handles.js";
2
2
 
3
3
  export { APPS_RESERVED_RESOURCE_ID_PREFIX, APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
4
- export type { DataSourceDefinition, NotionAsCodeProperty, Parent } from "./intents.js";
4
+ export type {
5
+ DatabaseIntent,
6
+ DataSourceDefinition,
7
+ NotionAsCodeIcon,
8
+ NotionAsCodeProperty,
9
+ NotionAsCodePropertyConfig,
10
+ NotionAsCodeSchema,
11
+ NotionAsCodeSelectOption,
12
+ NotionAsCodeStatusOption,
13
+ Parent,
14
+ ResourceId,
15
+ } from "./intents.js";
5
16
  export type { CustomAgentArgs, CustomAgentHandle } from "./custom-agent.js";
6
17
  export type {
7
18
  ChildDatabaseArgs,
8
19
  ChildDatabaseFactory,
20
+ ChildMultiSourceDatabaseArgs,
9
21
  ChildSingleSourceDatabaseArgs,
22
+ DataSourceArgs,
10
23
  DataSourceHandle,
24
+ DataSourceMap,
11
25
  DatabaseArgs,
12
26
  DatabaseHandle,
27
+ MultiSourceDatabaseArgs,
13
28
  SingleSourceDatabaseArgs,
14
29
  SingleSourceDatabaseHandle,
15
30
  } from "./database.js";
16
31
  export type { ChildPageArgs, PageArgs, PageHandle } from "./page.js";
17
32
  export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
33
+ export type { ViewArgs, ViewHandle } from "./view.js";
34
+ export type {
35
+ AdvancedFilterSchema,
36
+ BasePropertyFilter,
37
+ BaseViewSchema,
38
+ BoardViewSchema,
39
+ CalendarViewSchema,
40
+ CheckboxGroupByFormat,
41
+ CheckboxPropertyFilter,
42
+ CoverAspectFormat,
43
+ CoverFormat,
44
+ CoverSizeFormat,
45
+ DatabaseViewSortDirection,
46
+ DateFilterMode,
47
+ DateGroupByFormat,
48
+ DatePropertyFilter,
49
+ DatePropertyFilterValue,
50
+ DatePropertyTypes,
51
+ ExactDatePropertyFilterValue,
52
+ ExactRangeDatePropertyFilterValue,
53
+ FeedViewSchema,
54
+ FilterSchema,
55
+ FormulaGroupByFormat,
56
+ GalleryViewSchema,
57
+ GroupByFormat,
58
+ GroupByFormatBase,
59
+ GroupFormat,
60
+ ListViewSchema,
61
+ LocationGroupByFormat,
62
+ MultiSelectPropertyFilter,
63
+ NumberGroupByFormat,
64
+ NumberPropertyFilter,
65
+ PersonGroupByFormat,
66
+ PersonPropertyFilter,
67
+ PropertyFilterSchema,
68
+ PropertyFormat,
69
+ PropertyViewSortSchema,
70
+ PropertyVisibility,
71
+ RelationGroupByFormat,
72
+ RelationPropertyFilter,
73
+ RelativeDatePreset,
74
+ RelativeDatePropertyFilterValue,
75
+ RelativeDateRangeUnit,
76
+ RelativeToTodayDatePropertyFilterValue,
77
+ SelectGroupByFormat,
78
+ SelectPropertyFilter,
79
+ StatusGroupByFormat,
80
+ StatusPropertyFilter,
81
+ TableViewSchema,
82
+ TextGroupByFormat,
83
+ TextPropertyFilter,
84
+ TimelineViewSchema,
85
+ ViewSchema,
86
+ ViewType,
87
+ } from "./views.js";
18
88
 
19
89
  /** Experimental Notion-as-Code facade for Apps SDK project metadata. */
20
90
  export const notion = createNotion();