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.
- package/README.md +5 -3
- package/dist/index.js +12 -4
- package/package.json +10 -8
- package/server/client/dist/assets/{ArrowRight.es-CD1IMhU2.js → ArrowRight.es-BlNsZQFs.js} +1 -1
- package/server/client/dist/assets/AuxiliaryPanelHeader-HAttwVby.js +1 -0
- package/server/client/dist/assets/Calendar-DExFS7xq.js +25 -0
- package/server/client/dist/assets/Callout-CcYyvHQP.js +1 -0
- package/server/client/dist/assets/CaretUp.es-C8iY5OlB.js +1 -0
- package/server/client/dist/assets/{CheckCircle.es-C7fkzE3t.js → CheckCircle.es-Dqr3WImn.js} +1 -1
- package/server/client/dist/assets/{Copy.es-D6rl6b78.js → Copy.es-fMv0ETir.js} +1 -1
- package/server/client/dist/assets/{File.es-CcjtzE38.js → File.es-CHtXXafR.js} +1 -1
- package/server/client/dist/assets/FileText.es-DPIwDCEe.js +1 -0
- package/server/client/dist/assets/{FlowArrow.es-CAIThqWk.js → FlowArrow.es-Dxee98ir.js} +1 -1
- package/server/client/dist/assets/Graph-CdEu8eAn.js +46 -0
- package/server/client/dist/assets/Graph-hVj6vAYJ.js +1 -0
- package/server/client/dist/assets/{Image.es-BDpo5R66.js → Image.es-CjXY1JVf.js} +1 -1
- package/server/client/dist/assets/Info.es-CYxg-Vku.js +1 -0
- package/server/client/dist/assets/LinkSimple.es-B-P-7Iem.js +1 -0
- package/server/client/dist/assets/{Note-Dq2Szaxd.js → Note-CGI-Y1__.js} +4 -4
- package/server/client/dist/assets/{NotePropertiesPanel-Cu3RgDic.js → NotePropertiesPanel-6P7UfRBR.js} +2 -2
- package/server/client/dist/assets/{Plus.es-P9Fjwtud.js → Plus.es-Dy6onD-Z.js} +1 -1
- package/server/client/dist/assets/Reminders-DnuALBL6.js +1 -0
- package/server/client/dist/assets/{Search-DVROxY_B.js → Search-B_BRjOyG.js} +2 -2
- package/server/client/dist/assets/{SurfaceCard-D_OU_Oy6.js → SurfaceCard-D5QxOte_.js} +1 -1
- package/server/client/dist/assets/Tag-2hcR9dd4.js +1 -0
- package/server/client/dist/assets/TagNotes-BE20Ji5s.js +1 -0
- package/server/client/dist/assets/{Trash.es-BeR1sZMe.js → Trash.es-XEcWG_aq.js} +1 -1
- package/server/client/dist/assets/{ViewChip-D-Ax5PdL.js → ViewChip-BVkZAXyx.js} +1 -1
- package/server/client/dist/assets/Views-CQXgnbUQ.js +1 -0
- package/server/client/dist/assets/app-4M3jUhmd.js +151 -0
- package/server/client/dist/assets/app-DjDwGe0t.css +1 -0
- package/server/client/dist/assets/calendar-data-wB0ifXdo.js +1 -0
- package/server/client/dist/assets/{image.api-CtfyPwkV.js → image.api-ChvlfuZg.js} +1 -1
- package/server/client/dist/assets/index-CaT-yZJw.js +1 -0
- package/server/client/dist/assets/{manage-image-m-7v_Z1W.js → manage-image-CSJ3Flgc.js} +1 -1
- package/server/client/dist/assets/manage-image-detail-Bu9yymBn.js +1 -0
- package/server/client/dist/assets/mcp-D8footel.js +5 -0
- package/server/client/dist/assets/{mcp-admin-adapter-Cw6w5mfg.js → mcp-admin-adapter-D0R8p9az.js} +1 -1
- package/server/client/dist/assets/{note-ui-CYsZc3DI.js → note-ui-BaqzP-YU.js} +1 -1
- package/server/client/dist/assets/placeholder-BZ-p6ahW.js +34 -0
- package/server/client/dist/assets/{properties-B93_V2uD.js → properties-B7Fv95qY.js} +1 -1
- package/server/client/dist/assets/route-preload.js +2 -2
- package/server/client/dist/assets/{search-admin-adapter-Dj0KKKrJ.js → search-admin-adapter-Daf7e2YQ.js} +1 -1
- package/server/client/dist/assets/search-ddYl3qH-.js +1 -0
- package/server/client/dist/assets/{tag.api-BJ6eaHui.js → tag.api-DEUROVKQ.js} +1 -1
- package/server/client/dist/assets/time-PxWila4-.js +361 -0
- package/server/client/dist/assets/trash-BHida7BG.js +1 -0
- package/server/client/dist/assets/{url-BvjSC8PU.js → url-DKUMNVCI.js} +1 -1
- package/server/client/dist/assets/useNoteMutate-BjvZUmtX.js +9 -0
- package/server/client/dist/assets/{useReminderMutate-kTBRSjYc.js → useReminderMutate-C5n-5LM-.js} +2 -2
- package/server/client/dist/index.html +6 -6
- package/server/dist/app.js +91 -8
- package/server/dist/features/auth/http/api.js +9 -8
- package/server/dist/features/auth/http/pages.js +27 -28
- package/server/dist/features/auth/service.js +7 -27
- package/server/dist/features/cache/graphql/cache.type-defs.js +1 -1
- package/server/dist/features/image/http/upload.js +2 -2
- package/server/dist/features/mcp-admin/http/handlers.js +9 -9
- package/server/dist/features/note/http/mcp.js +17 -17
- package/server/dist/features/note/services/properties.js +32 -8
- package/server/dist/features/search/http/handlers.js +10 -10
- package/server/dist/features/search/search-manager.js +34 -6
- package/server/dist/features/tag/http/mcp.js +2 -2
- package/server/dist/features/view/graphql/view.query.resolver.js +20 -0
- package/server/dist/features/view/graphql/view.type-defs.js +17 -0
- package/server/dist/features/view/services/workspace.js +116 -1
- package/server/dist/modules/auth-guard.js +36 -53
- package/server/dist/modules/blocknote.js +16 -9
- package/server/dist/modules/error-handler.js +17 -20
- package/server/dist/modules/mcp-auth.js +50 -56
- package/server/dist/modules/rate-limit.js +23 -23
- package/server/dist/modules/server-events-handler.js +15 -13
- package/server/dist/modules/session-store.js +6 -8
- package/server/dist/paths.js +2 -1
- package/server/dist/routes/api.js +105 -71
- package/server/dist/routes/auth-pages.js +21 -11
- package/server/dist/routes/client.js +60 -72
- package/server/dist/routes/graphql.js +31 -26
- package/server/dist/routes/mcp.js +25 -35
- package/server/dist/server.js +14 -16
- package/server/client/dist/assets/AuxiliaryPanelHeader-zoJGsYWR.js +0 -1
- package/server/client/dist/assets/Calendar-6otw_EJn.js +0 -25
- package/server/client/dist/assets/Callout-C1QiCWCa.js +0 -1
- package/server/client/dist/assets/Graph-BHsmPDPh.js +0 -46
- package/server/client/dist/assets/Reminders-DKm0GFI2.js +0 -1
- package/server/client/dist/assets/Tag-wMHTRwlF.js +0 -1
- package/server/client/dist/assets/TagNotes-BRza6i14.js +0 -1
- package/server/client/dist/assets/Views-CL7rC05G.js +0 -1
- package/server/client/dist/assets/app-CyQtGTtn.js +0 -139
- package/server/client/dist/assets/app-DGFNtdpS.css +0 -1
- package/server/client/dist/assets/index-BEfKfbvr.js +0 -1
- package/server/client/dist/assets/manage-image-detail-BNYOCSmu.js +0 -1
- package/server/client/dist/assets/mcp-B-aGeAQD.js +0 -4
- package/server/client/dist/assets/placeholder-C5fNzKrL.js +0 -34
- package/server/client/dist/assets/search-BmXuuK26.js +0 -1
- package/server/client/dist/assets/sortable.esm-DgSW3bAA.js +0 -5
- package/server/client/dist/assets/time-DI902Sxa.js +0 -361
- package/server/client/dist/assets/trash-GulvRh9f.js +0 -1
- package/server/client/dist/assets/useNoteMutate-xEIypxi5.js +0 -9
- package/server/dist/modules/logger.js +0 -53
- 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,
|
|
74
|
-
|
|
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,
|
|
78
|
+
return async (req, reply) => {
|
|
79
79
|
const status = await manager.saveConfig(parseConfig(req.body), parseApiKeyInput(req.body));
|
|
80
|
-
|
|
80
|
+
return reply.status(200).send(status);
|
|
81
81
|
};
|
|
82
82
|
};
|
|
83
83
|
const createSearchAdminTestConnectionHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
84
|
-
return async (req,
|
|
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
|
-
|
|
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,
|
|
91
|
+
return async (req, reply) => {
|
|
92
92
|
const models = await listModels(parseBaseUrl(req.body), parseApiKeyInput(req.body));
|
|
93
|
-
|
|
93
|
+
return reply.status(200).send({ models });
|
|
94
94
|
};
|
|
95
95
|
};
|
|
96
96
|
const createSearchAdminReindexHandler = (manager = getDefaultSemanticSearchManager()) => {
|
|
97
|
-
return async (_req,
|
|
97
|
+
return async (_req, reply) => {
|
|
98
98
|
const result = await manager.startReindex();
|
|
99
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
9
|
-
const
|
|
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
|
|
9
|
+
return void 0;
|
|
12
10
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 (
|
|
22
|
+
return (_request, _reply, done) => done();
|
|
32
23
|
}
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 (
|
|
60
|
-
if (authConfig.mode === "open" || isAuthenticatedRequest(
|
|
61
|
-
|
|
40
|
+
return (request, reply, done) => {
|
|
41
|
+
if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
|
|
42
|
+
done();
|
|
62
43
|
return;
|
|
63
44
|
}
|
|
64
|
-
|
|
45
|
+
void reply.code(401).headers(JSON_HEADERS).send(buildUnauthorizedPayload());
|
|
65
46
|
};
|
|
66
47
|
};
|
|
67
48
|
const requireSessionForGraphql = (authConfig) => {
|
|
68
|
-
return (
|
|
69
|
-
if (authConfig.mode === "open" || isAuthenticatedRequest(
|
|
70
|
-
|
|
49
|
+
return (request, reply, done) => {
|
|
50
|
+
if (authConfig.mode === "open" || isAuthenticatedRequest(request)) {
|
|
51
|
+
done();
|
|
71
52
|
return;
|
|
72
53
|
}
|
|
73
|
-
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
|
39
|
-
|
|
35
|
+
if (isCsrfTokenError(error)) {
|
|
36
|
+
void reply.status(403).send({
|
|
40
37
|
code: "CSRF_TOKEN_INVALID",
|
|
41
|
-
message: error.message
|
|
42
|
-
})
|
|
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
|
-
|
|
45
|
+
void reply.status(500).send({
|
|
49
46
|
code: "INTERNAL_SERVER_ERROR",
|
|
50
47
|
message: "Internal Server Error"
|
|
51
|
-
})
|
|
48
|
+
});
|
|
52
49
|
};
|
|
53
50
|
};
|
|
54
51
|
export {
|