ocean-brain 0.12.3 → 0.13.1

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 (101) hide show
  1. package/README.md +5 -3
  2. package/dist/index.js +12 -4
  3. package/package.json +10 -8
  4. package/server/client/dist/assets/{ArrowRight.es-CD1IMhU2.js → ArrowRight.es-BlNsZQFs.js} +1 -1
  5. package/server/client/dist/assets/AuxiliaryPanelHeader-HAttwVby.js +1 -0
  6. package/server/client/dist/assets/Calendar-DExFS7xq.js +25 -0
  7. package/server/client/dist/assets/Callout-CcYyvHQP.js +1 -0
  8. package/server/client/dist/assets/CaretUp.es-C8iY5OlB.js +1 -0
  9. package/server/client/dist/assets/{CheckCircle.es-C7fkzE3t.js → CheckCircle.es-Dqr3WImn.js} +1 -1
  10. package/server/client/dist/assets/{Copy.es-D6rl6b78.js → Copy.es-fMv0ETir.js} +1 -1
  11. package/server/client/dist/assets/{File.es-CcjtzE38.js → File.es-CHtXXafR.js} +1 -1
  12. package/server/client/dist/assets/FileText.es-DPIwDCEe.js +1 -0
  13. package/server/client/dist/assets/{FlowArrow.es-CAIThqWk.js → FlowArrow.es-Dxee98ir.js} +1 -1
  14. package/server/client/dist/assets/Graph-CdEu8eAn.js +46 -0
  15. package/server/client/dist/assets/Graph-hVj6vAYJ.js +1 -0
  16. package/server/client/dist/assets/{Image.es-BDpo5R66.js → Image.es-CjXY1JVf.js} +1 -1
  17. package/server/client/dist/assets/Info.es-CYxg-Vku.js +1 -0
  18. package/server/client/dist/assets/LinkSimple.es-B-P-7Iem.js +1 -0
  19. package/server/client/dist/assets/{Note-Dq2Szaxd.js → Note-CGI-Y1__.js} +4 -4
  20. package/server/client/dist/assets/{NotePropertiesPanel-Cu3RgDic.js → NotePropertiesPanel-6P7UfRBR.js} +2 -2
  21. package/server/client/dist/assets/{Plus.es-P9Fjwtud.js → Plus.es-Dy6onD-Z.js} +1 -1
  22. package/server/client/dist/assets/Reminders-DnuALBL6.js +1 -0
  23. package/server/client/dist/assets/{Search-DVROxY_B.js → Search-B_BRjOyG.js} +2 -2
  24. package/server/client/dist/assets/{SurfaceCard-D_OU_Oy6.js → SurfaceCard-D5QxOte_.js} +1 -1
  25. package/server/client/dist/assets/Tag-2hcR9dd4.js +1 -0
  26. package/server/client/dist/assets/TagNotes-BE20Ji5s.js +1 -0
  27. package/server/client/dist/assets/{Trash.es-BeR1sZMe.js → Trash.es-XEcWG_aq.js} +1 -1
  28. package/server/client/dist/assets/{ViewChip-D-Ax5PdL.js → ViewChip-BVkZAXyx.js} +1 -1
  29. package/server/client/dist/assets/Views-CQXgnbUQ.js +1 -0
  30. package/server/client/dist/assets/app-4M3jUhmd.js +151 -0
  31. package/server/client/dist/assets/app-DjDwGe0t.css +1 -0
  32. package/server/client/dist/assets/calendar-data-wB0ifXdo.js +1 -0
  33. package/server/client/dist/assets/{image.api-CtfyPwkV.js → image.api-ChvlfuZg.js} +1 -1
  34. package/server/client/dist/assets/index-CaT-yZJw.js +1 -0
  35. package/server/client/dist/assets/{manage-image-m-7v_Z1W.js → manage-image-CSJ3Flgc.js} +1 -1
  36. package/server/client/dist/assets/manage-image-detail-Bu9yymBn.js +1 -0
  37. package/server/client/dist/assets/mcp-D8footel.js +5 -0
  38. package/server/client/dist/assets/{mcp-admin-adapter-Cw6w5mfg.js → mcp-admin-adapter-D0R8p9az.js} +1 -1
  39. package/server/client/dist/assets/{note-ui-CYsZc3DI.js → note-ui-BaqzP-YU.js} +1 -1
  40. package/server/client/dist/assets/placeholder-BZ-p6ahW.js +34 -0
  41. package/server/client/dist/assets/{properties-B93_V2uD.js → properties-B7Fv95qY.js} +1 -1
  42. package/server/client/dist/assets/route-preload.js +2 -2
  43. package/server/client/dist/assets/{search-admin-adapter-Dj0KKKrJ.js → search-admin-adapter-Daf7e2YQ.js} +1 -1
  44. package/server/client/dist/assets/search-ddYl3qH-.js +1 -0
  45. package/server/client/dist/assets/{tag.api-BJ6eaHui.js → tag.api-DEUROVKQ.js} +1 -1
  46. package/server/client/dist/assets/time-PxWila4-.js +361 -0
  47. package/server/client/dist/assets/trash-BHida7BG.js +1 -0
  48. package/server/client/dist/assets/{url-BvjSC8PU.js → url-DKUMNVCI.js} +1 -1
  49. package/server/client/dist/assets/useNoteMutate-BjvZUmtX.js +9 -0
  50. package/server/client/dist/assets/{useReminderMutate-kTBRSjYc.js → useReminderMutate-C5n-5LM-.js} +2 -2
  51. package/server/client/dist/index.html +6 -6
  52. package/server/dist/app.js +91 -8
  53. package/server/dist/features/auth/http/api.js +9 -8
  54. package/server/dist/features/auth/http/pages.js +27 -28
  55. package/server/dist/features/auth/service.js +7 -27
  56. package/server/dist/features/cache/graphql/cache.type-defs.js +1 -1
  57. package/server/dist/features/image/http/upload.js +2 -2
  58. package/server/dist/features/mcp-admin/http/handlers.js +9 -9
  59. package/server/dist/features/note/http/mcp.js +17 -17
  60. package/server/dist/features/note/services/properties.js +32 -8
  61. package/server/dist/features/search/http/handlers.js +10 -10
  62. package/server/dist/features/search/search-manager.js +34 -6
  63. package/server/dist/features/tag/http/mcp.js +2 -2
  64. package/server/dist/features/view/graphql/view.query.resolver.js +20 -0
  65. package/server/dist/features/view/graphql/view.type-defs.js +17 -0
  66. package/server/dist/features/view/services/workspace.js +116 -1
  67. package/server/dist/modules/auth-guard.js +36 -53
  68. package/server/dist/modules/blocknote.js +16 -9
  69. package/server/dist/modules/error-handler.js +17 -20
  70. package/server/dist/modules/mcp-auth.js +50 -56
  71. package/server/dist/modules/rate-limit.js +23 -23
  72. package/server/dist/modules/server-events-handler.js +15 -13
  73. package/server/dist/modules/session-store.js +6 -8
  74. package/server/dist/paths.js +2 -1
  75. package/server/dist/routes/api.js +105 -71
  76. package/server/dist/routes/auth-pages.js +21 -11
  77. package/server/dist/routes/client.js +60 -72
  78. package/server/dist/routes/graphql.js +31 -26
  79. package/server/dist/routes/mcp.js +25 -35
  80. package/server/dist/server.js +14 -16
  81. package/server/client/dist/assets/AuxiliaryPanelHeader-zoJGsYWR.js +0 -1
  82. package/server/client/dist/assets/Calendar-6otw_EJn.js +0 -25
  83. package/server/client/dist/assets/Callout-C1QiCWCa.js +0 -1
  84. package/server/client/dist/assets/Graph-BHsmPDPh.js +0 -46
  85. package/server/client/dist/assets/Reminders-DKm0GFI2.js +0 -1
  86. package/server/client/dist/assets/Tag-wMHTRwlF.js +0 -1
  87. package/server/client/dist/assets/TagNotes-BRza6i14.js +0 -1
  88. package/server/client/dist/assets/Views-CL7rC05G.js +0 -1
  89. package/server/client/dist/assets/app-CyQtGTtn.js +0 -139
  90. package/server/client/dist/assets/app-DGFNtdpS.css +0 -1
  91. package/server/client/dist/assets/index-BEfKfbvr.js +0 -1
  92. package/server/client/dist/assets/manage-image-detail-BNYOCSmu.js +0 -1
  93. package/server/client/dist/assets/mcp-B-aGeAQD.js +0 -4
  94. package/server/client/dist/assets/placeholder-C5fNzKrL.js +0 -34
  95. package/server/client/dist/assets/search-BmXuuK26.js +0 -1
  96. package/server/client/dist/assets/sortable.esm-DgSW3bAA.js +0 -5
  97. package/server/client/dist/assets/time-DI902Sxa.js +0 -361
  98. package/server/client/dist/assets/trash-GulvRh9f.js +0 -1
  99. package/server/client/dist/assets/useNoteMutate-xEIypxi5.js +0 -9
  100. package/server/dist/modules/logger.js +0 -53
  101. package/server/dist/modules/use-async.js +0 -10
@@ -70,33 +70,33 @@ const parseBaseUrl = (value) => {
70
70
  return baseUrl;
71
71
  };
72
72
  const createSearchAdminStatusHandler = (manager = getDefaultSemanticSearchManager()) => {
73
- return async (_req, res) => {
74
- res.status(200).json(await manager.getStatus()).end();
73
+ return async (_req, reply) => {
74
+ return reply.status(200).send(await manager.getStatus());
75
75
  };
76
76
  };
77
77
  const createSearchAdminSaveConfigHandler = (manager = getDefaultSemanticSearchManager()) => {
78
- return async (req, res) => {
78
+ return async (req, reply) => {
79
79
  const status = await manager.saveConfig(parseConfig(req.body), parseApiKeyInput(req.body));
80
- res.status(200).json(status).end();
80
+ return reply.status(200).send(status);
81
81
  };
82
82
  };
83
83
  const createSearchAdminTestConnectionHandler = (manager = getDefaultSemanticSearchManager()) => {
84
- return async (req, res) => {
84
+ return async (req, reply) => {
85
85
  const config = parseConfig({ ...req.body, enabled: true });
86
86
  const result = await manager.testConnection(config, parseApiKeyInput(req.body));
87
- res.status(200).json(result).end();
87
+ return reply.status(200).send(result);
88
88
  };
89
89
  };
90
90
  const createSearchAdminListModelsHandler = (listModels = (baseUrl, apiKeyInput) => getDefaultSemanticSearchManager().listModels(baseUrl, apiKeyInput)) => {
91
- return async (req, res) => {
91
+ return async (req, reply) => {
92
92
  const models = await listModels(parseBaseUrl(req.body), parseApiKeyInput(req.body));
93
- res.status(200).json({ models }).end();
93
+ return reply.status(200).send({ models });
94
94
  };
95
95
  };
96
96
  const createSearchAdminReindexHandler = (manager = getDefaultSemanticSearchManager()) => {
97
- return async (_req, res) => {
97
+ return async (_req, reply) => {
98
98
  const result = await manager.startReindex();
99
- res.status(result.started ? 202 : 200).json(result).end();
99
+ return reply.status(result.started ? 202 : 200).send(result);
100
100
  };
101
101
  };
102
102
  export {
@@ -355,6 +355,21 @@ class SemanticSearchManager {
355
355
  this.noteSyncReconciliationTimer = null;
356
356
  }
357
357
  }
358
+ async close() {
359
+ this.stopBackgroundSync();
360
+ const activeOperations = [];
361
+ if (this.activeReindex) {
362
+ activeOperations.push(this.activeReindex);
363
+ }
364
+ if (this.activeNoteSync) {
365
+ activeOperations.push(this.activeNoteSync);
366
+ }
367
+ if (this.activeReconciliation) {
368
+ activeOperations.push(this.activeReconciliation);
369
+ }
370
+ await Promise.allSettled(activeOperations);
371
+ await this.dependencies.vectorIndex.close?.();
372
+ }
358
373
  async trySearch(query, limit) {
359
374
  const status = await this.getStatus();
360
375
  if (!status.available) {
@@ -389,6 +404,7 @@ class SemanticSearchManager {
389
404
  }
390
405
  }
391
406
  let defaultSemanticSearchManager = null;
407
+ let defaultSemanticSearchUnsubscribers = [];
392
408
  const getDefaultSemanticSearchManager = () => {
393
409
  if (!defaultSemanticSearchManager) {
394
410
  defaultSemanticSearchManager = new SemanticSearchManager({
@@ -415,17 +431,29 @@ const getDefaultSemanticSearchManager = () => {
415
431
  }),
416
432
  apiKeyStore: new FileEmbeddingApiKeyStore(paths.embeddingApiKey)
417
433
  });
418
- subscribeServerEvents((event) => {
419
- void defaultSemanticSearchManager?.scheduleNoteSync(Number(event.noteId)).catch(() => void 0);
420
- });
421
- subscribeSemanticSearchNoteChanges((noteId) => {
422
- void defaultSemanticSearchManager?.scheduleNoteSync(noteId).catch(() => void 0);
423
- });
434
+ defaultSemanticSearchUnsubscribers = [
435
+ subscribeServerEvents((event) => {
436
+ void defaultSemanticSearchManager?.scheduleNoteSync(Number(event.noteId)).catch(() => void 0);
437
+ }),
438
+ subscribeSemanticSearchNoteChanges((noteId) => {
439
+ void defaultSemanticSearchManager?.scheduleNoteSync(noteId).catch(() => void 0);
440
+ })
441
+ ];
424
442
  defaultSemanticSearchManager.startBackgroundSync();
425
443
  }
426
444
  return defaultSemanticSearchManager;
427
445
  };
446
+ const closeDefaultSemanticSearchManager = async () => {
447
+ const manager = defaultSemanticSearchManager;
448
+ defaultSemanticSearchManager = null;
449
+ for (const unsubscribe of defaultSemanticSearchUnsubscribers) {
450
+ unsubscribe();
451
+ }
452
+ defaultSemanticSearchUnsubscribers = [];
453
+ await manager?.close();
454
+ };
428
455
  export {
429
456
  SemanticSearchManager,
457
+ closeDefaultSemanticSearchManager,
430
458
  getDefaultSemanticSearchManager
431
459
  };
@@ -1,14 +1,14 @@
1
1
  import { ensureTagByName, InvalidTagNameError } from "../../../features/tag/services/organization.js";
2
2
  import { createAppError } from "../../../modules/error-handler.js";
3
3
  const createMcpCreateTagHandler = (ensureTag = ensureTagByName) => {
4
- return async (req, res) => {
4
+ return async (req, reply) => {
5
5
  const name = req.body?.name;
6
6
  if (typeof name !== "string") {
7
7
  throw createAppError(400, "INVALID_TAG_NAME", "A tag name is required.");
8
8
  }
9
9
  try {
10
10
  const result = await ensureTag(name);
11
- res.status(200).json(result).end();
11
+ return reply.status(200).send(result);
12
12
  } catch (error) {
13
13
  if (error instanceof InvalidTagNameError) {
14
14
  throw createAppError(400, "INVALID_TAG_NAME", error.message);
@@ -4,6 +4,7 @@ import {
4
4
  getNotesByProperties,
5
5
  getViewSectionBoardColumn,
6
6
  getViewSectionById,
7
+ getViewSectionCalendarNotes,
7
8
  getViewSectionNotes,
8
9
  getViewWorkspace
9
10
  } from "../../../features/view/services/workspace.js";
@@ -11,6 +12,7 @@ const createViewQueryResolvers = (deps = {
11
12
  getNotesByProperties,
12
13
  getViewSectionBoardColumn,
13
14
  getViewSectionById,
15
+ getViewSectionCalendarNotes,
14
16
  getViewSectionNotes,
15
17
  getViewWorkspace
16
18
  }) => ({
@@ -42,6 +44,24 @@ const createViewQueryResolvers = (deps = {
42
44
  }
43
45
  return sectionNotes;
44
46
  },
47
+ viewSectionCalendarNotes: async (_, { id, dateRange }) => {
48
+ try {
49
+ const notes = await deps.getViewSectionCalendarNotes(id, dateRange);
50
+ if (!notes) {
51
+ throw "NOT FOUND";
52
+ }
53
+ return notes;
54
+ } catch (error) {
55
+ if (error instanceof InvalidNotePropertyInputError) {
56
+ throw new GraphQLError(error.message, {
57
+ extensions: {
58
+ code: "INVALID_NOTE_PROPERTY_INPUT"
59
+ }
60
+ });
61
+ }
62
+ throw error;
63
+ }
64
+ },
45
65
  viewSectionBoardColumn: async (_, {
46
66
  id,
47
67
  optionValue = null,
@@ -39,6 +39,8 @@ const viewType = gql`
39
39
  tableColumns: [ViewTableColumn!]!
40
40
  tablePropertyKeys: [String!]!
41
41
  boardGroupByPropertyKey: String
42
+ calendarDateField: ViewCalendarDateField!
43
+ calendarDatePropertyKey: String
42
44
  }
43
45
 
44
46
  type ViewBoardNote {
@@ -52,6 +54,12 @@ const viewType = gql`
52
54
  notes: [ViewBoardNote!]!
53
55
  }
54
56
 
57
+ type ViewCalendarNote {
58
+ id: ID!
59
+ title: String!
60
+ calendarDate: String!
61
+ }
62
+
55
63
  enum ViewDisplayType {
56
64
  list
57
65
  table
@@ -67,6 +75,12 @@ const viewType = gql`
67
75
  updatedAt
68
76
  }
69
77
 
78
+ enum ViewCalendarDateField {
79
+ createdAt
80
+ updatedAt
81
+ property
82
+ }
83
+
70
84
  enum ViewPropertyFilterOperator {
71
85
  equals
72
86
  notEquals
@@ -100,6 +114,8 @@ const viewType = gql`
100
114
  tableColumns: [ViewTableColumn!]
101
115
  tablePropertyKeys: [String!]
102
116
  boardGroupByPropertyKey: String
117
+ calendarDateField: ViewCalendarDateField
118
+ calendarDatePropertyKey: String
103
119
  }
104
120
 
105
121
  input ViewSectionInput {
@@ -132,6 +148,7 @@ const viewQuery = gql`
132
148
  sortBy: ViewSortBy
133
149
  sortOrder: ViewSortOrder
134
150
  ): Notes!
151
+ viewSectionCalendarNotes(id: ID!, dateRange: DateRangeInput!): [ViewCalendarNote!]!
135
152
  viewSectionBoardColumn(id: ID!, optionValue: String, pagination: PaginationInput): ViewBoardColumnNotes!
136
153
  notesByProperties(input: NotesByPropertiesInput!, pagination: PaginationInput): Notes!
137
154
  }
@@ -13,10 +13,12 @@ const DEFAULT_VIEW_NOTES_QUERY_LIMIT = 20;
13
13
  const MAX_VIEW_NOTES_QUERY_LIMIT = 50;
14
14
  const MAX_VIEW_PROPERTY_FILTERS = 10;
15
15
  const MAX_VIEW_TABLE_PROPERTY_COLUMNS = 6;
16
+ const MAX_VIEW_CALENDAR_RANGE_MS = 32 * 24 * 60 * 60 * 1e3;
16
17
  const DEFAULT_VIEW_DISPLAY_TYPE = "list";
17
18
  const DEFAULT_VIEW_SORT_BY = "updatedAt";
18
19
  const DEFAULT_VIEW_SORT_ORDER = "desc";
19
20
  const DEFAULT_VIEW_TABLE_COLUMNS = ["title", "tags", "properties", "createdAt", "updatedAt"];
21
+ const DEFAULT_VIEW_CALENDAR_DATE_FIELD = "createdAt";
20
22
  const orderedViewSectionTagsInclude = {
21
23
  tags: {
22
24
  orderBy: [{ order: "asc" }, { createdAt: "asc" }]
@@ -75,11 +77,15 @@ const normalizeViewTableColumns = (columns) => {
75
77
  return uniqueColumns.includes("title") ? uniqueColumns : ["title", ...uniqueColumns];
76
78
  };
77
79
  const normalizeViewTablePropertyKeys = (keys) => Array.from(new Set((keys ?? []).map(normalizePropertyKey))).slice(0, MAX_VIEW_TABLE_PROPERTY_COLUMNS);
80
+ const normalizeViewCalendarDateField = (value) => value === "updatedAt" || value === "property" ? value : DEFAULT_VIEW_CALENDAR_DATE_FIELD;
78
81
  const normalizeViewDisplayOptions = (options) => {
82
+ const calendarDateField = normalizeViewCalendarDateField(options?.calendarDateField);
79
83
  return {
80
84
  tableColumns: normalizeViewTableColumns(options?.tableColumns),
81
85
  tablePropertyKeys: normalizeViewTablePropertyKeys(options?.tablePropertyKeys),
82
- boardGroupByPropertyKey: options?.boardGroupByPropertyKey ? normalizePropertyKey(options.boardGroupByPropertyKey) : null
86
+ boardGroupByPropertyKey: options?.boardGroupByPropertyKey ? normalizePropertyKey(options.boardGroupByPropertyKey) : null,
87
+ calendarDateField,
88
+ calendarDatePropertyKey: calendarDateField === "property" && options?.calendarDatePropertyKey ? normalizePropertyKey(options.calendarDatePropertyKey) : null
83
89
  };
84
90
  };
85
91
  const normalizeFilterValue = ({
@@ -218,6 +224,9 @@ const normalizeViewSectionInput = (input) => {
218
224
  if (displayType === "board" && propertyFilters.some((filter) => filter.key === displayOptions.boardGroupByPropertyKey)) {
219
225
  throw new InvalidNotePropertyInputError("A board grouping property cannot also be used as a section filter.");
220
226
  }
227
+ if (displayType === "calendar" && displayOptions.calendarDateField === "property" && !displayOptions.calendarDatePropertyKey) {
228
+ throw new InvalidNotePropertyInputError("Calendar sections require a date property.");
229
+ }
221
230
  return {
222
231
  title: trimmedTitle || buildDefaultSectionTitle(tagNames, propertyFilters),
223
232
  displayType,
@@ -386,6 +395,19 @@ const buildStoredViewQuery = async (db, section) => {
386
395
  throw new InvalidNotePropertyInputError("Board grouping properties require at least one option.");
387
396
  }
388
397
  }
398
+ if (section.displayType === "calendar" && section.displayOptions.calendarDateField === "property") {
399
+ const calendarDatePropertyKey = section.displayOptions.calendarDatePropertyKey ?? "";
400
+ const calendarDateProperty = await db.propertyDefinition.findUnique({
401
+ where: { key: calendarDatePropertyKey },
402
+ select: { valueType: true }
403
+ });
404
+ if (!calendarDateProperty) {
405
+ throw new InvalidNotePropertyInputError(`Property ${calendarDatePropertyKey} is not defined.`);
406
+ }
407
+ if (calendarDateProperty.valueType !== "date") {
408
+ throw new InvalidNotePropertyInputError("Calendar sections can only use a date property.");
409
+ }
410
+ }
389
411
  return serializeStoredViewQuery({
390
412
  propertyFilters,
391
413
  sortBy: section.sortBy,
@@ -549,6 +571,38 @@ const buildViewNotesWhere = (query) => {
549
571
  const buildViewSectionWhere = (section) => {
550
572
  return buildViewNotesWhere(section);
551
573
  };
574
+ const normalizeViewCalendarDateRange = (dateRange) => {
575
+ const start = new Date(dateRange.start);
576
+ const end = new Date(dateRange.end);
577
+ const duration = end.getTime() - start.getTime();
578
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || duration <= 0 || duration > MAX_VIEW_CALENDAR_RANGE_MS) {
579
+ throw new InvalidNotePropertyInputError("Calendar date range must be valid and cover no more than 32 days.");
580
+ }
581
+ return { start, end };
582
+ };
583
+ const buildViewSectionCalendarWhere = (section, dateRange, propertyDefinitionId) => {
584
+ const sectionWhere = buildViewSectionWhere(section);
585
+ if (section.displayOptions.calendarDateField === "property" && propertyDefinitionId === void 0) {
586
+ throw new InvalidNotePropertyInputError("Calendar date property definition is required.");
587
+ }
588
+ const dateWhere = section.displayOptions.calendarDateField === "property" ? {
589
+ properties: {
590
+ some: {
591
+ propertyDefinitionId,
592
+ dateValue: {
593
+ gte: dateRange.start,
594
+ lt: dateRange.end
595
+ }
596
+ }
597
+ }
598
+ } : {
599
+ [section.displayOptions.calendarDateField]: {
600
+ gte: dateRange.start,
601
+ lt: dateRange.end
602
+ }
603
+ };
604
+ return Object.keys(sectionWhere).length === 0 ? dateWhere : { AND: [sectionWhere, dateWhere] };
605
+ };
552
606
  const buildViewBoardColumnWhere = (section, propertyDefinitionId, optionId) => {
553
607
  const sectionWhere = buildViewSectionWhere(section);
554
608
  const columnWhere = optionId === null ? {
@@ -596,6 +650,63 @@ const getViewSectionNotes = async (id, pagination, sort = {}) => {
596
650
  notes
597
651
  };
598
652
  };
653
+ const getViewSectionCalendarNotes = async (id, dateRangeInput) => {
654
+ const section = await models.viewSection.findUnique({
655
+ where: { id: parseViewId(id) },
656
+ include: orderedViewSectionTagsInclude
657
+ });
658
+ if (!section) {
659
+ return null;
660
+ }
661
+ const serializedSection = serializeViewSection(section);
662
+ if (serializedSection.displayType !== "calendar") {
663
+ throw new InvalidNotePropertyInputError("This section is not configured as a calendar.");
664
+ }
665
+ const dateRange = normalizeViewCalendarDateRange(dateRangeInput);
666
+ const dateField = serializedSection.displayOptions.calendarDateField;
667
+ if (dateField === "property") {
668
+ const propertyKey = serializedSection.displayOptions.calendarDatePropertyKey;
669
+ const propertyDefinition = propertyKey ? await models.propertyDefinition.findUnique({
670
+ where: { key: propertyKey },
671
+ select: { id: true, valueType: true }
672
+ }) : null;
673
+ if (!propertyDefinition || propertyDefinition.valueType !== "date") {
674
+ throw new InvalidNotePropertyInputError("The calendar date property is unavailable.");
675
+ }
676
+ const notes2 = await models.note.findMany({
677
+ orderBy: [{ id: "asc" }],
678
+ where: buildViewSectionCalendarWhere(serializedSection, dateRange, propertyDefinition.id),
679
+ select: {
680
+ id: true,
681
+ title: true,
682
+ properties: {
683
+ where: { propertyDefinitionId: propertyDefinition.id },
684
+ select: { dateValue: true },
685
+ take: 1
686
+ }
687
+ }
688
+ });
689
+ return notes2.flatMap((note) => {
690
+ const dateValue = note.properties[0]?.dateValue;
691
+ return dateValue ? [{ id: note.id, title: note.title, calendarDate: dateValue.toISOString().slice(0, 10) }] : [];
692
+ }).sort((left, right) => left.calendarDate.localeCompare(right.calendarDate) || left.id - right.id);
693
+ }
694
+ const notes = await models.note.findMany({
695
+ orderBy: [{ [dateField]: "asc" }, { id: "asc" }],
696
+ where: buildViewSectionCalendarWhere(serializedSection, dateRange),
697
+ select: {
698
+ id: true,
699
+ title: true,
700
+ createdAt: true,
701
+ updatedAt: true
702
+ }
703
+ });
704
+ return notes.map((note) => ({
705
+ id: note.id,
706
+ title: note.title,
707
+ calendarDate: note[dateField].toISOString()
708
+ }));
709
+ };
599
710
  const getViewSectionBoardColumn = async (id, optionValue, pagination) => {
600
711
  const section = await models.viewSection.findUnique({
601
712
  where: { id: parseViewId(id) },
@@ -888,6 +999,7 @@ export {
888
999
  buildPropertyFilterWhere,
889
1000
  buildViewBoardColumnWhere,
890
1001
  buildViewNotesWhere,
1002
+ buildViewSectionCalendarWhere,
891
1003
  buildViewSectionWhere,
892
1004
  clampViewSectionLimit,
893
1005
  createViewSection,
@@ -898,9 +1010,12 @@ export {
898
1010
  getNotesByPropertiesWithDb,
899
1011
  getViewSectionBoardColumn,
900
1012
  getViewSectionById,
1013
+ getViewSectionCalendarNotes,
901
1014
  getViewSectionNotes,
902
1015
  getViewWorkspace,
903
1016
  hydratePropertyFilters,
1017
+ normalizeViewCalendarDateField,
1018
+ normalizeViewCalendarDateRange,
904
1019
  normalizeViewDisplayOptions,
905
1020
  normalizeViewNotesPagination,
906
1021
  normalizeViewNotesQueryInput,
@@ -1,76 +1,57 @@
1
1
  import { buildUnauthorizedGraphqlPayload, buildUnauthorizedPayload } from "@baejino/auth";
2
- import session from "express-session";
3
2
  import { GraphQLError } from "graphql";
4
- import lusca from "lusca";
5
3
  import { sanitizeRedirectPath } from "./auth-redirect.js";
6
- import { AUTH_SESSION_IDLE_TIMEOUT_MS, createSessionStore } from "./session-store.js";
7
4
  const JSON_HEADERS = { "Content-Type": "application/json" };
8
- const isAuthenticatedRequest = (req) => Boolean(req.session?.authenticated);
9
- const createSessionMiddleware = (authConfig) => {
5
+ const CSRF_COOKIE_NAME = "XSRF-TOKEN";
6
+ const isAuthenticatedRequest = (request) => Boolean(request.session?.authenticated);
7
+ const issueCsrfToken = (authConfig, reply) => {
10
8
  if (authConfig.mode !== "password") {
11
- return (_req, _res, next) => next();
9
+ return void 0;
12
10
  }
13
- return session({
14
- secret: authConfig.sessionSecret,
15
- name: authConfig.cookieName,
16
- store: createSessionStore(),
17
- resave: false,
18
- saveUninitialized: false,
19
- rolling: true,
20
- cookie: {
21
- maxAge: AUTH_SESSION_IDLE_TIMEOUT_MS,
22
- httpOnly: true,
23
- sameSite: "lax",
24
- secure: process.env.NODE_ENV === "production",
25
- path: "/"
26
- }
11
+ const token = reply.generateCsrf();
12
+ reply.setCookie(CSRF_COOKIE_NAME, token, {
13
+ httpOnly: false,
14
+ path: "/",
15
+ sameSite: "lax",
16
+ secure: process.env.NODE_ENV === "production"
27
17
  });
18
+ return token;
28
19
  };
29
20
  const createCsrfProtection = (authConfig) => {
30
21
  if (authConfig.mode !== "password") {
31
- return (_req, _res, next) => next();
22
+ return (_request, _reply, done) => done();
32
23
  }
33
- return lusca.csrf({
34
- angular: true,
35
- cookie: {
36
- options: {
37
- path: "/",
38
- sameSite: "lax",
39
- secure: process.env.NODE_ENV === "production"
40
- }
41
- }
42
- });
43
- };
44
- const isCsrfTokenError = (error) => error instanceof Error && error.message.startsWith("CSRF token ");
45
- const buildLoginRedirectPath = (req) => {
46
- const nextPath = sanitizeRedirectPath(req.body?.next);
47
- return `/login?next=${encodeURIComponent(nextPath)}`;
48
- };
49
- const createLoginCsrfFailureHandler = (authConfig) => {
50
- return (error, req, res, next) => {
51
- if (authConfig.mode !== "password" || !isCsrfTokenError(error) || req.method !== "POST" || req.path !== "/login" || isAuthenticatedRequest(req)) {
52
- next(error);
24
+ return (request, reply, done) => {
25
+ if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS") {
26
+ done();
53
27
  return;
54
28
  }
55
- res.redirect(303, buildLoginRedirectPath(req));
29
+ request.server.csrfProtection(request, reply, done);
56
30
  };
57
31
  };
32
+ const isCsrfTokenError = (error) => error instanceof Error && ("code" in error ? error.code === "FST_CSRF_INVALID_TOKEN" || error.code === "FST_CSRF_MISSING_SECRET" : error.message.toLowerCase().includes("csrf"));
33
+ const buildLoginCsrfRedirectPath = (request) => {
34
+ const body = request.body;
35
+ const nextPath = sanitizeRedirectPath(body?.next);
36
+ return `/login?next=${encodeURIComponent(nextPath)}`;
37
+ };
38
+ const shouldRedirectLoginCsrfFailure = (error, request, authConfig) => authConfig.mode === "password" && isCsrfTokenError(error) && request.method === "POST" && request.url.split("?")[0] === "/login" && !isAuthenticatedRequest(request);
58
39
  const requireSessionForWrite = (authConfig) => {
59
- return (req, res, next) => {
60
- if (authConfig.mode === "open" || isAuthenticatedRequest(req)) {
61
- next();
40
+ return (request, reply, done) => {
41
+ if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
42
+ done();
62
43
  return;
63
44
  }
64
- res.status(401).set(JSON_HEADERS).json(buildUnauthorizedPayload()).end();
45
+ void reply.code(401).headers(JSON_HEADERS).send(buildUnauthorizedPayload());
65
46
  };
66
47
  };
67
48
  const requireSessionForGraphql = (authConfig) => {
68
- return (req, res, next) => {
69
- if (authConfig.mode === "open" || isAuthenticatedRequest(req)) {
70
- next();
49
+ return (request, reply, done) => {
50
+ if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
51
+ done();
71
52
  return;
72
53
  }
73
- res.status(401).set(JSON_HEADERS).json(buildUnauthorizedGraphqlPayload()).end();
54
+ void reply.code(401).headers(JSON_HEADERS).send(buildUnauthorizedGraphqlPayload());
74
55
  };
75
56
  };
76
57
  const createMutationAuthValidationRule = () => {
@@ -94,11 +75,13 @@ const createMutationAuthValidationRule = () => {
94
75
  };
95
76
  };
96
77
  export {
78
+ buildLoginCsrfRedirectPath,
97
79
  createCsrfProtection,
98
- createLoginCsrfFailureHandler,
99
80
  createMutationAuthValidationRule,
100
- createSessionMiddleware,
101
81
  isAuthenticatedRequest,
82
+ isCsrfTokenError,
83
+ issueCsrfToken,
102
84
  requireSessionForGraphql,
103
- requireSessionForWrite
85
+ requireSessionForWrite,
86
+ shouldRedirectLoginCsrfFailure
104
87
  };
@@ -83,10 +83,12 @@ function mapBlockContent(content, mapInline) {
83
83
  ...content,
84
84
  rows: content.rows?.map((row) => ({
85
85
  ...row,
86
- cells: row.cells?.map((cell) => ({
87
- ...cell,
88
- content: cell.content?.map(mapInline)
89
- }))
86
+ cells: row.cells?.map(
87
+ (cell) => Array.isArray(cell) ? cell.map(mapInline) : {
88
+ ...cell,
89
+ content: cell.content?.map(mapInline)
90
+ }
91
+ )
90
92
  }))
91
93
  };
92
94
  }
@@ -103,10 +105,15 @@ async function mapBlockContentAsync(content, mapInline) {
103
105
  (content.rows ?? []).map(async (row) => ({
104
106
  ...row,
105
107
  cells: await Promise.all(
106
- (row.cells ?? []).map(async (cell) => ({
107
- ...cell,
108
- content: cell.content ? (await Promise.all(cell.content.map(mapInline))).flat() : cell.content
109
- }))
108
+ (row.cells ?? []).map(async (cell) => {
109
+ if (Array.isArray(cell)) {
110
+ return (await Promise.all(cell.map(mapInline))).flat();
111
+ }
112
+ return {
113
+ ...cell,
114
+ content: cell.content ? (await Promise.all(cell.content.map(mapInline))).flat() : cell.content
115
+ };
116
+ })
110
117
  )
111
118
  }))
112
119
  )
@@ -124,7 +131,7 @@ function visitBlockContent(content, visitInline) {
124
131
  }
125
132
  for (const row of content.rows ?? []) {
126
133
  for (const cell of row.cells ?? []) {
127
- for (const inline of cell.content ?? []) {
134
+ for (const inline of Array.isArray(cell) ? cell : cell.content ?? []) {
128
135
  visitInline(inline);
129
136
  }
130
137
  }
@@ -1,3 +1,4 @@
1
+ import { buildLoginCsrfRedirectPath, isCsrfTokenError, shouldRedirectLoginCsrfFailure } from "./auth-guard.js";
1
2
  class AppError extends Error {
2
3
  code;
3
4
  status;
@@ -13,42 +14,38 @@ class AppError extends Error {
13
14
  const createAppError = (status, code, message, details) => {
14
15
  return new AppError(status, code, message, details);
15
16
  };
16
- const hasErrorType = (error, type) => error instanceof Error && "type" in error && error.type === type;
17
- const createErrorHandler = () => {
18
- return (error, _req, res, next) => {
19
- if (res.headersSent) {
20
- next(error);
17
+ const createErrorHandler = (authConfig) => {
18
+ return (error, request, reply) => {
19
+ if (reply.sent) {
20
+ request.log.error({ error }, "Error occurred after the response was sent");
21
+ return;
22
+ }
23
+ if (shouldRedirectLoginCsrfFailure(error, request, authConfig)) {
24
+ void reply.redirect(buildLoginCsrfRedirectPath(request), 303);
21
25
  return;
22
26
  }
23
27
  if (error instanceof AppError) {
24
- res.status(error.status).json({
28
+ void reply.status(error.status).send({
25
29
  code: error.code,
26
30
  message: error.message,
27
31
  ...error.details ? { details: error.details } : {}
28
- }).end();
29
- return;
30
- }
31
- if (hasErrorType(error, "encoding.unsupported")) {
32
- res.status(415).json({
33
- code: "UNSUPPORTED_CONTENT_ENCODING",
34
- message: "Compressed request bodies are not supported."
35
- }).end();
32
+ });
36
33
  return;
37
34
  }
38
- if (error instanceof Error && error.message.startsWith("CSRF token ")) {
39
- res.status(403).json({
35
+ if (isCsrfTokenError(error)) {
36
+ void reply.status(403).send({
40
37
  code: "CSRF_TOKEN_INVALID",
41
- message: error.message
42
- }).end();
38
+ message: error instanceof Error ? error.message : "Invalid CSRF token"
39
+ });
43
40
  return;
44
41
  }
45
42
  const message = error instanceof Error ? error.stack || error.message : String(error);
46
43
  process.stderr.write(`[error] ${message}
47
44
  `);
48
- res.status(500).json({
45
+ void reply.status(500).send({
49
46
  code: "INTERNAL_SERVER_ERROR",
50
47
  message: "Internal Server Error"
51
- }).end();
48
+ });
52
49
  };
53
50
  };
54
51
  export {