@vicacha-devs/strapi-plugin-notes 0.0.0-stage → 1.0.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.
- package/LICENSE +22 -0
- package/README.md +20 -2
- package/dist/admin/NotesHomePage-BBESzSxZ.js +457 -0
- package/dist/admin/NotesHomePage-C5eNNxl9.cjs +475 -0
- package/dist/admin/ar-NJ-XJhAU.cjs +64 -0
- package/dist/admin/ar-uSBvoP4-.js +64 -0
- package/dist/admin/de-DlFY4yCr.js +64 -0
- package/dist/admin/de-d-M42GQs.cjs +64 -0
- package/dist/admin/en-C8MGI5Fp.cjs +64 -0
- package/dist/admin/en-DQUtJt3K.js +64 -0
- package/dist/admin/es-BZY_kUPd.cjs +64 -0
- package/dist/admin/es-DcTVGahO.js +64 -0
- package/dist/admin/fr-D6Ism7Y3.cjs +64 -0
- package/dist/admin/fr-QW7vMjBs.js +64 -0
- package/dist/admin/index-C7es-Q8Z.cjs +1551 -0
- package/dist/admin/index-DP_1HzPT.js +1535 -0
- package/dist/admin/index.cjs +4 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/it-DyBVYzjD.cjs +64 -0
- package/dist/admin/it-F24NTVne.js +64 -0
- package/dist/admin/pt-B4OG4ZiY.js +64 -0
- package/dist/admin/pt-BOHJueLP.cjs +64 -0
- package/dist/admin/ru-DGiT4YZI.js +64 -0
- package/dist/admin/ru-DhigZM8k.cjs +64 -0
- package/dist/admin/src/components/DeleteNoteDialog.d.ts +10 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/NoteListItem.d.ts +10 -0
- package/dist/admin/src/components/NoteModal.d.ts +18 -0
- package/dist/admin/src/components/NotesHubFilterBar.d.ts +14 -0
- package/dist/admin/src/components/NotesSidePanel.d.ts +13 -0
- package/dist/admin/src/components/PluginIcon.d.ts +2 -0
- package/dist/admin/src/hooks/useNotes.d.ts +12 -0
- package/dist/admin/src/index.d.ts +21 -0
- package/dist/admin/src/pages/NotesHomePage.d.ts +3 -0
- package/dist/admin/src/pluginId.d.ts +4 -0
- package/dist/admin/src/utils/getTrad.d.ts +2 -0
- package/dist/admin/src/utils/index.d.ts +1 -0
- package/dist/admin/tr-CNFZeutu.cjs +64 -0
- package/dist/admin/tr-CwzORNP2.js +64 -0
- package/dist/admin/zh-BriU4KiK.cjs +64 -0
- package/dist/admin/zh-C1EjzGrj.js +64 -0
- package/dist/package.json +3 -0
- package/dist/server/index.cjs +1212 -0
- package/dist/server/index.js +1212 -0
- package/dist/server/index.mjs +1212 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +8 -0
- package/dist/server/src/content-types/index.d.ts +70 -0
- package/dist/server/src/content-types/note/schema.json.d.ts +68 -0
- package/dist/server/src/controllers/index.d.ts +2 -0
- package/dist/server/src/controllers/notesController.d.ts +27 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +2 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +17 -0
- package/dist/server/src/routes/content-api.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +30 -0
- package/dist/server/src/services/index.d.ts +2 -0
- package/dist/server/src/services/notesQueryHelpers.d.ts +11 -0
- package/dist/server/src/services/notesService.d.ts +25 -0
- package/dist/shared/index.cjs +525 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +525 -0
- package/dist/shared/index.mjs +525 -0
- package/package.json +102 -3
|
@@ -0,0 +1,1212 @@
|
|
|
1
|
+
const PLUGIN_ID = "notes";
|
|
2
|
+
const NOTE_CONTENT_TYPE_UID = "plugin::notes.note";
|
|
3
|
+
const NOTE_CATEGORIES = ["general", "todo", "blocker", "idea"];
|
|
4
|
+
const MAX_NOTE_TITLE_LENGTH = 255;
|
|
5
|
+
const MAX_NOTE_CONTENT_LENGTH = 2e4;
|
|
6
|
+
const MAX_ENTITY_SLUG_LENGTH = 160;
|
|
7
|
+
const MAX_ENTITY_ID_LENGTH = 128;
|
|
8
|
+
const MAX_LOCALE_LENGTH = 32;
|
|
9
|
+
const ENTITY_SLUG_PATTERN = /^[a-zA-Z0-9_:.-]{1,160}$/;
|
|
10
|
+
const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
11
|
+
const LOCALE_PATTERN = /^[a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8}){0,4}$/;
|
|
12
|
+
const FORBIDDEN_IDENTIFIER_TOKENS = /* @__PURE__ */ new Set([
|
|
13
|
+
"__proto__",
|
|
14
|
+
"constructor",
|
|
15
|
+
"prototype"
|
|
16
|
+
]);
|
|
17
|
+
function isForbiddenIdentifier(value) {
|
|
18
|
+
const lower = value.toLowerCase();
|
|
19
|
+
if (FORBIDDEN_IDENTIFIER_TOKENS.has(lower)) return true;
|
|
20
|
+
if (value === "." || value === ".." || value.includes("..") || value.startsWith(".") || value.endsWith(".")) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return lower.split(/[:./]+/).some((segment) => FORBIDDEN_IDENTIFIER_TOKENS.has(segment));
|
|
24
|
+
}
|
|
25
|
+
function toPositiveSafeInt(input) {
|
|
26
|
+
if (typeof input !== "number" || !Number.isFinite(input)) return null;
|
|
27
|
+
const truncated = Math.trunc(input);
|
|
28
|
+
return Number.isSafeInteger(truncated) && truncated >= 1 ? truncated : null;
|
|
29
|
+
}
|
|
30
|
+
function isValidNoteCategory(value) {
|
|
31
|
+
return typeof value === "string" && NOTE_CATEGORIES.includes(value);
|
|
32
|
+
}
|
|
33
|
+
function normalizeNoteCategory(value) {
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
const normalized = value.trim().toLowerCase();
|
|
36
|
+
if (isValidNoteCategory(normalized)) return normalized;
|
|
37
|
+
}
|
|
38
|
+
return "general";
|
|
39
|
+
}
|
|
40
|
+
function sanitizePlainText(input, maxLength) {
|
|
41
|
+
if (typeof input !== "string") return "";
|
|
42
|
+
const cleaned = input.replace(/\0/g, "").replace(/\r\n/g, "\n").trim();
|
|
43
|
+
return cleaned.length > maxLength ? cleaned.slice(0, maxLength) : cleaned;
|
|
44
|
+
}
|
|
45
|
+
function sanitizeEntitySlug(input) {
|
|
46
|
+
if (typeof input !== "string" || input.includes("\0")) return "";
|
|
47
|
+
const cleaned = input.trim();
|
|
48
|
+
if (!cleaned || cleaned.length > MAX_ENTITY_SLUG_LENGTH || isForbiddenIdentifier(cleaned) || !ENTITY_SLUG_PATTERN.test(cleaned)) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
function sanitizeEntityDocumentId(input) {
|
|
54
|
+
const numeric = toPositiveSafeInt(input);
|
|
55
|
+
if (numeric !== null) return String(numeric);
|
|
56
|
+
if (typeof input !== "string" || input.includes("\0")) return "";
|
|
57
|
+
const cleaned = input.trim();
|
|
58
|
+
if (!cleaned || cleaned.length > MAX_ENTITY_ID_LENGTH || isForbiddenIdentifier(cleaned) || !SAFE_ID_PATTERN.test(cleaned)) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
return cleaned;
|
|
62
|
+
}
|
|
63
|
+
function parseStringEntityId(input) {
|
|
64
|
+
if (input.includes("\0")) return null;
|
|
65
|
+
const trimmed = input.trim();
|
|
66
|
+
if (!trimmed || trimmed.length > MAX_ENTITY_ID_LENGTH || isForbiddenIdentifier(trimmed)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (/^\d+$/.test(trimmed)) {
|
|
70
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
71
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
72
|
+
}
|
|
73
|
+
return SAFE_ID_PATTERN.test(trimmed) ? trimmed : null;
|
|
74
|
+
}
|
|
75
|
+
function sanitizeEntityId(input) {
|
|
76
|
+
const numeric = toPositiveSafeInt(input);
|
|
77
|
+
if (numeric !== null) {
|
|
78
|
+
return numeric;
|
|
79
|
+
}
|
|
80
|
+
if (typeof input === "string") {
|
|
81
|
+
return parseStringEntityId(input);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function isWildcardOrInvalidLocale(cleaned) {
|
|
86
|
+
if (!cleaned || cleaned === "all" || cleaned === "*") {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return cleaned.length > MAX_LOCALE_LENGTH || isForbiddenIdentifier(cleaned);
|
|
90
|
+
}
|
|
91
|
+
function sanitizeEntityLocale(input) {
|
|
92
|
+
if (typeof input !== "string" || input.includes("\0")) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const cleaned = input.trim();
|
|
96
|
+
if (isWildcardOrInvalidLocale(cleaned)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return LOCALE_PATTERN.test(cleaned) ? cleaned : null;
|
|
100
|
+
}
|
|
101
|
+
function extractPrimitiveAuthorId(rawUser) {
|
|
102
|
+
const numeric = toPositiveSafeInt(rawUser);
|
|
103
|
+
if (numeric !== null) {
|
|
104
|
+
return numeric;
|
|
105
|
+
}
|
|
106
|
+
if (typeof rawUser === "string" && rawUser.trim()) {
|
|
107
|
+
return sanitizeEntityDocumentId(rawUser) || null;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function sanitizePrimitiveAuthor(rawUser, fallback) {
|
|
112
|
+
const primitiveId = extractPrimitiveAuthorId(rawUser);
|
|
113
|
+
if (!fallback && primitiveId === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
id: primitiveId,
|
|
118
|
+
firstname: "",
|
|
119
|
+
lastname: "",
|
|
120
|
+
username: "",
|
|
121
|
+
displayName: fallback || `#${String(primitiveId)}`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function asPlainObject(value, fallback) {
|
|
125
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : fallback;
|
|
126
|
+
}
|
|
127
|
+
function unwrapAuthorRecord(rawUser) {
|
|
128
|
+
const nestedData = asPlainObject(rawUser.data, rawUser);
|
|
129
|
+
const attrs = asPlainObject(nestedData.attributes, nestedData);
|
|
130
|
+
const id = extractPrimitiveAuthorId(nestedData.id ?? rawUser.id);
|
|
131
|
+
return { attrs, id };
|
|
132
|
+
}
|
|
133
|
+
function sanitizeNoteAuthor(rawUser, fallbackDisplayName) {
|
|
134
|
+
const fallback = sanitizePlainText(fallbackDisplayName, 120);
|
|
135
|
+
if (!rawUser || typeof rawUser !== "object" || Array.isArray(rawUser)) {
|
|
136
|
+
return sanitizePrimitiveAuthor(rawUser, fallback);
|
|
137
|
+
}
|
|
138
|
+
const { attrs, id } = unwrapAuthorRecord(rawUser);
|
|
139
|
+
const firstname = sanitizePlainText(attrs.firstname, 80);
|
|
140
|
+
const lastname = sanitizePlainText(attrs.lastname, 80);
|
|
141
|
+
const username = sanitizePlainText(attrs.username, 80);
|
|
142
|
+
const explicitDisplayName = sanitizePlainText(attrs.displayName, 120);
|
|
143
|
+
const fullName = [firstname, lastname].filter(Boolean).join(" ").trim();
|
|
144
|
+
const displayName = fullName || username || explicitDisplayName || fallback || "";
|
|
145
|
+
if (!displayName && id === null) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
id,
|
|
150
|
+
firstname,
|
|
151
|
+
lastname,
|
|
152
|
+
username,
|
|
153
|
+
displayName: displayName || `#${String(id)}`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function extractRecordId(root, attrs) {
|
|
157
|
+
if (typeof root.id === "number" && Number.isFinite(root.id) || typeof root.id === "string") {
|
|
158
|
+
return root.id;
|
|
159
|
+
}
|
|
160
|
+
if (typeof attrs.id === "number" && Number.isFinite(attrs.id) || typeof attrs.id === "string") {
|
|
161
|
+
return attrs.id;
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
function sanitizeOptionalIsoString(value) {
|
|
166
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
167
|
+
}
|
|
168
|
+
function unwrapRawNoteAttributes(raw) {
|
|
169
|
+
const root = asPlainObject(raw, {});
|
|
170
|
+
const rawAttrs = asPlainObject(root.attributes, {});
|
|
171
|
+
const attrs = { ...root, ...rawAttrs };
|
|
172
|
+
return { root, attrs };
|
|
173
|
+
}
|
|
174
|
+
function normalizeNoteRecord(raw) {
|
|
175
|
+
const { root, attrs } = unwrapRawNoteAttributes(raw);
|
|
176
|
+
const id = extractRecordId(root, attrs);
|
|
177
|
+
const documentId = sanitizeEntityDocumentId(root.documentId ?? attrs.documentId) || String(id || "");
|
|
178
|
+
const entityId = sanitizeEntityId(attrs.entityId);
|
|
179
|
+
const entityDocumentId = sanitizeEntityDocumentId(attrs.entityDocumentId) || (entityId !== null ? String(entityId) : "");
|
|
180
|
+
const createdBy = sanitizeNoteAuthor(
|
|
181
|
+
attrs.createdBy,
|
|
182
|
+
attrs.createdByName ?? attrs.authorName
|
|
183
|
+
);
|
|
184
|
+
const updatedBy = sanitizeNoteAuthor(attrs.updatedBy, attrs.updatedByName) ?? createdBy;
|
|
185
|
+
const createdAt = sanitizeOptionalIsoString(attrs.createdAt);
|
|
186
|
+
const updatedAt = sanitizeOptionalIsoString(attrs.updatedAt) ?? createdAt;
|
|
187
|
+
const normalizedAttributes = {
|
|
188
|
+
title: sanitizePlainText(attrs.title, MAX_NOTE_TITLE_LENGTH),
|
|
189
|
+
content: sanitizePlainText(attrs.content, MAX_NOTE_CONTENT_LENGTH),
|
|
190
|
+
category: normalizeNoteCategory(attrs.category),
|
|
191
|
+
isPinned: Boolean(attrs.isPinned),
|
|
192
|
+
entitySlug: sanitizeEntitySlug(attrs.entitySlug),
|
|
193
|
+
entityId,
|
|
194
|
+
entityDocumentId,
|
|
195
|
+
entityLocale: sanitizeEntityLocale(attrs.entityLocale),
|
|
196
|
+
createdAt,
|
|
197
|
+
updatedAt,
|
|
198
|
+
createdBy,
|
|
199
|
+
updatedBy
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
documentId,
|
|
204
|
+
...normalizedAttributes,
|
|
205
|
+
attributes: normalizedAttributes
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function validateNoteCreatePayload(input) {
|
|
209
|
+
const raw = unwrapDataEnvelope(input);
|
|
210
|
+
const errors = [];
|
|
211
|
+
const title = sanitizePlainText(raw.title, MAX_NOTE_TITLE_LENGTH);
|
|
212
|
+
const content = sanitizePlainText(raw.content, MAX_NOTE_CONTENT_LENGTH);
|
|
213
|
+
const category = normalizeNoteCategory(raw.category);
|
|
214
|
+
const isPinned = Boolean(raw.isPinned);
|
|
215
|
+
const entitySlug = sanitizeEntitySlug(raw.entitySlug);
|
|
216
|
+
const entityId = sanitizeEntityId(raw.entityId);
|
|
217
|
+
const entityDocumentId = sanitizeEntityDocumentId(raw.entityDocumentId) || (entityId !== null ? String(entityId) : "");
|
|
218
|
+
const entityLocale = sanitizeEntityLocale(raw.entityLocale);
|
|
219
|
+
if (!title) {
|
|
220
|
+
errors.push("Note title is required.");
|
|
221
|
+
}
|
|
222
|
+
if (!entitySlug) {
|
|
223
|
+
errors.push("A valid target entitySlug is required.");
|
|
224
|
+
}
|
|
225
|
+
if (!entityDocumentId && entityId === null) {
|
|
226
|
+
errors.push("Either entityDocumentId or entityId is required.");
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
valid: errors.length === 0,
|
|
230
|
+
errors,
|
|
231
|
+
data: {
|
|
232
|
+
title,
|
|
233
|
+
content,
|
|
234
|
+
category,
|
|
235
|
+
isPinned,
|
|
236
|
+
entitySlug,
|
|
237
|
+
entityId,
|
|
238
|
+
entityDocumentId,
|
|
239
|
+
entityLocale
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function applyEntityTargetUpdates(raw, hasOwn, data, errors) {
|
|
244
|
+
if (hasOwn("entityDocumentId")) {
|
|
245
|
+
const docId = sanitizeEntityDocumentId(raw.entityDocumentId);
|
|
246
|
+
if (docId) data.entityDocumentId = docId;
|
|
247
|
+
}
|
|
248
|
+
if (hasOwn("entityId")) {
|
|
249
|
+
const entityId = sanitizeEntityId(raw.entityId);
|
|
250
|
+
if (entityId !== null) data.entityId = entityId;
|
|
251
|
+
}
|
|
252
|
+
if (hasOwn("entitySlug")) {
|
|
253
|
+
const slug = sanitizeEntitySlug(raw.entitySlug);
|
|
254
|
+
if (slug) {
|
|
255
|
+
data.entitySlug = slug;
|
|
256
|
+
} else if (raw.entitySlug !== void 0 && raw.entitySlug !== null) {
|
|
257
|
+
errors.push("A valid target entitySlug is required.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function validateNoteUpdatePayload(input) {
|
|
262
|
+
const raw = unwrapDataEnvelope(input);
|
|
263
|
+
const errors = [];
|
|
264
|
+
const data = {};
|
|
265
|
+
const hasOwn = (key) => Object.prototype.hasOwnProperty.call(raw, key);
|
|
266
|
+
if (hasOwn("title")) {
|
|
267
|
+
const title = sanitizePlainText(raw.title, MAX_NOTE_TITLE_LENGTH);
|
|
268
|
+
if (!title) {
|
|
269
|
+
errors.push("Note title cannot be empty.");
|
|
270
|
+
} else {
|
|
271
|
+
data.title = title;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (hasOwn("content")) {
|
|
275
|
+
data.content = sanitizePlainText(raw.content, MAX_NOTE_CONTENT_LENGTH);
|
|
276
|
+
}
|
|
277
|
+
if (hasOwn("category")) {
|
|
278
|
+
data.category = normalizeNoteCategory(raw.category);
|
|
279
|
+
}
|
|
280
|
+
if (hasOwn("isPinned")) {
|
|
281
|
+
data.isPinned = Boolean(raw.isPinned);
|
|
282
|
+
}
|
|
283
|
+
if (hasOwn("entityLocale")) {
|
|
284
|
+
data.entityLocale = sanitizeEntityLocale(raw.entityLocale);
|
|
285
|
+
}
|
|
286
|
+
applyEntityTargetUpdates(raw, hasOwn, data, errors);
|
|
287
|
+
if (Object.keys(data).length === 0 && errors.length === 0) {
|
|
288
|
+
errors.push("At least one valid note field must be provided for update.");
|
|
289
|
+
}
|
|
290
|
+
return { valid: errors.length === 0, errors, data };
|
|
291
|
+
}
|
|
292
|
+
function unwrapDataEnvelope(input) {
|
|
293
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
const obj = input;
|
|
297
|
+
if (obj.data && typeof obj.data === "object" && !Array.isArray(obj.data)) {
|
|
298
|
+
return obj.data;
|
|
299
|
+
}
|
|
300
|
+
if (obj.body && typeof obj.body === "object" && !Array.isArray(obj.body)) {
|
|
301
|
+
return obj.body;
|
|
302
|
+
}
|
|
303
|
+
return obj;
|
|
304
|
+
}
|
|
305
|
+
function sortNotesPinnedFirst(notes, direction = "DESC") {
|
|
306
|
+
return [...notes].sort((a, b) => {
|
|
307
|
+
if (a.isPinned !== b.isPinned) {
|
|
308
|
+
return a.isPinned ? -1 : 1;
|
|
309
|
+
}
|
|
310
|
+
const timeA = a.createdAt ? Date.parse(a.createdAt) : 0;
|
|
311
|
+
const timeB = b.createdAt ? Date.parse(b.createdAt) : 0;
|
|
312
|
+
if (timeA !== timeB && !Number.isNaN(timeA) && !Number.isNaN(timeB)) {
|
|
313
|
+
return direction === "ASC" ? timeA - timeB : timeB - timeA;
|
|
314
|
+
}
|
|
315
|
+
return String(a.id).localeCompare(String(b.id));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function matchesEntityIdentifier(note, docIdFilter, entityIdFilter) {
|
|
319
|
+
if (!docIdFilter && entityIdFilter === null) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
const noteIdStr = note.entityId !== null ? String(note.entityId) : "";
|
|
323
|
+
if (docIdFilter) {
|
|
324
|
+
if (note.entityDocumentId === docIdFilter || noteIdStr === docIdFilter) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (entityIdFilter !== null) {
|
|
329
|
+
const filterStr = String(entityIdFilter);
|
|
330
|
+
if (noteIdStr === filterStr || note.entityDocumentId === filterStr) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
function matchesLocaleFilter(note, localeFilter, includeAllLocales) {
|
|
337
|
+
if (!localeFilter || includeAllLocales) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
return !note.entityLocale || note.entityLocale === localeFilter;
|
|
341
|
+
}
|
|
342
|
+
function matchesSearchFilter(note, searchFilter) {
|
|
343
|
+
if (!searchFilter) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
const haystack = [
|
|
347
|
+
note.title,
|
|
348
|
+
note.content,
|
|
349
|
+
note.entitySlug,
|
|
350
|
+
note.createdBy?.displayName ?? "",
|
|
351
|
+
note.updatedBy?.displayName ?? ""
|
|
352
|
+
].join(" ").toLowerCase();
|
|
353
|
+
return haystack.includes(searchFilter);
|
|
354
|
+
}
|
|
355
|
+
function matchesAllNoteFilters(note, filters) {
|
|
356
|
+
if (filters.slugFilter && note.entitySlug !== filters.slugFilter) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if (!matchesEntityIdentifier(note, filters.docIdFilter, filters.entityIdFilter)) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (!matchesLocaleFilter(note, filters.localeFilter, filters.includeAllLocales)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
if (filters.categoryFilter && note.category !== filters.categoryFilter) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (typeof filters.isPinned === "boolean" && note.isPinned !== filters.isPinned) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return matchesSearchFilter(note, filters.searchFilter);
|
|
372
|
+
}
|
|
373
|
+
function filterNotesList(notes, params = {}) {
|
|
374
|
+
const filters = {
|
|
375
|
+
slugFilter: params.entitySlug ? sanitizeEntitySlug(params.entitySlug) : "",
|
|
376
|
+
docIdFilter: params.entityDocumentId ? sanitizeEntityDocumentId(params.entityDocumentId) : "",
|
|
377
|
+
entityIdFilter: params.entityId !== void 0 && params.entityId !== null ? sanitizeEntityId(params.entityId) : null,
|
|
378
|
+
localeFilter: sanitizeEntityLocale(params.entityLocale),
|
|
379
|
+
includeAllLocales: params.includeAllLocales,
|
|
380
|
+
categoryFilter: params.category && params.category !== "all" ? normalizeNoteCategory(params.category) : null,
|
|
381
|
+
isPinned: params.isPinned,
|
|
382
|
+
searchFilter: sanitizePlainText(params.search, 160).toLowerCase()
|
|
383
|
+
};
|
|
384
|
+
const filtered = notes.filter((note) => matchesAllNoteFilters(note, filters));
|
|
385
|
+
const sorted = sortNotesPinnedFirst(filtered, params.sort ?? "DESC");
|
|
386
|
+
const offset = typeof params.offset === "number" && Number.isFinite(params.offset) && params.offset > 0 ? Math.trunc(params.offset) : 0;
|
|
387
|
+
const limit = typeof params.limit === "number" && Number.isFinite(params.limit) && params.limit > 0 ? Math.trunc(params.limit) : void 0;
|
|
388
|
+
if (offset > 0 || limit !== void 0) {
|
|
389
|
+
return sorted.slice(
|
|
390
|
+
offset,
|
|
391
|
+
limit !== void 0 ? offset + limit : void 0
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
return sorted;
|
|
395
|
+
}
|
|
396
|
+
function computeNotesSummaryStats(notes) {
|
|
397
|
+
const byCategory = {
|
|
398
|
+
general: 0,
|
|
399
|
+
todo: 0,
|
|
400
|
+
blocker: 0,
|
|
401
|
+
idea: 0
|
|
402
|
+
};
|
|
403
|
+
let pinned = 0;
|
|
404
|
+
const slugSet = /* @__PURE__ */ new Set();
|
|
405
|
+
for (const note of notes) {
|
|
406
|
+
if (note.isPinned) {
|
|
407
|
+
pinned += 1;
|
|
408
|
+
}
|
|
409
|
+
const cat = normalizeNoteCategory(note.category);
|
|
410
|
+
byCategory[cat] += 1;
|
|
411
|
+
if (note.entitySlug) {
|
|
412
|
+
slugSet.add(note.entitySlug);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
total: notes.length,
|
|
417
|
+
pinned,
|
|
418
|
+
byCategory,
|
|
419
|
+
entitySlugs: Array.from(slugSet).sort()
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const bootstrap = async ({ strapi }) => {
|
|
423
|
+
const actionProvider = strapi.admin?.services?.permission?.actionProvider;
|
|
424
|
+
if (actionProvider && typeof actionProvider.registerMany === "function") {
|
|
425
|
+
await actionProvider.registerMany([
|
|
426
|
+
{
|
|
427
|
+
section: "plugins",
|
|
428
|
+
displayName: "Read notes",
|
|
429
|
+
uid: "read",
|
|
430
|
+
pluginName: PLUGIN_ID
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
section: "plugins",
|
|
434
|
+
displayName: "Create & edit notes",
|
|
435
|
+
uid: "update",
|
|
436
|
+
pluginName: PLUGIN_ID
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
section: "plugins",
|
|
440
|
+
displayName: "Delete notes",
|
|
441
|
+
uid: "delete",
|
|
442
|
+
pluginName: PLUGIN_ID
|
|
443
|
+
}
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const config = {
|
|
448
|
+
default: {
|
|
449
|
+
defaultSortOrder: "DESC",
|
|
450
|
+
enableLocaleScoping: true
|
|
451
|
+
},
|
|
452
|
+
validator(config2) {
|
|
453
|
+
if (!config2 || typeof config2 !== "object" || Array.isArray(config2)) {
|
|
454
|
+
throw new Error("Invalid notes plugin config: expected an object.");
|
|
455
|
+
}
|
|
456
|
+
if (config2.defaultSortOrder !== void 0 && config2.defaultSortOrder !== "ASC" && config2.defaultSortOrder !== "DESC") {
|
|
457
|
+
throw new Error(
|
|
458
|
+
'Invalid notes plugin config: defaultSortOrder must be "ASC" or "DESC".'
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (config2.enableLocaleScoping !== void 0 && typeof config2.enableLocaleScoping !== "boolean") {
|
|
462
|
+
throw new Error(
|
|
463
|
+
"Invalid notes plugin config: enableLocaleScoping must be a boolean."
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const kind = "collectionType";
|
|
469
|
+
const collectionName = "notes";
|
|
470
|
+
const info = { "singularName": "note", "pluralName": "notes", "displayName": "Notes", "description": "Editorial notes attached to Content Manager entity records" };
|
|
471
|
+
const options = { "draftAndPublish": false };
|
|
472
|
+
const pluginOptions = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
|
|
473
|
+
const attributes = { "title": { "type": "string", "required": true, "maxLength": 255 }, "content": { "type": "text" }, "category": { "type": "enumeration", "enum": ["general", "todo", "blocker", "idea"], "default": "general", "required": true }, "isPinned": { "type": "boolean", "default": false }, "entityId": { "type": "integer" }, "entityDocumentId": { "type": "string", "maxLength": 128 }, "entitySlug": { "type": "string", "required": true, "maxLength": 160 }, "entityLocale": { "type": "string", "maxLength": 32 }, "createdByName": { "type": "string", "maxLength": 120 }, "updatedByName": { "type": "string", "maxLength": 120 } };
|
|
474
|
+
const noteSchema = {
|
|
475
|
+
kind,
|
|
476
|
+
collectionName,
|
|
477
|
+
info,
|
|
478
|
+
options,
|
|
479
|
+
pluginOptions,
|
|
480
|
+
attributes
|
|
481
|
+
};
|
|
482
|
+
const contentTypes = {
|
|
483
|
+
note: {
|
|
484
|
+
schema: noteSchema
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
function asObjectRecord(value) {
|
|
488
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
489
|
+
}
|
|
490
|
+
function getNotesPluginConfig(strapi) {
|
|
491
|
+
const raw = typeof strapi?.config?.get === "function" ? strapi.config.get(`plugin::${PLUGIN_ID}`, {}) : {};
|
|
492
|
+
const configObj = asObjectRecord(raw);
|
|
493
|
+
const defaultSortOrder = configObj.defaultSortOrder === "ASC" ? "ASC" : "DESC";
|
|
494
|
+
const enableLocaleScoping = typeof configObj.enableLocaleScoping === "boolean" ? configObj.enableLocaleScoping : true;
|
|
495
|
+
return { defaultSortOrder, enableLocaleScoping };
|
|
496
|
+
}
|
|
497
|
+
function extractFilterValue(value) {
|
|
498
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
499
|
+
const obj = value;
|
|
500
|
+
if ("$eq" in obj) {
|
|
501
|
+
return obj.$eq;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
function pickQueryField(rawQuery, filtersObj, key) {
|
|
507
|
+
return rawQuery[key] ?? extractFilterValue(filtersObj[key]) ?? rawQuery[`filters[${key}]`] ?? rawQuery[`filters[${key}][$eq]`];
|
|
508
|
+
}
|
|
509
|
+
function resolveSortDirection(rawQuery, sortObj, defaultSortOrder = "DESC") {
|
|
510
|
+
const candidates = [
|
|
511
|
+
rawQuery.sort,
|
|
512
|
+
sortObj.createdAt,
|
|
513
|
+
rawQuery["sort[createdAt]"]
|
|
514
|
+
];
|
|
515
|
+
const rawSort = candidates.find((val) => typeof val === "string");
|
|
516
|
+
if (typeof rawSort !== "string" || !rawSort.trim()) {
|
|
517
|
+
return defaultSortOrder;
|
|
518
|
+
}
|
|
519
|
+
return rawSort.toUpperCase().includes("ASC") ? "ASC" : "DESC";
|
|
520
|
+
}
|
|
521
|
+
function parseOptionalBoolean(value) {
|
|
522
|
+
if (value === true || value === "true") return true;
|
|
523
|
+
if (value === false || value === "false") return false;
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
function parseOptionalNonNegativeInt(value, minValue) {
|
|
527
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
528
|
+
const truncated = Math.trunc(value);
|
|
529
|
+
return truncated >= minValue ? truncated : void 0;
|
|
530
|
+
}
|
|
531
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
532
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
533
|
+
return Number.isSafeInteger(parsed) && parsed >= minValue ? parsed : void 0;
|
|
534
|
+
}
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
function parseCategoryFilter(rawCategory) {
|
|
538
|
+
if (rawCategory === "all" || !rawCategory) {
|
|
539
|
+
return "all";
|
|
540
|
+
}
|
|
541
|
+
return normalizeNoteCategory(rawCategory);
|
|
542
|
+
}
|
|
543
|
+
function parseNoteQueryFilters(rawQuery = {}, pluginConfig) {
|
|
544
|
+
const filtersObj = asObjectRecord(rawQuery.filters);
|
|
545
|
+
const sortObj = asObjectRecord(rawQuery.sort);
|
|
546
|
+
const paginationObj = asObjectRecord(rawQuery.pagination);
|
|
547
|
+
const localeScopingEnabled = pluginConfig?.enableLocaleScoping !== false;
|
|
548
|
+
const defaultSortOrder = pluginConfig?.defaultSortOrder === "ASC" ? "ASC" : "DESC";
|
|
549
|
+
const rawSlug = pickQueryField(rawQuery, filtersObj, "entitySlug");
|
|
550
|
+
const rawDocId = pickQueryField(rawQuery, filtersObj, "entityDocumentId");
|
|
551
|
+
const rawEntityId = pickQueryField(rawQuery, filtersObj, "entityId");
|
|
552
|
+
const rawLocale = pickQueryField(rawQuery, filtersObj, "entityLocale");
|
|
553
|
+
const rawCategory = rawQuery.category ?? extractFilterValue(filtersObj.category);
|
|
554
|
+
const rawPinned = rawQuery.isPinned ?? extractFilterValue(filtersObj.isPinned);
|
|
555
|
+
const includeAllLocales = !localeScopingEnabled || parseOptionalBoolean(rawQuery.includeAllLocales) === true;
|
|
556
|
+
return {
|
|
557
|
+
entitySlug: sanitizeEntitySlug(rawSlug) || void 0,
|
|
558
|
+
entityDocumentId: sanitizeEntityDocumentId(rawDocId) || void 0,
|
|
559
|
+
entityId: sanitizeEntityId(rawEntityId),
|
|
560
|
+
entityLocale: localeScopingEnabled ? sanitizeEntityLocale(rawLocale) : null,
|
|
561
|
+
includeAllLocales,
|
|
562
|
+
category: parseCategoryFilter(rawCategory),
|
|
563
|
+
isPinned: parseOptionalBoolean(rawPinned),
|
|
564
|
+
search: sanitizePlainText(rawQuery.search ?? rawQuery._q, 160) || void 0,
|
|
565
|
+
sort: resolveSortDirection(rawQuery, sortObj, defaultSortOrder),
|
|
566
|
+
limit: parseOptionalNonNegativeInt(
|
|
567
|
+
rawQuery.limit ?? paginationObj.limit ?? paginationObj.pageSize,
|
|
568
|
+
1
|
|
569
|
+
),
|
|
570
|
+
offset: parseOptionalNonNegativeInt(
|
|
571
|
+
rawQuery.offset ?? paginationObj.offset ?? paginationObj.start,
|
|
572
|
+
0
|
|
573
|
+
)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function buildDbWhereClause(filters = {}) {
|
|
577
|
+
const where = {};
|
|
578
|
+
if (filters.entitySlug) {
|
|
579
|
+
where.entitySlug = filters.entitySlug;
|
|
580
|
+
}
|
|
581
|
+
if (filters.category && filters.category !== "all") {
|
|
582
|
+
where.category = filters.category;
|
|
583
|
+
}
|
|
584
|
+
if (typeof filters.isPinned === "boolean") {
|
|
585
|
+
where.isPinned = filters.isPinned;
|
|
586
|
+
}
|
|
587
|
+
return where;
|
|
588
|
+
}
|
|
589
|
+
function hasAuthorIdentityFields(record) {
|
|
590
|
+
const keys = ["firstname", "lastname", "username", "displayName"];
|
|
591
|
+
return keys.some(
|
|
592
|
+
(key) => typeof record[key] === "string" && record[key].trim().length > 0
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
function hasPopulatedAuthorIdentity(value) {
|
|
596
|
+
if (!value || typeof value !== "object") {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
const root = value;
|
|
600
|
+
if (hasAuthorIdentityFields(root)) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
const nestedData = asObjectRecord(root.data);
|
|
604
|
+
if (hasAuthorIdentityFields(nestedData)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
const nestedAttrs = asObjectRecord(nestedData.attributes ?? root.attributes);
|
|
608
|
+
return hasAuthorIdentityFields(nestedAttrs);
|
|
609
|
+
}
|
|
610
|
+
function resolvePopulatedAuthor(dbValue, fallbackAuthor) {
|
|
611
|
+
if (hasPopulatedAuthorIdentity(dbValue)) {
|
|
612
|
+
return dbValue;
|
|
613
|
+
}
|
|
614
|
+
return fallbackAuthor ?? dbValue;
|
|
615
|
+
}
|
|
616
|
+
class NotesValidationError extends Error {
|
|
617
|
+
status = 400;
|
|
618
|
+
details;
|
|
619
|
+
constructor(errors) {
|
|
620
|
+
super(errors.join(" "));
|
|
621
|
+
this.name = "NotesValidationError";
|
|
622
|
+
this.details = errors;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function getDbQuery(strapi) {
|
|
626
|
+
if (typeof strapi?.db?.query === "function") {
|
|
627
|
+
return strapi.db.query(NOTE_CONTENT_TYPE_UID);
|
|
628
|
+
}
|
|
629
|
+
if (typeof strapi?.query === "function") {
|
|
630
|
+
return strapi.query(NOTE_CONTENT_TYPE_UID);
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
function getDocService(strapi) {
|
|
635
|
+
if (typeof strapi?.documents === "function") {
|
|
636
|
+
return strapi.documents(NOTE_CONTENT_TYPE_UID) ?? null;
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function hasExternalStorage(strapi) {
|
|
641
|
+
return Boolean(getDbQuery(strapi) || getDocService(strapi));
|
|
642
|
+
}
|
|
643
|
+
function hasNamedActorDisplay(actor) {
|
|
644
|
+
if (!actor) return false;
|
|
645
|
+
return Boolean(
|
|
646
|
+
actor.firstname || actor.lastname || actor.username || actor.displayName && !actor.displayName.startsWith("#")
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
function mergeUpdatedNoteRecord(existing, updated, actor) {
|
|
650
|
+
return normalizeNoteRecord({
|
|
651
|
+
...existing,
|
|
652
|
+
...updated,
|
|
653
|
+
createdBy: resolvePopulatedAuthor(updated.createdBy, existing.createdBy),
|
|
654
|
+
updatedBy: resolvePopulatedAuthor(
|
|
655
|
+
updated.updatedBy,
|
|
656
|
+
actor ?? existing.updatedBy
|
|
657
|
+
)
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const MAX_SQL_INT32 = 2147483647;
|
|
661
|
+
function toSqlInt32(value) {
|
|
662
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_SQL_INT32) {
|
|
663
|
+
return value;
|
|
664
|
+
}
|
|
665
|
+
if (typeof value === "string" && /^\d{1,10}$/.test(value)) {
|
|
666
|
+
const parsed = Number.parseInt(value, 10);
|
|
667
|
+
if (parsed >= 1 && parsed <= MAX_SQL_INT32) {
|
|
668
|
+
return parsed;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
async function queryDbByIdentifier(strapi, cleanStr) {
|
|
674
|
+
const dbQuery = getDbQuery(strapi);
|
|
675
|
+
if (!dbQuery || typeof dbQuery.findOne !== "function") {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
const numericId = toSqlInt32(cleanStr);
|
|
679
|
+
if (numericId !== null) {
|
|
680
|
+
const byId = await dbQuery.findOne({
|
|
681
|
+
where: { id: numericId },
|
|
682
|
+
populate: ["createdBy", "updatedBy"]
|
|
683
|
+
});
|
|
684
|
+
if (byId) return byId;
|
|
685
|
+
}
|
|
686
|
+
const byDocId = await dbQuery.findOne({
|
|
687
|
+
where: { documentId: cleanStr },
|
|
688
|
+
populate: ["createdBy", "updatedBy"]
|
|
689
|
+
});
|
|
690
|
+
return byDocId ?? null;
|
|
691
|
+
}
|
|
692
|
+
async function queryDocServiceByIdentifier(strapi, cleanStr) {
|
|
693
|
+
const docService = getDocService(strapi);
|
|
694
|
+
if (!docService || typeof docService.findOne !== "function") {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
const found = await docService.findOne({
|
|
698
|
+
documentId: cleanStr,
|
|
699
|
+
populate: ["createdBy", "updatedBy"]
|
|
700
|
+
});
|
|
701
|
+
return found ?? null;
|
|
702
|
+
}
|
|
703
|
+
async function persistCreatedNote(strapi, createData, actor) {
|
|
704
|
+
const dbQuery = getDbQuery(strapi);
|
|
705
|
+
if (dbQuery && typeof dbQuery.create === "function") {
|
|
706
|
+
const created = await dbQuery.create({
|
|
707
|
+
data: createData,
|
|
708
|
+
populate: ["createdBy", "updatedBy"]
|
|
709
|
+
});
|
|
710
|
+
return normalizeNoteRecord({
|
|
711
|
+
...created,
|
|
712
|
+
createdBy: resolvePopulatedAuthor(created.createdBy, actor),
|
|
713
|
+
updatedBy: resolvePopulatedAuthor(created.updatedBy, actor)
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
const docService = getDocService(strapi);
|
|
717
|
+
if (docService && typeof docService.create === "function") {
|
|
718
|
+
const created = await docService.create({
|
|
719
|
+
data: createData,
|
|
720
|
+
populate: ["createdBy", "updatedBy"]
|
|
721
|
+
});
|
|
722
|
+
return normalizeNoteRecord({
|
|
723
|
+
...created,
|
|
724
|
+
createdBy: resolvePopulatedAuthor(created.createdBy, actor),
|
|
725
|
+
updatedBy: resolvePopulatedAuthor(created.updatedBy, actor)
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
async function persistUpdatedNote(strapi, existing, updateData, actor) {
|
|
731
|
+
const dbQuery = getDbQuery(strapi);
|
|
732
|
+
if (dbQuery && typeof dbQuery.update === "function" && existing.id !== void 0) {
|
|
733
|
+
const updated = await dbQuery.update({
|
|
734
|
+
where: { id: existing.id },
|
|
735
|
+
data: updateData,
|
|
736
|
+
populate: ["createdBy", "updatedBy"]
|
|
737
|
+
});
|
|
738
|
+
return updated ? mergeUpdatedNoteRecord(existing, updated, actor) : null;
|
|
739
|
+
}
|
|
740
|
+
const docService = getDocService(strapi);
|
|
741
|
+
if (docService && typeof docService.update === "function" && existing.documentId) {
|
|
742
|
+
const updated = await docService.update({
|
|
743
|
+
documentId: String(existing.documentId),
|
|
744
|
+
data: updateData,
|
|
745
|
+
populate: ["createdBy", "updatedBy"]
|
|
746
|
+
});
|
|
747
|
+
return updated ? mergeUpdatedNoteRecord(existing, updated, actor) : null;
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
const notesService = ({ strapi }) => {
|
|
752
|
+
const memoryFallback = [];
|
|
753
|
+
let nextMemoryId = 1;
|
|
754
|
+
async function fetchRawNotes(filters) {
|
|
755
|
+
const where = buildDbWhereClause(filters);
|
|
756
|
+
const hasWhere = Object.keys(where).length > 0;
|
|
757
|
+
const sortDir = (filters.sort ?? "DESC").toLowerCase();
|
|
758
|
+
const dbQuery = getDbQuery(strapi);
|
|
759
|
+
if (dbQuery && typeof dbQuery.findMany === "function") {
|
|
760
|
+
const rows = await dbQuery.findMany({
|
|
761
|
+
...hasWhere ? { where } : {},
|
|
762
|
+
populate: ["createdBy", "updatedBy"],
|
|
763
|
+
orderBy: { isPinned: "desc", createdAt: sortDir }
|
|
764
|
+
});
|
|
765
|
+
return Array.isArray(rows) ? rows : [];
|
|
766
|
+
}
|
|
767
|
+
const docService = getDocService(strapi);
|
|
768
|
+
if (docService && typeof docService.findMany === "function") {
|
|
769
|
+
const docs = await docService.findMany({
|
|
770
|
+
...hasWhere ? { filters: where } : {},
|
|
771
|
+
populate: ["createdBy", "updatedBy"],
|
|
772
|
+
sort: { isPinned: "desc", createdAt: sortDir }
|
|
773
|
+
});
|
|
774
|
+
return Array.isArray(docs) ? docs : [];
|
|
775
|
+
}
|
|
776
|
+
return [...memoryFallback];
|
|
777
|
+
}
|
|
778
|
+
async function findRawNoteByIdentifier(identifier) {
|
|
779
|
+
const cleanStr = String(identifier ?? "").trim();
|
|
780
|
+
if (!cleanStr) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
const fromDb = await queryDbByIdentifier(strapi, cleanStr);
|
|
784
|
+
if (fromDb) return fromDb;
|
|
785
|
+
const fromDoc = await queryDocServiceByIdentifier(strapi, cleanStr);
|
|
786
|
+
if (fromDoc) return fromDoc;
|
|
787
|
+
if (hasExternalStorage(strapi)) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const fromMemory = memoryFallback.find(
|
|
791
|
+
(item) => String(item.id) === cleanStr || String(item.documentId) === cleanStr
|
|
792
|
+
);
|
|
793
|
+
return fromMemory ?? null;
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
async findWithMeta(rawQuery = {}) {
|
|
797
|
+
const pluginConfig = getNotesPluginConfig(strapi);
|
|
798
|
+
const filters = parseNoteQueryFilters(rawQuery, pluginConfig);
|
|
799
|
+
const rawList = await fetchRawNotes(filters);
|
|
800
|
+
const normalized = rawList.map((item) => normalizeNoteRecord(item));
|
|
801
|
+
const matched = filterNotesList(normalized, {
|
|
802
|
+
...filters,
|
|
803
|
+
limit: void 0,
|
|
804
|
+
offset: void 0
|
|
805
|
+
});
|
|
806
|
+
const summary = computeNotesSummaryStats(matched);
|
|
807
|
+
const data = filterNotesList(matched, {
|
|
808
|
+
sort: filters.sort,
|
|
809
|
+
limit: filters.limit,
|
|
810
|
+
offset: filters.offset
|
|
811
|
+
});
|
|
812
|
+
return {
|
|
813
|
+
data,
|
|
814
|
+
total: matched.length,
|
|
815
|
+
summary
|
|
816
|
+
};
|
|
817
|
+
},
|
|
818
|
+
async find(rawQuery = {}) {
|
|
819
|
+
const { data } = await this.findWithMeta(rawQuery);
|
|
820
|
+
return data;
|
|
821
|
+
},
|
|
822
|
+
async findOne(identifier) {
|
|
823
|
+
const found = await findRawNoteByIdentifier(identifier);
|
|
824
|
+
return found ? normalizeNoteRecord(found) : null;
|
|
825
|
+
},
|
|
826
|
+
async getSummaryStats(rawQuery = {}) {
|
|
827
|
+
const { summary } = await this.findWithMeta(rawQuery);
|
|
828
|
+
return summary;
|
|
829
|
+
},
|
|
830
|
+
async create(input, actorUser) {
|
|
831
|
+
const validation = validateNoteCreatePayload(input);
|
|
832
|
+
if (!validation.valid) {
|
|
833
|
+
throw new NotesValidationError(validation.errors);
|
|
834
|
+
}
|
|
835
|
+
const pluginConfig = getNotesPluginConfig(strapi);
|
|
836
|
+
const actor = sanitizeNoteAuthor(actorUser);
|
|
837
|
+
const actorName = hasNamedActorDisplay(actor) ? actor?.displayName ?? "" : "";
|
|
838
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
839
|
+
const numericEntityId = toSqlInt32(validation.data.entityId);
|
|
840
|
+
const createData = {
|
|
841
|
+
title: validation.data.title,
|
|
842
|
+
content: validation.data.content,
|
|
843
|
+
category: validation.data.category,
|
|
844
|
+
isPinned: validation.data.isPinned,
|
|
845
|
+
entitySlug: validation.data.entitySlug,
|
|
846
|
+
entityId: numericEntityId,
|
|
847
|
+
entityDocumentId: validation.data.entityDocumentId,
|
|
848
|
+
entityLocale: pluginConfig.enableLocaleScoping ? validation.data.entityLocale : null,
|
|
849
|
+
createdByName: actorName,
|
|
850
|
+
updatedByName: actorName
|
|
851
|
+
};
|
|
852
|
+
if (actor?.id !== null && actor?.id !== void 0) {
|
|
853
|
+
createData.createdBy = actor.id;
|
|
854
|
+
createData.updatedBy = actor.id;
|
|
855
|
+
}
|
|
856
|
+
const persisted = await persistCreatedNote(strapi, createData, actor);
|
|
857
|
+
if (persisted) {
|
|
858
|
+
return persisted;
|
|
859
|
+
}
|
|
860
|
+
const memId = nextMemoryId++;
|
|
861
|
+
const record = {
|
|
862
|
+
id: memId,
|
|
863
|
+
documentId: `note_${memId}`,
|
|
864
|
+
...createData,
|
|
865
|
+
createdAt: nowIso,
|
|
866
|
+
updatedAt: nowIso,
|
|
867
|
+
createdBy: actor,
|
|
868
|
+
updatedBy: actor
|
|
869
|
+
};
|
|
870
|
+
memoryFallback.push(record);
|
|
871
|
+
return normalizeNoteRecord(record);
|
|
872
|
+
},
|
|
873
|
+
async update(identifier, input, actorUser) {
|
|
874
|
+
const validation = validateNoteUpdatePayload(input);
|
|
875
|
+
if (!validation.valid) {
|
|
876
|
+
throw new NotesValidationError(validation.errors);
|
|
877
|
+
}
|
|
878
|
+
const existing = await findRawNoteByIdentifier(identifier);
|
|
879
|
+
if (!existing) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
const pluginConfig = getNotesPluginConfig(strapi);
|
|
883
|
+
const actor = sanitizeNoteAuthor(actorUser);
|
|
884
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
885
|
+
const updateData = {
|
|
886
|
+
...validation.data
|
|
887
|
+
};
|
|
888
|
+
if ("entityId" in updateData) {
|
|
889
|
+
const rawEntityId = updateData.entityId;
|
|
890
|
+
updateData.entityId = toSqlInt32(rawEntityId);
|
|
891
|
+
if (updateData.entityId === null && rawEntityId !== null && rawEntityId !== void 0 && !updateData.entityDocumentId && !existing.entityDocumentId) {
|
|
892
|
+
updateData.entityDocumentId = String(rawEntityId);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (!pluginConfig.enableLocaleScoping && "entityLocale" in validation.data) {
|
|
896
|
+
updateData.entityLocale = null;
|
|
897
|
+
}
|
|
898
|
+
if (hasNamedActorDisplay(actor) && actor?.displayName) {
|
|
899
|
+
updateData.updatedByName = actor.displayName;
|
|
900
|
+
}
|
|
901
|
+
if (actor?.id !== null && actor?.id !== void 0) {
|
|
902
|
+
updateData.updatedBy = actor.id;
|
|
903
|
+
}
|
|
904
|
+
if (hasExternalStorage(strapi)) {
|
|
905
|
+
return persistUpdatedNote(strapi, existing, updateData, actor);
|
|
906
|
+
}
|
|
907
|
+
const memIndex = memoryFallback.findIndex(
|
|
908
|
+
(item) => String(item.id) === String(existing.id) || String(item.documentId) === String(existing.documentId)
|
|
909
|
+
);
|
|
910
|
+
const merged = {
|
|
911
|
+
...existing,
|
|
912
|
+
...updateData,
|
|
913
|
+
updatedAt: nowIso,
|
|
914
|
+
updatedBy: resolvePopulatedAuthor(actor, existing.updatedBy)
|
|
915
|
+
};
|
|
916
|
+
if (memIndex >= 0) {
|
|
917
|
+
memoryFallback[memIndex] = merged;
|
|
918
|
+
}
|
|
919
|
+
return normalizeNoteRecord(merged);
|
|
920
|
+
},
|
|
921
|
+
async delete(identifier) {
|
|
922
|
+
const existing = await findRawNoteByIdentifier(identifier);
|
|
923
|
+
if (!existing) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const normalized = normalizeNoteRecord(existing);
|
|
927
|
+
const dbQuery = getDbQuery(strapi);
|
|
928
|
+
if (dbQuery && typeof dbQuery.delete === "function" && existing.id !== void 0) {
|
|
929
|
+
await dbQuery.delete({
|
|
930
|
+
where: { id: existing.id }
|
|
931
|
+
});
|
|
932
|
+
return normalized;
|
|
933
|
+
}
|
|
934
|
+
const docService = getDocService(strapi);
|
|
935
|
+
if (docService && typeof docService.delete === "function" && existing.documentId) {
|
|
936
|
+
await docService.delete({
|
|
937
|
+
documentId: String(existing.documentId)
|
|
938
|
+
});
|
|
939
|
+
return normalized;
|
|
940
|
+
}
|
|
941
|
+
const memIndex = memoryFallback.findIndex(
|
|
942
|
+
(item) => String(item.id) === String(existing.id) || String(item.documentId) === String(existing.documentId)
|
|
943
|
+
);
|
|
944
|
+
if (memIndex >= 0) {
|
|
945
|
+
memoryFallback.splice(memIndex, 1);
|
|
946
|
+
}
|
|
947
|
+
return normalized;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
};
|
|
951
|
+
function sendJson(ctx, payload, statusCode = 200) {
|
|
952
|
+
ctx.status = statusCode;
|
|
953
|
+
ctx.body = payload;
|
|
954
|
+
if (typeof ctx.send === "function" && statusCode === 200) {
|
|
955
|
+
ctx.send(payload);
|
|
956
|
+
}
|
|
957
|
+
return payload;
|
|
958
|
+
}
|
|
959
|
+
function sendBadRequest(ctx, message, details) {
|
|
960
|
+
if (typeof ctx.badRequest === "function") {
|
|
961
|
+
return ctx.badRequest(message, { errors: details ?? [message] });
|
|
962
|
+
}
|
|
963
|
+
return sendJson(
|
|
964
|
+
ctx,
|
|
965
|
+
{
|
|
966
|
+
data: null,
|
|
967
|
+
error: {
|
|
968
|
+
status: 400,
|
|
969
|
+
name: "ValidationError",
|
|
970
|
+
message,
|
|
971
|
+
details: { errors: details ?? [message] }
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
400
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
function sendNotFound(ctx, message = "Note not found.") {
|
|
978
|
+
if (typeof ctx.notFound === "function") {
|
|
979
|
+
return ctx.notFound(message);
|
|
980
|
+
}
|
|
981
|
+
return sendJson(
|
|
982
|
+
ctx,
|
|
983
|
+
{
|
|
984
|
+
data: null,
|
|
985
|
+
error: {
|
|
986
|
+
status: 404,
|
|
987
|
+
name: "NotFoundError",
|
|
988
|
+
message
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
404
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
const notesController = ({ strapi }) => {
|
|
995
|
+
const getService = () => strapi.plugin(PLUGIN_ID).service("notesService");
|
|
996
|
+
return {
|
|
997
|
+
async find(ctx) {
|
|
998
|
+
const query = ctx.query ?? {};
|
|
999
|
+
const service = getService();
|
|
1000
|
+
if (typeof service.findWithMeta === "function") {
|
|
1001
|
+
const { data, total, summary: summary2 } = await service.findWithMeta(query);
|
|
1002
|
+
return sendJson(ctx, {
|
|
1003
|
+
data,
|
|
1004
|
+
meta: {
|
|
1005
|
+
total,
|
|
1006
|
+
summary: summary2
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
const notes = await service.find(query);
|
|
1011
|
+
const summary = computeNotesSummaryStats(notes);
|
|
1012
|
+
return sendJson(ctx, {
|
|
1013
|
+
data: notes,
|
|
1014
|
+
meta: {
|
|
1015
|
+
total: notes.length,
|
|
1016
|
+
summary
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
},
|
|
1020
|
+
async stats(ctx) {
|
|
1021
|
+
const query = ctx.query ?? {};
|
|
1022
|
+
const summary = await getService().getSummaryStats(query);
|
|
1023
|
+
return sendJson(ctx, { data: summary });
|
|
1024
|
+
},
|
|
1025
|
+
async findOne(ctx) {
|
|
1026
|
+
const id = ctx.params?.id ?? "";
|
|
1027
|
+
if (!id) {
|
|
1028
|
+
return sendBadRequest(ctx, "Note identifier is required.");
|
|
1029
|
+
}
|
|
1030
|
+
const note = await getService().findOne(id);
|
|
1031
|
+
if (!note) {
|
|
1032
|
+
return sendNotFound(ctx);
|
|
1033
|
+
}
|
|
1034
|
+
return sendJson(ctx, { data: note });
|
|
1035
|
+
},
|
|
1036
|
+
async create(ctx) {
|
|
1037
|
+
try {
|
|
1038
|
+
const created = await getService().create(
|
|
1039
|
+
ctx.request?.body ?? {},
|
|
1040
|
+
ctx.state?.user
|
|
1041
|
+
);
|
|
1042
|
+
return sendJson(ctx, { data: created }, 201);
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
if (err instanceof NotesValidationError) {
|
|
1045
|
+
return sendBadRequest(ctx, err.message, err.details);
|
|
1046
|
+
}
|
|
1047
|
+
throw err;
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
async update(ctx) {
|
|
1051
|
+
const id = ctx.params?.id ?? "";
|
|
1052
|
+
if (!id) {
|
|
1053
|
+
return sendBadRequest(ctx, "Note identifier is required.");
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const updated = await getService().update(
|
|
1057
|
+
id,
|
|
1058
|
+
ctx.request?.body ?? {},
|
|
1059
|
+
ctx.state?.user
|
|
1060
|
+
);
|
|
1061
|
+
if (!updated) {
|
|
1062
|
+
return sendNotFound(ctx);
|
|
1063
|
+
}
|
|
1064
|
+
return sendJson(ctx, { data: updated });
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
if (err instanceof NotesValidationError) {
|
|
1067
|
+
return sendBadRequest(ctx, err.message, err.details);
|
|
1068
|
+
}
|
|
1069
|
+
throw err;
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
async delete(ctx) {
|
|
1073
|
+
const id = ctx.params?.id ?? "";
|
|
1074
|
+
if (!id) {
|
|
1075
|
+
return sendBadRequest(ctx, "Note identifier is required.");
|
|
1076
|
+
}
|
|
1077
|
+
const deleted = await getService().delete(id);
|
|
1078
|
+
if (!deleted) {
|
|
1079
|
+
return sendNotFound(ctx);
|
|
1080
|
+
}
|
|
1081
|
+
return sendJson(ctx, { data: deleted });
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
};
|
|
1085
|
+
const controllers = {
|
|
1086
|
+
notesController,
|
|
1087
|
+
noteController: notesController,
|
|
1088
|
+
note: notesController
|
|
1089
|
+
};
|
|
1090
|
+
const destroy = ({ strapi }) => {
|
|
1091
|
+
strapi.log?.debug?.("[@vicacha-devs/strapi-plugin-notes] Destroyed plugin.");
|
|
1092
|
+
};
|
|
1093
|
+
const register = ({ strapi }) => {
|
|
1094
|
+
strapi.log?.debug?.("[@vicacha-devs/strapi-plugin-notes] Registered plugin.");
|
|
1095
|
+
};
|
|
1096
|
+
const readPolicies = [
|
|
1097
|
+
"admin::isAuthenticatedAdmin",
|
|
1098
|
+
{
|
|
1099
|
+
name: "admin::hasPermissions",
|
|
1100
|
+
config: { actions: [`plugin::${PLUGIN_ID}.read`] }
|
|
1101
|
+
}
|
|
1102
|
+
];
|
|
1103
|
+
const updatePolicies = [
|
|
1104
|
+
"admin::isAuthenticatedAdmin",
|
|
1105
|
+
{
|
|
1106
|
+
name: "admin::hasPermissions",
|
|
1107
|
+
config: { actions: [`plugin::${PLUGIN_ID}.update`] }
|
|
1108
|
+
}
|
|
1109
|
+
];
|
|
1110
|
+
const deletePolicies = [
|
|
1111
|
+
"admin::isAuthenticatedAdmin",
|
|
1112
|
+
{
|
|
1113
|
+
name: "admin::hasPermissions",
|
|
1114
|
+
config: { actions: [`plugin::${PLUGIN_ID}.delete`] }
|
|
1115
|
+
}
|
|
1116
|
+
];
|
|
1117
|
+
const admin = {
|
|
1118
|
+
type: "admin",
|
|
1119
|
+
routes: [
|
|
1120
|
+
{
|
|
1121
|
+
method: "GET",
|
|
1122
|
+
path: "/notes",
|
|
1123
|
+
handler: "notesController.find",
|
|
1124
|
+
config: {
|
|
1125
|
+
policies: readPolicies
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
method: "GET",
|
|
1130
|
+
path: "/notes/stats",
|
|
1131
|
+
handler: "notesController.stats",
|
|
1132
|
+
config: {
|
|
1133
|
+
policies: readPolicies
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
method: "GET",
|
|
1138
|
+
path: "/notes/:id",
|
|
1139
|
+
handler: "notesController.findOne",
|
|
1140
|
+
config: {
|
|
1141
|
+
policies: readPolicies
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
method: "POST",
|
|
1146
|
+
path: "/notes",
|
|
1147
|
+
handler: "notesController.create",
|
|
1148
|
+
config: {
|
|
1149
|
+
policies: updatePolicies
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
method: "PUT",
|
|
1154
|
+
path: "/notes/:id",
|
|
1155
|
+
handler: "notesController.update",
|
|
1156
|
+
config: {
|
|
1157
|
+
policies: updatePolicies
|
|
1158
|
+
}
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
method: "DELETE",
|
|
1162
|
+
path: "/notes/:id",
|
|
1163
|
+
handler: "notesController.delete",
|
|
1164
|
+
config: {
|
|
1165
|
+
policies: deletePolicies
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
]
|
|
1169
|
+
};
|
|
1170
|
+
const contentApi = {
|
|
1171
|
+
type: "content-api",
|
|
1172
|
+
routes: [
|
|
1173
|
+
{
|
|
1174
|
+
method: "GET",
|
|
1175
|
+
path: "/notes",
|
|
1176
|
+
handler: "notesController.find",
|
|
1177
|
+
config: {
|
|
1178
|
+
policies: []
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
method: "GET",
|
|
1183
|
+
path: "/notes/:id",
|
|
1184
|
+
handler: "notesController.findOne",
|
|
1185
|
+
config: {
|
|
1186
|
+
policies: []
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
};
|
|
1191
|
+
const routes = {
|
|
1192
|
+
admin,
|
|
1193
|
+
"content-api": contentApi
|
|
1194
|
+
};
|
|
1195
|
+
const services = {
|
|
1196
|
+
notesService,
|
|
1197
|
+
noteService: notesService,
|
|
1198
|
+
note: notesService
|
|
1199
|
+
};
|
|
1200
|
+
const plugin = {
|
|
1201
|
+
register,
|
|
1202
|
+
bootstrap,
|
|
1203
|
+
destroy,
|
|
1204
|
+
config,
|
|
1205
|
+
controllers,
|
|
1206
|
+
routes,
|
|
1207
|
+
services,
|
|
1208
|
+
contentTypes
|
|
1209
|
+
};
|
|
1210
|
+
export {
|
|
1211
|
+
plugin as default
|
|
1212
|
+
};
|