@notionhq/apps 0.0.17 → 0.0.19

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/AGENTS.md +27 -0
  2. package/README.md +77 -35
  3. package/dist/cli/build.d.ts.map +1 -1
  4. package/dist/cli/build.js +12 -7
  5. package/dist/cli/emit-manifest.d.ts +1 -1
  6. package/dist/cli/emit-manifest.d.ts.map +1 -1
  7. package/dist/cli/emit-manifest.js +66 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/notion-as-code/custom-agent.d.ts +1 -0
  12. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  13. package/dist/notion-as-code/custom-agent.js +1 -1
  14. package/dist/notion-as-code/database.d.ts +90 -48
  15. package/dist/notion-as-code/database.d.ts.map +1 -1
  16. package/dist/notion-as-code/database.js +135 -45
  17. package/dist/notion-as-code/database.test.d.ts +2 -0
  18. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  19. package/dist/notion-as-code/index.d.ts +2 -2
  20. package/dist/notion-as-code/index.d.ts.map +1 -1
  21. package/dist/notion-as-code/intents.d.ts +34 -2
  22. package/dist/notion-as-code/intents.d.ts.map +1 -1
  23. package/dist/notion-as-code/page.d.ts +1 -0
  24. package/dist/notion-as-code/page.d.ts.map +1 -1
  25. package/dist/notion-as-code/page.js +1 -0
  26. package/dist/notion-as-code/schema.d.ts +8 -4
  27. package/dist/notion-as-code/schema.d.ts.map +1 -1
  28. package/dist/notion-as-code/views.d.ts +29 -10
  29. package/dist/notion-as-code/views.d.ts.map +1 -1
  30. package/dist/sync.d.ts +33 -15
  31. package/dist/sync.d.ts.map +1 -1
  32. package/dist/sync.js +12 -0
  33. package/dist/workflow-access-types.test.d.ts +2 -0
  34. package/dist/workflow-access-types.test.d.ts.map +1 -0
  35. package/dist/workflow-access.d.ts +68 -0
  36. package/dist/workflow-access.d.ts.map +1 -0
  37. package/dist/workflow-access.js +135 -0
  38. package/dist/workflow-state.d.ts +13 -0
  39. package/dist/workflow-state.d.ts.map +1 -0
  40. package/dist/workflow-state.js +107 -0
  41. package/dist/workflow.d.ts +65 -9
  42. package/dist/workflow.d.ts.map +1 -1
  43. package/dist/workflow.js +119 -5
  44. package/docs/BUILD.md +134 -4
  45. package/package.json +1 -1
  46. package/skills/notion-as-code/SKILL.md +89 -49
  47. package/skills/sync/SKILL.md +21 -18
  48. package/skills/workflow/SKILL.md +101 -17
  49. package/src/cli/build.test.ts +148 -64
  50. package/src/cli/build.ts +13 -8
  51. package/src/cli/emit-manifest.ts +107 -0
  52. package/src/index.ts +21 -1
  53. package/src/notion-as-code/custom-agent.ts +2 -1
  54. package/src/notion-as-code/database.test.ts +661 -0
  55. package/src/notion-as-code/database.ts +349 -127
  56. package/src/notion-as-code/index.ts +16 -1
  57. package/src/notion-as-code/intents.ts +39 -2
  58. package/src/notion-as-code/page.ts +2 -0
  59. package/src/notion-as-code/schema.ts +11 -3
  60. package/src/notion-as-code/views.ts +29 -10
  61. package/src/sync.test.ts +295 -0
  62. package/src/sync.ts +85 -21
  63. package/src/workflow-access-types.test.ts +69 -0
  64. package/src/workflow-access.ts +299 -0
  65. package/src/workflow-state.ts +152 -0
  66. package/src/workflow.test.ts +441 -1
  67. package/src/workflow.ts +230 -13
@@ -1,197 +1,418 @@
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";
15
16
  import type { ViewSchema } from "./views.js";
16
17
 
17
- /** Input for declaring a database and its optional data sources. */
18
- export type DatabaseArgs<
19
- DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
20
- > = {
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> = {
21
25
  resourceId: ResourceId;
22
- /** Omit to create a private top-level database in the Apps workspace. */
23
- parent?: Parent;
24
- dataSources?: DataSources;
25
- views?: readonly ViewSchema[];
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> = {
116
+ readonly resourceType: "dataSource";
68
117
  readonly resourceId: string;
69
- readonly schema: Properties;
70
- readonly database: Database<AppsSchemaForProperties<Properties>>;
118
+ readonly schema: Schema;
119
+ readonly database: Database<AppsSchemaForSchema<Schema>>;
71
120
  addPage: (args: ChildPageArgs) => PageHandle;
72
121
  };
73
122
 
74
- /** Handle for a database with data sources indexed by resource ID. */
75
- export type DatabaseHandle<
76
- DataSources extends readonly DataSourceDefinition[] = readonly DataSourceDefinition[],
77
- > = {
123
+ type DatabaseHandleBase = {
124
+ readonly resourceType: "database";
78
125
  readonly resourceId: string;
79
- readonly dataSources: {
80
- readonly [ResourceId in DataSources[number]["resourceId"]]: DataSourceHandle<
81
- DataSourcePropertiesForResourceId<DataSources, ResourceId>
82
- >;
83
- };
84
126
  addView: (view: ViewSchema) => void;
85
127
  };
86
128
 
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
- };
129
+ /** Map each authoring source key to a handle backed by that source's own schema. */
130
+ export type DatabaseHandle<DataSources extends DataSourceMap = DataSourceMap> =
131
+ DatabaseHandleBase & {
132
+ readonly datasources: {
133
+ readonly [DataSourceName in keyof DataSources]: DataSourceHandle<
134
+ DataSources[DataSourceName]["schema"]
135
+ >;
136
+ };
137
+ };
93
138
 
139
+ /** A database whose sole data source is available without a key lookup. */
140
+ export type SingleSourceDatabaseHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> =
141
+ DatabaseHandleBase & {
142
+ readonly dataSource: DataSourceHandle<Schema>;
143
+ };
144
+
145
+ /**
146
+ * Overloaded callable used by page.addDatabase/teamspace.addDatabase. Each form
147
+ * returns its matching handle while preserving literal source and schema inference.
148
+ */
94
149
  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 },
150
+ <const Schema extends NotionAsCodeSchema>(
151
+ databaseResourceId: ResourceId,
152
+ args: ChildSingleSourceDatabaseArgs<Schema>,
153
+ ): SingleSourceDatabaseHandle<Schema>;
154
+ <const DataSources extends DataSourceMap>(
155
+ databaseResourceId: ResourceId,
156
+ args: ChildMultiSourceDatabaseArgs<DataSources>,
100
157
  ): DatabaseHandle<DataSources>;
158
+ (
159
+ databaseResourceId: ResourceId,
160
+ args: Omit<ViewOnlyDatabaseArgs, "parent">,
161
+ ): DatabaseHandle<{}>;
101
162
  };
102
163
 
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 },
164
+ /** Declare a database, defaulting to the Apps workspace. */
165
+ export function database<const Schema extends NotionAsCodeSchema>(
166
+ databaseResourceId: ResourceId,
167
+ args: SingleSourceDatabaseArgs<Schema>,
168
+ ): SingleSourceDatabaseHandle<Schema>;
169
+ export function database<const DataSources extends DataSourceMap>(
170
+ databaseResourceId: ResourceId,
171
+ args: MultiSourceDatabaseArgs<DataSources>,
109
172
  ): DatabaseHandle<DataSources>;
110
173
  export function database(
111
- args: DatabaseArgs | SingleSourceDatabaseArgs,
174
+ databaseResourceId: ResourceId,
175
+ args: ViewOnlyDatabaseArgs,
176
+ ): DatabaseHandle<{}>;
177
+ export function database(
178
+ databaseResourceId: ResourceId,
179
+ args: DatabaseArgs,
112
180
  ): DatabaseHandle | SingleSourceDatabaseHandle {
113
- if (args.parent) {
114
- assertParent(args.parent);
181
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
182
+ throw new Error("Notion-as-Code database arguments must be an object");
115
183
  }
116
- const parent = args.parent ?? {
117
- type: "resourceId",
184
+ const parentValue = args.parent;
185
+ if (parentValue !== undefined) {
186
+ assertParent(parentValue);
187
+ }
188
+ const parent = parentValue ?? {
189
+ type: "resourceId" as const,
118
190
  resourceId: APPS_WORKSPACE_RESOURCE_ID,
119
191
  };
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;
192
+ return buildDatabaseHandle(databaseResourceId, args, parent);
193
+ }
194
+
195
+ /** Bind a parent for page.addDatabase/teamspace.addDatabase. */
196
+ export function createChildDatabase(parent: Parent): ChildDatabaseFactory {
197
+ assertParent(parent);
198
+
199
+ function addDatabase<const Schema extends NotionAsCodeSchema>(
200
+ databaseResourceId: ResourceId,
201
+ args: ChildSingleSourceDatabaseArgs<Schema>,
202
+ ): SingleSourceDatabaseHandle<Schema>;
203
+ function addDatabase<const DataSources extends DataSourceMap>(
204
+ databaseResourceId: ResourceId,
205
+ args: ChildMultiSourceDatabaseArgs<DataSources>,
206
+ ): DatabaseHandle<DataSources>;
207
+ function addDatabase(
208
+ databaseResourceId: ResourceId,
209
+ args: Omit<ViewOnlyDatabaseArgs, "parent">,
210
+ ): DatabaseHandle<{}>;
211
+ function addDatabase(
212
+ databaseResourceId: ResourceId,
213
+ args: ChildDatabaseArgs,
214
+ ): DatabaseHandle | SingleSourceDatabaseHandle {
215
+ return buildDatabaseHandle(databaseResourceId, args, parent);
216
+ }
217
+
218
+ return addDatabase;
219
+ }
220
+ /**
221
+ * Internal bridge pairing an authoring key/schema with its serialized definition
222
+ * during database recording and handle creation; never serialized itself.
223
+ */
224
+ type SourceEntry = {
225
+ key: string;
226
+ schema: NotionAsCodeSchema;
227
+ definition: DataSourceDefinition;
228
+ };
229
+
230
+ /**
231
+ * Build a database handle from one of the authoring forms. Form checks happen
232
+ * before normalization or recording so mixed and malformed declarations are atomic.
233
+ */
234
+ function buildDatabaseHandle(
235
+ databaseResourceId: ResourceId,
236
+ args: DatabaseArgs,
237
+ parent: Parent,
238
+ ): DatabaseHandle | SingleSourceDatabaseHandle {
239
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
240
+ throw new Error("Notion-as-Code database arguments must be an object");
241
+ }
242
+ if (typeof databaseResourceId !== "string") {
243
+ throw new Error("Notion-as-Code database resourceId must be a string");
244
+ }
245
+ assertUserResourceId(databaseResourceId);
246
+
247
+ const hasSchema = Object.prototype.hasOwnProperty.call(args, "schema");
248
+ const hasDataSources = Object.prototype.hasOwnProperty.call(args, "datasources");
249
+ if (hasSchema && hasDataSources) {
250
+ throw new Error("Specify either schema or datasources, not both");
251
+ }
252
+
253
+ if (args.schema !== undefined) {
125
254
  const source: DataSourceDefinition = {
126
- resourceId: `${args.resourceId}-source`,
255
+ resourceId: args.dataSourceResourceId,
127
256
  name: args.name,
128
- properties,
257
+ properties: normalizeProperties(args.schema),
129
258
  };
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}"`);
259
+ const handle = recordDatabase(databaseResourceId, args, parent, [
260
+ { key: "dataSource", schema: args.schema, definition: source },
261
+ ]);
262
+ const dataSource = handle.datasources.dataSource;
263
+ if (dataSource === undefined) {
264
+ throw new Error(`Database "${databaseResourceId}" is missing its data source`);
134
265
  }
135
- return { ...handle, dataSource };
266
+ return {
267
+ resourceType: "database",
268
+ resourceId: databaseResourceId,
269
+ dataSource,
270
+ addView: handle.addView,
271
+ };
136
272
  }
137
- return createDatabase(args, parent);
273
+
274
+ if (args.datasources !== undefined) {
275
+ if (
276
+ typeof args.datasources !== "object" ||
277
+ args.datasources === null ||
278
+ Array.isArray(args.datasources)
279
+ ) {
280
+ throw new Error("Notion-as-Code datasources must be an object");
281
+ }
282
+ const sourceEntries = Object.entries(args.datasources).map(([key, sourceArgs]) =>
283
+ normalizeDataSource(key, sourceArgs),
284
+ );
285
+ return recordDatabase(databaseResourceId, args, parent, sourceEntries);
286
+ }
287
+
288
+ return recordDatabase(databaseResourceId, args, parent, []);
138
289
  }
139
290
 
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 });
291
+ /** Strip authoring-only fields from database arguments before serializing metadata. */
292
+ function normalizeDatabaseMetadata(args: DatabaseArgs): DatabaseMetadata {
293
+ const {
294
+ parent: _parent,
295
+ schema: _schema,
296
+ datasources: _datasources,
297
+ dataSourceResourceId: _dataSourceResourceId,
298
+ ...metadata
299
+ } = args;
300
+ return metadata;
301
+ }
302
+
303
+ /** Normalize a keyed declaration into its serialized definition while retaining key/schema for handles. */
304
+ function normalizeDataSource(key: string, sourceArgs: DataSourceArgs): SourceEntry {
305
+ if (typeof sourceArgs !== "object" || sourceArgs === null || Array.isArray(sourceArgs)) {
306
+ throw new Error(`Data source "${key}" must be an object`);
307
+ }
308
+ const { schema, ...sourceMetadata } = sourceArgs;
309
+ return {
310
+ key,
311
+ schema,
312
+ definition: {
313
+ ...sourceMetadata,
314
+ name: key,
315
+ properties: normalizeProperties(schema),
316
+ },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Normalize keyed property configurations to the serialized property array.
322
+ * Property names always come from schema keys; authoring name fields are ignored.
323
+ */
324
+ function normalizeProperties(schema: NotionAsCodeSchema): NotionAsCodeProperty[] {
325
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema)) {
326
+ throw new Error("Notion-as-Code schema must be an object");
327
+ }
328
+ const properties: NotionAsCodeProperty[] = [];
329
+ for (const [key, propertyConfig] of Object.entries(schema)) {
330
+ if (
331
+ typeof propertyConfig !== "object" ||
332
+ propertyConfig === null ||
333
+ Array.isArray(propertyConfig)
334
+ ) {
335
+ throw new Error(`Property "${key}" must be an object`);
153
336
  }
154
- return database({ ...args, parent });
337
+ if (typeof propertyConfig.resourceId !== "string") {
338
+ throw new Error(`Property "${key}" must specify a resourceId`);
339
+ }
340
+ properties.push({
341
+ ...propertyConfig,
342
+ name: key,
343
+ });
155
344
  }
156
- return addDatabase;
345
+ return properties;
157
346
  }
158
347
 
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 ?? [];
348
+ /** Validate the views shape and require at least one data source or view. */
349
+ function assertSourceOrView(sourceEntries: readonly SourceEntry[], views: unknown): void {
350
+ if (views !== undefined && !Array.isArray(views)) {
351
+ throw new Error("Notion-as-Code database views must be an array");
352
+ }
353
+ if (sourceEntries.length === 0 && (!Array.isArray(views) || views.length === 0)) {
354
+ throw new Error("Notion-as-Code database must define a data source or view");
355
+ }
356
+ }
357
+
358
+ /** Record a normalized database intent and create handles for its data sources. */
359
+ function recordDatabase(
360
+ databaseResourceId: ResourceId,
361
+ args: DatabaseArgs,
362
+ parent: Parent,
363
+ sourceEntries: readonly SourceEntry[],
364
+ ): DatabaseHandle {
365
+ const metadata = normalizeDatabaseMetadata(args);
366
+ assertSourceOrView(sourceEntries, metadata.views);
367
+ const intent: DatabaseIntent = {
368
+ ...metadata,
369
+ resourceId: databaseResourceId,
370
+ parent,
371
+ dataSources: sourceEntries.map(({ definition }) => definition),
372
+ };
373
+ return createDatabase(intent, sourceEntries);
374
+ }
375
+
376
+ /** Create a normalized database handle while preserving each source's authoring schema. */
377
+ function createDatabase(
378
+ intent: DatabaseIntent,
379
+ sourceEntries: readonly SourceEntry[],
380
+ ): DatabaseHandle {
381
+ assertUserResourceId(intent.resourceId);
163
382
  const dataSourceIds = new Set<string>();
164
- const handles: Record<string, DataSourceHandle> = {};
383
+ const handles: Record<string, DataSourceHandle> = Object.create(null);
165
384
 
166
- for (const dataSource of dataSources) {
167
- assertUserResourceId(dataSource.resourceId);
168
- if (dataSourceIds.has(dataSource.resourceId)) {
385
+ for (const { key, schema, definition } of sourceEntries) {
386
+ if (typeof definition.resourceId !== "string") {
387
+ throw new Error(`Data source "${key}" must specify a resourceId`);
388
+ }
389
+ assertUserResourceId(definition.resourceId);
390
+ if (dataSourceIds.has(definition.resourceId)) {
169
391
  throw new Error(
170
- `Database "${args.resourceId}" has duplicate data source "${dataSource.resourceId}"`,
392
+ `Database "${intent.resourceId}" has duplicate data source "${definition.resourceId}"`,
171
393
  );
172
394
  }
173
- dataSourceIds.add(dataSource.resourceId);
174
- handles[dataSource.resourceId] = createDataSourceHandle(dataSource);
395
+ dataSourceIds.add(definition.resourceId);
396
+ handles[key] = createDataSourceHandle(definition, schema);
175
397
  }
176
398
 
177
- recordIntent({
178
- type: "database",
179
- ...args,
180
- parent,
181
- dataSources: [...dataSources],
182
- } satisfies { type: "database" } & DatabaseIntent);
399
+ recordIntent({ ...intent, type: "database" });
183
400
 
184
401
  return {
185
- resourceId: args.resourceId,
186
- dataSources: handles,
402
+ resourceType: "database",
403
+ resourceId: intent.resourceId,
404
+ datasources: handles,
187
405
  addView(view) {
188
- recordIntent({ type: "view", databaseResourceId: args.resourceId, view });
406
+ recordIntent({ type: "view", databaseResourceId: intent.resourceId, view });
189
407
  },
190
408
  };
191
409
  }
192
410
 
193
- /** Create a data source handle backed by the Apps database used by syncs. */
194
- function createDataSourceHandle(dataSource: DataSourceDefinition): DataSourceHandle {
411
+ /** Create a data-source handle backed by the Apps database used by syncs. */
412
+ function createDataSourceHandle(
413
+ dataSource: DataSourceDefinition,
414
+ schema: NotionAsCodeSchema,
415
+ ): DataSourceHandle {
195
416
  const propertyResourceIds = new Set<string>();
196
417
  const propertyNames = new Set<string>();
197
418
  for (const property of dataSource.properties) {
@@ -211,8 +432,9 @@ function createDataSourceHandle(dataSource: DataSourceDefinition): DataSourceHan
211
432
  }
212
433
 
213
434
  return {
435
+ resourceType: "dataSource",
214
436
  resourceId: dataSource.resourceId,
215
- schema: dataSource.properties,
437
+ schema,
216
438
  database: createAppsDatabase(dataSource.resourceId, dataSource),
217
439
  addPage(pageArgs) {
218
440
  return createPage(pageArgs, { type: "resourceId", resourceId: dataSource.resourceId });
@@ -1,15 +1,30 @@
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";
@@ -1,5 +1,4 @@
1
1
  import type { CustomAgentArgs } from "./custom-agent.js";
2
- import type { DatabaseArgs } from "./database.js";
3
2
  import type { PageArgs } from "./page.js";
4
3
  import type { TeamspaceArgs } from "./teamspace.js";
5
4
  import type { ViewSchema } from "./views.js";
@@ -66,6 +65,7 @@ type UnsupportedProperty = PropertyBase & {
66
65
  [key: string]: unknown;
67
66
  };
68
67
 
68
+ /** A normalized Notion-as-Code property with its resolved display name. */
69
69
  export type NotionAsCodeProperty =
70
70
  | (PropertyBase & { type: "title" })
71
71
  | (PropertyBase & { type: "text" })
@@ -92,6 +92,28 @@ export type NotionAsCodeProperty =
92
92
  | (PropertyBase & { type: "file" })
93
93
  | UnsupportedProperty;
94
94
 
95
+ /**
96
+ * `T extends unknown` applies Omit to each property variant separately, preserving
97
+ * variant-specific fields such as required status options. Plain Omit on the union
98
+ * would retain only shared keys. Pick also restores type/resourceId on the
99
+ * unsupported variant, whose string index signature would otherwise erase them.
100
+ */
101
+ type DistributivePropertyConfig<T extends NotionAsCodeProperty> = T extends unknown
102
+ ? Omit<T, "name"> & Pick<T, "type" | "resourceId">
103
+ : never;
104
+
105
+ /**
106
+ * Authoring properties are keyed by their schema object name. A property-level
107
+ * `name` override would make the key and serialized property diverge, so it is
108
+ * explicitly rejected even for the string-indexed unsupported-property variant.
109
+ */
110
+ export type NotionAsCodePropertyConfig = DistributivePropertyConfig<NotionAsCodeProperty> & {
111
+ name?: never;
112
+ };
113
+ /** A keyed property schema, with object keys supplying serialized property names. */
114
+ export type NotionAsCodeSchema = Record<string, NotionAsCodePropertyConfig>;
115
+
116
+ /** The normalized data-source shape emitted in provisioning artifacts. */
95
117
  export type DataSourceDefinition<
96
118
  Properties extends readonly NotionAsCodeProperty[] = readonly NotionAsCodeProperty[],
97
119
  > = {
@@ -107,9 +129,24 @@ export type TeamspaceIntent = TeamspaceArgs & {
107
129
  parent: Parent;
108
130
  };
109
131
 
110
- export type DatabaseIntent = DatabaseArgs & {
132
+ /**
133
+ * The normalized database shape emitted in provisioning artifacts.
134
+ *
135
+ * This intentionally does not depend on the authoring argument shape. Authoring
136
+ * accepts keyed schemas and data-source maps; the recorder always stores arrays.
137
+ */
138
+ export type DatabaseIntent = {
139
+ resourceId: ResourceId;
111
140
  parent: Parent;
112
141
  dataSources: readonly DataSourceDefinition[];
142
+ views?: readonly ViewSchema[];
143
+ icon?: NotionAsCodeIcon;
144
+ // TODO: Port cover types.
145
+ cover?: unknown;
146
+ hideDataSourceTitle?: boolean;
147
+ hideDatabaseTitleIfEmpty?: boolean;
148
+ name?: string;
149
+ description?: string;
113
150
  };
114
151
 
115
152
  export type PageIntent = PageArgs & { parent: Parent };
@@ -23,6 +23,7 @@ export type PageArgs = {
23
23
  export type ChildPageArgs = Omit<PageArgs, "parent">;
24
24
 
25
25
  export type PageHandle = {
26
+ readonly resourceType: "page";
26
27
  readonly resourceId: string;
27
28
  addDatabase: ChildDatabaseFactory;
28
29
  addPage: (args: ChildPageArgs) => PageHandle;
@@ -50,6 +51,7 @@ export function createPage(args: ChildPageArgs | PageArgs, parent: Parent): Page
50
51
  } satisfies { type: "page" } & PageIntent);
51
52
 
52
53
  return {
54
+ resourceType: "page",
53
55
  resourceId: args.resourceId,
54
56
  addDatabase: createChildDatabase({ type: "resourceId", resourceId: args.resourceId }),
55
57
  addPage(pageArgs) {