mirror of
https://github.com/abort-retry-ignore/joplock.git
synced 2026-04-28 01:49:30 +00:00
212 lines
6.4 KiB
JavaScript
212 lines
6.4 KiB
JavaScript
const MODEL_TYPE_NOTE = 1;
|
|
const MODEL_TYPE_FOLDER = 2;
|
|
const MODEL_TYPE_RESOURCE = 4;
|
|
const TRASH_FOLDER_ID = 'de1e7ede1e7ede1e7ede1e7ede1e7ede';
|
|
|
|
const decodeItemContent = content => {
|
|
if (!content) return {};
|
|
const raw = Buffer.isBuffer(content) ? content.toString('utf8') : `${content}`;
|
|
if (!raw) return {};
|
|
return JSON.parse(raw);
|
|
};
|
|
|
|
const mapFolderRow = row => {
|
|
const content = decodeItemContent(row.content);
|
|
return {
|
|
id: row.jop_id,
|
|
parentId: row.jop_parent_id || '',
|
|
title: content.title || '',
|
|
icon: content.icon || '',
|
|
deletedTime: Number(content.deleted_time || 0),
|
|
createdTime: Number(content.created_time || row.created_time || 0),
|
|
updatedTime: Number(row.jop_updated_time || content.updated_time || 0),
|
|
};
|
|
};
|
|
|
|
const mapNoteRow = row => {
|
|
const content = decodeItemContent(row.content);
|
|
const body = content.body || '';
|
|
return {
|
|
id: row.jop_id,
|
|
parentId: row.jop_parent_id || '',
|
|
title: content.title || '',
|
|
body,
|
|
bodyPreview: body.slice(0, 240),
|
|
isTodo: !!Number(content.is_todo || 0),
|
|
todoCompleted: Number(content.todo_completed || 0),
|
|
deletedTime: Number(content.deleted_time || 0),
|
|
createdTime: Number(content.created_time || row.created_time || 0),
|
|
updatedTime: Number(row.jop_updated_time || content.updated_time || 0),
|
|
};
|
|
};
|
|
|
|
const mapNoteHeaderRow = row => {
|
|
return {
|
|
id: row.jop_id,
|
|
parentId: row.jop_parent_id || '',
|
|
title: row.title || '',
|
|
deletedTime: Number(row.deleted_time || 0),
|
|
updatedTime: Number(row.jop_updated_time || 0),
|
|
};
|
|
};
|
|
|
|
const deletedFilterSql = mode => {
|
|
if (mode === 'only') return ' AND COALESCE((convert_from(content, \'UTF8\')::json->>\'deleted_time\')::bigint, 0) > 0';
|
|
if (mode === 'all') return '';
|
|
return ' AND COALESCE((convert_from(content, \'UTF8\')::json->>\'deleted_time\')::bigint, 0) = 0';
|
|
};
|
|
|
|
const createItemService = database => {
|
|
return {
|
|
async foldersByUserId(userId) {
|
|
const result = await database.query(`
|
|
SELECT id, jop_id, jop_parent_id, jop_updated_time, created_time, content
|
|
FROM items
|
|
WHERE owner_id = $1 AND jop_type = $2${deletedFilterSql('exclude')}
|
|
ORDER BY LOWER(COALESCE(convert_from(content, 'UTF8')::json->>'title', '')) ASC, created_time ASC
|
|
`, [userId, MODEL_TYPE_FOLDER]);
|
|
|
|
return result.rows.map(mapFolderRow);
|
|
},
|
|
|
|
async folderByUserIdAndJopId(userId, folderId) {
|
|
const result = await database.query(`
|
|
SELECT id, jop_id, jop_parent_id, jop_updated_time, created_time, content
|
|
FROM items
|
|
WHERE owner_id = $1 AND jop_type = $2 AND jop_id = $3
|
|
LIMIT 1
|
|
`, [userId, MODEL_TYPE_FOLDER, folderId]);
|
|
|
|
const row = result.rows[0];
|
|
if (!row) return null;
|
|
return mapFolderRow(row);
|
|
},
|
|
|
|
async notesByUserId(userId, options = {}) {
|
|
const folderId = options.folderId || '';
|
|
const deleted = options.deleted || 'exclude';
|
|
const params = [userId, MODEL_TYPE_NOTE];
|
|
let where = `WHERE owner_id = $1 AND jop_type = $2${deletedFilterSql(deleted)}`;
|
|
|
|
if (folderId) {
|
|
params.push(folderId);
|
|
where += ` AND jop_parent_id = $${params.length}`;
|
|
}
|
|
|
|
const result = await database.query(`
|
|
SELECT id, jop_id, jop_parent_id, jop_updated_time, created_time, content
|
|
FROM items
|
|
${where}
|
|
ORDER BY jop_updated_time DESC, created_time DESC
|
|
`, params);
|
|
|
|
return result.rows.map(mapNoteRow);
|
|
},
|
|
|
|
async noteHeadersByUserId(userId, options = {}) {
|
|
const deleted = options.deleted || 'exclude';
|
|
const result = await database.query(`
|
|
SELECT
|
|
jop_id,
|
|
jop_parent_id,
|
|
jop_updated_time,
|
|
COALESCE(convert_from(content, 'UTF8')::json->>'title', '') AS title,
|
|
COALESCE((convert_from(content, 'UTF8')::json->>'deleted_time')::bigint, 0) AS deleted_time
|
|
FROM items
|
|
WHERE owner_id = $1 AND jop_type = $2${deletedFilterSql(deleted)}
|
|
ORDER BY jop_updated_time DESC, created_time DESC
|
|
`, [userId, MODEL_TYPE_NOTE]);
|
|
|
|
return result.rows.map(mapNoteHeaderRow);
|
|
},
|
|
|
|
async searchNotes(userId, query) {
|
|
if (!query || !query.trim()) return [];
|
|
const pattern = `%${query.trim()}%`;
|
|
const result = await database.query(`
|
|
SELECT id, jop_id, jop_parent_id, jop_updated_time, created_time, content
|
|
FROM (
|
|
SELECT id, jop_id, jop_parent_id, jop_updated_time, created_time, content,
|
|
convert_from(content, 'UTF8')::json AS parsed
|
|
FROM items
|
|
WHERE owner_id = $1 AND jop_type = $2
|
|
) sub
|
|
WHERE COALESCE((parsed->>'deleted_time')::bigint, 0) = 0
|
|
AND (
|
|
parsed->>'title' ILIKE $3
|
|
OR regexp_replace(
|
|
regexp_replace(parsed->>'body', '!?\[[^\]]*\]\(:/[a-f0-9]+\)', '', 'g'),
|
|
'data:image/[^;]+;base64,[A-Za-z0-9+/=]+', '', 'g'
|
|
) ILIKE $3
|
|
)
|
|
ORDER BY jop_updated_time DESC, created_time DESC
|
|
LIMIT 50
|
|
`, [userId, MODEL_TYPE_NOTE, pattern]);
|
|
|
|
return result.rows.map(mapNoteRow);
|
|
},
|
|
|
|
async noteByUserIdAndJopId(userId, noteId, options = {}) {
|
|
const deleted = options.deleted || 'exclude';
|
|
const result = await database.query(`
|
|
SELECT id, jop_id, jop_parent_id, jop_updated_time, created_time, content
|
|
FROM items
|
|
WHERE owner_id = $1 AND jop_type = $2 AND jop_id = $3${deletedFilterSql(deleted)}
|
|
LIMIT 1
|
|
`, [userId, MODEL_TYPE_NOTE, noteId]);
|
|
|
|
const row = result.rows[0];
|
|
if (!row) return null;
|
|
return mapNoteRow(row);
|
|
},
|
|
|
|
// Returns the binary content of a resource blob (.resource/<id>)
|
|
async resourceBlobByUserId(userId, resourceId) {
|
|
const blobName = `.resource/${resourceId}`;
|
|
const result = await database.query(`
|
|
SELECT content
|
|
FROM items
|
|
WHERE owner_id = $1 AND name = $2
|
|
LIMIT 1
|
|
`, [userId, blobName]);
|
|
|
|
const row = result.rows[0];
|
|
if (!row) return null;
|
|
return row.content; // Buffer
|
|
},
|
|
|
|
// Returns resource metadata (mime, filename, etc.) from the .md item
|
|
async resourceMetaByUserId(userId, resourceId) {
|
|
const result = await database.query(`
|
|
SELECT content
|
|
FROM items
|
|
WHERE owner_id = $1 AND jop_type = $2 AND jop_id = $3
|
|
LIMIT 1
|
|
`, [userId, MODEL_TYPE_RESOURCE, resourceId]);
|
|
|
|
const row = result.rows[0];
|
|
if (!row) return null;
|
|
const content = decodeItemContent(row.content);
|
|
return {
|
|
id: resourceId,
|
|
title: content.title || '',
|
|
mime: content.mime || 'application/octet-stream',
|
|
filename: content.filename || '',
|
|
fileExtension: content.file_extension || '',
|
|
size: Number(content.size || 0),
|
|
};
|
|
},
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
MODEL_TYPE_FOLDER,
|
|
MODEL_TYPE_NOTE,
|
|
MODEL_TYPE_RESOURCE,
|
|
TRASH_FOLDER_ID,
|
|
createItemService,
|
|
decodeItemContent,
|
|
mapFolderRow,
|
|
mapNoteHeaderRow,
|
|
mapNoteRow,
|
|
};
|