@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
@@ -0,0 +1,573 @@
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
+ * Database declarations require at least one data source or at least one view.
415
+ * A linked-only database may omit `datasources`, or use `datasources: {}`, only
416
+ * when its `views` list is nonempty. `{}`, `{ datasources: {} }`, and
417
+ * `{ datasources: {}, views: [] }` are invalid.
418
+ *
419
+ * @example
420
+ * // A linked-only database can use a typed table view for an explicit source.
421
+ * const linkedTableView: TableViewSchema = {
422
+ * resourceId: "issues-table-view",
423
+ * type: "table",
424
+ * dataSourceResourceId: "issues-source",
425
+ * properties: [{ property: "issue-title", visible: true }]
426
+ * };
427
+ * const linkedDatabase = notion.database("issues-linked-database", {
428
+ * parent: { type: "resourceId", resourceId: "project-page" },
429
+ * views: [linkedTableView]
430
+ * })
431
+ *
432
+ * @example
433
+ * // When creating a database with a data source
434
+ * const db = notion.database("my-database", {
435
+ * dataSourceResourceId: "my-datasource",
436
+ * name: "Main",
437
+ * schema: { Name: { resourceId: "my-title", type: "title" } }
438
+ * })
439
+ *
440
+ * // Views must reference that data source
441
+ * db.addView({
442
+ * resourceId: "my-table-view",
443
+ * type: "table",
444
+ * dataSourceResourceId: "my-datasource" // REQUIRED: matches data source above
445
+ * })
446
+ *
447
+ * @example
448
+ * // The explicit empty `datasources` form is valid with a nonempty view list.
449
+ * const explicitLinkedDatabase = notion.database("issues-linked-database-explicit", {
450
+ * datasources: {},
451
+ * views: [{
452
+ * resourceId: "issues-table-view-explicit",
453
+ * type: "table",
454
+ * dataSourceResourceId: "issues-source",
455
+ * properties: [{ property: "issue-title", visible: true }]
456
+ * }]
457
+ * })
458
+ */
459
+ export type BaseViewSchema = {
460
+ resourceId: ResourceId;
461
+ name?: string;
462
+ /**
463
+ * Notion icon shown in the view tab, including tabs in database page layouts.
464
+ * View icons do not support emoji or uploaded files. Omit to use the default
465
+ * icon for the view type.
466
+ */
467
+ icon?: Extract<NotionAsCodeIcon, { type: "notion_icon" }>;
468
+ type: ViewType;
469
+ /** Resource ID of a script-created or existing data source. */
470
+ dataSourceResourceId: ResourceId;
471
+ /**
472
+ * Optional resource ID of a template page in this view's data source to use as
473
+ * the view default template.
474
+ *
475
+ * The referenced page must be created with `template: true` and be parented to
476
+ * this same data source.
477
+ */
478
+ defaultTemplate?: ResourceId;
479
+ /**
480
+ * Optional: create this view as a linked view referenced by `<database>` tags
481
+ * in page content markdown or use it as a tab in a database layout. It does
482
+ * not attach to the database block's main view tabs.
483
+ */
484
+ ephemeral?: boolean;
485
+ sorts?: Array<PropertyViewSortSchema>;
486
+ /** Optional filter to control which pages appear in this view. */
487
+ filters?: FilterSchema;
488
+ /** Whether page icons are shown in this view. */
489
+ showPageIcon?: boolean;
490
+ };
491
+
492
+ export type TableViewSchema = BaseViewSchema & {
493
+ type: "table";
494
+ properties?: Array<PropertyFormat>;
495
+ wrap?: boolean;
496
+ groupBy?: GroupByFormat;
497
+ };
498
+
499
+ export type BoardViewSchema = BaseViewSchema & {
500
+ type: "board";
501
+ properties?: Array<PropertyFormat>;
502
+ groupBy?: GroupByFormat;
503
+ columns?: Array<GroupFormat>;
504
+ cover?: CoverFormat;
505
+ coverSize?: CoverSizeFormat;
506
+ coverAspect?: CoverAspectFormat;
507
+ wrap?: boolean;
508
+ };
509
+
510
+ export type CalendarViewSchema = BaseViewSchema & {
511
+ type: "calendar";
512
+ properties?: Array<PropertyFormat>;
513
+ /**
514
+ * REQUIRED: ResourceId of the date property to use for the calendar.
515
+ * Must match the `resourceId` of a date property in the database schema.
516
+ *
517
+ * @example
518
+ * calendarBy: "due-date-prop" // resourceId of a property of type "date"
519
+ */
520
+ calendarBy: ResourceId;
521
+ showWeekends?: boolean;
522
+ };
523
+
524
+ export type ListViewSchema = BaseViewSchema & {
525
+ type: "list";
526
+ properties?: Array<PropertyFormat>;
527
+ groupBy?: GroupByFormat;
528
+ };
529
+
530
+ export type GalleryViewSchema = BaseViewSchema & {
531
+ type: "gallery";
532
+ properties?: Array<PropertyFormat>;
533
+ cover?: CoverFormat;
534
+ coverSize?: CoverSizeFormat;
535
+ coverAspect?: CoverAspectFormat;
536
+ };
537
+
538
+ export type FeedViewSchema = BaseViewSchema & {
539
+ type: "feed";
540
+ properties?: Array<PropertyFormat>;
541
+ wrap?: boolean;
542
+ showAuthorByline?: boolean;
543
+ };
544
+
545
+ export type TimelineViewSchema = BaseViewSchema & {
546
+ type: "timeline";
547
+ properties?: Array<PropertyFormat>;
548
+ tableProperties?: Array<PropertyFormat>;
549
+ /**
550
+ * REQUIRED: ResourceId of the date property to use for the timeline start.
551
+ * Must match the `resourceId` of a date property in the database schema.
552
+ *
553
+ * @example
554
+ * timelineBy: "start-date-prop" // resourceId of a property of type "date"
555
+ */
556
+ timelineBy: ResourceId;
557
+ /**
558
+ * Optional: ResourceId of the date property to use for the timeline end
559
+ * (for date ranges). Must match the `resourceId` of a date property in
560
+ * the database schema.
561
+ */
562
+ timelineByEnd?: ResourceId;
563
+ showTable?: boolean;
564
+ };
565
+
566
+ export type ViewSchema =
567
+ | TableViewSchema
568
+ | BoardViewSchema
569
+ | CalendarViewSchema
570
+ | ListViewSchema
571
+ | GalleryViewSchema
572
+ | FeedViewSchema
573
+ | 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
+ }