ocean-brain 0.5.0 → 0.6.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/dist/mcp.js +660 -31
- package/package.json +2 -1
- package/server/client/dist/assets/{Calendar-BU9XgX2N.js → Calendar-BJHLksEZ.js} +1 -1
- package/server/client/dist/assets/{Callout-C6tVCxOT.js → Callout-wFx9oCas.js} +1 -1
- package/server/client/dist/assets/Graph-9piERaNR.js +1 -0
- package/server/client/dist/assets/{Image-DxWhlIDG.js → Image-DK0lnaK2.js} +1 -1
- package/server/client/dist/assets/{Image.es-BjGK7y6Y.js → Image.es-Be7par_j.js} +1 -1
- package/server/client/dist/assets/Note-CC9ybxXL.js +21 -0
- package/server/client/dist/assets/{Plus.es-EcmtS_uw.js → Plus.es-BKm7uDYu.js} +1 -1
- package/server/client/dist/assets/Reminders-DRxA8v98.js +1 -0
- package/server/client/dist/assets/Search-BunSQrqv.js +1 -0
- package/server/client/dist/assets/{SurfaceCard-CO0oVxeL.js → SurfaceCard-FhlC8VSR.js} +1 -1
- package/server/client/dist/assets/Tag-z_crJ5JP.js +1 -0
- package/server/client/dist/assets/TagNotes-D7wIJ4jy.js +1 -0
- package/server/client/dist/assets/{Trash.es-DmwfSy3l.js → Trash.es-ByMVwx0d.js} +1 -1
- package/server/client/dist/assets/ViewNotes-Duo9XmcJ.js +1 -0
- package/server/client/dist/assets/Views-D4Z_O9Kd.js +1 -0
- package/server/client/dist/assets/{image.api-DeF9ck7g.js → image.api-B5Qi-Txv.js} +1 -1
- package/server/client/dist/assets/{index-BZUGWdOU.css → index-Cd81UxEh.css} +1 -1
- package/server/client/dist/assets/index-cHJQCo8V.js +1 -0
- package/server/client/dist/assets/index-nYj7BL3a.js +327 -0
- package/server/client/dist/assets/manage-image-detail-DcZggB1M.js +1 -0
- package/server/client/dist/assets/manage-image-glbtBibn.js +1 -0
- package/server/client/dist/assets/{mcp-Bh743HBz.js → mcp-BBUpQdS9.js} +1 -1
- package/server/client/dist/assets/{note-core-Z6kqulGB.js → note-core-DMZcZ4yV.js} +53 -53
- package/server/client/dist/assets/{note-vendor-i1M2FSI2.js → note-vendor-uuJO2MTD.js} +1 -1
- package/server/client/dist/assets/{placeholder-XQqo1Fvr.js → placeholder-B_qb_qkc.js} +5 -5
- package/server/client/dist/assets/properties-CiyQv6Tb.js +1 -0
- package/server/client/dist/assets/trash-CQnj4Rmv.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-D21PurIS.js → useReminderMutate-CSKliBcc.js} +1 -1
- package/server/client/dist/assets/view-dashboard-B5fKgvk_.js +99 -0
- package/server/client/dist/index.html +4 -4
- package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
- package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.mutation.resolver.js +119 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.query.resolver.js +14 -0
- package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.type-defs.js +100 -0
- package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
- package/server/dist/features/note/http/mcp.js +394 -1
- package/server/dist/features/note/http/mcp.js.map +1 -1
- package/server/dist/features/note/services/markdown-intent-write.js +495 -0
- package/server/dist/features/note/services/markdown-intent-write.js.map +1 -0
- package/server/dist/features/note/services/markdown-patch.js +601 -0
- package/server/dist/features/note/services/markdown-patch.js.map +1 -0
- package/server/dist/features/note/services/properties.js +874 -0
- package/server/dist/features/note/services/properties.js.map +1 -0
- package/server/dist/features/note/services/snapshot.js +94 -12
- package/server/dist/features/note/services/snapshot.js.map +1 -1
- package/server/dist/features/note/services/trash.js +70 -2
- package/server/dist/features/note/services/trash.js.map +1 -1
- package/server/dist/features/note/services/write.js +21 -7
- package/server/dist/features/note/services/write.js.map +1 -1
- package/server/dist/features/view/graphql/view.query.resolver.js +31 -1
- package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -1
- package/server/dist/features/view/graphql/view.type-defs.js +57 -1
- package/server/dist/features/view/graphql/view.type-defs.js.map +1 -1
- package/server/dist/features/view/services/workspace.js +367 -43
- package/server/dist/features/view/services/workspace.js.map +1 -1
- package/server/dist/modules/blocknote.js +367 -7
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/routes/mcp.js +21 -1
- package/server/dist/routes/mcp.js.map +1 -1
- package/server/prisma/migrations/20260527073000_0017_note_properties/migration.sql +105 -0
- package/server/prisma/migrations/20260531073000_0018_view_query/migration.sql +3 -0
- package/server/prisma/schema.prisma +226 -130
- package/server/client/dist/assets/Graph-Dond2hGK.js +0 -1
- package/server/client/dist/assets/Note-Cr6GtXUM.js +0 -21
- package/server/client/dist/assets/Reminders-BlLLT_x0.js +0 -1
- package/server/client/dist/assets/Search-CrZ0ap8x.js +0 -1
- package/server/client/dist/assets/Tag-vws5AfNW.js +0 -1
- package/server/client/dist/assets/TagNotes-Fmf1Kgim.js +0 -1
- package/server/client/dist/assets/ViewNotes-BALN7IfH.js +0 -1
- package/server/client/dist/assets/Views-Ccc_Ml5U.js +0 -1
- package/server/client/dist/assets/index-BSVE2cGd.js +0 -1
- package/server/client/dist/assets/index-BvKBTjQq.js +0 -246
- package/server/client/dist/assets/manage-image-IoTXaMBP.js +0 -1
- package/server/client/dist/assets/manage-image-detail-BRTXwOsh.js +0 -1
- package/server/client/dist/assets/trash-hiJIg6-c.js +0 -1
- 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
|