@intranefr/superbackend 1.5.2 → 1.5.3
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/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +4 -2
- package/src/controllers/admin.controller.js +11 -5
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +138 -0
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/helpers/mongooseHelper.js +6 -6
- package/src/helpers/scriptBase.js +2 -2
- package/src/middleware.js +136 -29
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/Markdown.js +75 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/jsonConfigs.service.js +22 -10
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +328 -37
- package/src/services/telegram.service.js +130 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +36 -5
- package/views/admin-experiments.ejs +1 -1
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +221 -4
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +3 -0
- package/analysis-only.skill +0 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const Markdown = require('../models/Markdown');
|
|
4
|
+
|
|
5
|
+
// Error codes
|
|
6
|
+
const ERROR_CODES = {
|
|
7
|
+
VALIDATION: 'VALIDATION',
|
|
8
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
9
|
+
PATH_NOT_UNIQUE: 'PATH_NOT_UNIQUE',
|
|
10
|
+
INVALID_MARKDOWN: 'INVALID_MARKDOWN',
|
|
11
|
+
INVALID_GROUP_CODE: 'INVALID_GROUP_CODE',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Path operations
|
|
15
|
+
function normalizeGroupCode(group_code) {
|
|
16
|
+
if (!group_code) return '';
|
|
17
|
+
|
|
18
|
+
return String(group_code)
|
|
19
|
+
.trim()
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9_-]/g, '')
|
|
22
|
+
.replace(/_{3,}/g, '__') // Normalize multiple underscores
|
|
23
|
+
.replace(/^_|_$/g, ''); // Remove leading/trailing
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseGroupCode(group_code) {
|
|
27
|
+
if (!group_code) return [];
|
|
28
|
+
return group_code.split('__').filter(part => part.length > 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildGroupCode(parts) {
|
|
32
|
+
return parts.filter(part => part.length > 0).join('__');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeCategory(category) {
|
|
36
|
+
const str = String(category || '').trim().toLowerCase();
|
|
37
|
+
if (!str) return 'general';
|
|
38
|
+
|
|
39
|
+
return str
|
|
40
|
+
.normalize('NFKD')
|
|
41
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
42
|
+
.replace(/[^a-z0-9_-]/g, '')
|
|
43
|
+
.replace(/_{2,}/g, '_')
|
|
44
|
+
.replace(/^_|_$/g, '');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeSlugBase(title) {
|
|
48
|
+
const str = String(title || '').trim().toLowerCase();
|
|
49
|
+
if (!str) return 'markdown';
|
|
50
|
+
|
|
51
|
+
const slug = str
|
|
52
|
+
.normalize('NFKD')
|
|
53
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
54
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
55
|
+
.replace(/(^-|-$)/g, '')
|
|
56
|
+
.replace(/-{2,}/g, '-');
|
|
57
|
+
|
|
58
|
+
return slug || 'markdown';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function randomSuffix4() {
|
|
62
|
+
return crypto.randomBytes(2).toString('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function generateUniqueSlugFromTitle(title, category, group_code, { maxAttempts = 10 } = {}) {
|
|
66
|
+
const base = normalizeSlugBase(title);
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < maxAttempts; i += 1) {
|
|
69
|
+
const candidate = `${base}-${randomSuffix4()}`;
|
|
70
|
+
// eslint-disable-next-line no-await-in-loop
|
|
71
|
+
const existing = await Markdown.findOne({
|
|
72
|
+
category: String(category).trim(),
|
|
73
|
+
group_code: group_code ? String(group_code).trim() : '',
|
|
74
|
+
slug: candidate
|
|
75
|
+
}).select('_id').lean();
|
|
76
|
+
if (!existing) return candidate;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error('Failed to generate unique slug');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function validatePathUniqueness(category, group_code, slug, excludeId = null) {
|
|
83
|
+
const query = {
|
|
84
|
+
category: String(category).trim(),
|
|
85
|
+
group_code: group_code ? String(group_code).trim() : '',
|
|
86
|
+
slug: String(slug).trim(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (excludeId) {
|
|
90
|
+
query._id = { $ne: excludeId };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const existing = await Markdown.findOne(query).select('_id').lean();
|
|
94
|
+
return !existing;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateMarkdownContent(markdownRaw) {
|
|
98
|
+
if (typeof markdownRaw !== 'string') {
|
|
99
|
+
const err = new Error('markdownRaw must be a string');
|
|
100
|
+
err.code = ERROR_CODES.VALIDATION;
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Basic markdown validation (can be extended)
|
|
105
|
+
const content = String(markdownRaw).trim();
|
|
106
|
+
if (content.length > 1000000) { // 1MB limit
|
|
107
|
+
const err = new Error('markdownRaw content too large (max 1MB)');
|
|
108
|
+
err.code = ERROR_CODES.VALIDATION;
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return content;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Core CRUD operations
|
|
116
|
+
async function getMarkdownByPath(category, group_code, slug) {
|
|
117
|
+
const doc = await Markdown.findOne({
|
|
118
|
+
category: String(category).trim(),
|
|
119
|
+
group_code: group_code ? String(group_code).trim() : '',
|
|
120
|
+
slug: String(slug).trim(),
|
|
121
|
+
publicEnabled: true,
|
|
122
|
+
status: 'published'
|
|
123
|
+
}).lean();
|
|
124
|
+
|
|
125
|
+
if (!doc) {
|
|
126
|
+
const err = new Error('Markdown not found');
|
|
127
|
+
err.code = ERROR_CODES.NOT_FOUND;
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return doc;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function createMarkdown({ title, category, group_code, markdownRaw, publicEnabled = false, cacheTtlSeconds = 0, ownerUserId, orgId }) {
|
|
135
|
+
// Validation
|
|
136
|
+
const normalizedTitle = String(title || '').trim();
|
|
137
|
+
if (!normalizedTitle) {
|
|
138
|
+
const err = new Error('title is required');
|
|
139
|
+
err.code = ERROR_CODES.VALIDATION;
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalizedCategory = normalizeCategory(category);
|
|
144
|
+
if (!normalizedCategory) {
|
|
145
|
+
const err = new Error('category is required');
|
|
146
|
+
err.code = ERROR_CODES.VALIDATION;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalizedGroupCode = normalizeGroupCode(group_code);
|
|
151
|
+
const normalizedSlug = await generateUniqueSlugFromTitle(normalizedTitle, normalizedCategory, normalizedGroupCode);
|
|
152
|
+
const validatedMarkdown = validateMarkdownContent(markdownRaw);
|
|
153
|
+
|
|
154
|
+
// Validate uniqueness
|
|
155
|
+
if (!(await validatePathUniqueness(normalizedCategory, normalizedGroupCode, normalizedSlug))) {
|
|
156
|
+
const err = new Error('Path must be unique (category + group_code + slug)');
|
|
157
|
+
err.code = ERROR_CODES.PATH_NOT_UNIQUE;
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const createData = {
|
|
162
|
+
title: normalizedTitle,
|
|
163
|
+
slug: normalizedSlug,
|
|
164
|
+
category: normalizedCategory,
|
|
165
|
+
group_code: normalizedGroupCode,
|
|
166
|
+
markdownRaw: validatedMarkdown,
|
|
167
|
+
publicEnabled: Boolean(publicEnabled),
|
|
168
|
+
cacheTtlSeconds: Number(cacheTtlSeconds || 0) || 0,
|
|
169
|
+
ownerUserId,
|
|
170
|
+
orgId,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const doc = await Markdown.create(createData);
|
|
174
|
+
|
|
175
|
+
return doc.toObject();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function getMarkdownById(id) {
|
|
179
|
+
return Markdown.findById(id)
|
|
180
|
+
.select('title slug category group_code markdownRaw publicEnabled status cacheTtlSeconds updatedAt createdAt ownerUserId orgId')
|
|
181
|
+
.lean();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function updateMarkdown(id, patch) {
|
|
185
|
+
const doc = await Markdown.findById(id);
|
|
186
|
+
if (!doc) {
|
|
187
|
+
const err = new Error('Markdown not found');
|
|
188
|
+
err.code = ERROR_CODES.NOT_FOUND;
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const oldCategory = doc.category;
|
|
193
|
+
const oldGroupCode = doc.group_code;
|
|
194
|
+
const oldSlug = doc.slug;
|
|
195
|
+
|
|
196
|
+
// Update fields
|
|
197
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'title')) {
|
|
198
|
+
const title = String(patch.title || '').trim();
|
|
199
|
+
if (!title) {
|
|
200
|
+
const err = new Error('title is required');
|
|
201
|
+
err.code = ERROR_CODES.VALIDATION;
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
doc.title = title;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'category')) {
|
|
208
|
+
doc.category = normalizeCategory(patch.category);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'group_code')) {
|
|
212
|
+
doc.group_code = normalizeGroupCode(patch.group_code);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'markdownRaw')) {
|
|
216
|
+
doc.markdownRaw = validateMarkdownContent(patch.markdownRaw);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'publicEnabled')) {
|
|
220
|
+
doc.publicEnabled = Boolean(patch.publicEnabled);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'status')) {
|
|
224
|
+
const validStatuses = ['draft', 'published', 'archived'];
|
|
225
|
+
if (!validStatuses.includes(patch.status)) {
|
|
226
|
+
const err = new Error('Invalid status. Must be draft, published, or archived');
|
|
227
|
+
err.code = ERROR_CODES.VALIDATION;
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
doc.status = patch.status;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'cacheTtlSeconds')) {
|
|
234
|
+
const ttl = Number(patch.cacheTtlSeconds || 0);
|
|
235
|
+
doc.cacheTtlSeconds = Number.isNaN(ttl) ? 0 : Math.max(0, ttl);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'ownerUserId')) {
|
|
239
|
+
doc.ownerUserId = patch.ownerUserId;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'orgId')) {
|
|
243
|
+
doc.orgId = patch.orgId;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Validate path uniqueness if category, group_code, or slug changed
|
|
247
|
+
if (doc.category !== oldCategory || doc.group_code !== oldGroupCode) {
|
|
248
|
+
if (!(await validatePathUniqueness(doc.category, doc.group_code, doc.slug, id))) {
|
|
249
|
+
const err = new Error('Path must be unique (category + group_code + slug)');
|
|
250
|
+
err.code = ERROR_CODES.PATH_NOT_UNIQUE;
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await doc.save();
|
|
256
|
+
|
|
257
|
+
return doc.toObject();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function deleteMarkdown(id) {
|
|
261
|
+
const doc = await Markdown.findByIdAndDelete(id).lean();
|
|
262
|
+
if (!doc) {
|
|
263
|
+
const err = new Error('Markdown not found');
|
|
264
|
+
err.code = ERROR_CODES.NOT_FOUND;
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { success: true };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// List operations
|
|
272
|
+
async function listMarkdowns(filters = {}, pagination = {}, options = {}) {
|
|
273
|
+
const {
|
|
274
|
+
category,
|
|
275
|
+
group_code,
|
|
276
|
+
status,
|
|
277
|
+
ownerUserId,
|
|
278
|
+
orgId,
|
|
279
|
+
search
|
|
280
|
+
} = filters;
|
|
281
|
+
|
|
282
|
+
const { isAdmin = false } = options;
|
|
283
|
+
|
|
284
|
+
const { page = 1, limit = 50, sort = { updatedAt: -1 } } = pagination;
|
|
285
|
+
const skip = Math.max(0, (page - 1) * limit);
|
|
286
|
+
const normalizedLimit = Math.min(Number(limit) || 50, 200);
|
|
287
|
+
|
|
288
|
+
// Build filter
|
|
289
|
+
const filter = {};
|
|
290
|
+
|
|
291
|
+
if (category) {
|
|
292
|
+
filter.category = String(category).trim();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (group_code) {
|
|
296
|
+
filter.group_code = String(group_code).trim();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Apply status filter: explicit status or default for non-admin
|
|
300
|
+
if (status) {
|
|
301
|
+
filter.status = String(status);
|
|
302
|
+
} else if (!isAdmin) {
|
|
303
|
+
filter.status = 'published';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (ownerUserId) {
|
|
307
|
+
filter.ownerUserId = ownerUserId;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (orgId) {
|
|
311
|
+
filter.orgId = orgId;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (search) {
|
|
315
|
+
filter.$or = [
|
|
316
|
+
{ title: { $regex: search, $options: 'i' } },
|
|
317
|
+
{ markdownRaw: { $regex: search, $options: 'i' } }
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Execute query with pagination
|
|
322
|
+
const [items, total] = await Promise.all([
|
|
323
|
+
Markdown.find(filter)
|
|
324
|
+
.sort(sort)
|
|
325
|
+
.skip(skip)
|
|
326
|
+
.limit(normalizedLimit)
|
|
327
|
+
.select('title slug category group_code markdownRaw publicEnabled status cacheTtlSeconds updatedAt createdAt ownerUserId orgId')
|
|
328
|
+
.lean(),
|
|
329
|
+
Markdown.countDocuments(filter),
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
return { items, total, limit: normalizedLimit, skip };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Tree structure for explorer mode
|
|
336
|
+
async function getMarkdownTree(category, options = {}) {
|
|
337
|
+
const { isAdmin = false } = options;
|
|
338
|
+
const normalizedCategory = String(category || '').trim();
|
|
339
|
+
if (!normalizedCategory) return {};
|
|
340
|
+
|
|
341
|
+
// Build filter based on admin mode
|
|
342
|
+
const filter = { category: normalizedCategory };
|
|
343
|
+
if (!isAdmin) {
|
|
344
|
+
filter.status = 'published';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const docs = await Markdown.find(filter)
|
|
348
|
+
.select('group_code slug title status')
|
|
349
|
+
.lean();
|
|
350
|
+
|
|
351
|
+
// Build tree structure
|
|
352
|
+
const tree = {};
|
|
353
|
+
|
|
354
|
+
for (const doc of docs) {
|
|
355
|
+
const parts = parseGroupCode(doc.group_code);
|
|
356
|
+
let current = tree;
|
|
357
|
+
|
|
358
|
+
// Navigate/create folder structure
|
|
359
|
+
for (const part of parts) {
|
|
360
|
+
if (!current[part]) {
|
|
361
|
+
current[part] = { _type: 'folder', children: {} };
|
|
362
|
+
}
|
|
363
|
+
current = current[part].children;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Add file
|
|
367
|
+
current[doc.slug] = {
|
|
368
|
+
_type: 'file',
|
|
369
|
+
title: doc.title,
|
|
370
|
+
slug: doc.slug,
|
|
371
|
+
group_code: doc.group_code,
|
|
372
|
+
status: doc.status
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return tree;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Folder contents for explorer mode (exact folder matching)
|
|
380
|
+
async function getFolderContents(category, group_code, pagination = {}, options = {}) {
|
|
381
|
+
const { isAdmin = false } = options;
|
|
382
|
+
const normalizedCategory = String(category || '').trim();
|
|
383
|
+
const normalizedGroupCode = group_code ? String(group_code).trim() : '';
|
|
384
|
+
|
|
385
|
+
const { page = 1, limit = 100, sort = { title: 1 } } = pagination;
|
|
386
|
+
const skip = Math.max(0, (page - 1) * limit);
|
|
387
|
+
const normalizedLimit = Math.min(Number(limit) || 100, 200);
|
|
388
|
+
|
|
389
|
+
// Exact match only (no prefix matching for Windows Explorer-style navigation)
|
|
390
|
+
const filter = {
|
|
391
|
+
category: normalizedCategory,
|
|
392
|
+
group_code: normalizedGroupCode
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (!isAdmin) {
|
|
396
|
+
filter.status = 'published';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const [items, total] = await Promise.all([
|
|
400
|
+
Markdown.find(filter)
|
|
401
|
+
.sort(sort)
|
|
402
|
+
.skip(skip)
|
|
403
|
+
.limit(normalizedLimit)
|
|
404
|
+
.select('title slug category group_code markdownRaw publicEnabled status cacheTtlSeconds updatedAt createdAt ownerUserId orgId')
|
|
405
|
+
.lean(),
|
|
406
|
+
Markdown.countDocuments(filter),
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
const result = { items, total, limit: normalizedLimit, skip };
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Get unique group codes for tree building (performance optimized)
|
|
414
|
+
async function getUniqueGroupCodes(category, options = {}) {
|
|
415
|
+
const { isAdmin = false } = options;
|
|
416
|
+
const normalizedCategory = String(category || '').trim();
|
|
417
|
+
|
|
418
|
+
const filter = { category: normalizedCategory };
|
|
419
|
+
if (!isAdmin) {
|
|
420
|
+
filter.status = 'published';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Use distinct to get all existing group codes
|
|
424
|
+
const groupCodes = await Markdown.distinct('group_code', filter);
|
|
425
|
+
|
|
426
|
+
// Normalize results: convert null/undefined to "" and ensure uniqueness
|
|
427
|
+
const normalizedCodes = Array.from(new Set(groupCodes.map(code => code || '')));
|
|
428
|
+
|
|
429
|
+
// Explicitly check for documents where group_code field might be missing
|
|
430
|
+
// because distinct() omits values for documents where the field is missing.
|
|
431
|
+
const hasMissingField = await Markdown.findOne({
|
|
432
|
+
...filter,
|
|
433
|
+
group_code: { $exists: false }
|
|
434
|
+
}).select('_id').lean();
|
|
435
|
+
|
|
436
|
+
if (hasMissingField && !normalizedCodes.includes('')) {
|
|
437
|
+
normalizedCodes.push('');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return normalizedCodes;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Search functionality
|
|
444
|
+
async function searchMarkdowns(query, options = {}) {
|
|
445
|
+
const { category, group_code, limit = 50 } = options;
|
|
446
|
+
|
|
447
|
+
const searchFilter = {
|
|
448
|
+
status: 'published',
|
|
449
|
+
publicEnabled: true,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
if (category) {
|
|
453
|
+
searchFilter.category = String(category).trim();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (group_code) {
|
|
457
|
+
searchFilter.group_code = String(group_code).trim();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Text search
|
|
461
|
+
if (query) {
|
|
462
|
+
searchFilter.$or = [
|
|
463
|
+
{ title: { $regex: query, $options: 'i' } },
|
|
464
|
+
{ markdownRaw: { $regex: query, $options: 'i' } }
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return Markdown.find(searchFilter)
|
|
469
|
+
.select('title slug category group_code updatedAt')
|
|
470
|
+
.sort({ updatedAt: -1 })
|
|
471
|
+
.limit(Number(limit))
|
|
472
|
+
.lean();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function upsertMarkdown({ title, category, group_code, slug, markdownRaw, publicEnabled = false, status = 'published', ownerUserId, orgId }) {
|
|
476
|
+
const normalizedCategory = normalizeCategory(category);
|
|
477
|
+
const normalizedGroupCode = normalizeGroupCode(group_code);
|
|
478
|
+
const normalizedSlug = slug || normalizeSlugBase(title);
|
|
479
|
+
|
|
480
|
+
const query = {
|
|
481
|
+
category: normalizedCategory,
|
|
482
|
+
group_code: normalizedGroupCode,
|
|
483
|
+
slug: normalizedSlug,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const update = {
|
|
487
|
+
title: String(title || '').trim(),
|
|
488
|
+
markdownRaw: validateMarkdownContent(markdownRaw),
|
|
489
|
+
publicEnabled: Boolean(publicEnabled),
|
|
490
|
+
status,
|
|
491
|
+
ownerUserId,
|
|
492
|
+
orgId,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const doc = await Markdown.findOneAndUpdate(query, update, {
|
|
496
|
+
new: true,
|
|
497
|
+
upsert: true,
|
|
498
|
+
setDefaultsOnInsert: true,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return doc.toObject();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
module.exports = {
|
|
505
|
+
ERROR_CODES,
|
|
506
|
+
normalizeGroupCode,
|
|
507
|
+
parseGroupCode,
|
|
508
|
+
buildGroupCode,
|
|
509
|
+
normalizeCategory,
|
|
510
|
+
validatePathUniqueness,
|
|
511
|
+
getMarkdownByPath,
|
|
512
|
+
createMarkdown,
|
|
513
|
+
upsertMarkdown,
|
|
514
|
+
getMarkdownById,
|
|
515
|
+
updateMarkdown,
|
|
516
|
+
deleteMarkdown,
|
|
517
|
+
listMarkdowns,
|
|
518
|
+
getMarkdownTree,
|
|
519
|
+
getFolderContents,
|
|
520
|
+
getUniqueGroupCodes,
|
|
521
|
+
searchMarkdowns,
|
|
522
|
+
};
|