Coder Social home page Coder Social logo

usehenri / henri Goto Github PK

View Code? Open in Web Editor NEW
51.0 8.0 6.0 10.72 MB

The versatile Javascript framework

Home Page: https://usehenri.io

License: MIT License

JavaScript 97.96% CSS 0.05% HTML 1.65% Vue 0.08% Dockerfile 0.27%
nodejs framework react react-server-render nextjs henri mongoose disk vue graphql server-side-rendering hacktoberfest

henri's Introduction

henri - the versatile javascript framework

npm version npm downloads Build Status Coverage Status Join slack

henri is an easy to learn rails-like, server-side rendered (react & vue) with powerful ORMs

How to use

Install

  yarn global add henri

  # or

  npm install -g henri

Create a new project

  henri new <folder name>

The above command will create a directory structure similar to this:

├── app
│   ├── controllers
│   ├── helpers
│   ├── models
│   └── views
│       ├── assets
│       ├── components
│       ├── pages
│       ├── public
│       │   ├── css
│       │   ├── fonts
│       │   ├── img
│       │   ├── js
│       │   └── patterns
│       └── styles
├── config
│   ├── default.json
│   ├── production.json
│   ├── routes.js
│   └── webpack.js            <- Overload Next.js webpack settings
├── test
│   ├── controllers
│   ├── helpers
│   ├── models
│   └── views
├── package.json

If you have a Ruby on Rails background, this might look familiar.

One last step to start coding is:

  cd <folder name>
  henri server

And you're good to go!

Configuration

The configuration is a json file located in the config directory.

henri will try to load the file matching your NODE_ENV and will fallback to default.

You can have a default.json, production.json, etc.

{
  "stores": {
    "default": {
      "adapter": "mongoose",
      "url": "mongodb://user:[email protected]:10914/henri-test"
    },
    "dev": {
      "adapter": "disk"
    }
  },
  "secret": "25bb9ed0b0c44cc3549f1a09fc082a1aa3ec91fbd4ce9a090b",
  "renderer": "react"
}

Models

You can easily add models under app/models.

They will be autoloaded and available throughout your application (exposed globally).

We use Mongoose for MongoDB, Sequelize for SQL adapters and Waterline for the disk adapter.

You can use the command-line to generate models:

# henri g model modelname name:string! age:number notes:string birthday:date!
// app/models/User.js

// Whenever you have a User model, it will be overloaded with the following:

// email: string
// password: string
// beforeCreate: encrypts the password
// beforeUpdate: encrypts the password

module.exports = {
  store: 'dev', // see the demo configuration up there
  name: 'user_collection', // will use user_collection' instead of 'users'
  schema: {
    firstName: { type: 'string' },
    lastName: String,
    tasks: {},
  },
};
// app/models/Tasks.js

module.exports = {
  store: 'default', // see the demo configuration up there
  schema: {
    name: { type: 'string', required: true },
    category: {
      type: 'string',
      validations: {
        isIn: ['urgent', 'high', 'medium', 'low'],
      },
      defaultsTo: 'low',
    },
  },
};

Disk

The disk adapter is using Waterline to provide disk-based storage.

This is not for production and you can easily port your models to other adapters.

  yarn add @usehenri/disk

  # or

  npm install @usehenri/disk --save

MongoDB

The MongoDB adapter is using Mongoose to provide a MongoDB ODM.

  yarn add @usehenri/mongoose

  # or

  npm install @usehenri/mongoose --save

MySQL

The MySQL adapter is using Sequelize to provide a MySQL ORM.

  yarn add @usehenri/mysql

  # or

  npm install @usehenri/mysql --save

MSSQL

The MSSQL adapter is using Sequelize to provide a MSSQL ORM.

  yarn add @usehenri/mssql

  # or

  npm install @usehenri/mssql --save

PostgreSQL

The PostgresQL adapter is also using Sequelize to provide a PostgresQL ORM.

  yarn add @usehenri/postgresql

  # or

  npm install @usehenri/postgresql --save

GraphQL

You can add a graphql key to your schema file and they will be automatically loaded, merged and available.

Definition

// app/models/Task.js

const types = require('@usehenri/mongoose/types');

module.exports = {
  schema: {
    description: { type: types.STRING, required: true },
    type: { type: types.ObjectId, ref: 'Type', required: true },
    location: { type: types.ObjectId, ref: 'Location', required: true },
    reference: { type: types.STRING, required: true },
    notes: { type: types.STRING },
    oos: { type: types.BOOLEAN, default: false },
  },
  options: {
    timestamps: true,
  },
  graphql: {
    types: `
      type Task {
        _id: ID!
        reference: String!
        description: String!
        location: Location
        type: Type
        notes: String!
        oos: Boolean
      }
      type Query {
        tasks: [Task]
        task(_id: ID!): Task
      }
    `,
    resolvers: {
      Query: {
        tasks: async () => {
          return Task.find()
            .populate('type location')
            .exec();
        },
        task: async (_, id) => await Task.findOne(id).populate('type'),
      },
    },
  },
};

Query

You will be able to query this anywhere. Even as an argument to res.render(). See below:

// app/controllers/tasks.js

// henri has a gql function which does nothing but help editors parse gql...!
const { gql } = henri;

module.exports = {
  index: async (req, res) => {
    return res.render('/tasks', {
      graphql: gql`
        {
          tasks {
            _id
            reference
            description
            type {
              _id
              name
              prefix
              counter
            }
            location {
              _id
              name
            }
          }
          locations {
            _id
            name
          }
        }
      `,
    });
  },
};

Views

You can use React, Vue and Handlebars as renderer. They are all server-side rendered and the first two options use webpack to push updates to the browser.

React

We use next.js to render pages and inject data from controllers. You can only add pages and if the defined routes don't match, and next matches a route, it will be rendered.

Usage (config file):

{
  "renderer": "react"
}

Example:

// app/views/pages/log.js

import React from 'react';
import Link from 'next/link';

export default data => (
  <div>
    <div>{data}</div>
    <Link href="/home">
      <a>Home</a>
    </Link>
  </div>
);

You can also add webpack configuration in config/webpack.js:

// If you want to have jQuery as a global...

module.exports = {
  webpack: async (config, { dev }, webpack) => {
    config.plugins.push(
      new webpack.ProvidePlugin({
        $: 'jquery',
        jQuery: 'jquery',
      })
    );
    return config;
  },
};

Inferno

You can use Inferno instead of React in production. In development, React will be used for hot re/loading.

Installation:

yarn add react react-dom inferno inferno-compat inferno-server

Usage (config file):

{
  "renderer": "inferno"
}

Preact

You can use Preact instead of React in production. In development, React will be used for hot re/loading.

Installation:

yarn add react react-dom preact preact-compat

Usage (config file):

{
  "renderer": "preact"
}

Vue.js

We use Nuxt.js to render pages and inject data from controllers. You can only add pages and if the defined routes don't match, and nuxt matches a route, it will be rendered.

Usage (config file):

{
  "renderer": "vue"
}

Example:

<template>
  <div>
    <h1>Welcome!</h1>
    <nuxt-link to="/about">About page</nuxt-link>
  </div>
</template>

Handlebars

The handlebars options renders your .html or .hbs files under app/views/pages.

It will also load partials from app/views/partials

Usage (config file):

{
  "renderer": "template"
}

Example:

<html>

<head>
  <title>Hello!</title>
</head>

<body>
  {{> somePartials }}
  <li>Some data: {{hello}}</li>
</body>

</html>

Fetching data again

You can refetch data from any data-hydrated controller endpoint with GET using the application/json header.

Controllers

You can easily add controllers under app/controllers.

They will be autoloaded and available throughout your application.

Controllers are auto-reloaded on save.

// app/controllers/User.js

module.exports = {
  info: async (req, res) => {
    if (!req.isAuthenticated()) {
      return res.status(403).send("Sorry! You can't see that.");
    }
    const { user } = henri;
    if (await User.count({ email: '[email protected]' })) {
      await User.update({ email: '[email protected]' }, { password: 'blue' });
      return res.send('user exists.');
    }
    try {
      await user.compare('moo', pass);
      res.send('logged in!');
    } catch (error) {
      res.send('not good');
    }
  },
  create: (req, res) => {
    await User.create({ email: '[email protected]', password: 'moo' });
  },
  fetch: async (req, res) => {
    const users = await User.find();
    res.send(users);
  },
  postinfo: async (req, res) => {
    let data = req.isAuthenticated() ? await User.find() : {};
    res.render('/log', data);
  }
};

Routes

Routes are defined in config/routes.js. Also, any pages in app/views/pages will be rendered if no routes match before.

Routes are a simple object with a key standing as a route or an action verb (used by express) and a route.

If you want to access the res.render data, you can make the call with application/json header. Everything else will be rendered.

// config/routes.js

module.exports = {
  '/test': 'user#info', // default to 'get /test'

  '/abc/:id': 'moo#iii', // as this controller does not exists, route won't be loaded

  '/user/find': 'user#fetch',

  'get /poo': 'user#postinfo',

  'post /poo': 'user#create',

  'get /secured': {
    controller: 'secureController#index',
    roles: ['admin'],
  },

  'resources todo': {
    controller: 'todo',
  },

  'crud categories': {
    scope: 'api',
    controller: 'categories',
    omit: ['destroy'], // DELETE route will not be loaded
  },
};

Roles

You can specify an array of roles which need to be matched to access the routes.

CRUD

The crud keyword (instead of http verbs) will create routes in a predefined way:

// 'crud happy': 'life'

GET /happy => life#index
POST /happy => life#create
PATCH /happy/:id => life#update
PUT /happy/:id => life#update
DELETE /happy/:id => life#destroy

Resources

The resources keyword (instead of http verbs) add views target to CRUD, ending up with:

// 'resources happy': 'life'

GET /happy => life#index
POST /happy => life#create
PATCH /happy/:id => life#update
PUT /happy/:id => life#update
DELETE /happy/:id => life#destroy

GET /happy/:id/edit => life#edit
GET /happy/new => life#new
GET /happy/:id => life#show

Scope

You can add scope to your routes to prefix them with anything you want.

Omit (crud & resources only)

You can add omit array to your routes to prevent this route to be created.

Mail

We use nodemailer to provide email capabilities.

When running tests, we use nodemailer's ethereal fake-mail service.

Config

{
  "mail": {
    // ...Same as nodemailer's config
  }
}

Send

We provide a wrapper around nodemailer.SendMail:

await henri.mail.send({
  from: '"Henri Server" <[email protected]>', // sender address
  to: '[email protected], [email protected]', // list of receivers
  subject: 'Hello ✔', // Subject line
  text: 'Hello world?', // plain text body
  html: '<b>Hello world?</b>', // html body
});

If you are using the test accounts, you will see a link to your email in the console.

You can access nodemailer's package directly from henri.mail.nodemailer and transporter from henri.mail.transporter.

Workers

You can add files under app/workers and they will be auto-loaded, watched and reloaded.

If they export a start() and a stop() method, they will be call when initializing and tearing down (reload also).

Example:

let timer;

const start = h => {
  h.pen.info('worker started');
  timer = setInterval(
    () => h.pen.warn(`the argument is the henri object`),
    5000
  );
};

const stop = () => clearInterval(timer);

module.exports = { start, stop };

Under the hood

Vision

Bundle the best tools in a structured environment to provide a stable and fast-paced development experience.

Modules

We use a 8 levels boot system.

  1. All modules are scanned and put in a sequence with same-level modules

  2. We cycle from level 0 to 7, initializing all the same-level modules in a concurrent way

  3. If the module is reloadable, it will unwind and rewind in the same sequence on reloads

See the Contributing section for more information

Plans

  • Add helpers integration
  • Add documentation!
  • Build a website
  • Report bugs!

Contributing

  • Step by step wiki here

Thanks to the following and their contributors

Author

  • Félix-Antoine Paradis (@reel)

henri's People

Contributors

dependabot[bot] avatar maximepatry avatar reel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

henri's Issues

Explore using class interchangeably with objects in controllers

At the moment, a controller file exports an object.

Check to see if it would be possible to export a class in some file to add extensibility.

This would remove boilerplate code and add some placeholder while in dev.

Provide 2-3 default, extensible classes that would define the normal patterns.

Improve model validation on mongoose

When adding a model (mongoose for starter), we need to validate if the format is ok and not throw...

We should display a message similar to the routes -> controller

next.js babel plugin issue

Seems like the recent babel upgrade in next.js is causing problems if you don't have the matching babel versions on our side

Server is not reloading on changes, only next.js

When changing the application codebase outside app/views, the server is not refreshed.

There is two options:

  1. Wrapping henri server with nodemon
  2. Wrapping henri server with PM2 (which might also be good for production release)
  3. Explore cache invalidation

Another good thing would be to default to one of these options and support the others with a switch.

The PM2 options with cache invalidation as default would be nice.

Add the ability to turn on/off some features

The main idea is to be able to turn on/off some features with .env or via package.json:

  ...
  "henri" {
    "frontend": false,
    "backend": true,
    "socketio": false,
    "rest": true
  }

Similar options in ,env file or via environment variables:

HENRI_OPTS="frontend=false;backend:true;socketio:false;rest:true"

Defaults to true if missing.

So, if you want to deploy multiple backends without nextjs overhead (?!?) or vice-versa, this can be done easily.

backend: false would disable mostly everything but Next.js

Is this necessary? Are there use cases? cookies would not be parsed and SSR partially broken without data injection...

Unhandled promise

Upon restarting, we get the following error with Next.js:

(node:13782) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: "listener" argument must be a function
(node:13782) DeprecationWarning: Unhandled promise rejections are deprecated. In thefuture, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Add a destroy cli command

Add a destroy cli command for scaffolds that need to be removed.

The following options will be available:

  • model
  • controller
  • crud
  • scaffold

Ex: henri destroy controller <name>

Ask for user permission with inquire before destroying and make .backup if there is no .git folder

Installation

I tried to install the frameworks on Windows.
So I followed the guide:
npm i -g henri
npm i @usehenri/disk

Then I had this issue:

C:\Users\Nathan\Desktop\projects\rocket-elevators>henri server

       henri ✏  0.34.4-alpha.1 => dev +10ms                                                                     15:29:56

        boot ✏  running from cli                                                                                15:29:56
     modules ✏  config => loaded => 1/10 +8ms                                                                   15:29:56
        mail ✏  no mail configuration found                                                                     15:29:56
     graphql ✏  graphql playground started                                                                      15:29:56
     modules ✏  mail => loaded => 2/10 +6ms                                                                     15:29:56
     modules ✏  graphql => loaded => 3/10                                                                       15:29:56
     modules ✏  controllers => loaded => 4/10 +9ms                                                              15:29:56
     modules ✏  server => loaded => 5/10                                                                        15:29:56
     modules ✏  runlevel 3 => Cannot find module 'C:\Users\Nathan\Desktop\projects\rocket-elevators\node_modules\@usehenri\react\engine\index' +645ms          15:29:57
     modules ✏  init => unable to init correctly                                                                15:29:57

     promise ✏  Error: henri - unable to execute init()                                                         15:29:57

     promise ✏      at Promise (C:\Users\Nathan\AppData\Roaming\npm\node_modules\henri\node_modules\@usehenri\core\src\henri.js:79:15) +6ms          15:29:57
     promise ✏      at <anonymous>                                                                              15:29:57
     promise ✏      at process._tickCallback (internal/process/next_tick.js:188:7)                              15:29:57
     promise ✏      at startup (bootstrap_node.js:191:16)                                                       15:29:57
     promise ✏      at bootstrap_node.js:612:3 +2ms

After doing npm i @usehenri/react asked by Reel I get a new error:

C:\Users\Nathan\Desktop\projects\rocket-elevators>henri server

       henri ✏  0.34.4-alpha.1 => dev +33ms                                                                     16:06:22

        boot ✏  running from cli                                                                                16:06:22
     modules ✏  config => loaded => 1/10 +13ms                                                                  16:06:22
        mail ✏  no mail configuration found                                                                     16:06:22
     graphql ✏  graphql playground started                                                                      16:06:22
     modules ✏  mail => loaded => 2/10                                                                          16:06:22
     modules ✏  graphql => loaded => 3/10 +6ms                                                                  16:06:22
     modules ✏  controllers => loaded => 4/10 +12ms                                                             16:06:22
     modules ✏  server => loaded => 5/10                                                                        16:06:22
     modules ✏  runlevel 3 => Cannot find module '@zeit/next-sass' +3155ms                                      16:06:25
     modules ✏  init => unable to init correctly                                                                16:06:25

     promise ✏  Error: henri - unable to execute init() +9ms                                                    16:06:25

     promise ✏      at Promise (C:\Users\Nathan\AppData\Roaming\npm\node_modules\henri\node_modules\@usehenri\core\src\henri.js:79:15)           16:06:25
     promise ✏      at <anonymous>                                                                              16:06:25
     promise ✏      at process._tickCallback (internal/process/next_tick.js:188:7) +6ms                         16:06:25
     promise ✏      at startup (bootstrap_node.js:191:16)                                                       16:06:25
     promise ✏      at bootstrap_node.js:612:3

Message if node version's not good

When the node version is lower than required Seems like henri is unable to load. Please, reinstall... instead of something like please update your node version to ...

Add a defaultPage & securePage wrapper

Add a component that would register REST and SocketIO, handle injected authentication token server-side and wrap the feathers app methods to enhance them. Similar to src/client/components/application.js in the example app.

import feathers from 'feathers/client';
import rest from 'feathers-rest/client';
import hooks from 'feathers-hooks';
import authentication from 'feathers-authentication-client';

import 'isomorphic-fetch';

const app = feathers()
// eslint-disable-next-line no-undef
  .configure(rest('http://localhost:3030').fetch(fetch))
  .configure(hooks());

if (process.browser) {
  app.configure(authentication({
    storage: window.localStorage
  }));
}

export default (Page) => <Page {...this.props} app={app} />;

Or a full class. I will add some more info later on.

Expand addon system to every modules

Add the ability to add normally filesystem loaded functions/methods/object dynamically in henri constructor so it can be loaded by the other modules.

ex: If I create a module that needs a certain model, I will be able to add it to henri.addons.models[] so it can be loaded by the models module.

Warnings on scaffold

When running henri g scaffold <name> <entity:type>, we get the following warnings and errors:

No parser and no filepath given, using 'babel' the parser now but this will throw an error in the future. Please specify a parser or a filepath so one can be inferred.
Error: Cannot find module '.../.../app/routes.js'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.