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
@@ -0,0 +1,874 @@
1
+ import models, { Prisma } from "../../../models.js";
2
+ import { buildNoteSearchProjection } from "./search.js";
3
+ import { captureNoteBaseline } from "./snapshot.js";
4
+ import { createNoteVersionConflictError, MissingNoteVersionError, parseNoteVersion } from "./write-conflict.js";
5
+ const PROPERTY_KEY_MAX_LENGTH = 80;
6
+ const PROPERTY_NAME_MAX_LENGTH = 120;
7
+ const PROPERTY_TEXT_VALUE_MAX_LENGTH = 2e3;
8
+ const PROPERTY_OPTION_LABEL_MAX_LENGTH = 80;
9
+ const PROPERTY_OPTION_VALUE_MAX_LENGTH = 80;
10
+ const PROPERTY_OPTION_LIMIT = 50;
11
+ const PROPERTY_PATCH_SET_LIMIT = 50;
12
+ const PROPERTY_PATCH_DELETE_LIMIT = 50;
13
+ class InvalidNotePropertyInputError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "InvalidNotePropertyInputError";
17
+ }
18
+ }
19
+ class NotePropertyDeleteConfirmationRequiredError extends Error {
20
+ code = "NOTE_PROPERTY_DELETE_CONFIRMATION_REQUIRED";
21
+ affectedNoteCount;
22
+ constructor(key, affectedNoteCount) {
23
+ super(`Property ${key} is used by ${affectedNoteCount} note(s). Confirm deletion to remove those values.`);
24
+ this.name = "NotePropertyDeleteConfirmationRequiredError";
25
+ this.affectedNoteCount = affectedNoteCount;
26
+ }
27
+ }
28
+ const isRecordNotFoundError = (error) => {
29
+ return error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025";
30
+ };
31
+ const normalizePropertyKey = (value) => {
32
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
33
+ if (!normalized) {
34
+ throw new InvalidNotePropertyInputError("Property key is required.");
35
+ }
36
+ if (normalized.length > PROPERTY_KEY_MAX_LENGTH) {
37
+ throw new InvalidNotePropertyInputError(`Property key must be ${PROPERTY_KEY_MAX_LENGTH} characters or fewer.`);
38
+ }
39
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(normalized)) {
40
+ throw new InvalidNotePropertyInputError("Property key must use letters, numbers, dashes, or underscores.");
41
+ }
42
+ return normalized;
43
+ };
44
+ const normalizePropertyName = (value, key) => {
45
+ const normalized = value?.trim() || key;
46
+ if (normalized.length > PROPERTY_NAME_MAX_LENGTH) {
47
+ throw new InvalidNotePropertyInputError(
48
+ `Property name must be ${PROPERTY_NAME_MAX_LENGTH} characters or fewer.`
49
+ );
50
+ }
51
+ return normalized;
52
+ };
53
+ const normalizeOptionValue = (value) => {
54
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
55
+ if (!normalized) {
56
+ throw new InvalidNotePropertyInputError("Property option value is required.");
57
+ }
58
+ if (normalized.length > PROPERTY_OPTION_VALUE_MAX_LENGTH) {
59
+ throw new InvalidNotePropertyInputError(
60
+ `Property option value must be ${PROPERTY_OPTION_VALUE_MAX_LENGTH} characters or fewer.`
61
+ );
62
+ }
63
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(normalized)) {
64
+ throw new InvalidNotePropertyInputError(
65
+ "Property option value must use letters, numbers, dashes, or underscores."
66
+ );
67
+ }
68
+ return normalized;
69
+ };
70
+ const normalizeOptionLabel = (value) => {
71
+ const normalized = value.trim();
72
+ if (!normalized) {
73
+ throw new InvalidNotePropertyInputError("Property option label is required.");
74
+ }
75
+ if (normalized.length > PROPERTY_OPTION_LABEL_MAX_LENGTH) {
76
+ throw new InvalidNotePropertyInputError(
77
+ `Property option label must be ${PROPERTY_OPTION_LABEL_MAX_LENGTH} characters or fewer.`
78
+ );
79
+ }
80
+ return normalized;
81
+ };
82
+ const normalizePropertyOptions = (options) => {
83
+ const normalizedOptions = options ?? [];
84
+ if (normalizedOptions.length > PROPERTY_OPTION_LIMIT) {
85
+ throw new InvalidNotePropertyInputError(`Select properties can have up to ${PROPERTY_OPTION_LIMIT} options.`);
86
+ }
87
+ const seenValues = /* @__PURE__ */ new Set();
88
+ return normalizedOptions.map((option, index) => {
89
+ const label = normalizeOptionLabel(option.label);
90
+ const value = normalizeOptionValue(option.value ?? label);
91
+ if (seenValues.has(value)) {
92
+ throw new InvalidNotePropertyInputError("Property options contain duplicate values.");
93
+ }
94
+ seenValues.add(value);
95
+ return {
96
+ label,
97
+ value,
98
+ color: option.color?.trim() || null,
99
+ order: Number.isFinite(Number(option.order)) ? Number(option.order) : index
100
+ };
101
+ });
102
+ };
103
+ const normalizePropertyOptionUpdates = (options, existingOptions) => {
104
+ const normalizedOptions = options ?? [];
105
+ if (normalizedOptions.length > PROPERTY_OPTION_LIMIT) {
106
+ throw new InvalidNotePropertyInputError(`Select properties can have up to ${PROPERTY_OPTION_LIMIT} options.`);
107
+ }
108
+ const existingOptionById = new Map(existingOptions.map((option) => [String(option.id), option]));
109
+ const seenValues = /* @__PURE__ */ new Set();
110
+ return normalizedOptions.map((option, index) => {
111
+ const label = normalizeOptionLabel(option.label);
112
+ const existingOption = option.id === void 0 || option.id === null ? null : existingOptionById.get(String(option.id));
113
+ if (option.id !== void 0 && option.id !== null && !existingOption) {
114
+ throw new InvalidNotePropertyInputError("Property option does not belong to this property.");
115
+ }
116
+ const value = normalizeOptionValue(option.value ?? existingOption?.value ?? label);
117
+ if (existingOption && value !== existingOption.value) {
118
+ throw new InvalidNotePropertyInputError(
119
+ `Property option ${existingOption.value} value cannot be changed. Create a new option instead.`
120
+ );
121
+ }
122
+ if (seenValues.has(value)) {
123
+ throw new InvalidNotePropertyInputError("Property options contain duplicate values.");
124
+ }
125
+ seenValues.add(value);
126
+ return {
127
+ id: existingOption?.id,
128
+ label,
129
+ value,
130
+ color: option.color?.trim() || null,
131
+ order: Number.isFinite(Number(option.order)) ? Number(option.order) : index
132
+ };
133
+ });
134
+ };
135
+ const normalizeTextValue = (value) => {
136
+ if (value.length > PROPERTY_TEXT_VALUE_MAX_LENGTH) {
137
+ throw new InvalidNotePropertyInputError(
138
+ `Property text value must be ${PROPERTY_TEXT_VALUE_MAX_LENGTH} characters or fewer.`
139
+ );
140
+ }
141
+ return value;
142
+ };
143
+ const normalizeUrlValue = (value) => {
144
+ const trimmedValue = normalizeTextValue(value.trim());
145
+ if (!trimmedValue) {
146
+ throw new InvalidNotePropertyInputError("URL property value is required.");
147
+ }
148
+ let url;
149
+ try {
150
+ url = new URL(trimmedValue);
151
+ } catch {
152
+ throw new InvalidNotePropertyInputError("URL property values must be valid http(s) URLs.");
153
+ }
154
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
155
+ throw new InvalidNotePropertyInputError("URL property values must use http or https.");
156
+ }
157
+ return url.toString();
158
+ };
159
+ const normalizeDateValue = (value) => {
160
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
161
+ throw new InvalidNotePropertyInputError("Date property values must use YYYY-MM-DD.");
162
+ }
163
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
164
+ if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
165
+ throw new InvalidNotePropertyInputError("Date property value is invalid.");
166
+ }
167
+ return date;
168
+ };
169
+ const normalizeBooleanValue = (value) => {
170
+ if (value === "true") {
171
+ return true;
172
+ }
173
+ if (value === "false") {
174
+ return false;
175
+ }
176
+ throw new InvalidNotePropertyInputError("Boolean property values must be true or false.");
177
+ };
178
+ const normalizeNumberValue = (value) => {
179
+ const numberValue = Number(value);
180
+ if (!Number.isFinite(numberValue)) {
181
+ throw new InvalidNotePropertyInputError("Number property value must be finite.");
182
+ }
183
+ return numberValue;
184
+ };
185
+ const serializeOption = (option) => ({
186
+ id: String(option.id),
187
+ label: option.label,
188
+ value: option.value,
189
+ color: option.color,
190
+ order: option.order
191
+ });
192
+ const serializeDefinitionOption = (option) => ({
193
+ id: String(option.id),
194
+ label: option.label,
195
+ value: option.value,
196
+ color: option.color,
197
+ order: option.order
198
+ });
199
+ const serializePropertyValue = (property) => {
200
+ switch (property.definition.valueType) {
201
+ case "number":
202
+ return property.numberValue === null ? "" : String(property.numberValue);
203
+ case "date":
204
+ return property.dateValue ? property.dateValue.toISOString().slice(0, 10) : "";
205
+ case "boolean":
206
+ return property.boolValue === null ? "" : String(property.boolValue);
207
+ case "select":
208
+ return property.option?.value ?? "";
209
+ case "url":
210
+ case "text":
211
+ default:
212
+ return property.textValue ?? "";
213
+ }
214
+ };
215
+ const serializeNoteProperty = (property) => ({
216
+ key: property.definition.key,
217
+ name: property.definition.name,
218
+ value: serializePropertyValue(property),
219
+ valueType: property.definition.valueType,
220
+ option: property.option ? serializeOption(property.option) : null,
221
+ createdAt: property.createdAt.toISOString(),
222
+ updatedAt: property.updatedAt.toISOString()
223
+ });
224
+ const serializeNoteProperties = (properties) => {
225
+ return properties.map(serializeNoteProperty).sort((left, right) => left.key.localeCompare(right.key));
226
+ };
227
+ const serializePropertyDefinition = (definition) => ({
228
+ key: definition.key,
229
+ name: definition.name,
230
+ valueType: definition.valueType,
231
+ noteCount: definition._count.properties,
232
+ options: definition.options.sort((left, right) => left.order - right.order).map(serializeDefinitionOption),
233
+ updatedAt: definition.updatedAt.toISOString()
234
+ });
235
+ const assertPropertyOptionUpdateKeepsUsedValues = ({
236
+ existingOptions,
237
+ nextOptions
238
+ }) => {
239
+ const nextValues = new Set(nextOptions.map((option) => option.value));
240
+ const removedUsedOption = existingOptions.find(
241
+ (option) => option._count.properties > 0 && !nextValues.has(option.value)
242
+ );
243
+ if (removedUsedOption) {
244
+ throw new InvalidNotePropertyInputError(
245
+ `Property option ${removedUsedOption.value} is used by notes and cannot be removed.`
246
+ );
247
+ }
248
+ };
249
+ const isRecord = (value) => {
250
+ return typeof value === "object" && value !== null && !Array.isArray(value);
251
+ };
252
+ const parseViewQueryRecord = (value) => {
253
+ if (!value) {
254
+ return null;
255
+ }
256
+ try {
257
+ const parsed = JSON.parse(value);
258
+ return isRecord(parsed) ? parsed : null;
259
+ } catch {
260
+ return null;
261
+ }
262
+ };
263
+ const getNormalizedViewFilterKey = (filter) => {
264
+ if (typeof filter.key !== "string") {
265
+ return null;
266
+ }
267
+ try {
268
+ return normalizePropertyKey(filter.key);
269
+ } catch {
270
+ return null;
271
+ }
272
+ };
273
+ const renamePropertyFiltersInViewQuery = ({
274
+ query,
275
+ key,
276
+ name
277
+ }) => {
278
+ const parsed = parseViewQueryRecord(query);
279
+ if (!parsed || !Array.isArray(parsed.propertyFilters)) {
280
+ return null;
281
+ }
282
+ let changed = false;
283
+ const propertyFilters = parsed.propertyFilters.map((filter) => {
284
+ if (!isRecord(filter) || getNormalizedViewFilterKey(filter) !== key) {
285
+ return filter;
286
+ }
287
+ changed = true;
288
+ return {
289
+ ...filter,
290
+ key,
291
+ name
292
+ };
293
+ });
294
+ if (!changed) {
295
+ return null;
296
+ }
297
+ return JSON.stringify({
298
+ ...parsed,
299
+ propertyFilters
300
+ });
301
+ };
302
+ const normalizeReferencedOptionValue = (value) => {
303
+ return typeof value === "string" ? normalizeOptionValue(value) : null;
304
+ };
305
+ const findReferencedRemovedPropertyOptionValue = ({
306
+ query,
307
+ key,
308
+ removedValues
309
+ }) => {
310
+ const parsed = parseViewQueryRecord(query);
311
+ if (!parsed || !Array.isArray(parsed.propertyFilters)) {
312
+ return null;
313
+ }
314
+ for (const filter of parsed.propertyFilters) {
315
+ if (!isRecord(filter) || getNormalizedViewFilterKey(filter) !== key) {
316
+ continue;
317
+ }
318
+ if (filter.valueType !== "select" || filter.operator === "exists" || filter.operator === "notExists") {
319
+ continue;
320
+ }
321
+ try {
322
+ const optionValue = normalizeReferencedOptionValue(filter.value);
323
+ if (optionValue && removedValues.has(optionValue)) {
324
+ return optionValue;
325
+ }
326
+ } catch {
327
+ continue;
328
+ }
329
+ }
330
+ return null;
331
+ };
332
+ const buildTypedValueData = async (input, definitionId, tx = models) => {
333
+ const resetValues = {
334
+ textValue: null,
335
+ textValueNormalized: null,
336
+ numberValue: null,
337
+ dateValue: null,
338
+ boolValue: null,
339
+ optionId: null
340
+ };
341
+ switch (input.valueType) {
342
+ case "number": {
343
+ const numberValue = normalizeNumberValue(input.value);
344
+ return { ...resetValues, numberValue };
345
+ }
346
+ case "date": {
347
+ const dateValue = normalizeDateValue(input.value);
348
+ return { ...resetValues, dateValue };
349
+ }
350
+ case "boolean": {
351
+ const boolValue = normalizeBooleanValue(input.value);
352
+ return { ...resetValues, boolValue };
353
+ }
354
+ case "select": {
355
+ const optionValue = normalizeOptionValue(input.value);
356
+ const option = await tx.propertyOption.findUnique({
357
+ where: {
358
+ propertyDefinitionId_value: {
359
+ propertyDefinitionId: definitionId,
360
+ value: optionValue
361
+ }
362
+ }
363
+ });
364
+ if (!option) {
365
+ throw new InvalidNotePropertyInputError(`Property option ${optionValue} is not defined.`);
366
+ }
367
+ return { ...resetValues, optionId: option.id };
368
+ }
369
+ case "url": {
370
+ const urlValue = normalizeUrlValue(input.value);
371
+ return { ...resetValues, textValue: urlValue, textValueNormalized: urlValue.toLowerCase() };
372
+ }
373
+ case "text": {
374
+ const textValue = normalizeTextValue(input.value);
375
+ return { ...resetValues, textValue, textValueNormalized: textValue.toLowerCase() };
376
+ }
377
+ default:
378
+ throw new InvalidNotePropertyInputError("Unsupported property value type.");
379
+ }
380
+ };
381
+ const normalizePatch = (patch) => {
382
+ const set = patch.set ?? [];
383
+ const deleteKeys = patch.deleteKeys ?? [];
384
+ if (set.length === 0 && deleteKeys.length === 0) {
385
+ throw new InvalidNotePropertyInputError("At least one property change is required.");
386
+ }
387
+ if (set.length > PROPERTY_PATCH_SET_LIMIT || deleteKeys.length > PROPERTY_PATCH_DELETE_LIMIT) {
388
+ throw new InvalidNotePropertyInputError("Too many property changes in one request.");
389
+ }
390
+ const normalizedSet = set.map((item) => {
391
+ const key = normalizePropertyKey(item.key);
392
+ return {
393
+ ...item,
394
+ key,
395
+ name: normalizePropertyName(item.name, key)
396
+ };
397
+ });
398
+ const normalizedDeleteKeys = deleteKeys.map(normalizePropertyKey);
399
+ const seenSetKeys = /* @__PURE__ */ new Set();
400
+ const seenDeleteKeys = /* @__PURE__ */ new Set();
401
+ for (const item of normalizedSet) {
402
+ if (seenSetKeys.has(item.key)) {
403
+ throw new InvalidNotePropertyInputError("Property patch contains duplicate keys.");
404
+ }
405
+ seenSetKeys.add(item.key);
406
+ }
407
+ for (const key of normalizedDeleteKeys) {
408
+ if (seenDeleteKeys.has(key)) {
409
+ throw new InvalidNotePropertyInputError("Property patch contains duplicate delete keys.");
410
+ }
411
+ if (seenSetKeys.has(key)) {
412
+ throw new InvalidNotePropertyInputError("Property patch cannot set and delete the same key.");
413
+ }
414
+ seenDeleteKeys.add(key);
415
+ }
416
+ return { set: normalizedSet, deleteKeys: normalizedDeleteKeys };
417
+ };
418
+ const normalizePatchByKey = (patch) => {
419
+ const set = patch.set ?? [];
420
+ const deleteKeys = patch.deleteKeys ?? [];
421
+ if (set.length === 0 && deleteKeys.length === 0) {
422
+ throw new InvalidNotePropertyInputError("At least one property change is required.");
423
+ }
424
+ if (set.length > PROPERTY_PATCH_SET_LIMIT || deleteKeys.length > PROPERTY_PATCH_DELETE_LIMIT) {
425
+ throw new InvalidNotePropertyInputError("Too many property changes in one request.");
426
+ }
427
+ const normalizedSet = set.map((item) => ({
428
+ key: normalizePropertyKey(item.key),
429
+ value: item.value
430
+ }));
431
+ const normalizedDeleteKeys = deleteKeys.map(normalizePropertyKey);
432
+ const seenSetKeys = /* @__PURE__ */ new Set();
433
+ const seenDeleteKeys = /* @__PURE__ */ new Set();
434
+ for (const item of normalizedSet) {
435
+ if (seenSetKeys.has(item.key)) {
436
+ throw new InvalidNotePropertyInputError("Property patch contains duplicate keys.");
437
+ }
438
+ seenSetKeys.add(item.key);
439
+ }
440
+ for (const key of normalizedDeleteKeys) {
441
+ if (seenDeleteKeys.has(key)) {
442
+ throw new InvalidNotePropertyInputError("Property patch contains duplicate delete keys.");
443
+ }
444
+ if (seenSetKeys.has(key)) {
445
+ throw new InvalidNotePropertyInputError("Property patch cannot set and delete the same key.");
446
+ }
447
+ seenDeleteKeys.add(key);
448
+ }
449
+ return { set: normalizedSet, deleteKeys: normalizedDeleteKeys };
450
+ };
451
+ const resolveNotePropertiesPatchValueTypes = async (patch, tx = models) => {
452
+ const normalizedPatch = normalizePatchByKey(patch);
453
+ if (normalizedPatch.set.length === 0) {
454
+ return normalizePatch({ deleteKeys: normalizedPatch.deleteKeys });
455
+ }
456
+ const definitions = await tx.propertyDefinition.findMany({
457
+ where: { key: { in: normalizedPatch.set.map((item) => item.key) } },
458
+ select: {
459
+ key: true,
460
+ name: true,
461
+ valueType: true
462
+ }
463
+ });
464
+ const definitionByKey = new Map(definitions.map((definition) => [definition.key, definition]));
465
+ return normalizePatch({
466
+ set: normalizedPatch.set.map((item) => {
467
+ const definition = definitionByKey.get(item.key);
468
+ if (!definition) {
469
+ throw new InvalidNotePropertyInputError(
470
+ `Property ${item.key} is not defined. Create it in property settings first.`
471
+ );
472
+ }
473
+ return {
474
+ key: item.key,
475
+ name: definition.name,
476
+ value: item.value,
477
+ valueType: definition.valueType
478
+ };
479
+ }),
480
+ deleteKeys: normalizedPatch.deleteKeys
481
+ });
482
+ };
483
+ const validateNotePropertiesPatchValues = async (patch, tx = models) => {
484
+ const normalizedPatch = normalizePatch(patch);
485
+ for (const item of normalizedPatch.set) {
486
+ const definition = await tx.propertyDefinition.findUnique({ where: { key: item.key } });
487
+ if (!definition) {
488
+ throw new InvalidNotePropertyInputError(
489
+ `Property ${item.key} is not defined. Create it in property settings first.`
490
+ );
491
+ }
492
+ if (definition.valueType !== item.valueType) {
493
+ throw new InvalidNotePropertyInputError(
494
+ `Property ${item.key} already uses ${definition.valueType} values.`
495
+ );
496
+ }
497
+ await buildTypedValueData(item, definition.id, tx);
498
+ }
499
+ return normalizedPatch;
500
+ };
501
+ const listNoteProperties = async (noteId) => {
502
+ const properties = await models.noteProperty.findMany({
503
+ where: { noteId },
504
+ include: { definition: true, option: true },
505
+ orderBy: [{ definition: { key: "asc" } }]
506
+ });
507
+ return serializeNoteProperties(properties);
508
+ };
509
+ const listNotePropertyKeys = async (input) => {
510
+ const normalizedQuery = input?.query?.trim();
511
+ const where = normalizedQuery ? {
512
+ OR: [{ key: { contains: normalizedQuery } }, { name: { contains: normalizedQuery } }]
513
+ } : void 0;
514
+ const take = Math.max(1, Math.min(100, Number(input?.limit ?? 50)));
515
+ const skip = Math.max(0, Number(input?.offset ?? 0));
516
+ const [definitions, totalCount] = await Promise.all([
517
+ models.propertyDefinition.findMany({
518
+ where,
519
+ orderBy: { key: "asc" },
520
+ take,
521
+ skip,
522
+ include: { options: { orderBy: { order: "asc" } }, _count: { select: { properties: true } } }
523
+ }),
524
+ models.propertyDefinition.count({ where })
525
+ ]);
526
+ return {
527
+ totalCount,
528
+ keys: definitions.map(serializePropertyDefinition)
529
+ };
530
+ };
531
+ const createNotePropertyDefinition = async (input) => {
532
+ const key = normalizePropertyKey(input.key);
533
+ const name = normalizePropertyName(input.name, key);
534
+ const options = normalizePropertyOptions(input.options);
535
+ if (input.valueType !== "select" && options.length > 0) {
536
+ throw new InvalidNotePropertyInputError("Only select properties can have options.");
537
+ }
538
+ if (input.valueType === "select" && options.length === 0) {
539
+ throw new InvalidNotePropertyInputError("Select properties require at least one option.");
540
+ }
541
+ try {
542
+ const definition = await models.propertyDefinition.create({
543
+ data: {
544
+ key,
545
+ name,
546
+ valueType: input.valueType,
547
+ ...input.valueType === "select" ? {
548
+ options: {
549
+ create: options
550
+ }
551
+ } : {}
552
+ },
553
+ include: { options: { orderBy: { order: "asc" } }, _count: { select: { properties: true } } }
554
+ });
555
+ return serializePropertyDefinition(definition);
556
+ } catch (error) {
557
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
558
+ throw new InvalidNotePropertyInputError(`Property ${key} already exists.`);
559
+ }
560
+ throw error;
561
+ }
562
+ };
563
+ const updateNotePropertyDefinition = async ({
564
+ key,
565
+ input
566
+ }) => {
567
+ const normalizedKey = normalizePropertyKey(key);
568
+ return models.$transaction(async (tx) => {
569
+ const definition = await tx.propertyDefinition.findUnique({
570
+ where: { key: normalizedKey },
571
+ include: {
572
+ options: {
573
+ orderBy: { order: "asc" },
574
+ include: { _count: { select: { properties: true } } }
575
+ },
576
+ _count: { select: { properties: true } }
577
+ }
578
+ });
579
+ if (!definition) {
580
+ return null;
581
+ }
582
+ const nextName = input.name !== void 0 ? normalizePropertyName(input.name, definition.key) : definition.name;
583
+ const shouldUpdateOptions = input.options !== void 0;
584
+ let nextOptions = null;
585
+ if (shouldUpdateOptions) {
586
+ nextOptions = normalizePropertyOptionUpdates(input.options, definition.options);
587
+ if (definition.valueType !== "select" && nextOptions.length > 0) {
588
+ throw new InvalidNotePropertyInputError("Only select properties can have options.");
589
+ }
590
+ if (definition.valueType === "select" && nextOptions.length === 0) {
591
+ throw new InvalidNotePropertyInputError("Select properties require at least one option.");
592
+ }
593
+ assertPropertyOptionUpdateKeepsUsedValues({
594
+ existingOptions: definition.options,
595
+ nextOptions
596
+ });
597
+ const nextValues = new Set(nextOptions.map((option) => option.value));
598
+ const removedValues = new Set(
599
+ definition.options.filter((option) => !nextValues.has(option.value)).map((option) => option.value)
600
+ );
601
+ if (removedValues.size > 0) {
602
+ const referencingViewSections = await tx.viewSection.findMany({
603
+ where: { query: { contains: definition.key } },
604
+ select: { title: true, query: true }
605
+ });
606
+ for (const section of referencingViewSections) {
607
+ const removedReferencedValue = findReferencedRemovedPropertyOptionValue({
608
+ query: section.query,
609
+ key: definition.key,
610
+ removedValues
611
+ });
612
+ if (removedReferencedValue) {
613
+ throw new InvalidNotePropertyInputError(
614
+ `Property option ${removedReferencedValue} is used by view ${section.title} and cannot be removed.`
615
+ );
616
+ }
617
+ }
618
+ }
619
+ }
620
+ await tx.propertyDefinition.update({
621
+ where: { id: definition.id },
622
+ data: { name: nextName }
623
+ });
624
+ if (nextName !== definition.name) {
625
+ const referencingViewSections = await tx.viewSection.findMany({
626
+ where: { query: { contains: definition.key } },
627
+ select: { id: true, query: true }
628
+ });
629
+ for (const section of referencingViewSections) {
630
+ const nextQuery = renamePropertyFiltersInViewQuery({
631
+ query: section.query,
632
+ key: definition.key,
633
+ name: nextName
634
+ });
635
+ if (nextQuery) {
636
+ await tx.viewSection.update({
637
+ where: { id: section.id },
638
+ data: { query: nextQuery }
639
+ });
640
+ }
641
+ }
642
+ }
643
+ if (nextOptions) {
644
+ const existingOptionByValue = new Map(definition.options.map((option) => [option.value, option]));
645
+ const nextValues = new Set(nextOptions.map((option) => option.value));
646
+ const removableOptionIds = definition.options.filter((option) => !nextValues.has(option.value) && option._count.properties === 0).map((option) => option.id);
647
+ if (removableOptionIds.length > 0) {
648
+ await tx.propertyOption.deleteMany({
649
+ where: {
650
+ id: { in: removableOptionIds }
651
+ }
652
+ });
653
+ }
654
+ for (const option of nextOptions) {
655
+ const existingOption = existingOptionByValue.get(option.value);
656
+ if (existingOption) {
657
+ await tx.propertyOption.update({
658
+ where: { id: existingOption.id },
659
+ data: {
660
+ label: option.label,
661
+ color: option.color,
662
+ order: option.order
663
+ }
664
+ });
665
+ continue;
666
+ }
667
+ await tx.propertyOption.create({
668
+ data: {
669
+ propertyDefinitionId: definition.id,
670
+ label: option.label,
671
+ value: option.value,
672
+ color: option.color,
673
+ order: option.order
674
+ }
675
+ });
676
+ }
677
+ }
678
+ const updatedDefinition = await tx.propertyDefinition.findUniqueOrThrow({
679
+ where: { id: definition.id },
680
+ include: { options: { orderBy: { order: "asc" } }, _count: { select: { properties: true } } }
681
+ });
682
+ return serializePropertyDefinition(updatedDefinition);
683
+ });
684
+ };
685
+ const deleteNotePropertyDefinition = async ({
686
+ key,
687
+ confirmImpact = false
688
+ }) => {
689
+ const normalizedKey = normalizePropertyKey(key);
690
+ return models.$transaction(async (tx) => {
691
+ const definition = await tx.propertyDefinition.findUnique({
692
+ where: { key: normalizedKey },
693
+ include: { _count: { select: { properties: true } } }
694
+ });
695
+ if (!definition) {
696
+ return null;
697
+ }
698
+ const affectedNoteCount = definition._count.properties;
699
+ if (affectedNoteCount > 0 && !confirmImpact) {
700
+ throw new NotePropertyDeleteConfirmationRequiredError(normalizedKey, affectedNoteCount);
701
+ }
702
+ if (affectedNoteCount > 0) {
703
+ await tx.note.updateMany({
704
+ where: {
705
+ properties: {
706
+ some: {
707
+ propertyDefinitionId: definition.id
708
+ }
709
+ }
710
+ },
711
+ data: {
712
+ updatedAt: /* @__PURE__ */ new Date()
713
+ }
714
+ });
715
+ }
716
+ await tx.propertyDefinition.delete({
717
+ where: { id: definition.id }
718
+ });
719
+ return {
720
+ key: definition.key,
721
+ name: definition.name,
722
+ valueType: definition.valueType,
723
+ affectedNoteCount,
724
+ deleted: true
725
+ };
726
+ });
727
+ };
728
+ const updateNotePropertiesWithVersionGuardAndSnapshot = async ({
729
+ id,
730
+ patch,
731
+ editSessionId,
732
+ expectedUpdatedAt,
733
+ force = false,
734
+ noteData,
735
+ snapshotMeta
736
+ }) => {
737
+ const normalizedPatch = normalizePatch(patch);
738
+ const expectedTimestamp = parseNoteVersion(expectedUpdatedAt);
739
+ if (expectedTimestamp === null && !force) {
740
+ throw new MissingNoteVersionError();
741
+ }
742
+ try {
743
+ const result = await models.$transaction(async (tx) => {
744
+ const existingNote = await tx.note.findUnique({
745
+ where: { id },
746
+ include: { properties: { include: { definition: true, option: true } } }
747
+ });
748
+ if (!existingNote) {
749
+ return null;
750
+ }
751
+ if (!force && existingNote.updatedAt.getTime() !== expectedTimestamp) {
752
+ throw createNoteVersionConflictError({
753
+ currentUpdatedAt: existingNote.updatedAt.getTime(),
754
+ expectedUpdatedAt: expectedTimestamp ?? existingNote.updatedAt.getTime()
755
+ });
756
+ }
757
+ const baseline = {
758
+ ...existingNote,
759
+ properties: existingNote.properties.map((property) => ({
760
+ key: property.definition.key,
761
+ name: property.definition.name,
762
+ valueType: property.definition.valueType,
763
+ textValue: property.textValue,
764
+ textValueNormalized: property.textValueNormalized,
765
+ numberValue: property.numberValue,
766
+ dateValue: property.dateValue,
767
+ boolValue: property.boolValue,
768
+ optionValue: property.option?.value ?? null,
769
+ optionLabel: property.option?.label ?? null,
770
+ optionColor: property.option?.color ?? null
771
+ }))
772
+ };
773
+ for (const item of normalizedPatch.set) {
774
+ const definition = await tx.propertyDefinition.findUnique({ where: { key: item.key } });
775
+ if (!definition) {
776
+ throw new InvalidNotePropertyInputError(
777
+ `Property ${item.key} is not defined. Create it in property settings first.`
778
+ );
779
+ }
780
+ if (definition.valueType !== item.valueType) {
781
+ throw new InvalidNotePropertyInputError(
782
+ `Property ${item.key} already uses ${definition.valueType} values.`
783
+ );
784
+ }
785
+ const valueData = await buildTypedValueData(item, definition.id, tx);
786
+ await tx.noteProperty.upsert({
787
+ where: {
788
+ noteId_propertyDefinitionId: {
789
+ noteId: id,
790
+ propertyDefinitionId: definition.id
791
+ }
792
+ },
793
+ create: {
794
+ noteId: id,
795
+ propertyDefinitionId: definition.id,
796
+ ...valueData
797
+ },
798
+ update: valueData
799
+ });
800
+ }
801
+ if (normalizedPatch.deleteKeys.length > 0) {
802
+ const definitions = await tx.propertyDefinition.findMany({
803
+ where: { key: { in: normalizedPatch.deleteKeys } },
804
+ select: { id: true }
805
+ });
806
+ if (definitions.length > 0) {
807
+ await tx.noteProperty.deleteMany({
808
+ where: {
809
+ noteId: id,
810
+ propertyDefinitionId: { in: definitions.map((definition) => definition.id) }
811
+ }
812
+ });
813
+ }
814
+ }
815
+ const updatedNote = await tx.note.update({
816
+ where: { id },
817
+ data: {
818
+ ...noteData?.title !== void 0 ? { title: noteData.title } : {},
819
+ ...noteData?.layout !== void 0 ? { layout: noteData.layout } : {},
820
+ ...noteData?.title !== void 0 ? buildNoteSearchProjection({
821
+ title: noteData.title,
822
+ content: existingNote.content
823
+ }) : {},
824
+ updatedAt: /* @__PURE__ */ new Date()
825
+ }
826
+ });
827
+ return { updatedNote, baseline };
828
+ });
829
+ if (!result) {
830
+ return null;
831
+ }
832
+ const snapshot = await captureNoteBaseline({
833
+ noteId: id,
834
+ baseline: result.baseline,
835
+ ...editSessionId && !force ? { editSessionId } : {},
836
+ ...snapshotMeta ? { meta: snapshotMeta } : {},
837
+ ...force ? { force: true } : {}
838
+ });
839
+ return {
840
+ note: result.updatedNote,
841
+ snapshot
842
+ };
843
+ } catch (error) {
844
+ if (isRecordNotFoundError(error)) {
845
+ return null;
846
+ }
847
+ throw error;
848
+ }
849
+ };
850
+ const updateNotePropertiesWithVersionGuard = async (input) => {
851
+ const result = await updateNotePropertiesWithVersionGuardAndSnapshot(input);
852
+ return result?.note ?? null;
853
+ };
854
+ export {
855
+ InvalidNotePropertyInputError,
856
+ NotePropertyDeleteConfirmationRequiredError,
857
+ assertPropertyOptionUpdateKeepsUsedValues,
858
+ createNotePropertyDefinition,
859
+ deleteNotePropertyDefinition,
860
+ findReferencedRemovedPropertyOptionValue,
861
+ listNoteProperties,
862
+ listNotePropertyKeys,
863
+ normalizePropertyKey,
864
+ normalizeUrlValue,
865
+ renamePropertyFiltersInViewQuery,
866
+ resolveNotePropertiesPatchValueTypes,
867
+ serializeNoteProperties,
868
+ serializeNoteProperty,
869
+ updateNotePropertiesWithVersionGuard,
870
+ updateNotePropertiesWithVersionGuardAndSnapshot,
871
+ updateNotePropertyDefinition,
872
+ validateNotePropertiesPatchValues
873
+ };
874
+ //# sourceMappingURL=properties.js.map