@notionhq/apps 0.0.16 → 0.0.17

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 (56) hide show
  1. package/README.md +27 -2
  2. package/dist/connections.d.ts +39 -11
  3. package/dist/connections.d.ts.map +1 -1
  4. package/dist/connections.js +85 -21
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/notion-as-code/database.d.ts +3 -2
  9. package/dist/notion-as-code/database.d.ts.map +1 -1
  10. package/dist/notion-as-code/handles.d.ts +2 -0
  11. package/dist/notion-as-code/handles.d.ts.map +1 -1
  12. package/dist/notion-as-code/handles.js +2 -0
  13. package/dist/notion-as-code/index.d.ts +3 -0
  14. package/dist/notion-as-code/index.d.ts.map +1 -1
  15. package/dist/notion-as-code/intents.d.ts +2 -1
  16. package/dist/notion-as-code/intents.d.ts.map +1 -1
  17. package/dist/notion-as-code/view.d.ts +12 -0
  18. package/dist/notion-as-code/view.d.ts.map +1 -0
  19. package/dist/notion-as-code/view.js +13 -0
  20. package/dist/notion-as-code/views-types.test.d.ts +2 -0
  21. package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
  22. package/dist/notion-as-code/views.d.ts +469 -0
  23. package/dist/notion-as-code/views.d.ts.map +1 -0
  24. package/dist/notion-as-code/views.js +0 -0
  25. package/dist/oauth.d.ts +11 -0
  26. package/dist/oauth.d.ts.map +1 -0
  27. package/dist/oauth.js +26 -0
  28. package/dist/providers.generated.d.ts +26 -86
  29. package/dist/providers.generated.d.ts.map +1 -1
  30. package/dist/providers.generated.js +54 -66
  31. package/dist/triggers.generated.d.ts +3 -3
  32. package/dist/triggers.generated.d.ts.map +1 -1
  33. package/dist/workflow.d.ts +8 -8
  34. package/dist/workflow.d.ts.map +1 -1
  35. package/dist/workflow.js +12 -6
  36. package/docs/CONNECTIONS.md +70 -22
  37. package/package.json +1 -1
  38. package/skills/connections/SKILL.md +7 -8
  39. package/skills/notion-as-code/SKILL.md +8 -2
  40. package/src/connections.test.ts +205 -41
  41. package/src/connections.ts +144 -38
  42. package/src/index.ts +1 -0
  43. package/src/notion-as-code/database.ts +3 -3
  44. package/src/notion-as-code/handles.ts +2 -0
  45. package/src/notion-as-code/index.ts +55 -0
  46. package/src/notion-as-code/intents.ts +2 -2
  47. package/src/notion-as-code/view.ts +23 -0
  48. package/src/notion-as-code/views-types.test.ts +59 -0
  49. package/src/notion-as-code/views.ts +554 -0
  50. package/src/oauth.ts +40 -0
  51. package/src/providers.generated.ts +68 -163
  52. package/src/triggers.generated.ts +4 -4
  53. package/src/workflow-connections-types.test.ts +16 -10
  54. package/src/workflow-types.test.ts +88 -16
  55. package/src/workflow.test.ts +45 -17
  56. package/src/workflow.ts +31 -17
@@ -2,6 +2,7 @@ import type { CustomAgentArgs } from "./custom-agent.js";
2
2
  import type { DatabaseArgs } from "./database.js";
3
3
  import type { PageArgs } from "./page.js";
4
4
  import type { TeamspaceArgs } from "./teamspace.js";
5
+ import type { ViewSchema } from "./views.js";
5
6
 
6
7
  /** Apps-specific resource IDs used while declaring Notion-as-Code metadata. */
7
8
  /** Synthetic workspace parent for teamspaces declared by an Apps project. */
@@ -116,8 +117,7 @@ export type CustomAgentIntent = CustomAgentArgs;
116
117
 
117
118
  export type ViewIntent = {
118
119
  databaseResourceId: ResourceId;
119
- // TODO: Port view types.
120
- view: unknown;
120
+ view: ViewSchema;
121
121
  };
122
122
 
123
123
  export type InfraAsCodeIntent =
@@ -0,0 +1,23 @@
1
+ import type { ResourceId } from "./intents.js";
2
+ import { recordIntent } from "./recorder.js";
3
+ import { assertUserResourceId } from "./resource.js";
4
+ import type { ViewSchema } from "./views.js";
5
+
6
+ /** Declare a view attached to a database resource. */
7
+ export type ViewArgs = ViewSchema & {
8
+ databaseResourceId: ResourceId;
9
+ };
10
+
11
+ export type ViewHandle = {
12
+ readonly resourceId: string;
13
+ };
14
+
15
+ /** Record a database view for provisioning without making a Notion API request. */
16
+ export function view(args: ViewArgs): ViewHandle {
17
+ assertUserResourceId(args.resourceId);
18
+ assertUserResourceId(args.databaseResourceId);
19
+ assertUserResourceId(args.dataSourceResourceId);
20
+ const { databaseResourceId, ...schema } = args;
21
+ recordIntent({ type: "view", databaseResourceId, view: schema });
22
+ return { resourceId: args.resourceId };
23
+ }
@@ -0,0 +1,59 @@
1
+ import { expectTypeOf, it } from "vitest";
2
+
3
+ import type {
4
+ DatabaseArgs,
5
+ DatabaseHandle,
6
+ DatePropertyFilter,
7
+ GroupByFormat,
8
+ ViewSchema,
9
+ } from "./index.js";
10
+
11
+ it("requires date properties for calendar and timeline views at both entrypoints", () => {
12
+ type MissingDateProperty = {
13
+ resourceId: "view";
14
+ dataSourceResourceId: "source";
15
+ type: "calendar" | "timeline";
16
+ };
17
+ expectTypeOf<MissingDateProperty>().not.toExtend<ViewSchema>();
18
+ expectTypeOf<MissingDateProperty>().not.toExtend<NonNullable<DatabaseArgs["views"]>[number]>();
19
+ expectTypeOf<MissingDateProperty>().not.toExtend<Parameters<DatabaseHandle["addView"]>[0]>();
20
+ });
21
+
22
+ it("couples date operators to their value shapes and requires a range bound", () => {
23
+ type DateFilter = {
24
+ type: "property";
25
+ propertyId: "due";
26
+ propertyType: "date";
27
+ };
28
+ expectTypeOf<
29
+ DateFilter & {
30
+ operator: "date_is_within";
31
+ value: { type: "exact_range"; value: { startDate: "2026-01-01" } };
32
+ }
33
+ >().toExtend<DatePropertyFilter>();
34
+ expectTypeOf<
35
+ DateFilter & {
36
+ operator: "date_is_within";
37
+ value: { type: "exact_range"; value: {} };
38
+ }
39
+ >().not.toExtend<DatePropertyFilter>();
40
+ expectTypeOf<
41
+ DateFilter & {
42
+ operator: "date_is_relative_to";
43
+ value: { type: "exact"; value: "2026-01-01" };
44
+ }
45
+ >().not.toExtend<DatePropertyFilter>();
46
+ });
47
+
48
+ it("limits status grouping options to status properties", () => {
49
+ expectTypeOf<{
50
+ type: "status";
51
+ property: "status";
52
+ statusBy: "group";
53
+ }>().toExtend<GroupByFormat>();
54
+ expectTypeOf<{
55
+ type: "select";
56
+ property: "status";
57
+ statusBy: "group";
58
+ }>().not.toExtend<GroupByFormat>();
59
+ });
@@ -0,0 +1,554 @@
1
+ import type { NotionAsCodeIcon, NotionAsCodeProperty, ResourceId } from "./intents.js";
2
+
3
+ // Ported from notion-next/src/server/helpers/infraAsCode/types.ts.
4
+ export type ViewType = "table" | "board" | "calendar" | "list" | "gallery" | "feed" | "timeline";
5
+
6
+ export type PropertyVisibility = "show" | "hide" | "hide_if_empty";
7
+
8
+ /**
9
+ * Format for a single visible property entry in a view (table / board / list /
10
+ * gallery / feed / timeline).
11
+ *
12
+ * `property` must be the `resourceId` of a property in the view's data
13
+ * source, matching the same convention used by board `groupBy.property`,
14
+ * `calendarBy`, and `timelineBy`.
15
+ *
16
+ */
17
+ export type PropertyFormat = {
18
+ /** ResourceId of the property in the view's data source. */
19
+ property: ResourceId;
20
+ visible?: boolean;
21
+ width?: number;
22
+ visibility?: PropertyVisibility;
23
+ };
24
+
25
+ /**
26
+ * Format for a single column entry in a board view.
27
+ *
28
+ * `property` must be the `resourceId` of a property in the view's data
29
+ * source, matching the same convention used by `PropertyFormat.property`
30
+ * and board `groupBy.property`.
31
+ *
32
+ */
33
+ export type GroupFormat = {
34
+ /** ResourceId of the property in the view's data source. */
35
+ property: ResourceId;
36
+ hidden?: boolean;
37
+ value?: {
38
+ type: string;
39
+ value?: string | boolean | number | null;
40
+ };
41
+ };
42
+
43
+ export type GroupByFormatBase = {
44
+ /** ResourceId of the property in the view's data source. */
45
+ property: ResourceId;
46
+ /** Whether groups with no pages are visible. Defaults to "show". */
47
+ emptyGroupVisibility?: "show" | "hide";
48
+ statusBy?: never;
49
+ };
50
+
51
+ export type SelectGroupByFormat = GroupByFormatBase & {
52
+ type: "select" | "multi_select";
53
+ };
54
+
55
+ export type StatusGroupByFormat = Omit<GroupByFormatBase, "statusBy"> & {
56
+ type: "status";
57
+ /** Groups status values by their canonical status group or individual option. */
58
+ statusBy?: "group" | "option";
59
+ };
60
+
61
+ export type PersonGroupByFormat = GroupByFormatBase & {
62
+ type: "person" | "created_by" | "last_edited_by";
63
+ };
64
+
65
+ export type DateGroupByFormat = GroupByFormatBase & {
66
+ type: "date" | "created_time" | "last_edited_time" | "last_visited_time";
67
+ };
68
+
69
+ export type TextGroupByFormat = GroupByFormatBase & {
70
+ type: "text" | "title" | "url" | "email" | "phone_number";
71
+ };
72
+
73
+ export type NumberGroupByFormat = GroupByFormatBase & {
74
+ type: "number";
75
+ };
76
+
77
+ export type CheckboxGroupByFormat = GroupByFormatBase & {
78
+ type: "checkbox";
79
+ };
80
+
81
+ export type RelationGroupByFormat = GroupByFormatBase & {
82
+ type: "relation";
83
+ };
84
+
85
+ export type LocationGroupByFormat = GroupByFormatBase & {
86
+ type: "location";
87
+ };
88
+
89
+ export type FormulaGroupByFormat = GroupByFormatBase & {
90
+ type: "formula";
91
+ };
92
+
93
+ /**
94
+ * Board view group-by configuration.
95
+ *
96
+ * `property` must be the `resourceId` of a property in the view's data
97
+ * source, matching the same convention used by `PropertyFormat.property`,
98
+ * `CalendarViewSchema.calendarBy`, and `TimelineViewSchema.timelineBy`.
99
+ *
100
+ */
101
+ export type GroupByFormat =
102
+ | SelectGroupByFormat
103
+ | StatusGroupByFormat
104
+ | PersonGroupByFormat
105
+ | DateGroupByFormat
106
+ | TextGroupByFormat
107
+ | NumberGroupByFormat
108
+ | CheckboxGroupByFormat
109
+ | RelationGroupByFormat
110
+ | LocationGroupByFormat
111
+ | FormulaGroupByFormat;
112
+
113
+ export type CoverFormat =
114
+ | { type: "page_cover" }
115
+ | { type: "page_content" }
116
+ | { type: "page_content_first" }
117
+ | { type: "property"; property: string };
118
+
119
+ export type CoverSizeFormat = "small" | "medium" | "large";
120
+
121
+ export type CoverAspectFormat = "contain" | "cover";
122
+
123
+ export type DatabaseViewSortDirection = "ascending" | "descending";
124
+
125
+ /**
126
+ * Sort schema for ordering database view results.
127
+ * Sorts are applied in order — all pages are first sorted by the first sort,
128
+ * then ties are broken by the second sort, and so on.
129
+ */
130
+ export type PropertyViewSortSchema = {
131
+ propertyId: string;
132
+ direction: DatabaseViewSortDirection;
133
+ };
134
+
135
+ type PropertyType = NotionAsCodeProperty["type"];
136
+
137
+ export type DatePropertyTypes = "date" | "created_time" | "last_edited_time";
138
+
139
+ export type BasePropertyFilter = {
140
+ propertyId: string;
141
+ type: "property";
142
+ };
143
+
144
+ export type TextPropertyFilter = {
145
+ propertyType: Extract<PropertyType, "text" | "title" | "url" | "email" | "phone_number">;
146
+ operator:
147
+ | "string_is"
148
+ | "string_is_not"
149
+ | "string_contains"
150
+ | "string_does_not_contain"
151
+ | "string_starts_with"
152
+ | "string_ends_with";
153
+ value: string;
154
+ } & BasePropertyFilter;
155
+
156
+ export type NumberPropertyFilter = {
157
+ propertyType: Extract<PropertyType, "number">;
158
+ operator:
159
+ | "number_equals"
160
+ | "number_does_not_equal"
161
+ | "number_greater_than"
162
+ | "number_less_than"
163
+ | "number_greater_than_or_equal_to"
164
+ | "number_less_than_or_equal_to";
165
+ value: number;
166
+ } & BasePropertyFilter;
167
+
168
+ export type CheckboxPropertyFilter = {
169
+ propertyType: Extract<PropertyType, "checkbox">;
170
+ operator: "checkbox_is" | "checkbox_is_not";
171
+ value: boolean;
172
+ } & BasePropertyFilter;
173
+
174
+ export type SelectPropertyFilter = {
175
+ propertyType: Extract<PropertyType, "select">;
176
+ operator: "enum_is" | "enum_is_not";
177
+ value: Array<string>;
178
+ } & BasePropertyFilter;
179
+
180
+ export type MultiSelectPropertyFilter = {
181
+ propertyType: Extract<PropertyType, "multi_select">;
182
+ operator: "enum_contains" | "enum_does_not_contain" | "enum_contains_all";
183
+ value: Array<string>;
184
+ } & BasePropertyFilter;
185
+
186
+ export type StatusPropertyFilter = {
187
+ propertyType: Extract<PropertyType, "status">;
188
+ operator: "status_is" | "status_is_not";
189
+ value: Array<string>;
190
+ } & BasePropertyFilter;
191
+
192
+ /**
193
+ * Date presets resolved relative to today, matching options such as
194
+ * "Today", "Tomorrow", and "One week ago".
195
+ */
196
+ export type RelativeDatePreset =
197
+ | "today"
198
+ | "tomorrow"
199
+ | "yesterday"
200
+ | "one_week_ago"
201
+ | "one_week_from_now"
202
+ | "one_month_ago"
203
+ | "one_month_from_now";
204
+
205
+ /**
206
+ * The unit spanned by a relative "is relative to today" date range.
207
+ */
208
+ export type RelativeDateRangeUnit = "day" | "week" | "month" | "year";
209
+
210
+ /**
211
+ * Which date a date property filter compares against. Notion date properties
212
+ * can hold a range, and `end_date` reads the far end of it.
213
+ */
214
+ export type DateFilterMode = "start_date" | "end_date";
215
+
216
+ export type ExactDatePropertyFilterValue = { type: "exact"; value: string };
217
+
218
+ export type RelativeDatePropertyFilterValue = {
219
+ type: "relative";
220
+ value: RelativeDatePreset;
221
+ };
222
+
223
+ export type RelativeToTodayDatePropertyFilterValue = {
224
+ type: "relative_to_today";
225
+ value: {
226
+ direction: "past" | "next" | "this";
227
+ count?: number;
228
+ unit: RelativeDateRangeUnit;
229
+ };
230
+ };
231
+
232
+ /**
233
+ * Value for the `date_is_within` operator, shown as "Is between" in the UI.
234
+ *
235
+ * Bounds are inclusive `YYYY-MM-DD` dates; supplying only one leaves the range
236
+ * open at the other end.
237
+ *
238
+ */
239
+ export type ExactRangeDatePropertyFilterValue = {
240
+ type: "exact_range";
241
+ value: { startDate: string; endDate?: string } | { startDate?: string; endDate: string };
242
+ };
243
+
244
+ /**
245
+ * Value for a date property filter.
246
+ *
247
+ * - `exact` matches a calendar date in `YYYY-MM-DD` format.
248
+ * - `relative` matches a date preset such as today or tomorrow.
249
+ * - `relative_to_today` matches a range relative to today. `unit` can be
250
+ * `day`, `week`, `month`, or `year`. `count` defaults to `1` when omitted
251
+ * and is unnecessary for `direction: "this"`.
252
+ * - `exact_range` matches a fixed calendar range, optionally open at one end.
253
+ *
254
+ */
255
+ export type DatePropertyFilterValue =
256
+ | ExactDatePropertyFilterValue
257
+ | RelativeDatePropertyFilterValue
258
+ | RelativeToTodayDatePropertyFilterValue
259
+ | ExactRangeDatePropertyFilterValue;
260
+
261
+ /**
262
+ * Date property filter.
263
+ *
264
+ * Single-date operators accept `exact` or `relative` values.
265
+ * `date_is_relative_to` requires a `relative_to_today` value.
266
+ * `date_is_within` requires an `exact_range` value.
267
+ *
268
+ * `dateFilterMode` chooses which end of a date range the comparison reads.
269
+ * It defaults to `"start_date"`. `"end_date"` falls back to the start date for
270
+ * rows whose date is a single day rather than a range, and has no effect on
271
+ * `created_time` / `last_edited_time` properties, which are never ranges.
272
+ *
273
+ * @example Exact date
274
+ * {
275
+ * type: "property",
276
+ * propertyId: "due",
277
+ * propertyType: "date",
278
+ * operator: "date_is_on_or_after",
279
+ * value: { type: "exact", value: "2025-10-03" }
280
+ * }
281
+ *
282
+ * @example Relative date preset
283
+ * {
284
+ * type: "property",
285
+ * propertyId: "due",
286
+ * propertyType: "date",
287
+ * operator: "date_is",
288
+ * value: { type: "relative", value: "today" }
289
+ * }
290
+ *
291
+ * @example Next two months
292
+ * {
293
+ * type: "property",
294
+ * propertyId: "due",
295
+ * propertyType: "date",
296
+ * operator: "date_is_relative_to",
297
+ * value: { type: "relative_to_today", value: { direction: "next", count: 2, unit: "month" } }
298
+ * }
299
+ *
300
+ * @example Sprints that end this week
301
+ * {
302
+ * type: "property",
303
+ * propertyId: "sprint",
304
+ * propertyType: "date",
305
+ * operator: "date_is_relative_to",
306
+ * value: { type: "relative_to_today", value: { direction: "this", unit: "week" } },
307
+ * dateFilterMode: "end_date"
308
+ * }
309
+ *
310
+ * @example Fixed calendar range
311
+ * {
312
+ * type: "property",
313
+ * propertyId: "due",
314
+ * propertyType: "date",
315
+ * operator: "date_is_within",
316
+ * value: { type: "exact_range", value: { startDate: "2026-01-01", endDate: "2026-03-31" } }
317
+ * }
318
+ *
319
+ * @example Range left open at the end
320
+ * {
321
+ * type: "property",
322
+ * propertyId: "due",
323
+ * propertyType: "date",
324
+ * operator: "date_is_within",
325
+ * value: { type: "exact_range", value: { startDate: "2026-01-01" } }
326
+ * }
327
+ *
328
+ */
329
+ export type DatePropertyFilter =
330
+ | ({
331
+ propertyType: DatePropertyTypes;
332
+ operator:
333
+ | "date_is"
334
+ | "date_is_before"
335
+ | "date_is_after"
336
+ | "date_is_on_or_before"
337
+ | "date_is_on_or_after";
338
+ value: ExactDatePropertyFilterValue | RelativeDatePropertyFilterValue;
339
+ dateFilterMode?: DateFilterMode;
340
+ } & BasePropertyFilter)
341
+ | ({
342
+ propertyType: DatePropertyTypes;
343
+ operator: "date_is_relative_to";
344
+ value: RelativeToTodayDatePropertyFilterValue;
345
+ dateFilterMode?: DateFilterMode;
346
+ } & BasePropertyFilter)
347
+ | ({
348
+ propertyType: DatePropertyTypes;
349
+ operator: "date_is_within";
350
+ value: ExactRangeDatePropertyFilterValue;
351
+ dateFilterMode?: DateFilterMode;
352
+ } & BasePropertyFilter);
353
+
354
+ export type RelationPropertyFilter = {
355
+ propertyType: Extract<PropertyType, "relation">;
356
+ operator: "relation_contains" | "relation_does_not_contain";
357
+ /** References related pages, or the current page when rendered as a page-layout tab. */
358
+ value: { type: "exact"; value: Array<ResourceId> } | { type: "relative"; value: "this_page" };
359
+ } & BasePropertyFilter;
360
+
361
+ /**
362
+ * Person property filter — matches rows whose person property contains (or
363
+ * does not contain) the viewer (the `"me"` template variable, resolved at
364
+ * read time).
365
+ */
366
+ export type PersonPropertyFilter = {
367
+ propertyType: Extract<PropertyType, "person" | "created_by" | "last_edited_by">;
368
+ operator: "person_contains" | "person_does_not_contain";
369
+ // TODO: Support more Person property types in the future
370
+ value: [{ type: "relative"; value: "me" }];
371
+ } & BasePropertyFilter;
372
+
373
+ /**
374
+ * Filter schema for filtering database views by property values.
375
+ * Used in view definitions to specify which pages should be visible.
376
+ *
377
+ * Each filter targets a specific property by ID and applies a type-appropriate
378
+ * filter operator with a comparison value.
379
+ *
380
+ * @example Text property filter
381
+ * {
382
+ * propertyId: "name-prop",
383
+ * type: "property",
384
+ * propertyType: "text",
385
+ * operator: "string_contains",
386
+ * value: "Project"
387
+ * }
388
+ */
389
+ export type PropertyFilterSchema =
390
+ | TextPropertyFilter
391
+ | NumberPropertyFilter
392
+ | CheckboxPropertyFilter
393
+ | SelectPropertyFilter
394
+ | MultiSelectPropertyFilter
395
+ | StatusPropertyFilter
396
+ | DatePropertyFilter
397
+ | RelationPropertyFilter
398
+ | PersonPropertyFilter;
399
+
400
+ export type FilterSchema = Array<PropertyFilterSchema | AdvancedFilterSchema>;
401
+
402
+ export type AdvancedFilterSchema = {
403
+ type: "advanced";
404
+ operator: "and" | "or";
405
+ filters: FilterSchema;
406
+ };
407
+
408
+ /**
409
+ * Base view schema shared by all view types.
410
+ *
411
+ * `dataSourceResourceId` must reference a data source created by the script or
412
+ * supplied through existing resources.
413
+ *
414
+ * @example
415
+ * // When creating a database with a data source
416
+ * const db = await notion.database({
417
+ * resourceId: "my-database",
418
+ * dataSources: [{ resourceId: "my-datasource", name: "Main", properties: [...] }]
419
+ * })
420
+ *
421
+ * // Views must reference that data source
422
+ * await db.addView({
423
+ * resourceId: "my-table-view",
424
+ * type: "table",
425
+ * dataSourceResourceId: "my-datasource" // REQUIRED: matches data source above
426
+ * })
427
+ *
428
+ * @example
429
+ * // A linked database can reference another script-created or existing data source
430
+ * const linkedDatabase = await notion.database({
431
+ * resourceId: "linked-database",
432
+ * parent: { type: "resourceId", resourceId: "project-page" },
433
+ * views: [{
434
+ * resourceId: "linked-table-view",
435
+ * type: "table",
436
+ * dataSourceResourceId: "my-datasource" // Created elsewhere or supplied through an existing collection
437
+ * }]
438
+ * })
439
+ */
440
+ export type BaseViewSchema = {
441
+ resourceId: ResourceId;
442
+ name?: string;
443
+ /**
444
+ * Notion icon shown in the view tab, including tabs in database page layouts.
445
+ * View icons do not support emoji or uploaded files. Omit to use the default
446
+ * icon for the view type.
447
+ */
448
+ icon?: Extract<NotionAsCodeIcon, { type: "notion_icon" }>;
449
+ type: ViewType;
450
+ /** Resource ID of a script-created or existing data source. */
451
+ dataSourceResourceId: ResourceId;
452
+ /**
453
+ * Optional resource ID of a template page in this view's data source to use as
454
+ * the view default template.
455
+ *
456
+ * The referenced page must be created with `template: true` and be parented to
457
+ * this same data source.
458
+ */
459
+ defaultTemplate?: ResourceId;
460
+ /**
461
+ * Optional: create this view as a linked view referenced by `<database>` tags
462
+ * in page content markdown or use it as a tab in a database layout. It does
463
+ * not attach to the database block's main view tabs.
464
+ */
465
+ ephemeral?: boolean;
466
+ sorts?: Array<PropertyViewSortSchema>;
467
+ /** Optional filter to control which pages appear in this view. */
468
+ filters?: FilterSchema;
469
+ /** Whether page icons are shown in this view. */
470
+ showPageIcon?: boolean;
471
+ };
472
+
473
+ export type TableViewSchema = BaseViewSchema & {
474
+ type: "table";
475
+ properties?: Array<PropertyFormat>;
476
+ wrap?: boolean;
477
+ groupBy?: GroupByFormat;
478
+ };
479
+
480
+ export type BoardViewSchema = BaseViewSchema & {
481
+ type: "board";
482
+ properties?: Array<PropertyFormat>;
483
+ groupBy?: GroupByFormat;
484
+ columns?: Array<GroupFormat>;
485
+ cover?: CoverFormat;
486
+ coverSize?: CoverSizeFormat;
487
+ coverAspect?: CoverAspectFormat;
488
+ wrap?: boolean;
489
+ };
490
+
491
+ export type CalendarViewSchema = BaseViewSchema & {
492
+ type: "calendar";
493
+ properties?: Array<PropertyFormat>;
494
+ /**
495
+ * REQUIRED: ResourceId of the date property to use for the calendar.
496
+ * Must match the `resourceId` of a date property in the database schema.
497
+ *
498
+ * @example
499
+ * calendarBy: "due-date-prop" // resourceId of a property of type "date"
500
+ */
501
+ calendarBy: ResourceId;
502
+ showWeekends?: boolean;
503
+ };
504
+
505
+ export type ListViewSchema = BaseViewSchema & {
506
+ type: "list";
507
+ properties?: Array<PropertyFormat>;
508
+ groupBy?: GroupByFormat;
509
+ };
510
+
511
+ export type GalleryViewSchema = BaseViewSchema & {
512
+ type: "gallery";
513
+ properties?: Array<PropertyFormat>;
514
+ cover?: CoverFormat;
515
+ coverSize?: CoverSizeFormat;
516
+ coverAspect?: CoverAspectFormat;
517
+ };
518
+
519
+ export type FeedViewSchema = BaseViewSchema & {
520
+ type: "feed";
521
+ properties?: Array<PropertyFormat>;
522
+ wrap?: boolean;
523
+ showAuthorByline?: boolean;
524
+ };
525
+
526
+ export type TimelineViewSchema = BaseViewSchema & {
527
+ type: "timeline";
528
+ properties?: Array<PropertyFormat>;
529
+ tableProperties?: Array<PropertyFormat>;
530
+ /**
531
+ * REQUIRED: ResourceId of the date property to use for the timeline start.
532
+ * Must match the `resourceId` of a date property in the database schema.
533
+ *
534
+ * @example
535
+ * timelineBy: "start-date-prop" // resourceId of a property of type "date"
536
+ */
537
+ timelineBy: ResourceId;
538
+ /**
539
+ * Optional: ResourceId of the date property to use for the timeline end
540
+ * (for date ranges). Must match the `resourceId` of a date property in
541
+ * the database schema.
542
+ */
543
+ timelineByEnd?: ResourceId;
544
+ showTable?: boolean;
545
+ };
546
+
547
+ export type ViewSchema =
548
+ | TableViewSchema
549
+ | BoardViewSchema
550
+ | CalendarViewSchema
551
+ | ListViewSchema
552
+ | GalleryViewSchema
553
+ | FeedViewSchema
554
+ | TimelineViewSchema;
package/src/oauth.ts ADDED
@@ -0,0 +1,40 @@
1
+ /** OAuth 2.0 provider configuration. The secret value is stored with the app. */
2
+ export type OAuthConfiguration = {
3
+ authorizationEndpoint: string;
4
+ tokenEndpoint: string;
5
+ clientId: string;
6
+ clientSecretEnv: string;
7
+ scope: string;
8
+ authorizationParams?: Record<string, string>;
9
+ accessTokenExpireMs?: number;
10
+ };
11
+
12
+ /** @internal */
13
+ export function validateOAuthConfiguration(config: OAuthConfiguration): void {
14
+ if (!config.clientId || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.clientSecretEnv))
15
+ throw new Error("OAuth connections require a client ID and clientSecretEnv secret name.");
16
+ for (const endpoint of [config.authorizationEndpoint, config.tokenEndpoint]) {
17
+ const url = new URL(endpoint);
18
+ if (url.protocol !== "https:" || url.username || url.password || url.hash)
19
+ throw new Error("OAuth endpoints must be HTTPS URLs without credentials or fragments.");
20
+ }
21
+ if (
22
+ config.accessTokenExpireMs !== undefined &&
23
+ (!Number.isFinite(config.accessTokenExpireMs) || config.accessTokenExpireMs <= 0)
24
+ )
25
+ throw new Error("OAuth token expiry must be positive.");
26
+ if (
27
+ Object.keys(config.authorizationParams ?? {}).some((key) =>
28
+ [
29
+ "state",
30
+ "redirect_uri",
31
+ "client_id",
32
+ "response_type",
33
+ "code_challenge",
34
+ "code_challenge_method",
35
+ "scope",
36
+ ].includes(key),
37
+ )
38
+ )
39
+ throw new Error("OAuth authorization parameters cannot override protocol fields.");
40
+ }