Привет читатель!
В данной статье я раскрою один из секретов Полишинеля, точнее то как создать и редактировать .md файлы на бэке в Nest.js, да звучит банально, но для таких же свитчеров как я думаю будет полезно.
Но для начала немного отойдем от сабжа и обсудим зачем, если кратко то я сейчас разрабатываю пет проект криптотрекера, и под впечатлением от Obsidian решил добавить функцию заметок себе. Ну и конечно чтобы это все работало и у меня и в Obsidian. Плюс уже на фронте можно прикрутить красоты с помощью LaTeX + Math.js и Mermaid, что актуально для финансовых приложений типа моего.
Пример ниже сделан на стеке: npm Nest.js Prisma SQLite.
Дополнительно используются пакеты: uuid и gray-matter.
Для справки:
uuid - генератор id, использую чтобы заранее сгенерировать id до записи в бд.
gray-matter - парсер для YAML, использую в примере для записи метаданных в заметку.
Можно конечно обойтись и без них, но не всегда стоит придумывать велосипед.
В примерах ниже я опустил DTO чтобы не раздувать по попусту текст, если они вам нужны то их можно найти у меня в гите вместе со схемами для бд, ссылка в профиле.
Настройка:
gray-matter
npm install --save gray-matter
import * as matter from 'gray-matter';
uuid
npm i uuid
import { v4 as uuidv4 } from 'uuid';
Дефолтные node фичи
import * as fs from 'fs';//для создания файла и работы с файловой системой
import * as path from 'path';//для работы с директориями
Код для всех функций из класса:
@Injectable()
export class NotesService {
private vaultDir = path.join(process.cwd(), 'vault', 'notes');// storage dir
constructor(private prisma: PrismaService,) {
if (!fs.existsSync(this.vaultDir)) {
fs.mkdirSync(this.vaultDir, { recursive: true });
}// check and create dir / { recursive: true } - create folder chain
}
...
}
Здесь определен путь для хранения файлов и его создание.
Создание заметки:
async createNote(dto: CreateNoteDto){
const id = uuidv4();//nead for meta in obs and db
const now = new Date().toISOString();
let tagNames: string[] = [];
if (dto.marks?.length) {
const tags = await this.prisma.marks.findMany({
where: { id: { in: dto.marks } },
select: { name: true },
});
tagNames = tags.map((t) => t.name);
}// get tag names by id
const frontmatter = {
id,
name: dto.name,
linkedTo: dto.linkedTo,
tags: tagNames,
createdAt: now,
updatedAt: now,
}//gen meta data for obsidian
const mdContent = matter.stringify(dto.content || '', frontmatter);//gen file with meta data
const fileName = dto.name + '.md';
const filePath = path.join(this.vaultDir, fileName);
fs.writeFileSync(filePath, mdContent, 'utf8');//save .md file
const note = await this.prisma.notes.create({
data: {
id,
name: dto.name,
userId: dto.userId,
linkedTo: dto.linkedTo,
createdAt: new Date(now),
updatedAt: new Date(now),
path: filePath,
marks: JSON.stringify(dto.marks)
},
});// save meta in db
return note;
}
В этом коде я заранее создаю id чтобы использовать его для записи в заметку, записываю полученные данные в файл который затем создается в указанной выше директории, при первом запуске директория создастся автоматически. Ну и в конце записываю данные в бд, для связывания с другими фичами приложения.
Тут есть важный момент, сам текст заметки передается в content строкой с переносами (/n) и имеет такой вид:
"content": "# BTC анализ\nПроверка уровня поддержки 60k.\n\n#btc #strategy\n\n```math\n2 * (3 + 4)\n```\n\n```mermaid\ngraph TD\nBTC-->ETH\nETH-->SOL\n```"
Здесь уже используется LaTeX + Math.js и Mermaid. Дальше в статье будет наглядный пример с результатом.
Редактирование заметки:
async updateNote(dto: UpdateNoteDto){
const note = await this.prisma.notes.findFirst({ where: {
id: dto.id,
userId: dto.userId
} });//check db
if (!note) throw new NotFoundException('Note not found');
const filePath = note.path;
if (!fs.existsSync(filePath)) throw new NotFoundException('File not found');
let tagNames: string[] = [];
if (dto.marks?.length) {
const tags = await this.prisma.marks.findMany({
where: { id: { in: dto.marks } },
select: { name: true },
});
tagNames = tags.map((t) => t.name);
}// get tag names by id
const fileContent = fs.readFileSync(filePath, 'utf8');//read file
const parsed = matter(fileContent);
const updatedFrontmatter = {
...parsed.data,
name: dto.name ?? parsed.data.name,
tags: tagNames,
updatedAt: new Date().toISOString(),
};// update meta
const newContent = dto.content ?? parsed.content;
const updatedFile = matter.stringify(newContent, updatedFrontmatter);// update content and meta
fs.writeFileSync(filePath, updatedFile, 'utf8');// save .md file
const updatedNote = await this.prisma.notes.update({
where: { id: dto.id },
data: {
name: updatedFrontmatter.name,
updatedAt: new Date(),
marks: JSON.stringify(dto.marks)
},
});// update meta in db
return updatedNote;
}
Здесь представлен код для изменения заметки, в целом он работает по тому же принципу, только здесь нам нужно сначала прочесть файл а потом внести изменения, ну и конечно не забываем обновить данные в бд.
Важно помнить, что при изменении текста нужно передавать всю заметку целиком, точечного изменять данные в .md файлах мы не можем, вернее это сложно и большого смысла в этом нет.
Получение заметки:
async getNoteById(id: string) {
const note = await this.prisma.notes.findFirst({ where: { id } });
if (!note) throw new NotFoundException('Note not found');
const fileContent = fs.readFileSync(note.path, 'utf8');//raed .md file
const parsed = matter(fileContent);// note body
return {
note,
parsed
}
}
Ответ
[
{
"id": "08ffce73-4e68-4c90-8e09-c77722dc6c80",
"userId": "08ffce73-4e68-4c90-8e09-c77722dc6c80",
"name": "Test",
"isArchived": false,
"marks": [
"35198333-3ff5-46c7-8eef-3c97d6b7652f",
"034caa3f-042b-4f36-a0dd-f1cd2f2e6c31"
],
"content": "# BTC анализ\nПроверка уровня поддержки 60k.\n\n#btc #strategy\n\n```math\n2 * (3 + 4)\n```\n\n```mermaid\ngraph TD\nBTC-->ETH\nETH-->SOL\n```"
}
]
Код на получение очевидно делится на 2 части, получение из бд и из файла.
Удаление заметки:
async deleteNote(id: string) {
const note = await this.prisma.notes.findUnique({ where: { id } });
if (!note) throw new NotFoundException('Note not found');
const filePath = note.path;
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);// delete .md file
}
await this.prisma.notes.delete({ where: { id } });
return { success: true, message: 'Note deleted' };
}
Код на удаление максимально простой, ищет файл и удаляет его, ну и чистит базу.
Конечно все эти примеры не без недостатков, но это та база которая работает и на которой можно дальше строить свое приложения.
Ну и давайте посмотри что мы имеем в итоге:

На скриншоте у нас созданная заметка открытая в Obsidian.
Сверху у нас созданные YAML свойства, которые продублированы в бд.
Здесь у нас 2 экземпляра тегов, верхние теги идут из приложения, а нижние переданы вручную для демонстрации.
В теории можно использовать оба вида тегов.
Диаграмма создана с помощью Mermaid.
Ниже таблица с данными из бд.
id | name | userId | createdAt | updatedAt | isArchived | linkedTo | path | marks |
b4094146-35d1-4d61-add9-a5695e32961f | Test | 08ffce73-4e68-4c90-8e09-c77722dc6c80 | 1760605735388 | 1760605801852 | 0 | 08ffce73-4e68-4c90-8e09-c77722dc6c80 | C:\Users\userName\Desktop\MoneyMovementMonitor\mmm-1\apps\backend\vault\notes\Test.md | ["35198333-3ff5-46c7-8eef-3c97d6b7652f","034caa3f-042b-4f36-a0dd-f1cd2f2e6c31"] |
В примере я уже использовал LaTeX (рендер формулы) и Mermaid (диаграмма), результат видно на скрине с Obsidian, они позволяют красиво оформить математические формулы и расчеты а также строить диаграммы с помощью markdown.
Подобным образом мы можем встроить заметки практически в любое приложение, будь то календарь, таск менеджер или криптотрекер. Дальше все зависит от вашей фантазии и здравого смысла.
Я же планирую в дальнейшем связать свои заметки с данными в бд, в частности с аналитикой, графиками, транзакциями.
Фронт часть я планирую описать когда перейду к разработке фронта, скорее всего там же будет блок с работой над ошибками и недочетами.
Надеюсь эта статья кому то будет полезна.
Полезные ссылки:
uuid - генератор ID
gray-matter - для YAML stringify/parse frontmatter.
LaTeX - для отображения формул
Math.js - для математических расчетов
Mermaid - для диаграмм