Topo

By Sails.js Tech Brasil

Exemplos de Helpers, Hooks e Policies

Como já citado anteriormente, Helpers são contruídos de acordo com a especificação machinepack E NÃO FUNCIONARÃO se forem escritos de outra forma, pois são interpretados pelo core do Sails. Observe que os Hooks e as Policies são funcões JavaScript padrão e não seguem o modelo machinepack.

Helpers devem estar localizados no diretório \api\helpers para que sejam acessíveis em qualquer parte do seu aplicativo. Utilize await sails.helpers.nomeHelper(params) para utilizá-los

Hooks devem estar localizados no diretório \api\hooks para que funcionem e serão executados durante a inicialização.

Policies devem estar localizadas no diretório \api\policies para que sejam localizadas pelo core Sails enquanto executa o arquivo de configuracao config/policies.js, durante a inicialização da aplicação.


Seguem abaixo alguns exemplos funcionais que pode utilizar livremente:

/* is-jwt-valid.js (Policy)
╔╗  ╔╗╔═══╗╔╗   ╔══╗╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗      ╔╗╔╗╔╗╔╗╔════╗
║╚╗╔╝║║╔═╗║║║   ╚╣╠╝╚╗╔╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║      ║║║║║║║║║╔╗╔╗║
╚╗║║╔╝║║ ║║║║    ║║  ║║║║║║ ║║ ║║║║║║ ║║║╚═╝║      ║║║║║║║║╚╝║║╚╝
 ║╚╝║ ║╚═╝║║║ ╔╗ ║║  ║║║║║╚═╝║ ║║║║║║ ║║║╔╗╔╝    ╔╗║║║╚╝╚╝║  ║║
 ╚╗╔╝ ║╔═╗║║╚═╝║╔╣╠╗╔╝╚╝║║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ║╚╝║╚╗╔╗╔╝ ╔╝╚╗
  ╚╝  ╚╝ ╚╝╚═══╝╚══╝╚═══╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚══╝ ╚╝╚╝  ╚══╝
*/

let jwt = require('jsonwebtoken')

module.exports = async function (req, res, proceed) {

  let token = req.headers['authorization'] + ''
  if (token && token.startsWith('Bearer')) {
    token = token.substring(7)
    /* verifica se o TOKEN recebido esta valido. */
    /* o TOKEN foi gerado anteriormente usando a */
    /* mesma JWT_SECRET                          */
    jwt.verify(token, process.env.JWT_SECRET, function (err, decoded) {
      if (!err) {
        return proceed();
      } else {
        console.log('Unauthorized!')
        return res.forbidden();
      }
    });
  } else {
    console.log('Invalid Token')
    return res.forbidden();
  }

};


    
(*) Consulte também como criar um JWToken válido e enviar ao navegador

/* is-auth-basic-valid.js (Policy)
╔╗  ╔╗╔═══╗╔╗   ╔══╗╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗    ╔═══╗╔╗ ╔╗╔════╗╔╗ ╔╗    ╔══╗ ╔═══╗╔═══╗╔══╗╔═══╗
║╚╗╔╝║║╔═╗║║║   ╚╣╠╝╚╗╔╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║    ║╔═╗║║║ ║║║╔╗╔╗║║║ ║║    ║╔╗║ ║╔═╗║║╔═╗║╚╣╠╝║╔═╗║
╚╗║║╔╝║║ ║║║║    ║║  ║║║║║║ ║║ ║║║║║║ ║║║╚═╝║    ║║ ║║║║ ║║╚╝║║╚╝║╚═╝║    ║╚╝╚╗║║ ║║║╚══╗ ║║ ║║ ╚╝
 ║╚╝║ ║╚═╝║║║ ╔╗ ║║  ║║║║║╚═╝║ ║║║║║║ ║║║╔╗╔╝    ║╚═╝║║║ ║║  ║║  ║╔═╗║    ║╔═╗║║╚═╝║╚══╗║ ║║ ║║ ╔╗
 ╚╗╔╝ ║╔═╗║║╚═╝║╔╣╠╗╔╝╚╝║║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ║╔═╗║║╚═╝║ ╔╝╚╗ ║║ ║║    ║╚═╝║║╔═╗║║╚═╝║╔╣╠╗║╚═╝║
  ╚╝  ╚╝ ╚╝╚═══╝╚══╝╚═══╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚╝ ╚╝╚═══╝ ╚══╝ ╚╝ ╚╝    ╚═══╝╚╝ ╚╝╚═══╝╚══╝╚═══╝
*/

module.exports = async function (req, res, proceed) {

  let auth = req.headers['authorization'] + ''
  if (auth && auth.startsWith('Basic ')) {
    auth = auth.substring(6)

    let decoded = window.atob(auth);
    let splited = decoded.split(`:`)
    let login = splited[0]
    let password = splited[1]

    /* adicione aqui seu algoritimo de verificacao    */
    /* do login e senha contra a informacao de seu BD */
    /* retornando 'false' se a verificacao falhar     */

    return false
  } else {
    console.log('Invalid Token')
    return res.forbidden();
  }

};
    

/* gerador-dv.js (Helper)
╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗    ╔═══╗╔═══╗    ╔═══╗╔══╗╔═══╗      ╔╗  ╔╗╔═══╗╔═══╗╔══╗ ╔═══╗╔══╗╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗
║╔═╗║║╔══╝║╔═╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║    ╚╗╔╗║║╔══╝    ╚╗╔╗║╚╣╠╝║╔═╗║      ║╚╗╔╝║║╔══╝║╔═╗║╚╣╠╝ ║╔══╝╚╣╠╝║╔═╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║
║║ ╚╝║╚══╗║╚═╝║║║ ║║ ║║║║║║ ║║║╚═╝║     ║║║║║╚══╗     ║║║║ ║║ ║║ ╚╝      ╚╗║║╔╝║╚══╗║╚═╝║ ║║  ║╚══╗ ║║ ║║ ╚╝║║ ║║ ║║║║║║ ║║║╚═╝║
║║╔═╗║╔══╝║╔╗╔╝║╚═╝║ ║║║║║║ ║║║╔╗╔╝     ║║║║║╔══╝     ║║║║ ║║ ║║╔═╗       ║╚╝║ ║╔══╝║╔╗╔╝ ║║  ║╔══╝ ║║ ║║ ╔╗║╚═╝║ ║║║║║║ ║║║╔╗╔╝
║╚╩═║║╚══╗║║║╚╗║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ╔╝╚╝║║╚══╗    ╔╝╚╝║╔╣╠╗║╚╩═║╔╗     ╚╗╔╝ ║╚══╗║║║╚╗╔╣╠╗╔╝╚╗  ╔╣╠╗║╚═╝║║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗
╚═══╝╚═══╝╚╝╚═╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚═══╝╚═══╝    ╚═══╝╚══╝╚═══╝╚╝      ╚╝  ╚═══╝╚╝╚═╝╚══╝╚══╝  ╚══╝╚═══╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝
*/



module.exports = {


  friendlyName: 'Gerador de Digito Verificador',


  description: 'Calcula o valor de um determinado digito verificador.',


  extendedDescription:
    `Calcula o valor de um determinado digito verificador (DV) a partir dos pesos relativos
        aplicáveis ao cálculo. Se aplica tanto para CNPJ quanto para CPF, não se limitando a estes.
           Utilize os pesos: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] para CPF e [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6] para CNPJ `,


  inputs: {
    digits: {type: 'string', required: true, description: 'Digitos ainda nao validados (sem o DV)', example: '316675800001'},
    weights: {type: 'ref', required: true, description: 'Array de pesos relativos atribuídos ao calculo', example: '[2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6]'},
  },

  exits: {
    success: {
      description: 'Calculo realizado com sucesso'
    },

    error: {
      description: 'Erro ao tentar calcular o DV',
    }
  },

  fn: async function ({digits, weights}) {

    const digitsLength = digits.length;
    const digitsLengthWithoutChecker = weights.length - 1;

    const sum = digits.split('').reduce((acc, digit, idx) => {
      return acc + +digit * weights[digitsLength - 1 - idx];
    }, 0);
    const sumDivisionRemainder = sum % 11;
    const checker = sumDivisionRemainder < 2 ? 0 : 11 - sumDivisionRemainder;

    if (digitsLength === digitsLengthWithoutChecker) {
      return await sails.helpers.geradorDv(`${digits}${checker}`, weights);
    }

    return `${digits[digitsLength - 1]}${checker}`;

  }

};

    

/* formata-cnpj-cpf.js (Helper)

 ╔═══╗╔═══╗╔═══╗╔═╗╔═╗╔═══╗╔════╗╔═══╗╔═══╗╔═══╗╔═══╗    ╔═══╗╔═╗ ╔╗╔═══╗  ╔╗             ╔═══╗╔═══╗ ╔═══╗
 ║╔══╝║╔═╗║║╔═╗║║║╚╝║║║╔═╗║║╔╗╔╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║    ║╔═╗║║║╚╗║║║╔═╗║  ║║             ║╔═╗║║╔═╗║ ║╔══╝
 ║╚══╗║║ ║║║╚═╝║║╔╗╔╗║║║ ║║╚╝║║╚╝║║ ║║ ║║║║║║ ║║║╚═╝║    ║║ ╚╝║╔╗╚╝║║╚═╝║  ║║             ║║ ╚╝║╚═╝║ ║╚══╗
 ║╔══╝║║ ║║║╔╗╔╝║║║║║║║╚═╝║  ║║  ║╚═╝║ ║║║║║║ ║║║╔╗╔╝    ║║ ╔╗║║╚╗║║║╔══╝╔╗║║    ╔═══╗    ║║ ╔╗║╔══╝ ║╔══╝
╔╝╚╗  ║╚═╝║║║║╚╗║║║║║║║╔═╗║ ╔╝╚╗ ║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ║╚═╝║║║ ║║║║║   ║╚╝║    ╚═══╝    ║╚═╝║║║   ╔╝╚╗
╚══╝  ╚═══╝╚╝╚═╝╚╝╚╝╚╝╚╝ ╚╝ ╚══╝ ╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚═══╝╚╝ ╚═╝╚╝   ╚══╝             ╚═══╝╚╝   ╚══╝
*/

  module.exports = {


  friendlyName: 'Formata CNPJ ou CPF Brasileiros',


  description: 'Recebe uma sequencia de numeros sem pontos ou traços e retorna formatado',


  inputs: {
    tipo: {type: 'string', required: true, example: 'CNPJ', description: 'Pode ser CNPJ  ou CPF'},
    digitos: {
      type: 'string',
      required: true,
      example: '01459385730',
      description: 'Pode ser CNPJ com 14 posições ou CPF com 11 posicões'
    }

  },


  exits: {

    success: {
      description: 'OK.',
    },

  },


  fn: async function ({tipo, digitos}) {
    tipo = tipo + ''.toUpperCase();
    digitos = digitos + '';

    if(tipo!=='CNPJ'&&tipo!=='CPF') {return 'Tipo Invalido. Informe CNPJ ou CPF';}

    /* Assume CPF como padrao */
    let correctDigitsLength = 11;
    let firstDotPosition = 2;
    let secondDotPosition = 5;
    let slashPosition = -1;
    let dashPosition = 8;

    if (tipo === 'CNPJ') {
      correctDigitsLength = 14;
      firstDotPosition = 1;
      secondDotPosition = 4;
      slashPosition = 7;
      dashPosition = 11;
    }
    if (digitos.length < 11 || digitos.length > 14) {
      return `O numero informado  ${digitos} deve ter no minimo 11 e no maximo 14 digitos: `;
    } else {
      const cleanDigits = digitos.replace(/\D/g, '');
      return cleanDigits
        .slice(0, correctDigitsLength)
        .split('')
        .reduce((acc, digit, idx) => {
          const result = `${acc}${digit}`;
          if (idx !== digitos.length - 1) {
            if (idx === firstDotPosition || idx === secondDotPosition) {
              return `${result}.`;
            }
            if (idx === slashPosition) {
              return `${result}/`;
            }
            if (idx === dashPosition) {
              return `${result}-`;
            }
          }
          return result;
        }, '');
    }
  }


};


    


/* jwt-login.js (Helper)

╔═══╗╔╗ ╔╗╔════╗╔═══╗╔═╗ ╔╗╔════╗╔══╗╔═══╗╔═══╗╔═══╗╔═══╗╔═══╗      ╔╗╔╗╔╗╔╗╔════╗
║╔═╗║║║ ║║║╔╗╔╗║║╔══╝║║╚╗║║║╔╗╔╗║╚╣╠╝║╔═╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║      ║║║║║║║║║╔╗╔╗║
║║ ║║║║ ║║╚╝║║╚╝║╚══╗║╔╗╚╝║╚╝║║╚╝ ║║ ║║ ╚╝║║ ║║ ║║║║║║ ║║║╚═╝║      ║║║║║║║║╚╝║║╚╝
║╚═╝║║║ ║║  ║║  ║╔══╝║║╚╗║║  ║║   ║║ ║║ ╔╗║╚═╝║ ║║║║║║ ║║║╔╗╔╝    ╔╗║║║╚╝╚╝║  ║║
║╔═╗║║╚═╝║ ╔╝╚╗ ║╚══╗║║ ║║║ ╔╝╚╗ ╔╣╠╗║╚═╝║║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ║╚╝║╚╗╔╗╔╝ ╔╝╚╗
╚╝ ╚╝╚═══╝ ╚══╝ ╚═══╝╚╝ ╚═╝ ╚══╝ ╚══╝╚═══╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚══╝ ╚╝╚╝  ╚══╝
*/

let jwt = require('jsonwebtoken');

module.exports = {

  friendlyName: 'Login e Autenticacao por Token (JWT)',

  description: 'Recebe uma requisição HTTP contendo como parametros de query (?name=xyz), valida a senha e retorna um JWT',

  extendedDescription: `!! Se faz necessario desabilitar o atributo CSRF no dicionario exportado em [security.js], !!
                        !! caso contrario, nao funcionara, sendo retornado "forbidden"                             !!`,

  inputs: {
    emailAddress: { type: 'string', required: true, description: 'email utilizado pelo Usuário na criação do mesmo' },
    password: { type: 'string', required: true, description: 'senha em texto claro' },
  },

  exits: {
    success: {
      description: 'Autenticado e retornado um JWT Assinado'
    },

  },

  fn: async function ({ emailAddress, password }) {

    // procura pelo email informado acima, no banco de dados.
    // (observe que o 'lowerCase' garante que a pesquisa seja 'case insensitive',
    // independentemente do banco de dados que se esta utilizando)
    // Observação: User deve estar definido no arquivo 'model/user.js'!

    let userRecord = await User.findOne({
      emailAddress: emailAddress.toLowerCase(),
    });

    // Se nenhum User for encontrado, então dispare o erro de saída "badCombo".
    if (!userRecord) {
      throw new Error('Not Found');
    }

    // Se o usuário existe, mas a senha está errada, dispare o erro "badCombo".
    // Observe que o método 'checkPassword' deve estar definido no diretório /api/helpers
    await sails.helpers.passwords.checkPassword(password, userRecord.password)
      .intercept('incorrect', 'badCombo');

    return {
      token: (jwt.sign({
        id: userRecord.id,
        exp: Math.floor(Date.now() / 1000) + (60 * 60),
        sub: userRecord.emailAddress,
        rol: [...userRecord.rules]
      }, process.env.JWT_SECRET))
    };

    // JWT_SECRET deve ter sido previamente exportado para o ambiente Node.js
  }
};

    
(*) Consulte também como criar um Validador JWT para validar o Token enviado pelo Cliente

/* send-twillio-sms.js (Helper)

╔═══╗╔═╗ ╔╗╔╗  ╔╗╔══╗╔═══╗╔═══╗╔═══╗╔═══╗    ╔═══╗╔═══╗    ╔═══╗╔═╗╔═╗╔═══╗
║╔══╝║║╚╗║║║╚╗╔╝║╚╣╠╝║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║    ╚╗╔╗║║╔══╝    ║╔═╗║║║╚╝║║║╔═╗║
║╚══╗║╔╗╚╝║╚╗║║╔╝ ║║ ║║ ║║ ║║║║║║ ║║║╚═╝║     ║║║║║╚══╗    ║╚══╗║╔╗╔╗║║╚══╗
║╔══╝║║╚╗║║ ║╚╝║  ║║ ║╚═╝║ ║║║║║║ ║║║╔╗╔╝     ║║║║║╔══╝    ╚══╗║║║║║║║╚══╗║
║╚══╗║║ ║║║ ╚╗╔╝ ╔╣╠╗║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ╔╝╚╝║║╚══╗    ║╚═╝║║║║║║║║╚═╝║
╚═══╝╚╝ ╚═╝  ╚╝  ╚══╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚═══╝╚═══╝    ╚═══╝╚╝╚╝╚╝╚═══╝
*/

const accountSid = process.env.TWILLIO_ACCOUNT_SID;
const authToken = process.env.TWILLIO_AUTH_TOKEN;
const twillioNumber = process.env.TWILLIO_PHONE_NUMBER
const client = require('twilio')(accountSid, authToken);


module.exports = {


  friendlyName: 'Send SMS',


  description: 'Send a SMS by Twillio provider.',


  extendedDescription: 'To get available this SMS feature we should to create an Twillio account at https://www.twilio.com/try-twilio/?utm_source=sendgrid&utm_medium=consoledash',


  inputs: {
    to: {type: 'string', required: true},
    body: {type: 'string', required: true}
  },

  exits: {
    success: {
      description: 'SMS sent by Twillio successfully!'
    },

    error: {
      description: 'If some kind of err occur, it often is due to credentials issues',
    }
  },

  fn: async function ({to, body}) {

    console.log(`Enviando SMS para ${to}...`)

    let message = await client.messages
      .create({
        body: body,
        from: twillioNumber,
        to: to
      })
    console.log(message.sid)
  }


}

    

/* format-brazilian-date.js (Helper)

 ╔═══╗╔═══╗╔═══╗╔═╗╔═╗╔═══╗╔════╗╔═══╗╔═══╗╔═══╗╔═══╗    ╔═══╗╔═══╗    ╔═══╗╔═══╗╔════╗╔═══╗    ╔══╗ ╔═══╗
 ║╔══╝║╔═╗║║╔═╗║║║╚╝║║║╔═╗║║╔╗╔╗║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║    ╚╗╔╗║║╔══╝    ╚╗╔╗║║╔═╗║║╔╗╔╗║║╔═╗║    ║╔╗║ ║╔═╗║
 ║╚══╗║║ ║║║╚═╝║║╔╗╔╗║║║ ║║╚╝║║╚╝║║ ║║ ║║║║║║ ║║║╚═╝║     ║║║║║╚══╗     ║║║║║║ ║║╚╝║║╚╝║║ ║║    ║╚╝╚╗║╚═╝║
 ║╔══╝║║ ║║║╔╗╔╝║║║║║║║╚═╝║  ║║  ║╚═╝║ ║║║║║║ ║║║╔╗╔╝     ║║║║║╔══╝     ║║║║║╚═╝║  ║║  ║╚═╝║    ║╔═╗║║╔╗╔╝
╔╝╚╗  ║╚═╝║║║║╚╗║║║║║║║╔═╗║ ╔╝╚╗ ║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ╔╝╚╝║║╚══╗    ╔╝╚╝║║╔═╗║ ╔╝╚╗ ║╔═╗║    ║╚═╝║║║║╚╗
╚══╝  ╚═══╝╚╝╚═╝╚╝╚╝╚╝╚╝ ╚╝ ╚══╝ ╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚═══╝╚═══╝    ╚═══╝╚╝ ╚╝ ╚══╝ ╚╝ ╚╝    ╚═══╝╚╝╚═╝
*/

module.exports = {


  friendlyName: 'Format brazilian date',


  description: '',


  inputs: {
    source: {type: 'string'},
  },


  exits: {

    success: {
      description: 'All done.',
    },

  },


  fn: async function ({source}) {
    return new Date(source).toLocaleString('pt-br')
  }


};
    


/* send-mailgun-email.js (Helper)
╔═══╗╔═╗ ╔╗╔╗  ╔╗╔══╗╔═══╗╔═══╗╔═══╗╔═══╗    ╔═══╗╔═══╗    ╔═╗╔═╗╔═══╗╔══╗╔╗   ╔═══╗╔╗ ╔╗╔═╗ ╔╗
║╔══╝║║╚╗║║║╚╗╔╝║╚╣╠╝║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║    ╚╗╔╗║║╔══╝    ║║╚╝║║║╔═╗║╚╣╠╝║║   ║╔═╗║║║ ║║║║╚╗║║
║╚══╗║╔╗╚╝║╚╗║║╔╝ ║║ ║║ ║║ ║║║║║║ ║║║╚═╝║     ║║║║║╚══╗    ║╔╗╔╗║║║ ║║ ║║ ║║   ║║ ╚╝║║ ║║║╔╗╚╝║
║╔══╝║║╚╗║║ ║╚╝║  ║║ ║╚═╝║ ║║║║║║ ║║║╔╗╔╝     ║║║║║╔══╝    ║║║║║║║╚═╝║ ║║ ║║ ╔╗║║╔═╗║║ ║║║║╚╗║║
║╚══╗║║ ║║║ ╚╗╔╝ ╔╣╠╗║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗    ╔╝╚╝║║╚══╗    ║║║║║║║╔═╗║╔╣╠╗║╚═╝║║╚╩═║║╚═╝║║║ ║║║
╚═══╝╚╝ ╚═╝  ╚╝  ╚══╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝    ╚═══╝╚═══╝    ╚╝╚╝╚╝╚╝ ╚╝╚══╝╚═══╝╚═══╝╚═══╝╚╝ ╚═╝
*/

var api_key = process.env.MAIL_GUN_API_KEY;
var domain = process.env.MAIL_GUN_DOMAIN;
var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain});


module.exports = {


  friendlyName: 'Send MailGun',


  description: 'Send a Mail by MailGun provider.',


  extendedDescription: 'To get available this MailGun feature we should to create an MailGun account at https://signup.mailgun.com/new/signup',


  inputs: {
    from: {type: 'string', required: true},
    to: {type: 'string', required: true},
    subject: {type: 'string', required: true},
    text: {type: 'string', required: true}
  },

  exits: {
    success: {
      description: 'MailGun sent successfully!'
    },

    error: {
      description: 'If some kind of err occur, it often is due to credentials issues',
    }
  },

  fn: async function ({from, to, subject, text}) {

    console.log(`Sendind MailGun to ${to}...`)

    var data = {
        from: from,
        to: to,
        subject: subject,
        text: text // or html: if you wanna send html instead pure txt
};

   mailgun.messages().send(data, function (error, body) {
   console.log(body);
   });
}


}
Componentes Uteis

Componentes são instâncias reutilizáveis do Vue com um nome. Nesse caso, paginador . Podemos usar esses componentes como um elemento personalizado da instância Parasails


//╔═══╗╔═══╗╔═╗╔═╗╔═══╗╔═══╗╔═╗ ╔╗╔═══╗╔═╗ ╔╗╔════╗╔═══╗    ╔═══╗╔═══╗╔═══╗╔══╗╔═╗ ╔╗╔═══╗╔═══╗╔═══╗╔═══╗
//║╔═╗║║╔═╗║║║╚╝║║║╔═╗║║╔═╗║║║╚╗║║║╔══╝║║╚╗║║║╔╗╔╗║║╔══╝    ║╔═╗║║╔═╗║║╔═╗║╚╣╠╝║║╚╗║║║╔═╗║╚╗╔╗║║╔═╗║║╔═╗║
//║║ ╚╝║║ ║║║╔╗╔╗║║╚═╝║║║ ║║║╔╗╚╝║║╚══╗║╔╗╚╝║╚╝║║╚╝║╚══╗    ║╚═╝║║║ ║║║║ ╚╝ ║║ ║╔╗╚╝║║║ ║║ ║║║║║║ ║║║╚═╝║
//║║ ╔╗║║ ║║║║║║║║║╔══╝║║ ║║║║╚╗║║║╔══╝║║╚╗║║  ║║  ║╔══╝    ║╔══╝║╚═╝║║║╔═╗ ║║ ║║╚╗║║║╚═╝║ ║║║║║║ ║║║╔╗╔╝
//║╚═╝║║╚═╝║║║║║║║║║   ║╚═╝║║║ ║║║║╚══╗║║ ║║║ ╔╝╚╗ ║╚══╗    ║║   ║╔═╗║║╚╩═║╔╣╠╗║║ ║║║║╔═╗║╔╝╚╝║║╚═╝║║║║╚╗
//╚═══╝╚═══╝╚╝╚╝╚╝╚╝   ╚═══╝╚╝ ╚═╝╚═══╝╚╝ ╚═╝ ╚══╝ ╚═══╝    ╚╝   ╚╝ ╚╝╚═══╝╚══╝╚╝ ╚═╝╚╝ ╚╝╚═══╝╚═══╝╚╝╚═╝

      /* \assets\js\components\paginador.js */

/**
 *
 * -----------------------------------------------------------------------------
 * Um paginador para grandes conjuntos de dados:
 *
 * @property content um array contendo o conjunto de registros, paginado
 * @property number o numero da pagina cujo cursor deve estar
 * @property size a quantidade de registros maxima esperada, por pagina
 * @property total o numero total de registros (filtrados) da base de dados
 *
 * How to: para incorporar a paginacao:
 *         <paginador :content="page.content" :number="page.number" :size="page.size" :total="page.totalpages" v-on:paginated="paginate($event)"></paginador>
 * -----------------------------------------------------------------------------
 */

parasails.registerComponent('paginador', {

  //  ╔═╗╦═╗╔═╗╔═╗╔═╗
  //  ╠═╝╠╦╝║ ║╠═╝╚═╗
  //  ╩  ╩╚═╚═╝╩  ╚═╝
  props: [
    'content',
    'number',
    'size',
    'total'
  ],

  //  ╦╔╗╔╦╔╦╗╦╔═╗╦    ╔═╗╔╦╗╔═╗╔╦╗╔═╗
  //  ║║║║║ ║ ║╠═╣║    ╚═╗ ║ ╠═╣ ║ ║╣
  //  ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝  ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
  data: function () {
    return {
      cont: [],
      num: 0,
      siz: 0,
      tot: 0
    };
  },

  //  ╦ ╦╔╦╗╔╦╗╦
  //  ╠═╣ ║ ║║║║
  //  ╩ ╩ ╩ ╩ ╩╩═╝
  template: `
     <div id='paginador' class='text-center'>
     <em class="fa fa-fast-backward m-2" @click='goToFirst()'></em>
            <em class="fa fa-backward m-2" @click='goToPrevious()'></em>
            <em class="badget m-2">{{+number + 1}}</em>
            <em class="fa fa-forward m-2" @click='goToNext()'></em>
            <em class="fa fa-fast-forward m-2" @click='goToLast()'></em>
            <br>
     <em class="fw-bold">[ {{+number * +size}} - {{(+number * +size) + +size }} / {{ (total) }} ]</em>
     </div>
  `,

  //  ╦  ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦  ╔═╗
  //  ║  ║╠╣ ║╣ ║  ╚╦╝║  ║  ║╣
  //  ╩═╝╩╚  ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
  beforeMount: function () {
    this.cont = this.content;
    this.num = this.number;
    this.siz = this.size;
    this.tot = this.total;

  },

  beforeDestroy: function () {

  },


  //  ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
  //  ║║║║ ║ ║╣ ╠╦╝╠═╣║   ║ ║║ ║║║║╚═╗
  //  ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
  methods: {

    goToFirst: function () {
      this.num = 0;
      this.$emit('paginated', { content: this.cont, number: this.num, size: this.siz, total: this.tot });
    },

    goToPrevious: function () {
      this.num = this.number - 1;
      if (this.num < 0) {this.num = 0;}
      this.$emit('paginated', { content: this.cont, number: this.num, size: this.siz, total: this.tot });


    },

    goToNext: function () {
      if(this.content.length < this.size) return;
      this.num = this.number + 1;
      let maxPage = this.total / this.size;
      if (this.num > maxPage) {this.num = maxPage;}
      this.$emit('paginated', { content: this.cont, number: this.num, size: this.siz, total: this.tot });

    },

    goToLast: function () {
      if(this.content.length < this.size) return;
      this.num = this.total / this.size;
      this.$emit('paginated', { content: this.cont, number: this.num, size: this.siz, total: this.tot });
    },


  }

});





        
Script para Carga de Dados FAKE

Um recurso útil para inclusão de dados falsos na base de dados, para testes

Para criar um Script utilize o comando sails generate script cria-dados-para-teste

Modifique o script/gerador de dados falsos de Churchs, conforme abaixo:

Para executar o Script utilize o comando sails run cria-dados-para-teste

Para incorporar o Script dentro de outro código, utilize await require('<path>/scripts/cria-dados-para-teste').fn()


module.exports = {

 // ___    ___    ___    _  ___    _____     ___    ___       ___   _____  ___    ___   _____     ___    ___       ___   _____  ___    _____  ___
 //(  _ \ (  _ \ |  _ \ (_)(  _ \ (_   _)   (  _ \ (  _ \    (  _ \(  _  )|  _ \ (  _ \(  _  )   (  _ \ (  _ \    (  _ \(  _  )(  _ \ (  _  )(  _ \
 //| (_(_)| ( (_)| (_) )| || |_) )/|| |     | | ) || (_(_)   | ( (_) (_) || (_) )| ( (_) (_) |   | | ) || (_(_)   | | ) | (_) || | ) || ( ) || (_(_)
 // \__ \ | |  _ |    / | ||  __/(_)| |     | | | )|  _)_    | |  _(  _  )|    / | | __(  _  )   | | | )|  _)_    | | | )  _  )| | | )| | | | \__ \
 //( )_) || (_( )| |\ \ | || |      | |     | |_) || (_( )   | (_( ) | | || |\ \ | |(_ ) | | |   | |_) || (_( )   | |_) | | | || |_) || (_) |( )_) |
 // \(___)(____/ (_) (_)(_)(_)      ( )     (____/ (____/    (____/(_) (_)(_) (_)(____/(_) (_)   (____/ (____/    (____/(_) (_)(____/ (_____) \(___)


  friendlyName: 'Cria dados para teste',


  description: 'Cria dados para teste utilizando a biblioteca faker como geradora de dados',


  fn: async function () {


    sails.log('Running custom shell script... (`sails run cria-dados-para-teste`)');

    // NAO SE ESQUECA DE INSTALAR A BIBLIOTECA COM [npm i @faker-js/faker --save-dev]
    const {faker} = require('@faker-js/faker')

    // OBSERVE QUE O OBJETO X RELACIONAL User JÁ EXISTE NATIVAMENTE NO Protótipo Sails.js
    let users = await User.find()
    let user = users[0]

    // OBSERVE QUE OS EXEMPLOS DE MODEL ABAIXO NÃO EXISTEM NATIVAMENTE NO SAILS!
    // Você deve substituir os Objetos por aqueles que você deseja carregar com dados falsos para teste
    // ou criar estes Model(os) com o comando [sails generate model nome-objeto]
    let church = await Church.create({
      fullName: faker.company.name(),
      shortName: faker.company.buzzNoun(),
      email: faker.internet.email(),
      address: faker.location.streetAddress(),
      site: faker.internet.url(),
      phone: faker.phone.number(),
      linktree: `https://linktr.ee/fake-cuidado-cristao`,
      tipo: 'DAUGHTER'
    }).fetch()


    for (let i = 1; i < 5; i++) {
      await Classroom.create({
        name: faker.lorem.word()
      })
    }

    for (let i = 1; i < 5; i++) {
      await Contribution.create({
        dtContribution: new Date(),
        value: faker.commerce.price(),
        propose: faker.commerce.productDescription(),
        userId: user.id
      })
    }

    for (let i = 1; i < 5; i++) {
      await Usercare.create({
        userId: user.id,
        dtContact: new Date(),
        record: faker.lorem.paragraph({min: 1, max: 3})
      })
    }

    await UserChurch.create({
      churchId: church.id,
      userId: user.id,
      type: 'CONGREGATION',
      dtAssociation: new Date()
    })

    let classrooms = await Classroom.find()
    classrooms.forEach(classroom=>{
      UserClassroom.create({
        dtAssociation: new Date(),
        type: 'CLASSMATE',
        userId: user.id
      }).then().catch(err=>console.log(err))
    })

    sails.log('Finished custom shell script... (`sails run cria-dados-para-teste`)');


  }


};



    
Formulário AJAX padrão



// ____  _____  ____  __  __  __  __  __      __    ____  ____  _____        __     ____    __    _  _      ____    __    ____   ____    __    _____
// ( ___)(  _  )(  _ \(  \/  )(  )(  )(  )    /__\  (  _ \(_  _)(  _  )      /__\   (_  _)  /__\  ( \/ )    (  _ \  /__\  (  _ \ (  _ \  /__\  (  _  )
// )__)  )(_)(  )   / )    (  )(__)(  )(__  /(  )\  )   / _)(_  )(_)(      /(  )\ /\_)(   /(  )\  )  (      )___/ /(  )\  )(_) ) )   / /(  )\  )(_)(
// (_)   (_____)(_)\_)(_/\/\_)(______)(____)(__)(__)(_)\_)(____)(_____)    (__)(__)\____) (__)(__)(_/\_)    (__)  (__)(__)(____/ (_)\_)(__)(__)(_____)

// <div id='formulario-ajax-padrao' v-cloak >

// <ajax-form action="updatePassword"
//            :syncing.sync="syncing"
//            :cloud-error.sync="cloudError"
//            :form-data="formData"
//            :form-rules="formRules"
//            :form-errors.sync="formErrors"
//            @submitted="submittedForm()">

//   <input   class="form-control"
//            id="password"
//            name="password"
//            type="password"
//            :class="[formErrors.password ? 'is-invalid' : '']"
//            v-model.trim="formData.password"
//            placeholder="••••••••"
//            autocomplete="new-password"
//            focus-first>

//        <cloud-error v-if="cloudError"></cloud-error>

//  <ajax-button type="submit" :syncing="syncing" class="btn btn-dark">Save changes</ajax-button>

//   </ajax-form>

//  </div>

parasails.registerPage('formulario-ajax-padrao', {
  //  ╦╔╗╔╦╔╦╗╦╔═╗╦    ╔═╗╔╦╗╔═╗╔╦╗╔═╗
  //  ║║║║║ ║ ║╠═╣║    ╚═╗ ║ ╠═╣ ║ ║╣
  //  ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝  ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
  data: {
    // Flag de sincronismo de estado (em execucao = true/aguardando = false).
    syncing: false,

    // Atributos/Dados editaveis do formulario que nao necessitam de validacao alguma
    formData: {
      rememberMe: true,
    },

    // Erros encontrados na validacao do formulario.
    // Internamente, o Sails grava {invalido = 'true' ou 'false'} para cada campo do formData.
    formErrors: { /* … */ },

    // Regras de validacao do formulario
    // Atributos / Dados do Formulario e regras de validacao
    // Atributos definidos aqui sao copiados internamente para o Objeto formData
    formRules: {
      emailAddress: {isEmail: true, required: true},
      fullName: {required: true},
      password: {required: true},
      confirmPassword: {required: true, sameAs: 'password'},
    },

    // Armazena os error retornados pelo servidor, se
    // e somente se, a requisicao ao servidor teve
    // como origem a API Cloud.js (veja arquivo assets/cloud.setup.js)
    // o Cloud insere neste campo somente a mensagem de erro, mas o JSON
    // de resposta tem muito mais informacoes e pode ser aproveitado pelo
    // programador. Vide `tratamento de erros Cloud.js` mais abaixo.
    cloudError: '',

    // Flag de sucesso da requisicao enviada.
    // Deve ser convertida por acao programada
    // dentro metodo invocado pelo evento 'submitted' [@submitted="submittedForm()"]
    // invocado pelo <ajax-form> para caso de sucesso.
    cloudSuccess: false,
  },

  //  ╦  ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦  ╔═╗
  //  ║  ║╠╣ ║╣ ║  ╚╦╝║  ║  ║╣
  //  ╩═╝╩╚  ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
  beforeMount: function() {
    //…
  },
  mounted: async function() {
    //…
  },

  //  ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
  //  ║║║║ ║ ║╣ ╠╦╝╠═╣║   ║ ║║ ║║║║╚═╗
  //  ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
  methods: {
    // Este metodo e um exemplo de metodo invocado pelo evento submitted
    // do componente <ajax-form>
    submittedForm: async function() {
      // Tudo aqui sera processado somente se a requisicao enviada
      // obtiver 200 OK como resposta.
      this.cloudSuccess = true;

    },
    // Cinco formas possiveis de submeter (submit) os dados para o backend:

    // 1) Com AJAX, informando diretamente o nome do dicionario registrado no arquivo cloud.setup.js. Ex.: 'updateBillingCard'
    // <ajax-form action='updateBillingCard'...></ajax-form> - os parametros (name) sao passados pelo Core Sails ao Cloud.js

    // 2) Com AJAX, delegando a requisicao para um Handler que acionara o Cloud.js indiretamente
    // <ajax-form :handle-submitting="handleSubmittingUpdateBillingCard"...> </ajax-form>
    // , passando-se os parametros esperados pelo metodo invocado. Ex.:
    handleSubmittingUpdateBillingCard: async function(argins) {
      var newPaymentSource = argins.newPaymentSource;
      await Cloud.updateBillingCard.with(newPaymentSource);
    },

    // 3) Diretamente com a tag html <form>. Ex. <form action='api/v1/account/update-billing-card'>... </form>,
    // mas neste caso e preciso informar o caminho exato da url requisitada e nao sera possivel capturar os erros,
    // se houver. Esta opcao, embora mais simples, inviabiliza o uso do recurso AJAX.

    // Nos casos acima, sempre sera necessario adicionar ao formulario o CSRF (<input type='hidden' name='_csrf' value='<%=_csrf>').
    // Para desativar esta exigencia, modifique o parametro csrf do arquivo 'config/security.js' para false

    // 4) Sem o uso de Formulários html, capturando os dados com o Vue e enviando com o recurso Cloud. Esta opção
    // simplifica o envio de dados e permite a captura de erros, mas tem a desvantagem de delegar o controle de
    // erros ao Handler, exemplos:
    // -- no arquivo *.ejs:
    <input class=`form-control` v-model=`newPaymentSource`> //observe que nao existe tag <form>, nem atributo `id`, nem `name`
    // -- no arquivo *.page.js (Vue 2):
    data: {  newPaymentSource = `` },
    // ...
    handleSubmittingUpdateBillingCard: function() {
      var newPaymentSource = this.newPaymentSource;
      if(newPaymentSource&&newPaymentSource.lenght > 3)
         Cloud.updateBillingCard
             .with(newPaymentSource)
             .then(result=>alert(`deu certo`))
             .catch(err=>{
                // Vide `tratamento de erros Cloud.js` mais abaixo.
                alert(`deu errado`)
             })
             .finally(alert(`Haja o que houver`))
    },

    // 5) Utilizando AJAX programaticamente, exemplo:
    //    $.post( "api/v1/account/update-billing-card", function( data ) {
    //       $( ".result" ).html( data );
    //    }).done(function() {
    //     alert( "second success" );
    //   })
    //   .fail(function() {
    //     alert( "error" );
    //   })
    //   .always(function() {
    //     alert( "finished" );
    //   });


  }
});



    

Tratando Erros com Cloud.js

//  _____ ____      _  _____  _    _   _ ____   ___
 // |_   _|  _ \    / \|_   _|/ \  | \ | |  _ \ / _ \
  // | | | |_) |  / _ \ | | / _ \ |  \| | | | | | | |
 //  | | |  _ <  / ___ \| |/ ___ \| |\  | |_| | |_| |
  // |_|_|_|_\_\/_/___\_\_/_/ __\_\_| \_|____/ \___/_  _   _ ____      _ ____
  // | ____|  _ \|  _ \ / _ \/ ___|   / ___| |   / _ \| | | |  _ \    | / ___|
  // |  _| | |_) | |_) | | | \___ \  | |   | |  | | | | | | | | | |_  | \___ \
  // | |___|  _ <|  _ <| |_| |___) | | |___| |__| |_| | |_| | |_| | |_| |___) |
  // |_____|_| \_\_| \_\\___/|____/   \____|_____\___/ \___/|____(_)___/|____/

   /*
       Cloud.js vem incorporado ao Sails.js como solucao para automacao de
        requisicoes HTTP. Em resumo, a API consulta o arquivo de definicao
        `cloud.setup.js` para identificar as rotas (paths) associadas a um
        determinado nome de rota Cloud. Uma vez identificado, a API faz a
        requisicao e controla os erros de resposta. No exemplo abaixo foi
        utilizada a abordagem identificada acima como 4) Uso do Vue 2 para
        enviar a requisição HTTP ao servidor.
   */

   // arquivo save-guideline.js

  module.exports = {


  friendlyName: 'Salvar guideline',


  description: 'Uma action 2 para salvar objetos Guideline',


  inputs: {
    id: {type: `string`},
    sequence: {type: `number`, required: true},
    text: { type: `string`, required: true, description: `free text`}
  },


  exits: {
    success: {
      description: `Done`
    },

    // Esta funcao action 2 retorna um erro 402 e uma
    // descricao da mensagem de erro ao requisitante http
    sequenceAlreadyInUse: {
      statusCode: 409,
      description: 'O numero de sequencia ja esta em uso!',
    },

  },


  fn: async function (inputs) {

    if(inputs.id){
      const guideline = await Guideline.updateOne(
        {id: inputs.id},
        {
          sequence: inputs.sequence,
          text: inputs.text
        }
      ).intercept('E_UNIQUE', 'sequenceAlreadyInUse')
     // este metodo `intercept`, intercepta os erros de banco de dados
     // e redireciona para a funcao exit `sequenceAlreadyInUse` definida
     // acima.

      return {guideline}
    }else{
      const guideline = await Guideline.create(
        {
          sequence: inputs.sequence,
          text: inputs.text
        }
      ).intercept('E_UNIQUE', 'sequenceAlreadyInUse')
     // este metodo `intercept`, intercepta os erros de banco de dados
     // e redireciona para a funcao exit `sequenceAlreadyInUse` definida
     // acima.

      return guideline
    }
  }
}

        /*

        Este é o JSON de resposta enviado pelo backend sails (controllers/save-guideline.js)
        quando o erro for interceptado. Observe atentamente o atributo "x-exit-description"
        do dicionario! ==> "O numero de sequencia ja esta em uso!",

        */

 {
  "name": "CloudError",
  "responseInfo": {
    "body": "Conflict",
    "statusCode": 409,
    "headers": {
      "cache-control": "no-cache, no-store",
      "connection": "keep-alive",
      "content-length": "8",
      "content-type": "text/plain; charset=utf-8",
      "date": "Sat, 16 Dec 2023 154843 GMT",
      "etag": "W/\"8-OfewgPiFJ3o3XA5wgKRYk2ZHNlU\"",
      "keep-alive": "timeout=5",
      "x-exit": "sequenceAlreadyInUse",
      "x-exit-description": "O numero de sequencia ja esta em uso!",
      "x-powered-by": "Sails <sailsjs.com>"
    },
    "data": "Conflict",
    "exit": "sequenceAlreadyInUse",
    "code": "sequenceAlreadyInUse"
  },
  "exit": "sequenceAlreadyInUse",
  "code": "sequenceAlreadyInUse"
}

        /*

        Este erro sera recebido pelo componente Vue 2 que fez a requisicao e
        sera tratado para resposta na View do Operador.

        Observe que aqui capturamos o atributo o atributo "x-exit-description",
        mas poderiamos ter feito de uso de quaiquer outros disponiveis no JSON
        de resposta.

        */

  parasails.registerPage('edit-guideline', {
  //  ╦╔╗╔╦╔╦╗╦╔═╗╦    ╔═╗╔╦╗╔═╗╔╦╗╔═╗
  //  ║║║║║ ║ ║╠═╣║    ╚═╗ ║ ╠═╣ ║ ║╣
  //  ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝  ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
  data: {
    message: {severity: ``, summary: ``, details: ``},
  },

  //  ╦  ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦  ╔═╗
  //  ║  ║╠╣ ║╣ ║  ╚╦╝║  ║  ║╣
  //  ╩═╝╩╚  ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
  beforeMount: function () {
    //…
  },
  mounted: async function () {
    //…
  },

  //  ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
  //  ║║║║ ║ ║╣ ╠╦╝╠═╣║   ║ ║║ ║║║║╚═╗
  //  ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
  methods: {
    save: function () {
      Cloud
        .saveGuideline
        .with(this.guideline)
        .then(() => {
          this.message.severity = `success`
          this.message.summary = `Salvo com sucesso`
          this.message.details = ``
        })
        .catch(
          err => {
            console.log(JSON.stringify(err))
            this.message.severity = `error`
            this.message.summary = `Erro ao Salvar`
            this.message.details = err.responseInfo.headers['x-exit-description']

          }
        )
    },

  }
})
    

Notificação Agendada
//  ____   ____ _   _ _____ ____  _   _ _     _____ ____
// / ___| / ___| | | | ____|  _ \| | | | |   | ____|  _ \
// \___ \| |   | |_| |  _| | | | | | | | |   |  _| | | | |
//  ___) | |___|  _  | |___| |_| | |_| | |___| |___| |_| |
// |____/ \____|_| |_|_____|____/ \___/|_____|_____|____/
//
//  _   _  ___ _____ ___ _____ ___ ____    _  _____ ___ ___  _   _
// | \ | |/ _ \_   _|_ _|  ___|_ _/ ___|  / \|_   _|_ _/ _ \| \ | |
// |  \| | | | || |  | || |_   | | |     / _ \ | |  | | | | |  \| |
// | |\  | |_| || |  | ||  _|  | | |___ / ___ \| |  | | |_| | |\  |
// |_| \_|\___/ |_| |___|_|   |___\____/_/   \_\_| |___\___/|_| \_|

module.exports = {


  friendlyName: 'Send Broadcast To Session',


  description: 'Envia uma mensagem para a sessao do usuario, no momento agendado.',

  extendedDescription: `
    Uma simplificacao aplicada da biblioteca
    (Node Schedule)[https://www.npmjs.com/package/node-schedule] em
    conjunto com os recursos de WebSocket nativos do
    (Sails.js WebSocket)[https://sailsjs.com/documentation/reference/web-sockets].
    Para saber mais, visite o respectivo sitio indicado acima`,


  inputs: {
    roomName: { type: `string`, description: `Nome da Sala escolhida para o broadcast`},

    sessionName: {type: `string`, description: `Nome da sessao alvo do broadcast`},

    cron: {
      type: 'string',
      description: `A cronologia no padrao Linux Cron a ser aplicada no broadcast`,
      extendedDescription: `
*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    │
│    │    │    │    │    └ dia da semana (0 - 7) (0 ou 7 para Domingo)
│    │    │    │    └───── numero do mes (1 - 12)
│    │    │    └────────── dia do mes (1 - 31)
│    │    └─────────────── hora (0 - 23)
│    └──────────────────── minuto (0 - 59)
└───────────────────────── segundo (0 - 59, opcional)
      `
    },
    notificationText: {
      type: 'string',
      description: `Texto a ser visualizado na notificacao por push`
    }
  },


  exits: {

    success: {
      description: 'Executado.',
    },

  },


  fn: async function (inputs) {
    const schedule = require('node-schedule');

    const job = schedule.scheduleJob(inputs.cron, () => {
      console.log(inputs.notificationText);
      sails.sockets.broadcast(inputs.roomName, inputs.sessionName, {notificationText: inputs.notificationText});
      // sera recebido pelo cliente WebSocket que esta ouvindo o Socket de nome [roomName] e sessao [sessionName]
      // ex.: io.socket.on(`sessionName`, ()=>{...})
    });
  }


};



    

Controller para Upload e Download de Arquivos

//  _____ ___ _     _____
// |  ___|_ _| |   | ____|
// | |_   | || |   |  _|
// |  _|  | || |___| |___
// |_|___|___|_____|_____| ____   ___  _     _     _____ ____
//  / ___/ _ \| \ | |_   _|  _ \ / _ \| |   | |   | ____|  _ \
// | |  | | | |  \| | | | | |_) | | | | |   | |   |  _| | |_) |
// | |__| |_| | |\  | | | |  _ <| |_| | |___| |___| |___|  _ <
//  \____\___/|_| \_| |_| |_| \_\\___/|_____|_____|_____|_| \_\

/**
 * FileController
 *
 * @description :: logica backend (Server-side) para controle de arquivos
 * @help        :: See http://links.sailsjs.org/docs/controllers
 */

module.exports = {



  /**
   * `FileController.upload()`
   *
   *  Envia arquivos para o servidor armazenar em disco local.
   */
  upload: function (req, res) {

    // e.g.
    // 0 => infinito
    // 240000 => 4 minutos (240,000 milisegundos)
    // etc.
    //
    // O padrao e de 2 minutos.

    res.setTimeout(0);

    req.file('nome-do-arquivo')
    .upload({

      // Devemos definir o tamanho maximo dos arquivos (em bytes)
      maxBytes: 1000000

    }, function whenDone(err, uploadedFiles) {
      if (err) return res.serverError(err);
      else return res.json({
        files: uploadedFiles,
        textParams: req.allParams()
      });
    });
  },

  /**
   * `FileController.s3upload()`
   *
   * Envia arquivos para o servidor armazenar em no AWS S3 (Buckets).
   *
   * NOTA:
   * Se o arquivo a ser enviado e realmente grande, considere aumentar o
   * timeout da conexao TCP no servidor.
   */
  s3upload: function (req, res) {

     // e.g.
    // 0 => infinito
    // 240000 => 4 minutos (240,000 milisegundos)
    // etc.
    //
    // O padrao e de 2 minutos.
    res.setTimeout(0);

    req.file('avatar').upload({
      adapter: require('skipper-s3'),
      bucket: process.env.BUCKET,
      key: process.env.KEY,
      secret: process.env.SECRET
    }, function whenDone(err, uploadedFiles) {
      if (err) return res.serverError(err);
      else return res.json({
        files: uploadedFiles,
        textParams: req.allParams()
      });
    });
  },

  /**
   * `FileController.gridFS()`
   *
   * Envia arquivos para o servidor armazenar no MongoDB GridFS.
   *
   * NOTA:
   * Se o arquivo a ser enviado e realmente grande, considere aumentar o
   * timeout da conexao TCP no servidor.
   */
   gridFS: function (req, res) {

     // e.g.
    // 0 => infinito
    // 240000 => 4 minutos (240,000 milisegundos)
    // etc.
    //
    // O padrao e de 2 minutos.
    res.setTimeout(0);

   req.file('avatar').upload({
       adapter: require('skipper-gridfs'),
       uri: 'mongodb://[username:password@]host1[:port1][/[database[.bucket]]'
    }, function whenDone(err, uploadedFiles) {
      if (err) return res.serverError(err);
      else return res.json({
        files: uploadedFiles,
        textParams: req.allParams()
    });


  },


  /**
   * FileController.download()
   *
   * Recebe arquivos do servidor, obtidos do armazenamento local.
   */
  download: function (req, res) {
    var Path = require('path');
    var fs = require('fs');

    // If a relative path was provided, resolve it relative
    // to the cwd (which is the top-level path of this sails app)
    fs.createReadStream(Path.resolve(req.param('path')))
    .on('error', function (err) {
      return res.serverError(err);
    })
    .pipe(res);
  }
};


    

Mensagem de Erro (Modal)


//  __  __
// |  \/  | ___ _ __  ___  __ _  __ _  ___ _ __ ___
// | |\/| |/ _ \ '_ \/ __|/ _` |/ _` |/ _ \ '_ ` _ \
// | |  | |  __/ | | \__ \ (_| | (_| |  __/ | | | | |
// |_|  |_|\___|_| |_|___/\__,_|\__, |\___|_| |_| |_|
//  __  __           _       _  |___/
// |  \/  | ___   __| | __ _| |
// | |\/| |/ _ \ / _` |/ _` | |
// | |  | | (_) | (_| | (_| | |
// |_|  |_|\___/ \__,_|\__,_|_|


/**
 * <message>
 * -----------------------------------------------------------------------------
 * A modal dialog pop-up.
 *
 * > Be careful adding other Vue.js lifecycle callbacks in this file!  The
 * > finnicky combination of Vue transitions and bootstrap modal animations used
 * > herein work, and are very well-tested in practical applications.  But any
 * > changes to that specific cocktail could be unpredictable, with unsavory
 * > consequences.
 *
 * @type {Component}
 *
 *
 * -----------------------------------------------------------------------------
 */

parasails.registerComponent('message', {
  //  ╔═╗╦═╗╔═╗╔═╗╔═╗
  //  ╠═╝╠╦╝║ ║╠═╝╚═╗
  //  ╩  ╩╚═╚═╝╩  ╚═╝
  props: [
    'severity',
    'summary',
    'details'
  ],

  //  ╦╔╗╔╦╔╦╗╦╔═╗╦    ╔═╗╔╦╗╔═╗╔╦╗╔═╗
  //  ║║║║║ ║ ║╠═╣║    ╚═╗ ║ ╠═╣ ║ ║╣
  //  ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝  ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
  data: function () {
    return {}
  },

  //  ╦ ╦╔╦╗╔╦╗╦
  //  ╠═╣ ║ ║║║║
  //  ╩ ╩ ╩ ╩ ╩╩═╝
  template: `

<!-- use:

<message id="msg" v-on:close="cleanMessage()" :severity="message.severity" :summary="message.summary" :details="message.details"></message>

it shows message according given severity (error|warn|success|info) if summary lenght is greater than 0. These details are optional

to clean message just clean message.summary value under cleanMessage() method.

-->

<div>

<div v-if="show()">

  <div class="modal fade show" tabindex="-1" style="display: block; overflow: visible; box-shadow: #5a5a5a">

  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">
           <div v-if="severity=='success'">
              <span class="fw-bolder text-success"><em class="fa fa-check"></em> {{summary}}</span>
           </div>
           <div v-if="severity=='info'">
              <span class="fw-bolder text-info"><em class="fa fa-check"></em> {{summary}}</span>
           </div>
           <div v-if="severity=='warn'">
                <span class="fw-bolder text-warning"><em class="fa fa-warning"></em> {{summary}}</span>
           </div>
           <div v-if="severity=='error'">
                <span class="fw-bolder text-danger"><em class="fa fa-bug"></em> {{summary}}</span>
           </div>
        </h5>
        <button type="button" class="btn-close" @click="$emit('close', '')" aria-label="Close"></button>
      </div>
      <div class="modal-body">
           <p class="text-muted">{{details}}</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" @click="$emit('close', '')"><em class="fa fa-close"></em></button>
      </div>
    </div>
  </div>
</div>

</div>

</div>
  `,

  //  ╦  ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦  ╔═╗
  //  ║  ║╠╣ ║╣ ║  ╚╦╝║  ║  ║╣
  //  ╩═╝╩╚  ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
  beforeMount: function () {

  },
  mounted: function () {

  },
  // ^Note that there is no `beforeDestroy()` lifecycle callback in this
  // component. This is on purpose, since the timing vs. `leave()` gets tricky.

  //  ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
  //  ║║║║ ║ ║╣ ╠╦╝╠═╣║   ║ ║║ ║║║║╚═╗
  //  ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
  methods: {

    show: function () {
      return this.summary !== ''
    }

  }
})


    

versioning
Controle de Versões
// __     _______ ____  ____ ___ ___  _   _ ___ _   _  ____
// \ \   / / ____|  _ \/ ___|_ _/ _ \| \ | |_ _| \ | |/ ___|
//  \ \ / /|  _| | |_) \___ \| | | | |  \| || ||  \| | |  _
//   \ V / | |___|  _ < ___) | | |_| | |\  || || |\  | |_| |
//    \_/  |_____|_| \_\____/___\___/|_| \_|___|_| \_|\____|


module.exports = {


  friendlyName: 'Versioning',


  description: 'Automatiza o controle de versoes.',

  extendedDescription: `
    Este script utiliza o arquivo ./VERSIONING.md para registro do historico de versoes
    de seu Prototipo Sails.js.

    Para que o Sails seja capaz de identificar corretamente a raiz do Prototipo, se faz
    necessario executa-lo durante a inicializacao do App (sails lift ou npm start). Por
    esse motivo requer que seja adicionado ao arquivo 'config/boostrap.js' uma linha de
    chamada da funcao 'fn' como se segue:

    require('/scripts/versioning.js').fn() // adicionar no inicio do corpo do modulo

    O Script compara a versao do Prototipo registrada no arquivo 'package.json' com   a
    lista de versoes registradas no VERSIONING.md, se nao encontrar,obtem do dicionario
    os atributos version e description para criar um registro do tipo:

    version: Mon Nov 13 2023 10:38:37 GMT-0300 (Horário Padrão de Brasília)- description

    Portanto, para manter seu historico de versoes atualizado, basta alterar os  valores
    dos atributos "version" e "description" a cada nova "release" lancada.

  `,


  fn: async function () {

    sails.log('Executando shell script customizado... (`sails run versioning`)');

    const path = require('path');
    const fs = require('fs');
    const infoPath = path.resolve(sails.config.appPath, 'package.json');
    const info = JSON.parse(fs.readFileSync(infoPath).toString());
    const fileName = path.resolve(sails.config.appPath, 'VERSIONING.md');

    fs.open(fileName, (err) => {
      if (err) {
        fs.writeFileSync(fileName, `v.0.0.0: ${new Date} - Start` + '\n');
      } else {
        const file = fs.readFileSync(fileName).toString();
        if (file.indexOf(`v.${info.version}`) !== -1) {
          console.log('Skipped');
          return;
        }

        fs.appendFile(fileName, `v.${info.version}: ${new Date()} - ${info.description}` + '\n', (err) => {
          if (err) {
            console.error(err);
          } else {
            console.log('Recorded');
          }
        });
      }
    });

    sails.log('Finished custom shell script... (`sails run versioning`)');

  }


};



    

# Para instalar a imagem mais atualizada...
# FROM registry.ccarj.intraer/mirror/library/node:lts-alpine AS build
#
# Para instalar a imagem compativel com a versao de 'geracao' do app
# FROM node:16.15.1 AS build

FROM node:16.15.1 AS build

# Definicao do diretorio de trabalho
WORKDIR /usr/local/app

# Transferencia do codigo fonte para a imagem
COPY ./ /usr/local/app/

# Instalacao de todas as dependencias
RUN npm install

# Atribuicao de permissoes de execucao
USER node
COPY . .
COPY --chown=node:node . .

# Exposicao da porta 80
EXPOSE 80:80

# Execucao em ambiente de producao
ENV NODE_ENV=production
CMD [ "node", "./app.js" ]
      
Para saber mais:
Início