Crie uma ferramenta CLI do gerenciador de tarefas com NodeJS e MongoDB

Aprenda como construir uma ferramenta CLI (Command Line Interface) de gerenciador de tarefas com Node.js e MongoDB. Este tutorial cobrirá os fundamentos da criação de uma ferramenta CLI, armazenamento de dados no MongoDB e implementação de operações CRUD básicas.

Olá a todos 👋 Neste tutorial, você aprenderá como fazer uma ferramenta CLI (Command Line Interface) simples do Gerenciador de Tarefas. Isso significa que você pode usar comandos para criar, visualizar, atualizar ou excluir seus todos.

Estaremos construindo esta ferramenta CLI usando NodeJS . Também usaremos o MongoDB como banco de dados para armazenar todas as nossas tarefas. Finalmente, usaremos alguns pacotes úteis do npm :

  • commander: Isso nos ajuda a construir a ferramenta CLI.
  • chalk: Isso torna as mensagens no terminal coloridas e fáceis de ler.
  • inquirer: Isso nos permite pedir informações ao usuário.
  • ora: Isso faz com que o terminal mostre boas animações giratórias.

Antes de começarmos, quero que você saiba que pode encontrar o código completo deste projeto no GitHub. Se você não tiver certeza sobre algo no código, poderá sempre consultar a versão final.

Índice:

  • Configuração do projeto
    1. Como criar o package.jsonarquivo
    2. Como instalar dependências
    3. Como converter módulos CommonJS em módulos ES
    4. Como criar a estrutura de pastas
  • Como se conectar ao banco de dados
    1. Como obter uma string de conexão MongoDB
    2. Código para conectar-se ao banco de dados
  • Como criar um modelo Mangusto
  • Trabalhando em operações CRUD
    1. Como criar Todos
    2. Como ler Todos
    3. Como excluir Todos
    4. Como atualizar Todos
  • Como escrever o ponto de entrada CLI usando Commander
  • Como testar a ferramenta CLI
  • Conclusão

Configuração do projeto

Bem-vindo à primeira seção deste manual! Aqui estaremos montando nosso projeto.

Isso envolve alguns passos simples: criar um novo diretório, configurar o package.jsonarquivo e instalar os pacotes npm necessários, como giz, investigador, comandante e outros sobre os quais falaremos em breve. Também organizaremos o projeto criando pastas.

Antes de começarmos, vamos garantir que você tenha o NodeJS instalado em seu sistema. Você pode obter a versão LTS mais recente neste site: https://nodejs.org/en .

Para verificar se o Node está instalado corretamente, digite este comando: node --version. Se você vir um número de versão, está tudo pronto! Caso contrário, você precisará solucionar os erros.

Assim que o NodeJS estiver instalado e funcionando, crie uma nova pasta chamada “todo”. Você pode usar seu editor de código favorito (prefiro Visual Studio Code) ou seguir estas etapas em seu terminal:

  1. Crie uma nova pasta:mkdir todo
  2. Entre na pasta:cd todo
  3. Abra-o em seu editor de código:code .

Como criar o package.jsonarquivo

A primeira e mais importante etapa é configurar o package.jsonarquivo. Mas não se preocupe em fazer isso manualmente. Você pode economizar tempo usando este comando:

npm init --yes

Concluída esta etapa, vamos passar para a próxima etapa e obter todas as coisas necessárias para o nosso projeto.

Como instalar dependências

Para construir este projeto, precisaremos de alguns pacotes. Basta executar este comando simples para obter todos eles:

npm i commander inquirer chalk ora mongoose nanoid dotenv

Como converter módulos CommonJS em módulos ES

Antes de continuar, vamos fazer uma pequena alteração no package.jsonarquivo. Remova esta linha: "main": "index.js"e adicione estas duas linhas:

"exports": "./index.js",
 "type": "module",

Com essas mudanças, estamos convertendo nosso projeto de Módulos CommonJS para Módulos ES. Isso significa que usaremos importem vez de require()trazer módulos e exportem vez de module.exportscompartilhar coisas entre arquivos.

Como criar a estrutura de pastas

Agora, vamos organizar nosso projeto configurando uma estrutura de pastas inteligente. Isso significa que criaremos pastas para armazenar nossos arquivos JavaScript de maneira organizada. Esta etapa é muito importante. Torna as coisas fáceis de gerenciar e se desenvolve sem problemas.

Estamos criando 3 pastas e 2 arquivos na pasta principal:

Primeira pasta: commands . Dentro desta pasta, você criará 4 arquivos. Os nomes dos arquivos e a descrição do código que eles conterão são mencionados abaixo:

  • addTask.js: Código para criar uma nova tarefa.
  • deleteTask.js: Código para excluir uma tarefa.
  • readTask.js: Código para exibir todos os todos.
  • updateTask.js: Código para atualizar uma tarefa.

Segunda pasta: db . Dentro desta pasta, adicione um arquivo chamado connectDB.js. Este arquivo conterá o código para conectar-se ao banco de dados MongoDB e desconectar quando necessário.

Terceira pasta: schema . Dentro dele, crie um arquivo chamado TodoSchema.js. Este arquivo armazena o esquema e o modelo do Mongoose. Basicamente, um projeto para nossas tarefas, é assim que nossas tarefas ficarão.

Primeiro arquivo: .env . Crie este arquivo dentro do diretório raiz/pasta principal do projeto. É aqui que você colocará sua string de conexão do MongoDB.

Segundo arquivo: Crie o index.jsarquivo no próprio diretório raiz que servirá como ponto de entrada do nosso projeto. É como se fosse a frente do projeto – onde tudo começa.

Quando terminarmos, as pastas do seu projeto deverão ficar mais ou menos assim:

Imagem mostrando a estrutura de pastas do projeto

Estrutura de pastas do projeto Como conectar-se ao banco de dados

Agora que você configurou o projeto com sucesso, é hora de mergulhar na parte emocionante.

Como obter uma string de conexão do MongoDB

Para acompanhar todos os nossos todos, precisamos de um local para armazená-los. É aí que entra o MongoDB Atlas. É como um serviço especial que gerencia bancos de dados para nós. A melhor parte? Você pode começar a usá-lo gratuitamente (sem necessidade de cartão de crédito).

Para se conectar a ele, tudo que você precisa é algo chamado string de conexão. Se o MongoDB Atlas é novo para você, não se preocupe. Confira este artigo fácil de seguir: Tutorial do MongoDB Atlas - Como começar . Fornece informações suficientes para começar a usar o Atlas. Quando terminar, você saberá como obter o que precisa, incluindo a cadeia de conexão.

Depois de ter essa cadeia de conexão, crie uma nova coisa chamada "variável de ambiente". É como um código secreto que seu projeto usa. Abra o .envarquivo e faça uma linha como esta: MONGO_URI=. Após o =, coloque sua string de conexão.

Lembre-se: Substitua <password>pela sua senha real e <username>pelo nome de usuário do administrador do banco de dados na string de conexão. Além disso, adicione todosentre /?na string. Quando terminar, seu .envarquivo deverá ficar parecido com isto:

MONGO_URI=mongodb+srv://<username>:<password>@cluster0.k5tmsld.mongodb.net/todos?retryWrites=true&w=majority

Código para conexão com o banco de dados

Agora, vamos mergulhar no código que conecta nossa ferramenta ao banco de dados MongoDB. Abra o ./db/connectDB.jsarquivo e vamos escrever um código para fazer essa conexão acontecer.

Primeiramente, precisamos trazer o dotenvpacote que pegamos anteriormente quando estávamos configurando o projeto e invocar o config()método em dotenv. Isso nos ajuda a carregar variáveis ​​de ambiente do .envarquivo. Veja como você faz isso:

import dotenv from 'dotenv'
dotenv.config()

A seguir, queremos importar mais alguns pacotes que usaremos aqui. Estes são mongoose, orae chalk:

import mongoose from 'mongoose'
import ora from 'ora'
import chalk from 'chalk'

Nota: mongoose é uma biblioteca Object Data Modeling (ODM) para MongoDB. Ele fornece uma abstração de nível superior, tornando mais fácil fazer coisas como adicionar, ler, atualizar e excluir coisas do banco de dados MongoDB.

Agora, vamos entrar em ação real. Definiremos duas funções aqui: connectDB()e disconnectDB().

A connectDB()função conterá o código para ajudar a conectar nosso aplicativo NodeJS ao banco de dados M0ngoDB usando mongoose. É como um telefonema conectando-os. Se não estabelecermos uma conexão primeiro, nosso aplicativo não será capaz de interagir com o banco de dados e realizar as diversas operações CRUD.

A disconnectDB()função faz o oposto. É como desligar o telefone depois que nosso aplicativo termina de falar com o banco de dados. Se não desconectarmos, é como manter a ligação mesmo depois de terminarmos.

Deixar de desconectar do banco de dados após terminarmos de interagir com ele pode causar vazamentos de recursos. Isso pode fazer com que seu aplicativo fique lento ou potencialmente trave com o tempo.

Deixe-me mostrar o código para ambas as funções:

export async function connectDB(){
    try {
        const spinner = ora('Connecting to the database...').start()
        await mongoose.connect(process.env.MONGO_URI)
        spinner.stop()
        console.log(chalk.greenBright('Successfully connected to database!!!'))   
    } catch (error) {
        console.log(chalk.redBright('Error: '), error);
        process.exit(1) 
    }
}

export async function disconnectDB(){
    try {
        await mongoose.disconnect()
        console.log(chalk.greenBright('Disconnected from the database.'))
    } catch(err) {
        console.log(chalk.redBright('Error: '), error);
        process.exit(1) 
    }
}

É muito código para digerir de uma só vez, então deixe-me explicar isso para você:

Na connectDB()função, a linha mongoose.connect(process.env.MONGO_URI)nos ajuda a realmente conectar ao banco de dados usando a string de conexão.

Lembra do .envarquivo? Estamos usando suas informações aqui. Para carregar a MONGO_URIvariável, usamos o dotenvpacote e chamamos a config()função e então podemos acessá-la usando process.env.MONGO_URI.

Como mongoose.connect()retorna uma promessa, usamos a awaitpalavra-chave anterior para garantir que prosseguiremos somente quando a promessa retornada for resolvida.

É possível encontrar alguns erros ao executar este código, por isso agrupamos todo o código em um try...catch()bloco para garantir que quaisquer erros que apareçam sejam tratados corretamente no catch()bloco.

O orapacote nos ajuda a mostrar um botão giratório enquanto nos conectamos ao banco de dados. Uma vez conectado com sucesso, paramos o botão giratório e mostramos uma mensagem feliz em verde usando chalk.

Se você notar, estamos fazendo a mesma coisa na disconnectDB()função. Mas, em vez de conectar, desconectamos do banco de dados usando mongoose.disconnect(). Nós o envolvemos em um bloco try-catch semelhante e novamente mostramos mensagens coloridas usando chalk.

Usamos exportbefore essas funções para permitir que outras partes do projeto as utilizem. Não se esqueça de adicionar estas duas linhas temporárias no final do arquivo por enquanto:

connectDB()
disconnectDB()

Agora, você pode executar o connectDB.jsarquivo usando o comando: node ./db/connectDB.jse esperar ver isto no console:

GIF mostrando as mensagens de saída mostradas no terminal quando o arquivo connectDB.js é executado

Saída vista no terminal quando connectDB.jso arquivo é executado. Ele mostra como nosso código se conecta com sucesso ao banco de dados e se desconecta dele, mostrando mensagens de console apropriadas quando invocamos os métodos connectDB()e .disconnectDB()

Conectar-se ao banco de dados é um grande passo, mas você está fazendo um grande progresso! Antes de prosseguir, remova as 2 linhas que você adicionou no final, pois elas foram adicionadas apenas para verificar se nossas funções de conexão e desconexão estão funcionando conforme o esperado.

Como criar um modelo Mangusto

Um modelo Mongoose é como uma ferramenta que nos ajuda a conversar com o banco de dados. Com ele, podemos facilmente fazer coisas como adicionar, ler, atualizar e excluir tarefas. É como um assistente útil que sabe como se comunicar com o banco de dados.

Para fazer este modelo, precisamos de algo chamado Schema. Basicamente define como deve ser cada tarefa. Pense nisso como um plano ou um conjunto de instruções que orienta como cada tarefa é criada, quais informações ela deve conter e como essas informações são organizadas. É como definir regras sobre como nossas tarefas são armazenadas no banco de dados.

Vamos construir esse esquema no ./schema/TodoSchema.jsarquivo. Abra-o e vamos começar. Primeiro, precisamos de duas ferramentas especiais: mongoosee nanoid. Usaremos nanoidpara criar IDs curtos e exclusivos para cada tarefa.

Digite estas linhas para importar as ferramentas:

import mongoose from 'mongoose'
import {nanoid} from 'nanoid'

Agora, usamos o mongoose.Schema()método para criar nosso esquema. Aqui está o código para isso:

const TodoSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        trim: true
    },
    detail: {
        type: String,
        required: true,
        trim: true
    },
    status: {
        type: String,
        required: true,
        enum: ['completed', 'pending'],
        default: 'pending',
        trim: true
    },
    code: {
        type: String,
        required: true,
        default: 'code',
        trim: true
    }
}, {timestamps: true})

Qualquer tarefa criada usando este esquema terá as seguintes propriedades:

  • name: Este é um título curto para a tarefa. O type: Stringenfatiza que só pode ser texto (uma String). Especifica required: trueque devemos fornecer isso ao criar uma tarefa e especifica trim: trueque quaisquer espaços extras no início ou no final do nome da tarefa serão removidos antes de salvá-la no banco de dados.
  • detail: Esta é uma descrição da tarefa. Tem exatamente as mesmas propriedades que name.
  • status: Isso mostra se a tarefa foi concluída ou não. A enum: ['completed', 'pending']propriedade especifica que só pode ser completedou pending. A default: 'pending'propriedade especifica que se você não definir a statuspropriedade ao criar a tarefa, ela será considerada pending.
  • code: este é um ID curto e exclusivo para a tarefa. Estamos atribuindo a ele um valor padrão de code. Este valor é apenas um espaço reservado e não tem nenhum significado real em termos de identificação da tarefa. Não se preocupe, mudaremos isso em breve.
  • É {timestamps: true}uma opção de configuração que adiciona automaticamente campos de carimbo de data/hora como createdAte updatedAtàs tarefas quando elas são criadas ou modificadas.

Definimos nosso esquema com sucesso – mas você pode estar se perguntando se a codepropriedade deveria ser exclusiva para cada tarefa. Atualmente ele armazena o mesmo valor, ou seja, " código ", para cada tarefa. Não se preocupe, nós consertaremos isso. Adicione este código no final:

TodoSchema.pre('save', function(next){
    this.code = nanoid(10)
    next()
})

Aqui, TodoSchema.pre('save', function(){....})nos ajuda a definir um gancho/função de pré-salvamento que é executado sempre antes que uma tarefa seja salva no banco de dados.

Dentro da função, usamos nanoid(10)para criar um ID exclusivo de 10 caracteres para a tarefa e colocar esse ID gerado no codecampo da tarefa (podemos realmente acessar qualquer propriedade/campo da tarefa usando a thispalavra-chave).

A última linha de código: next()basicamente informa ao computador que terminamos e ele pode finalmente salvar o documento agora. Com isso, geramos um ID exclusivo para cada tarefa criada usando o nanoidpacote.

Por último, faremos um Todosmodelo usando este TodoSchemablueprint e exportaremos. É assim:

const Todos = mongoose.model('Todos', TodoSchema)
export default Todos

E aí está! Construímos nosso esquema e modelo. Agora vamos prosseguir para a próxima seção deste tutorial.

Como trabalhar em operações CRUD

Parabéns por ter acompanhado com sucesso até aqui. Até agora, fizemos três coisas:

  1. Montamos o projeto
  2. Nós nos conectamos ao banco de dados MongoDB e
  3. Criamos o modelo Mongoose

A seguir, trabalharemos nas diversas operações CRUD, como criação, leitura, atualização e exclusão de tarefas de nosso banco de dados.

Como criar todos

Agora, vamos começar criando tarefas em nosso projeto. No início, planejei um processo simples em que você adiciona uma tarefa por vez ao banco de dados. Isso significa que quando você instrui a ferramenta a criar uma tarefa, ela solicita uma vez os detalhes da tarefa – como o nome e a descrição – e então salva a tarefa.

Mas então percebi: e se alguém quiser adicionar muitas tarefas rapidamente? Fazer isso um por um não é legal. Existem dois problemas:

  1. Se você tiver, digamos, 5 tarefas em mente, terá que digitar o comando create 5 vezes – uma para cada tarefa.
  2. Depois de inserir os detalhes da tarefa, você espera um pouco porque salvar coisas no banco de dados pode demorar, principalmente se a internet estiver lenta.

Esses problemas não são nada divertidos! Para corrigir isso, precisamos de uma maneira de adicionar várias tarefas de uma só vez. Veja como faremos isso:

Depois de inserir o nome e a descrição da tarefa, perguntaremos se você deseja adicionar mais tarefas. Se você inserir sim, continuamos o processo desde o início (pedindo que você insira novamente o nome e a descrição da próxima tarefa). Mas se você inserir não, o processo de solicitação de perguntas será interrompido e todas as tarefas inseridas serão salvas juntas no banco de dados. Dessa forma, você pode criar muitas tarefas sem o incômodo de fazer uma por uma. É tudo uma questão de tornar as coisas mais fáceis para você.

Estaremos escrevendo algum código no ./commands/addTask.jsarquivo. É aqui que a mágica acontece. Vamos decompô-lo passo a passo:

Primeiro, importamos os pacotes e funções necessários que criamos anteriormente. Você pode adicionar estas linhas de código para fazer isso:

import inquirer from "inquirer";
import { connectDB, disconnectDB } from '../db/connectDB.js'
import Todos from "../schema/TodoSchema.js";
import ora from "ora";
import chalk from "chalk";

Agora, criamos uma função assíncrona chamada input()para coletar o nome e os detalhes da tarefa do usuário. Veja como é:

async function input(){
    const answers = await inquirer.prompt([
        { name: 'name', message: 'Enter name of the task:', type: 'input' },
        { name: 'detail', message: 'Enter the details of the task:', type: 'input' },
    ])

    return answers
}

Em termos simples, input()solicita inquirerao usuário o nome e os detalhes da tarefa. As respostas são então retornadas como um objeto.

Mas espere, você pode se perguntar o que inquirer.prompt()está acontecendo. É um método do inquirerpacote que faz perguntas e aguarda respostas. Você fornece uma série de objetos de pergunta, cada um contendo detalhes como a mensagem a ser exibida ao usuário e o tipo de pergunta. A função retorna uma Promise, então esperamos awaitpelas respostas do usuário que são retornadas como um objeto.

Aqui { name: 'name', message: 'Enter name of the task:', type: 'input' }está a primeira pergunta que será feita ao usuário. A messagepropriedade contém a pergunta que será exibida ao usuário. No nosso caso, é: Enter the name of the task. O usuário será solicitado a inserir algum texto (uma String), já que esta questão é de type: 'input'. Isso name: 'name'significa que a resposta do usuário a esta pergunta será atribuída a uma propriedade chamada – nameno objeto da resposta.

O próximo objeto é a segunda pergunta que será feita ao usuário. Neste caso, uma mensagem será exibida no terminal: Enter the details of the taske a resposta do usuário será atribuída a uma propriedade chamada detailno objeto da resposta.

Para ver como o código acima funciona, você pode adicionar estas 2 linhas de código no final do arquivo:

const output = await input()
console.log(output)

Agora salve o arquivo e execute o código usando o comando: node ./commands/addTask.js. Isto é o que você verá ao executar o código:

Imagem do terminal mostrando o que a função `inquirer.prompt()` retorna como saída.

Saída vista no terminal quando apenas invocamos o input()método e executamos o código. Mostra como inquirer.jsretorna as respostas do usuário após o processo de solicitação de perguntas.

Agora podemos prosseguir com o resto do código e você pode remover as duas últimas linhas que acabou de adicionar.

Agora, vamos criar uma função chamada askQuestions()para reunir múltiplas tarefas. Isto é o que parece:

const askQuestions = async() => {

    const todoArray = []
    let loop = false

    do{
        const userRes = await input()
        todoArray.push(userRes)
        const confirmQ = await inquirer.prompt([{ name: 'confirm', message: 'Do you want to add more tasks?', type: 'confirm' }])
        if(confirmQ.confirm){
            loop = true
        } else {
            loop = false
        }
    } while(loop)

    return todoArray
}

Em askQuestions(), configuramos um loop que continua solicitando tarefas até que o usuário decida parar. Reunimos cada tarefa do usuário chamando a input()função, e a resposta do usuário retornada é enviada para o arquivo todoArray.

Em seguida, perguntamos se o usuário deseja adicionar mais tarefas por meio de uma pergunta de confirmação. Se eles disserem sim, definimos loopcomo truee o loop continua – caso contrário, looptorna-se falsee o loop termina. Finalmente, retornamos o array de tarefas, ou seja todoArray.

Você pode testar isso adicionando estas linhas de código no final do arquivo:

const output = await askQuestions()
console.log(output)

Ao executar o arquivo usando node ./commands/addTask.js, você verá um resultado semelhante ao que vê aqui:

Imagem do terminal mostrando a matriz de tarefas/todos retornados pela função `askQuestions()`

Saída vista no terminal quando invocamos o askQuestions()método e executamos o código. Mostra o array de tarefas retornadas pelo método quando o usuário não deseja continuar adicionando mais tarefas.

Estamos quase lá! Antes de continuar, não se esqueça de remover as duas últimas linhas que você adicionou agora. Depois de fazer isso, vamos em frente.

Até este ponto, conseguimos coletar com sucesso todas as tarefas que o usuário deseja criar.

Agora, vamos definir a última peça do quebra-cabeça: a addTask()função. Esta função reúne tudo e completa o processo de criação de tarefas. Aqui está o código completo:

export default async function addTask() {
    try {
        // calling askQuestions() to get array of todo's
        const userResponse = await askQuestions()

        // connecting to the database
        await connectDB()

        // Displaying a spinner with the following text message using ora 
        let spinner = ora('Creating the todos...').start()

        // looping over every todo in the userResponse array
        // and saving each todo in the database
        for(let i=0; i<userResponse.length; i++){
            const response = userResponse[i]
            await Todos.create(response)
        }

        // Stopping the spinner and displaying the success message
        spinner.stop()
        console.log(
            chalk.greenBright('Created the todos!')
        )

        // disconnecting the database
        await disconnectDB()
    } catch (error) {
        // Error Handling
        console.log('Something went wrong, Error: ', error)
        process.exit(1)
    }
}

A addTask()função começa chamando a askQuestions()função para reunir o conjunto de tarefas e atribuí-lo à userResponsevariável. Em seguida, ele se conecta ao banco de dados usando connectDB(), exibe um controle giratório usando orapara mostrar o processo de criação da tarefa, percorre cada tarefa na matriz e salva-a no banco de dados usando Todos.create(response).

Depois que todas as tarefas forem salvas, o controle giratório para, uma mensagem de sucesso é mostrada e então ele se desconecta do banco de dados usando disconnectDB().

Todo o código é agrupado em um try...catchbloco para lidar com possíveis erros normalmente.

Com este código, você concluiu o processo de criação de tarefas. Bom trabalho! Este foi provavelmente o trecho de código mais complexo de todo o projeto. Operações futuras, como tarefas de leitura, exclusão e atualização, serão bastante simples e fáceis em comparação. Dito isso, vamos prosseguir para a operação de leitura.

Como ler todos

Agora exploraremos como ler tarefas do banco de dados MongoDB. O processo é direto e orientarei você por todo o código do ./commands/readTask.jsarquivo:

Primeiro, vamos importar os pacotes e funções necessários no início do arquivo:

// Importing packages and functions
import { connectDB, disconnectDB } from '../db/connectDB.js'
import Todos from '../schema/TodoSchema.js'
import chalk from 'chalk'
import ora from 'ora'

Agora, vamos definir uma função assíncrona chamada readTask()que encapsula a lógica para tarefas de leitura. Toda a função é envolvida em um bloco try...catch para lidar com possíveis erros:

export default async function readTask(){
    try {
        // connecting to the database
        await connectDB()

        // starting the spinner
        const spinner = ora('Fetching all todos...').start()

        // fetching all the todos from the database 
        const todos = await Todos.find({})

        // stopping the spinner
        spinner.stop()

        // check if todos exist or not
        if(todos.length === 0){
            console.log(chalk.blueBright('You do not have any tasks yet!'))
        } else {
            todos.forEach(todo => {
                console.log(
                    chalk.cyanBright('Todo Code: ') + todo.code + '\n' + 
                    chalk.blueBright('Name: ') + todo.name + '\n' + 
                    chalk.yellowBright('Description: ') + todo.detail + '\n'
                )
            })
        }

        // disconnect from the database
        await disconnectDB()
    } catch (error) {
        // Error Handling
        console.log('Something went wrong, Error: ', error)
        process.exit(1)
    }
}

readTask()

Agora, vamos detalhar o código passo a passo:

  1. Estabelecemos uma conexão com o banco de dados MongoDB usando await connectDB().
  2. Iniciamos um spinner usando orapara indicar que estamos buscando todos os todos.
  3. Buscamos todos os todos do banco de dados usando Todos.find({}). Assim que o processo for concluído, a todosvariável conterá um array vazio (se não existir nenhuma tarefa no banco de dados) ou um array de tarefas.
  4. Após a conclusão da busca, paramos o controle giratório usando spinner.stop().
  5. Verificamos se há todos verificando se todos.lengthé igual a 0. Se for, exibimos uma mensagem em azul dizendo "Você ainda não tem nenhuma tarefa!". Se houver todos no array (o que significa que o comprimento do array não é igual a 0), percorremos cada tarefa no array e imprimimos seu código, nome e descrição usando para formatação de cores chalk.
  6. Finalmente, nos desconectamos do banco de dados usando await disconnectDB().

Na última linha do código, chamamos a readTask()função. Isso é apenas para fins de teste e você pode remover esta linha conforme as instruções.

Para executar o código, use o comando: node ./commands/readTask.js. Ao executar isso, você verá algo semelhante à saída mostrada aqui:

Nota: Eu já havia criado algumas tarefas aleatórias antes, então quando executo o readTask.jsarquivo, vejo isso em meu terminal:

GIF mostrando a saída que é exibida no terminal como resultado da execução do arquivo `readTask.js`

Saída vista no terminal quando readTask.jso arquivo é executado. Mostra como o código lê com sucesso todas as tarefas do banco de dados e as imprime no terminal.

Antes de prosseguirmos, não se esqueça de remover a última linha de código do readTask.jsarquivo, pois não precisaremos dela no futuro.

Com este código, você implementou com sucesso a funcionalidade de leitura para sua ferramenta CLI do gerenciador de tarefas. Bom trabalho! Nas próximas seções, exploraremos como excluir e atualizar tarefas.

Como excluir todos

Esta seção do tutorial aborda o processo simples de exclusão de todos do banco de dados. A lógica é simples: os usuários inserem o código Todo da tarefa que desejam excluir e nós removemos essa tarefa do banco de dados.

Vamos nos aprofundar no código para fazer isso acontecer no ./commands/deleteTask.jsarquivo.

A primeira etapa é importar os pacotes e funções necessários no início do arquivo, incluindo investigador, Todosmodelo, connectDB(), desconectaDB(), ora e giz.

// Importing packages and functions
import inquirer from "inquirer";
import Todos from '../schema/TodoSchema.js'
import {connectDB, disconnectDB} from '../db/connectDB.js'
import ora from "ora";
import chalk from "chalk";

A seguir, definiremos uma função assíncrona chamada getTaskCode(). A função desta função é solicitar ao usuário que insira o código da tarefa que deseja excluir usando inquirer. A função então corta o código inserido pelo usuário usando o trim()método e retorna o código cortado. O processo de corte é necessário para remover os espaços em branco iniciais ou finais que o código pode conter.

Aqui está o código da getTaskCode()função:

export async function getTaskCode(){
    try {
        // Prompting the user to enter the todo code
        const answers = await inquirer.prompt([
            {name: 'code', 'message': 'Enter the code of the todo: ', type: 'input'},
        ])

        // Trimming user's response so that the todo code does not contain any starting or trailing white spaces
        answers.code = answers.code.trim()

        return answers
    } catch (error) {
        console.log('Something went wrong...\n', error)
    }
}

Agora definiremos a função principal chamada deleteTask(). O código completo está abaixo:

export default async function deleteTask(){
    try {
        // Obtaining the todo code provided by user
        const userCode = await getTaskCode()

        // Connecting to the database
        await connectDB()

        // Starting the spinner
        const spinner = ora('Finding and Deleting the todo...').start()

        // Deleting the task
        const response = await Todos.deleteOne({code: userCode.code})

        // Stopping the spinner
        spinner.stop()

        // Checking the delete operation
        if(response.deletedCount === 0){
            console.log(chalk.redBright('Could not find any todo matching the provided name. Deletion failed.'))
        } else {
            console.log(chalk.greenBright('Deleted Task Successfully'))
        }

        // Disconnecting from the database
        await disconnectDB()
    } catch (error) {
        // Error Handling
        console.log('Something went wrong, Error: ', error)
        process.exit(1)
    }
}

Vamos detalhar esse código passo a passo:

  1. Obtemos o objeto de resposta que inclui o código de tarefa inserido pelo usuário chamando a getTaskCode()função definida acima. Em seguida, atribuímos esse objeto à userCodevariável.
  2. Nós nos conectamos ao banco de dados usando await connectDB().
  3. Iniciamos um controle giratório usando orapara indicar que estamos encontrando e excluindo a tarefa.
  4. Usamos Todos.deleteOne({ code: userCode.code })para pesquisar e excluir tarefas com um código correspondente. A resposta indicará se algum documento foi excluído ou não.
  5. Após a conclusão da operação, paramos o controle giratório usando spinner.stop().
  6. Usamos uma condição if...else para verificar a deletedCountpropriedade na resposta. Se for 0, imprimimos uma mensagem indicando que a tarefa com o código fornecido não foi encontrada e a exclusão falhou. Se deletedCountfor maior que 0, imprimimos uma mensagem de sucesso.
  7. Nós nos desconectamos do banco de dados usando await disconnectDB().

Se eu chamar a função: deleteTask()e depois executar o código usando node /commands/deleteTask.jso comando, verei isso em meu console:

GIF mostrando a saída que é exibida no terminal como resultado da execução do arquivo `deleteTask.js`

Saída vista no terminal quando deleteTask.jso arquivo é executado. Mostra como o código exclui com êxito uma única tarefa do banco de dados.

Como você pode ver no GIF acima, o código solicitará que você insira um código Todo para a tarefa que deseja excluir. Após a exclusão, você receberá uma mensagem de confirmação no console. Quando lemos todas as nossas tarefas após o processo de exclusão, não conseguimos ver a tarefa excluída. Isso implica que nosso código consegue fazer o que deveria fazer com sucesso!

Como atualizar todos

Nesta seção, veremos o código para atualizar uma tarefa específica. Atualizar uma tarefa é um pouco mais complicado em comparação com as operações anteriores. O processo se desenrola da seguinte forma:

  1. Solicita ao usuário que insira o código da tarefa a ser atualizada.
  2. Conecte-se ao banco de dados.
  3. Encontre a tarefa cuja propriedade de código corresponde à entrada do usuário.
  4. Se a tarefa não existir, exiba uma mensagem indicando a falha ao encontrar uma tarefa correspondente.
  5. Se a tarefa existir, solicite ao usuário que atualize o e nameda tarefa.descriptionstatus
  6. Se o usuário definir a propriedade de status de uma tarefa como "concluída", essa tarefa será excluída. Se definido como "pendente", o nome e a descrição da tarefa serão atualizados no banco de dados.
  7. Exiba uma mensagem de sucesso no console após a operação de atualização.

Vamos começar a codificar! A primeira coisa que você precisa fazer é importar todos os pacotes e funções que precisaremos para realizar este trabalho.

// Importing packages and functions
import {connectDB, disconnectDB} from '../db/connectDB.js'
import { getTaskCode } from './deleteTask.js'
import inquirer from 'inquirer'
import Todos from '../schema/TodoSchema.js'
import ora from 'ora'
import chalk from 'chalk'

Antes de começarmos a trabalhar em nossa updateTask()função, criaremos uma pequena função no mesmo arquivo chamada askUpdateQ(). A função desta função é solicitar ao usuário que insira os valores atualizados da tarefa, como nome, descrição e status da tarefa. No final, esta função retornará o objeto de resposta.

Aqui está o código para isso:

async function askUpdateQ(todo){
    try {
        // Prompting the user to update the todo data
        const update = await inquirer.prompt([
            {name: 'name', message: 'Update the name?', type: 'input', default: todo.name},
            {name: 'detail', message: 'Update the Description?', type: 'input', default: todo.detail},
            {name: 'status', message: 'Update the status', type: 'list', choices: ['pending', 'completed'], default: todo.status}
        ])

        return update
    } catch (error) {
        console.log('Something went wrong... \n', error)
    }
}

Duas coisas devem ser observadas aqui:

  1. todoé o objeto de tarefa original (a tarefa que o usuário deseja atualizar). Isso será passado para a askUpdateQ()função pela updateTask()função.
  2. Cada objeto de pergunta dentro da matriz passada inquirer.prompt()contém uma propriedade padrão definida com os valores originais da tarefa. Isso garante que, se o usuário pular uma pergunta, o valor padrão permanecerá inalterado.

Dito isso, agora vamos dar uma olhada no código da updateTask()função:

export default async function updateTask(){
    try {
        // Obtaining the task code entered by user by calling getTaskCode() method
        const userCode = await getTaskCode()

        // Connecting to the database
        await connectDB()

        // Starting the spinner
        const spinner = ora('Finding the todo...').start()

        // Finding the todo which the user wants to update
        const todo = await Todos.findOne({code: userCode.code})

        // Stopping the spinner
        spinner.stop()

        // Checking if the todo exists or not
        if(!todo){
            console.log(chalk.redBright('Could not find a Todo with the code you provided.'))
        } else{
            console.log(chalk.blueBright('Type the updated properties. Press Enter if you don\'t want to update the data.'))

            // Get the user's response of the updated data by calling askUpdateQ() method
            const update = await askUpdateQ(todo)

            // If user marked status as completed, we delete the todo else we update the data
            if(update.status === 'completed'){
                // Changing spinner text and starting it again
                spinner.text = 'Deleting the todo...'
                spinner.start()

                // Deleting the todo
                await Todos.deleteOne({_id : todo._id})

                // Stopping the spinner and display the success message
                spinner.stop()
                console.log(chalk.greenBright('Deleted the todo.'))
            } else {
                // Update the todo
                spinner.text = 'Updating the todo'
                spinner.start()
                await Todos.updateOne({_id: todo._id}, update, {runValidators: true})
                spinner.stop()
                console.log(chalk.greenBright('Updated the todo.'))
            }
        }
        // Disconnecting from the database
        await disconnectDB()
    } catch (error) {
        // Error Handling
        console.log('Something went wrong, Error: ', error)
        process.exit(1)
    }
}

Aqui está um detalhamento do código acima:

  1. Obtenha o código da tarefa que o usuário deseja atualizar. Para isso, utilizamos a getTaskCode()função definida no ./commands/deleteTask.jsarquivo. Simplesmente chamamos a função e atribuímos o objeto de resposta retornado à userCodevariável.
  2. Conecte-se ao banco de dados usando await connectDB().
  3. Inicie um controle giratório para indicar que o código está encontrando a tarefa.
  4. Use Todos.findOne({ code: userCode.code })para encontrar a tarefa que o usuário deseja atualizar e atribuí-la à todovariável. Estamos fazendo isso porque precisaremos dos valores originais da tarefa.
  5. Pare o girador.
  6. Se nenhuma tarefa correspondente for encontrada, exiba uma mensagem chalkindicando que a tarefa não foi encontrada.
  7. Se a tarefa for encontrada, solicite ao usuário que insira as propriedades atualizadas chamando a askUpdateQ()função e passe o todoobjeto (tarefa original) na função. Atribua o objeto retornado à updatevariável.
  8. Se o usuário marcar o status como "concluído", a tarefa será excluída do banco de dados usando deleteOne(). Se marcado como "pendente", o nome e a descrição da tarefa serão atualizados usando updateOne().

    updateOne()O método recebe 3 parâmetros - Objeto de Consulta, Objeto de Atualização e o objeto Opções. Aqui {_id: todo._id}está o objeto de consulta. O Mongoose pesquisa em toda a coleção uma tarefa cuja idpropriedade corresponda a todo_.id. Ao encontrar a tarefa, ele a substitui pelo objeto de atualização, que é updateo nosso caso. O terceiro parâmetro, { runValidators: true }, garante que o Mongoose valide oupdateobjeto contra as regras do esquema antes de executá-lo. Se a validação falhar, a atualização será rejeitada e você receberá um erro. Se a validação for bem-sucedida, o documento será atualizado com sucesso no banco de dados.

    Tanto no caso da Operação Excluir quanto de Atualizar, alteramos o texto do spinner utilizando spinner.texte iniciamos antes de realizar a operação e assim que a operação for concluída, paramos o spinner.
  9. Exiba mensagens de sucesso apropriadas no console com base na operação executada.
  10. Desconecte-se do banco de dados usando await disconnectDB().

Se eu chamar a updateTask()função e executar o código usando o comando: node ./commands/updateTask.js, verei algo assim em meu console:

GIF mostrando a saída que é exibida no terminal como resultado da execução do arquivo `updateTask.js`

Saída vista no terminal quando updateTask.jso arquivo é executado. Ele mostra como o código busca com sucesso a tarefa original e a substitui com sucesso pelos valores atualizados fornecidos pelo usuário.

Com isso, você implementou com sucesso todas as operações CRUD. Agora, vamos usar a commanderbiblioteca para reunir tudo e criar uma ferramenta CLI totalmente funcional.

Como escrever o ponto de entrada CLI usando Commander

Nos estágios finais do nosso projeto, aproveitaremos o poder da commanderbiblioteca para criar uma interface CLI amigável. Com commander, podemos definir diferentes comandos – como ler, adicionar, atualizar e excluir – de maneira organizada e intuitiva.

Nosso código residirá no index.jsarquivo, que serve como ponto de entrada de nossa aplicação. Abaixo está o código completo:

#!/usr/bin/env node

// Importing the required functions for each command
import addTask from './commands/addTask.js'
import deleteTask from './commands/deleteTask.js'
import readTask from './commands/readTask.js'
import updateTask from './commands/updateTask.js'

// Importing the Command class from Commander.js library
import { Command } from 'commander'

// Creating an instance of the Command class
const program = new Command()

// Setting the name and description of the CLI tool
program
.name('todo')
.description('Your terminal task manager!')
.version('1.0.0')

// Defining a command called 'add'
program
.command('add')
.description('Create a new todo.')
.action(addTask)

// Defining a command called 'read'
program
.command('read')
.description('Reads all the todos.')
.action(readTask)

// Defining a command called 'update'
program
.command('update')
.description('Updates a todo.')
.action(updateTask)

// Defining a command called 'delete'
program
.command('delete')
.description('Deletes a todo.')
.action(deleteTask)

// Parsing the command-line arguments and executing the corresponding actions
program.parse()
  1. A primeira linha, #!/usr/bin/env node, é um "shebang". Informa ao sistema para executar o script usando o interpretador Node.js. Isso nos permite executar o script diretamente da linha de comando sem digitar explicitamente nodeantes do nome do arquivo do script.
  2. A seguir, importamos todas as funções necessárias que contêm a lógica de cada comando.
  3. A linha import { Command } from 'commander'importa a Commandclasse da biblioteca Commander.js. A linha subsequente, const program = new Command(), cria uma instância da Commandclasse. Esta instância é essencial para definir e gerenciar comandos para nossa ferramenta CLI.
  4. Em seguida, definimos as informações da ferramenta CLI. Os métodos .name(), .description()e .version()definem o nome, a descrição e a versão de nossa ferramenta CLI. Esses detalhes são exibidos quando os usuários invocam a ferramenta com sinalizadores específicos, como --helpou --version.
  5. A seguir, definimos os vários comandos. Cada program.command()bloco define um comando para nossa ferramenta CLI. Dentro de cada bloco, o nome do comando é definido usando um argumento de string (por exemplo, 'add', 'read'). O .description()método fornece a descrição do comando, enquanto o .action()método associa uma função (por exemplo, addTask, readTask) a um comando específico. Quando um usuário insere um comando no terminal, esta função associada é executada.
  6. A program.parse()linha é essencial para analisar os argumentos da linha de comando fornecidos pelo usuário. Com base no comando inserido, Commander.js executará a função de ação associada.

Como testar a ferramenta CLI

Agora estamos quase na linha de chegada da criação de nossa ferramenta CLI do Gerenciador de Tarefas! Antes de podermos instalar e utilizar a ferramenta, só precisamos fazer um pequeno ajuste no package.jsonarquivo. Basta adicionar a seguinte entrada ao arquivo JSON:

"bin": {
  "todo": "index.js"
}

Se você já criou uma ferramenta CLI e deseja que os usuários a acessem a partir da linha de comando de maneira semelhante a comandos como nodeou npm, a propriedade "bin" entra em ação.

A propriedade "bin" no package.jsonarquivo permite especificar comandos que se tornarão acessíveis globalmente assim que o pacote for instalado. Em termos mais simples, permite criar atalhos para executar scripts ou funções específicas a partir da linha de comando.

O código fornecido instrui o Node.js a executar o script definido sempre index.jsque alguém entrar todono terminal. Em essência, isso transforma seu script em uma ferramenta de linha de comando acessível globalmente.

A etapa final antes de começar a usar a ferramenta é instalá-la globalmente em seu sistema! Execute o seguinte comando para fazer isso:

npm i -g .

Conclusão

Parabéns se você acompanhou até aqui! Agora você está totalmente equipado para começar a usar a ferramenta CLI do Gerenciador de Tarefas.

Aqui estão os comandos que você pode usar para operar a ferramenta:

  • todo add  – Crie uma nova tarefa
  • todo read– Leia todas as suas tarefas pendentes
  • todo update– Atualizar uma tarefa específica
  • todo delete– Excluir uma tarefa

Você também pode usar estas 2 opções usando a ferramenta:

  • todo --versionou todo -V– Para saber o número da versão desta ferramenta
  • todo --helpou todo -h– Para exibir ajuda para o comando

E isso encerra este manual. Espero que você tenha achado agradável e informativo.

Aqui está o link para o Repositório: Task Manager CLI Tool Repo .

Vejo você na próxima vez! 👋 ❤️ ✨

Fonte: https://www.freecodecamp.org

#node #nodejs #mongodb

1.10 GEEK