ocean-brain 0.5.0 → 0.6.0

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/dist/mcp.js +660 -31
  2. package/package.json +2 -1
  3. package/server/client/dist/assets/{Calendar-BU9XgX2N.js → Calendar-BytW4PeT.js} +1 -1
  4. package/server/client/dist/assets/{Callout-C6tVCxOT.js → Callout-9sJgGNRb.js} +1 -1
  5. package/server/client/dist/assets/Graph-o5u-NzrA.js +1 -0
  6. package/server/client/dist/assets/{Image.es-BjGK7y6Y.js → Image.es-B0LL3pb7.js} +1 -1
  7. package/server/client/dist/assets/Note-CYoYzZNu.js +21 -0
  8. package/server/client/dist/assets/{Plus.es-EcmtS_uw.js → Plus.es-DpxzgRtY.js} +1 -1
  9. package/server/client/dist/assets/Reminders-DAepTZg1.js +1 -0
  10. package/server/client/dist/assets/Search-6KH7An44.js +1 -0
  11. package/server/client/dist/assets/{SurfaceCard-CO0oVxeL.js → SurfaceCard-EMs50xSF.js} +1 -1
  12. package/server/client/dist/assets/Tag-C89j8x56.js +1 -0
  13. package/server/client/dist/assets/TagNotes-BTbu-f6v.js +1 -0
  14. package/server/client/dist/assets/{Trash.es-DmwfSy3l.js → Trash.es-C5x35NKF.js} +1 -1
  15. package/server/client/dist/assets/ViewNotes-DPEuki3y.js +1 -0
  16. package/server/client/dist/assets/Views-BGkuNhVd.js +1 -0
  17. package/server/client/dist/assets/{image.api-DeF9ck7g.js → image.api-DRhRQ0Gf.js} +1 -1
  18. package/server/client/dist/assets/{index-BZUGWdOU.css → index-Cd81UxEh.css} +1 -1
  19. package/server/client/dist/assets/{index-BvKBTjQq.js → index-DYlYUrjH.js} +118 -37
  20. package/server/client/dist/assets/index-DswSFFMb.js +1 -0
  21. package/server/client/dist/assets/manage-image-C6sHhWnI.js +1 -0
  22. package/server/client/dist/assets/manage-image-detail-CJkfybR9.js +1 -0
  23. package/server/client/dist/assets/{mcp-Bh743HBz.js → mcp-piouFDNm.js} +1 -1
  24. package/server/client/dist/assets/{placeholder-XQqo1Fvr.js → placeholder-DGz3CHej.js} +5 -5
  25. package/server/client/dist/assets/properties-CM4khczg.js +1 -0
  26. package/server/client/dist/assets/trash-C2lItz-b.js +1 -0
  27. package/server/client/dist/assets/{useReminderMutate-D21PurIS.js → useReminderMutate-DHjDDHKg.js} +1 -1
  28. package/server/client/dist/assets/view-dashboard-C17WsdL7.js +99 -0
  29. package/server/client/dist/index.html +2 -2
  30. package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
  31. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
  32. package/server/dist/features/note/graphql/note.mutation.resolver.js +119 -0
  33. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
  34. package/server/dist/features/note/graphql/note.query.resolver.js +14 -0
  35. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  36. package/server/dist/features/note/graphql/note.type-defs.js +100 -0
  37. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  38. package/server/dist/features/note/http/mcp.js +394 -1
  39. package/server/dist/features/note/http/mcp.js.map +1 -1
  40. package/server/dist/features/note/services/markdown-intent-write.js +478 -0
  41. package/server/dist/features/note/services/markdown-intent-write.js.map +1 -0
  42. package/server/dist/features/note/services/markdown-patch.js +601 -0
  43. package/server/dist/features/note/services/markdown-patch.js.map +1 -0
  44. package/server/dist/features/note/services/properties.js +874 -0
  45. package/server/dist/features/note/services/properties.js.map +1 -0
  46. package/server/dist/features/note/services/snapshot.js +94 -12
  47. package/server/dist/features/note/services/snapshot.js.map +1 -1
  48. package/server/dist/features/note/services/trash.js +70 -2
  49. package/server/dist/features/note/services/trash.js.map +1 -1
  50. package/server/dist/features/note/services/write.js +21 -7
  51. package/server/dist/features/note/services/write.js.map +1 -1
  52. package/server/dist/features/view/graphql/view.query.resolver.js +31 -1
  53. package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -1
  54. package/server/dist/features/view/graphql/view.type-defs.js +57 -1
  55. package/server/dist/features/view/graphql/view.type-defs.js.map +1 -1
  56. package/server/dist/features/view/services/workspace.js +367 -43
  57. package/server/dist/features/view/services/workspace.js.map +1 -1
  58. package/server/dist/modules/blocknote.js +47 -2
  59. package/server/dist/modules/blocknote.js.map +1 -1
  60. package/server/dist/routes/mcp.js +21 -1
  61. package/server/dist/routes/mcp.js.map +1 -1
  62. package/server/prisma/migrations/20260527073000_0017_note_properties/migration.sql +105 -0
  63. package/server/prisma/migrations/20260531073000_0018_view_query/migration.sql +3 -0
  64. package/server/prisma/schema.prisma +226 -130
  65. package/server/client/dist/assets/Graph-Dond2hGK.js +0 -1
  66. package/server/client/dist/assets/Note-Cr6GtXUM.js +0 -21
  67. package/server/client/dist/assets/Reminders-BlLLT_x0.js +0 -1
  68. package/server/client/dist/assets/Search-CrZ0ap8x.js +0 -1
  69. package/server/client/dist/assets/Tag-vws5AfNW.js +0 -1
  70. package/server/client/dist/assets/TagNotes-Fmf1Kgim.js +0 -1
  71. package/server/client/dist/assets/ViewNotes-BALN7IfH.js +0 -1
  72. package/server/client/dist/assets/Views-Ccc_Ml5U.js +0 -1
  73. package/server/client/dist/assets/index-BSVE2cGd.js +0 -1
  74. package/server/client/dist/assets/manage-image-IoTXaMBP.js +0 -1
  75. package/server/client/dist/assets/manage-image-detail-BRTXwOsh.js +0 -1
  76. package/server/client/dist/assets/trash-hiJIg6-c.js +0 -1
  77. package/server/client/dist/assets/view-dashboard-BuaZ4MvI.js +0 -90
@@ -1,9 +1,20 @@
1
+ import {
2
+ InvalidNotePropertyInputError,
3
+ normalizePropertyKey,
4
+ normalizeUrlValue
5
+ } from "../../../features/note/services/properties.js";
1
6
  import { buildNoteTagNamesWhere } from "../../../features/note/services/tag-filter.js";
2
7
  import models from "../../../models.js";
3
8
  const VIEW_WORKSPACE_ID = 1;
4
9
  const DEFAULT_VIEW_SECTION_LIMIT = 5;
5
10
  const MIN_VIEW_SECTION_LIMIT = 1;
6
11
  const MAX_VIEW_SECTION_LIMIT = 20;
12
+ const DEFAULT_VIEW_NOTES_QUERY_LIMIT = 20;
13
+ const MAX_VIEW_NOTES_QUERY_LIMIT = 50;
14
+ const MAX_VIEW_PROPERTY_FILTERS = 10;
15
+ const DEFAULT_VIEW_DISPLAY_TYPE = "list";
16
+ const DEFAULT_VIEW_SORT_BY = "updatedAt";
17
+ const DEFAULT_VIEW_SORT_ORDER = "desc";
7
18
  const orderedViewSectionTagsInclude = {
8
19
  tags: {
9
20
  orderBy: [{ order: "asc" }, { createdAt: "asc" }]
@@ -32,6 +43,116 @@ const normalizeViewTagNames = (values) => {
32
43
  const normalizedTagNames = values.flatMap((value) => value.split(","));
33
44
  return Array.from(new Set(normalizedTagNames.map(normalizeTagName).filter(Boolean)));
34
45
  };
46
+ const isViewPropertyFilterOperator = (value) => {
47
+ return value === "equals" || value === "before" || value === "after" || value === "exists" || value === "notExists";
48
+ };
49
+ const isPropertyValueType = (value) => {
50
+ return value === "text" || value === "url" || value === "number" || value === "date" || value === "boolean" || value === "select";
51
+ };
52
+ const normalizeViewDisplayType = (value) => {
53
+ return value === "calendar" ? "calendar" : DEFAULT_VIEW_DISPLAY_TYPE;
54
+ };
55
+ const normalizeViewSortBy = (value) => {
56
+ return value === "createdAt" || value === "title" ? value : DEFAULT_VIEW_SORT_BY;
57
+ };
58
+ const normalizeViewSortOrder = (value) => {
59
+ return value === "asc" ? "asc" : DEFAULT_VIEW_SORT_ORDER;
60
+ };
61
+ const normalizeFilterValue = ({
62
+ value,
63
+ valueType,
64
+ operator
65
+ }) => {
66
+ if (operator === "exists" || operator === "notExists") {
67
+ return null;
68
+ }
69
+ const normalizedValue = String(value ?? "").trim();
70
+ if (!normalizedValue) {
71
+ throw new InvalidNotePropertyInputError("Property filter value is required.");
72
+ }
73
+ if (valueType === "number" && !Number.isFinite(Number(normalizedValue))) {
74
+ throw new InvalidNotePropertyInputError("Number property filter value must be finite.");
75
+ }
76
+ if (valueType === "boolean" && normalizedValue !== "true" && normalizedValue !== "false") {
77
+ throw new InvalidNotePropertyInputError("Boolean property filter value must be true or false.");
78
+ }
79
+ if (valueType === "date") {
80
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalizedValue)) {
81
+ throw new InvalidNotePropertyInputError("Date property filter values must use YYYY-MM-DD.");
82
+ }
83
+ const date = /* @__PURE__ */ new Date(`${normalizedValue}T00:00:00.000Z`);
84
+ if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== normalizedValue) {
85
+ throw new InvalidNotePropertyInputError("Date property filter value is invalid.");
86
+ }
87
+ }
88
+ if ((operator === "before" || operator === "after") && valueType !== "date" && valueType !== "number") {
89
+ throw new InvalidNotePropertyInputError("Before and after filters require date or number properties.");
90
+ }
91
+ if (valueType === "url") {
92
+ return normalizeUrlValue(normalizedValue);
93
+ }
94
+ return normalizedValue;
95
+ };
96
+ const normalizeViewPropertyFilters = (filters) => {
97
+ const normalizedFilters = filters ?? [];
98
+ if (normalizedFilters.length > MAX_VIEW_PROPERTY_FILTERS) {
99
+ throw new InvalidNotePropertyInputError(`A view can have up to ${MAX_VIEW_PROPERTY_FILTERS} property filters.`);
100
+ }
101
+ return normalizedFilters.map((filter) => {
102
+ const key = normalizePropertyKey(filter.key);
103
+ const valueType = filter.valueType;
104
+ const operator = filter.operator;
105
+ if (!isPropertyValueType(valueType)) {
106
+ throw new InvalidNotePropertyInputError("Property filter value type is required.");
107
+ }
108
+ if (!isViewPropertyFilterOperator(operator)) {
109
+ throw new InvalidNotePropertyInputError("Property filter operator is invalid.");
110
+ }
111
+ const storedName = filter.name;
112
+ const name = typeof storedName === "string" && storedName.trim() ? storedName.trim() : key;
113
+ return {
114
+ key,
115
+ name,
116
+ valueType,
117
+ operator,
118
+ value: normalizeFilterValue({
119
+ value: filter.value,
120
+ valueType,
121
+ operator
122
+ })
123
+ };
124
+ }).filter((filter) => filter !== null);
125
+ };
126
+ const parseStoredViewQuery = (value) => {
127
+ if (!value) {
128
+ return {
129
+ propertyFilters: [],
130
+ sortBy: DEFAULT_VIEW_SORT_BY,
131
+ sortOrder: DEFAULT_VIEW_SORT_ORDER
132
+ };
133
+ }
134
+ try {
135
+ const parsed = JSON.parse(value);
136
+ return {
137
+ propertyFilters: normalizeViewPropertyFilters(parsed.propertyFilters ?? []),
138
+ sortBy: normalizeViewSortBy(parsed.sortBy),
139
+ sortOrder: normalizeViewSortOrder(parsed.sortOrder)
140
+ };
141
+ } catch {
142
+ return {
143
+ propertyFilters: [],
144
+ sortBy: DEFAULT_VIEW_SORT_BY,
145
+ sortOrder: DEFAULT_VIEW_SORT_ORDER
146
+ };
147
+ }
148
+ };
149
+ const serializeStoredViewQuery = ({ propertyFilters, sortBy, sortOrder }) => {
150
+ return JSON.stringify({
151
+ propertyFilters,
152
+ sortBy,
153
+ sortOrder
154
+ });
155
+ };
35
156
  const clampViewSectionLimit = (value) => {
36
157
  const numericValue = typeof value === "number" ? value : Number.NaN;
37
158
  if (!Number.isInteger(numericValue)) {
@@ -43,25 +164,48 @@ const normalizeViewTabTitle = (title) => {
43
164
  const trimmedTitle = title.trim();
44
165
  return trimmedTitle || "Untitled View";
45
166
  };
46
- const buildDefaultSectionTitle = (tagNames) => {
47
- if (tagNames.length === 0) {
48
- return "Tagged notes";
167
+ const buildDefaultSectionTitle = (tagNames, propertyFilters) => {
168
+ if (tagNames.length > 0) {
169
+ return tagNames.slice(0, 2).join(" + ");
49
170
  }
50
- return tagNames.slice(0, 2).join(" + ");
171
+ if (propertyFilters.length > 0) {
172
+ return propertyFilters.slice(0, 2).map((filter) => filter.name).join(" + ");
173
+ }
174
+ return "All notes";
51
175
  };
52
176
  const normalizeViewSectionInput = (input) => {
53
- const tagNames = normalizeViewTagNames(input.tagNames);
54
- if (tagNames.length === 0) {
55
- throw new Error("A view section requires at least one tag.");
56
- }
177
+ const tagNames = normalizeViewTagNames(input.tagNames ?? []);
178
+ const propertyFilters = normalizeViewPropertyFilters(input.propertyFilters);
57
179
  const trimmedTitle = input.title?.trim() ?? "";
180
+ const sortBy = normalizeViewSortBy(input.sortBy);
181
+ const sortOrder = normalizeViewSortOrder(input.sortOrder);
58
182
  return {
59
- title: trimmedTitle || buildDefaultSectionTitle(tagNames),
183
+ title: trimmedTitle || buildDefaultSectionTitle(tagNames, propertyFilters),
184
+ displayType: normalizeViewDisplayType(input.displayType),
60
185
  tagNames,
61
186
  mode: input.mode === "or" ? "or" : "and",
187
+ propertyFilters,
188
+ sortBy,
189
+ sortOrder,
62
190
  limit: clampViewSectionLimit(input.limit)
63
191
  };
64
192
  };
193
+ const normalizeViewNotesQueryInput = (input) => {
194
+ return {
195
+ tagNames: normalizeViewTagNames(input.tagNames ?? []),
196
+ mode: input.mode === "or" ? "or" : "and",
197
+ propertyFilters: normalizeViewPropertyFilters(input.propertyFilters),
198
+ sortBy: normalizeViewSortBy(input.sortBy),
199
+ sortOrder: normalizeViewSortOrder(input.sortOrder)
200
+ };
201
+ };
202
+ const normalizeViewNotesPagination = (pagination) => {
203
+ const numericLimit = Number(pagination?.limit ?? DEFAULT_VIEW_NOTES_QUERY_LIMIT);
204
+ const limit = Number.isInteger(numericLimit) ? Math.min(MAX_VIEW_NOTES_QUERY_LIMIT, Math.max(1, numericLimit)) : DEFAULT_VIEW_NOTES_QUERY_LIMIT;
205
+ const numericOffset = Number(pagination?.offset ?? 0);
206
+ const offset = Number.isInteger(numericOffset) ? Math.max(0, numericOffset) : 0;
207
+ return { limit, offset };
208
+ };
65
209
  const pickNextActiveViewTabId = (tabIds, deletedTabId, currentActiveTabId) => {
66
210
  const remainingTabIds = tabIds.filter((tabId) => tabId !== deletedTabId);
67
211
  if (remainingTabIds.length === 0) {
@@ -94,15 +238,22 @@ const parseViewId = (id) => {
94
238
  }
95
239
  return numericId;
96
240
  };
97
- const serializeViewSection = (section) => ({
98
- id: String(section.id),
99
- tabId: String(section.tabId),
100
- title: section.title,
101
- tagNames: section.tags.map((tag) => tag.name),
102
- mode: section.mode,
103
- limit: section.limit,
104
- order: section.order
105
- });
241
+ const serializeViewSection = (section) => {
242
+ const query = parseStoredViewQuery(section.query);
243
+ return {
244
+ id: String(section.id),
245
+ tabId: String(section.tabId),
246
+ title: section.title,
247
+ displayType: normalizeViewDisplayType(section.displayType),
248
+ tagNames: section.tags.map((tag) => tag.name),
249
+ mode: section.mode,
250
+ propertyFilters: query.propertyFilters,
251
+ sortBy: query.sortBy,
252
+ sortOrder: query.sortOrder,
253
+ limit: section.limit,
254
+ order: section.order
255
+ };
256
+ };
106
257
  const serializeViewTab = (tab) => ({
107
258
  id: String(tab.id),
108
259
  title: tab.title,
@@ -137,6 +288,52 @@ const readViewSection = async (db, id) => {
137
288
  });
138
289
  return section ? serializeViewSection(section) : null;
139
290
  };
291
+ const hydratePropertyFilters = async (db, filters, options = {}) => {
292
+ if (filters.length === 0) {
293
+ return [];
294
+ }
295
+ const definitions = await db.propertyDefinition.findMany({
296
+ where: { key: { in: filters.map((filter) => filter.key) } },
297
+ select: {
298
+ key: true,
299
+ name: true,
300
+ valueType: true,
301
+ options: {
302
+ select: {
303
+ value: true
304
+ }
305
+ }
306
+ }
307
+ });
308
+ const definitionByKey = new Map(definitions.map((definition) => [definition.key, definition]));
309
+ return filters.map((filter) => {
310
+ const definition = definitionByKey.get(filter.key);
311
+ if (!definition) {
312
+ throw new InvalidNotePropertyInputError(`Property ${filter.key} is not defined.`);
313
+ }
314
+ if (definition.valueType !== filter.valueType) {
315
+ throw new InvalidNotePropertyInputError(`Property ${filter.key} uses ${definition.valueType} values.`);
316
+ }
317
+ if (options.validateSelectOptions && filter.valueType === "select" && filter.operator !== "exists" && filter.operator !== "notExists") {
318
+ const optionValue = normalizeSelectFilterValue(filter.value ?? "");
319
+ if (!definition.options.some((option) => option.value === optionValue)) {
320
+ throw new InvalidNotePropertyInputError(`Property ${filter.key} option ${optionValue} is not defined.`);
321
+ }
322
+ }
323
+ return {
324
+ ...filter,
325
+ name: definition.name
326
+ };
327
+ });
328
+ };
329
+ const buildStoredViewQuery = async (db, section) => {
330
+ const propertyFilters = await hydratePropertyFilters(db, section.propertyFilters, { validateSelectOptions: true });
331
+ return serializeStoredViewQuery({
332
+ propertyFilters,
333
+ sortBy: section.sortBy,
334
+ sortOrder: section.sortOrder
335
+ });
336
+ };
140
337
  const getNextTabOrder = async (db) => {
141
338
  const lastTab = await db.viewTab.findFirst({
142
339
  orderBy: [{ order: "desc" }, { createdAt: "desc" }],
@@ -187,6 +384,94 @@ const getViewWorkspace = async () => {
187
384
  const getViewSectionById = async (id) => {
188
385
  return readViewSection(models, parseViewId(id));
189
386
  };
387
+ const normalizeSelectFilterValue = (value) => {
388
+ return value.trim().toLowerCase().replace(/\s+/g, "-");
389
+ };
390
+ const buildPropertyFilterValueWhere = (filter) => {
391
+ switch (filter.valueType) {
392
+ case "number": {
393
+ const numberValue = Number(filter.value);
394
+ if (filter.operator === "before") {
395
+ return { numberValue: { lt: numberValue } };
396
+ }
397
+ if (filter.operator === "after") {
398
+ return { numberValue: { gt: numberValue } };
399
+ }
400
+ return { numberValue };
401
+ }
402
+ case "date": {
403
+ const dateValue = /* @__PURE__ */ new Date(`${filter.value}T00:00:00.000Z`);
404
+ if (filter.operator === "before") {
405
+ return { dateValue: { lt: dateValue } };
406
+ }
407
+ if (filter.operator === "after") {
408
+ return { dateValue: { gt: dateValue } };
409
+ }
410
+ return { dateValue };
411
+ }
412
+ case "boolean":
413
+ return { boolValue: filter.value === "true" };
414
+ case "select":
415
+ return { option: { is: { value: normalizeSelectFilterValue(filter.value ?? "") } } };
416
+ case "url":
417
+ case "text":
418
+ default:
419
+ return { textValueNormalized: filter.value?.toLowerCase() ?? "" };
420
+ }
421
+ };
422
+ const buildPropertyFilterWhere = (filter) => {
423
+ const definitionWhere = {
424
+ definition: {
425
+ is: {
426
+ key: filter.key
427
+ }
428
+ }
429
+ };
430
+ if (filter.operator === "notExists") {
431
+ return {
432
+ properties: {
433
+ none: definitionWhere
434
+ }
435
+ };
436
+ }
437
+ if (filter.operator === "exists") {
438
+ return {
439
+ properties: {
440
+ some: definitionWhere
441
+ }
442
+ };
443
+ }
444
+ return {
445
+ properties: {
446
+ some: {
447
+ ...definitionWhere,
448
+ ...buildPropertyFilterValueWhere(filter)
449
+ }
450
+ }
451
+ };
452
+ };
453
+ const buildViewNotesWhere = (query) => {
454
+ const clauses = [];
455
+ if (query.tagNames.length > 0) {
456
+ clauses.push(buildNoteTagNamesWhere(query.tagNames, query.mode));
457
+ }
458
+ for (const filter of query.propertyFilters) {
459
+ clauses.push(buildPropertyFilterWhere(filter));
460
+ }
461
+ if (clauses.length === 0) {
462
+ return {};
463
+ }
464
+ return { AND: clauses };
465
+ };
466
+ const buildViewSectionWhere = (section) => {
467
+ return buildViewNotesWhere(section);
468
+ };
469
+ const buildViewNotesOrderBy = (query) => {
470
+ return [{ [query.sortBy]: query.sortOrder }, { id: "asc" }];
471
+ };
472
+ const buildViewSectionOrderBy = (section) => {
473
+ return buildViewNotesOrderBy(section);
474
+ };
190
475
  const getViewSectionNotes = async (id, pagination) => {
191
476
  const section = await models.viewSection.findUnique({
192
477
  where: { id: parseViewId(id) },
@@ -195,18 +480,12 @@ const getViewSectionNotes = async (id, pagination) => {
195
480
  if (!section) {
196
481
  return null;
197
482
  }
198
- const tagNames = section.tags.map((tag) => tag.name);
199
- if (tagNames.length === 0) {
200
- return {
201
- totalCount: 0,
202
- notes: []
203
- };
204
- }
205
- const where = buildNoteTagNamesWhere(tagNames, section.mode);
483
+ const serializedSection = serializeViewSection(section);
484
+ const where = buildViewSectionWhere(serializedSection);
206
485
  const [totalCount, notes] = await Promise.all([
207
486
  models.note.count({ where }),
208
487
  models.note.findMany({
209
- orderBy: { updatedAt: "desc" },
488
+ orderBy: buildViewSectionOrderBy(serializedSection),
210
489
  where,
211
490
  take: Number(pagination.limit),
212
491
  skip: Number(pagination.offset)
@@ -217,6 +496,33 @@ const getViewSectionNotes = async (id, pagination) => {
217
496
  notes
218
497
  };
219
498
  };
499
+ const getNotesByProperties = async (input, pagination) => {
500
+ const normalizedQuery = normalizeViewNotesQueryInput(input);
501
+ if (normalizedQuery.propertyFilters.length === 0) {
502
+ throw new InvalidNotePropertyInputError("At least one property filter is required.");
503
+ }
504
+ const query = {
505
+ ...normalizedQuery,
506
+ propertyFilters: await hydratePropertyFilters(models, normalizedQuery.propertyFilters, {
507
+ validateSelectOptions: true
508
+ })
509
+ };
510
+ const normalizedPagination = normalizeViewNotesPagination(pagination);
511
+ const where = buildViewNotesWhere(query);
512
+ const [totalCount, notes] = await Promise.all([
513
+ models.note.count({ where }),
514
+ models.note.findMany({
515
+ orderBy: buildViewNotesOrderBy(query),
516
+ where,
517
+ take: normalizedPagination.limit,
518
+ skip: normalizedPagination.offset
519
+ })
520
+ ]);
521
+ return {
522
+ totalCount,
523
+ notes
524
+ };
525
+ };
220
526
  const createViewTab = async (title) => {
221
527
  return models.$transaction(async (tx) => {
222
528
  const workspace = await ensureViewWorkspace(tx);
@@ -323,10 +629,13 @@ const createViewSection = async (tabId, input) => {
323
629
  throw new Error("View tab not found.");
324
630
  }
325
631
  const nextSectionOrder = await getNextSectionOrder(tx, numericTabId);
632
+ const query = await buildStoredViewQuery(tx, nextSection);
326
633
  const createdSection = await tx.viewSection.create({
327
634
  data: {
328
635
  tabId: numericTabId,
329
636
  title: nextSection.title,
637
+ displayType: nextSection.displayType,
638
+ query,
330
639
  mode: nextSection.mode,
331
640
  limit: nextSection.limit,
332
641
  order: nextSectionOrder,
@@ -345,23 +654,28 @@ const createViewSection = async (tabId, input) => {
345
654
  const updateViewSection = async (id, input) => {
346
655
  const sectionId = parseViewId(id);
347
656
  const nextSection = normalizeViewSectionInput(input);
348
- const updatedSection = await models.viewSection.update({
349
- where: { id: sectionId },
350
- data: {
351
- title: nextSection.title,
352
- mode: nextSection.mode,
353
- limit: nextSection.limit,
354
- tags: {
355
- deleteMany: {},
356
- create: nextSection.tagNames.map((tagName, index) => ({
357
- name: tagName,
358
- order: index
359
- }))
360
- }
361
- },
362
- include: orderedViewSectionTagsInclude
657
+ return models.$transaction(async (tx) => {
658
+ const query = await buildStoredViewQuery(tx, nextSection);
659
+ const updatedSection = await tx.viewSection.update({
660
+ where: { id: sectionId },
661
+ data: {
662
+ title: nextSection.title,
663
+ displayType: nextSection.displayType,
664
+ query,
665
+ mode: nextSection.mode,
666
+ limit: nextSection.limit,
667
+ tags: {
668
+ deleteMany: {},
669
+ create: nextSection.tagNames.map((tagName, index) => ({
670
+ name: tagName,
671
+ order: index
672
+ }))
673
+ }
674
+ },
675
+ include: orderedViewSectionTagsInclude
676
+ });
677
+ return serializeViewSection(updatedSection);
363
678
  });
364
- return serializeViewSection(updatedSection);
365
679
  };
366
680
  const deleteViewSection = async (id) => {
367
681
  const sectionId = parseViewId(id);
@@ -408,17 +722,27 @@ const reorderViewSections = async (tabId, sectionIds) => {
408
722
  });
409
723
  };
410
724
  export {
725
+ DEFAULT_VIEW_NOTES_QUERY_LIMIT,
411
726
  DEFAULT_VIEW_SECTION_LIMIT,
727
+ MAX_VIEW_NOTES_QUERY_LIMIT,
412
728
  MAX_VIEW_SECTION_LIMIT,
413
729
  MIN_VIEW_SECTION_LIMIT,
730
+ buildPropertyFilterWhere,
731
+ buildViewNotesWhere,
732
+ buildViewSectionWhere,
414
733
  clampViewSectionLimit,
415
734
  createViewSection,
416
735
  createViewTab,
417
736
  deleteViewSection,
418
737
  deleteViewTab,
738
+ getNotesByProperties,
419
739
  getViewSectionById,
420
740
  getViewSectionNotes,
421
741
  getViewWorkspace,
742
+ hydratePropertyFilters,
743
+ normalizeViewNotesPagination,
744
+ normalizeViewNotesQueryInput,
745
+ normalizeViewPropertyFilters,
422
746
  normalizeViewSectionInput,
423
747
  normalizeViewTabTitle,
424
748
  normalizeViewTagNames,