Table of Contents
- debugging
- console
- reading closest .env
- accessing .env parameters
- determining "dev" || "prod"
- express response extensions:
- access to request & response in react component (SSR):
- Get client ip address
- Temporary disabling prerender
- RESTful api (transport)
- Controllers
- JWT security
- Ping endpoint is
- Helpers tempates
- Building SSR component (static fetchData)
- Extra tools
(TOC generated using markdown-toc)
debugging
There are few buildin tools to explore different data structure, all of them are available thanks to library inspc: .
All of tools from this library are load by default. There is no need to import inspc. Just start using straight away methods like:
log(...)
log.t(...)
log.i(...)
log.dump(obj, [length = 2])
logw()
console
/bin/bash console.sh # will print all registered console scripts
Each command should have it's own help page under --help parameter
reading closest .env
It is usually for standalone scripts. In order get access to environment variables in roderic see chapter: accessing .env parameters
require('dotenv-up'){
override : false,
deep : 3,
}, false, 'preprocessor.js');
.. this expression will import all variables from .env (try to find up to 3 directories up) and add/override to process.env
See more: dotenv-up library...
accessing .env parameters
Just use process.env but it needs to be properly prepared to work correctly,
For the frontend use just execute below code right on top of every entry file (eg: front.entry.js)
import configPublic from 'public.config';
It will make sure to create individual environment variables from .env file if it doesn't exist natively in process.env. It will also export parameters to window.process.env and also it will export function to access variables to window.env
There is another special tool available to access environment variables that also provide fallback feature for non existing one:
# example .env file content:
# EXISTING=exvalue
import configPublic, { env } from 'public.config';
log.dump({
existing: env('EXISTING'), // exvalue
nonexisting: env('SOMETHING ELSE', 'fallback') // fallback
})
this tool can be used in the browser and also on the server side
WARNING: Keep in mind though that any environment variable that starts from PROTECTED_ will not be available in the browser |
---|
Environment variales should be always available also under process.env (both environments: on the server and in the browser). It's a good thing though to interact with process.env oboject in try catch blocks. Don't supress error though and at least print somenthing on the screen if something goes wrong like:
try {
if (typeof process.env.HIDE_TREE !== 'undefined') {
// do something
}
} catch (e) {
log.dump({
HIDE_TREE_cantdetermine: e
})
}
determining "dev" || "prod"
In normal case use process.env.NODE_ENV. There are though sometimes cases where you migth work beyound scope of webpack (for example all of the time while you will work in React scope) but you have to still be aware if webpack was executed in prod or dev then relay on generated preprocessor/dotenv.json and do:
import configPublic from 'public.config';
const isProd = configPublic.isProd();
express response extensions:
res.jsonNoCache({...json});
it makes internally:
return this.set({
'Cache-Control' : 'no-store, no-cache, must-revalidate',
'Pragma' : 'no-cache',
'Content-type' : 'application/json; charset=utf-8',
'Expires' : new Date().toUTCString(),
}).json(json);
res.jsonError(errstring);
it makes internally:
return this.status(409).jsonNoCache({
error: errstring
});
res.notFound(msg);
it makes internally:
return this.status(400).jsonNoCache({
error: msg
});
res.accessDenied(msg);
it makes internally:
return this.status(403).jsonNoCache({
error: msg
});
See also token field in Controllers section.
access to request & response in react component (SSR):
...
render() {
const {
staticContext,
} = this.props;
// wrap it ALWAYS in catch try because it will crash in browser
const userAgent = (function () {try {return staticContext.req.headers['user-agent']}catch(e){}}());
}
...
Get client ip address
To get ip address of client use :
req.clientIp
Temporary disabling prerender
This feature is for testing purposes (mainly):
If you add anywhere in currenlty loaded page url GET parameter called "noprerender" - (with or without value, it doesn't matter, existance of the parameter is tested internally) then prerendering on the particular page will be disabled, which means all states will be loaded as a separate fetch request in the browser.
Example:
# original url - prerender enabled:
http://localhost:1025/article/slug-to-the-article
# in order to diable prerender modify link like follow
http://localhost:1025/article/slug-to-the-article?noprerender
# or any combination of
http://localhost:1025/article/slug-to-the-article?noprerender=true
http://localhost:1025/article/slug-to-the-article?noprerender=false
http://localhost:1025/article/slug-to-the-article?noprerender=1
http://localhost:1025/article/slug-to-the-article?noprerender=0
http://localhost:1025/article/slug-to-the-article?otherparam=value&noprerender=1
You can create browser bookmark to faster switch between modes:
javascript:var k=(function(p,h,k){p.has(k)?p.delete(k):p.set(k,'');p=String(p);p?(p='?'+p):(p='');location.href=(location.href.split('#')[0]).split('?')[0]+p+h;}(new URLSearchParams(location.search),location.hash,'noprerender'))
Raw code:
var k = (function (p, h, k) {
p.has(k) ? p.delete(k) : p.set(k, '');
p = String(p);
p ? (p = '?' + p) : (p = '');
location.href = (location.href.split('#')[0]).split('?')[0] + p + h;
}(new URLSearchParams(location.search), location.hash, 'noprerender'))
RESTful api (transport)
import {
fetchJson,
fetchData,
} from 'transport';
const json = {data: 'value'}
const promise = fetchJson('/api/admin/buckets/set', {
method: 'post',
body: json,
always: () => dispatch(loaderOff())
});
Method will automaticly send auth cookie.
Most importantly: Method can be used from node.js and from browser just as it is shown on the example above.
Controllers
Controllers are build on top of Express Router Middleware.
Request object in controller have special field auth which is an object that represent payload of JWT security token. See route /config in users.js controller for example to check how to use it (look for "req.auth.id").
JWT security
access to JWT payload in the controller
req.auth
// - will be undefined if token not valid or not logged in
// - will be an object if token valid an has payload
// so just do:
if (req.auth) {
// user logged in
}
// or
if (req.auth && req.auth.role.indexOf('admin') > -1) {
// user has 'admin' role
// WARNING: always use two conditions in if statement like above because you can't
// execute req.auth.role.indexOf(...) if req.auth = undefined
}
Router
There is special (CLI) command to list all registered controllers with their corresponding urls:
/bin/bash console.sh routes
/bin/bash console.sh routes --json
Ping endpoint is
/__ping
will return 200 status code if it will be able to find more then 0 users in the users table, otherwise 409 status code.
Helpers tempates
datepicker
Unfortunately library "react-datepicker" is not prepared to work correctly in SSR mode so the "solution" is like follows: (This trick make useful few more libraries despite SSR being involved so it might be considered more general solution for component that don't render anything important from SEO perspective)
import DatePicker from 'react-datepicker';
import node from 'detect-node';
{!node && <DatePicker
locale="en"
showTimeSelect
timeIntervals={15}
timeFormat="HH:mm"
dateFormat="YYYY-MM-dd HH:mm"
onChange={date => editField('published_at', date)}
selected={form.published_at ? new Date(form.published_at) : null}
isClearable={true}
placeholderText="Not specified"
/>}
Semantic UI helpers
See more in the reference of the Button element
Button as a Router Link
To provide "Open Link in New Tab" feature under right click menu on button.
import {
NavLink,
Link
} from 'react-router-dom';
import {
Icon,
Button,
} from 'semantic-ui-react';
<Button
// onClick={loginSignOut}
icon size="mini"
as={Link}
to="/signout"
>
<Icon inverted color='black' name='log out' />
</Button>
Button as a regular link
import {
Icon,
Button,
} from 'semantic-ui-react';
<Button
// onClick={loginSignOut}
icon size="mini"
as="a"
href="/signout"
>
<Icon inverted color='black' name='log out' />
</Button>
Building SSR component (static fetchData)
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { connect } from 'react-redux';
import {
getLanguages,
getLangUrl,
getMenu,
getMainSection,
} from "../../_redux/reducers";
import * as actions from '../../_redux/actions';
import {
section,
} from '../parts/Footer';
const buildLoad = ({
store,
getRouterParams,
getRouterMatch,
getGetParameters,
}) => {
const {
lang,
} = getRouterParams();
return Promise.all([
store.dispatch(actions.languagesRequestFront(lang)),
store.dispatch(actions.menuRequest(lang)).then(menu => {
if (getMainSection(store.getState(), section)) {
log('footer already loaded');
return;
}
let menuData = [];
try {
menuData = menu.find(m => m.title === 'Footer block').children || [];
}
catch (e) {
log('Footer block e', e)
}
if (menuData.length) {
const slug = menuData[0].slug;
return store.dispatch(actions.mainFetch({
section,
json: {
lang,
slug,
// ...json,
// ...query,
// ...getParameters
},
url : `/endpoint/page`,
extract : json => json.page,
}))
}
})
]);
};
class Header extends Component {
constructor (...props) {
super(...props);
this.state = {
show: false,
}
}
//static fetchData = ({
// store,
// getRouterParams,
// getRouterMatch,
// getGetParameters,
//}) => {
// return buildLoad({
// store,
// getRouterParams,
// getRouterMatch,
// getGetParameters,
// });
//}
static fetchData = (...args) => buildLoad(...args);
getData = () => this.props.load();
componentDidMount() {
const { history: { action } } = this.props;
( ( this.props.languages === false ) || action === 'PUSH' ) && this.getData();
}
render() {
const {
languages = [],
selectedLanguage,
menu,
} = this.props;
const {
show,
} = this.state;
if (languages === false && ! selectedLanguage ) {
return null;
}
const mainMenu = menu ? ((menu.find(m => m.title === "Main menu") || {}).children) : []
return (
<div>
<div className="menu">
<div className="burger" onClick={() => {
this.setState({show: !show})
}}>
<div />
<div />
<div />
</div>
{/* logo */}
<div className="phone">
phone: 345 678 987
</div>
</div>
{/* <select
style={{float: 'right'}}
onChange={e => this.handleChange(e.target.value)}
value={selectedLanguage.shortname}
>
{languages.map(lang => (
<option
key={lang.id}
value={lang.shortname}
// selected={lang.id === selectedLanguage.id}
>{lang.name}</option>
))}
</select> */}
{(mainMenu && mainMenu.length) && <Level level={mainMenu} className={show ? 'show' : ''}/>}
</div>
);
}
}
const mapStateToProps = (state, params) => {
let lang;
try {
lang = params.match.params.lang;
}
catch (e) {
}
return ({
menu : getMenu(state),
languages : getLanguages(state),
selectedLanguage : getLangUrl(state, lang),
});
};
const mapDispatchToProps = (dispatch, props) => {
const {
history,
} = props;
return {
load(extra = {}) {
return buildLoad(dispatch.extract({
component: Header, // just for error logs
dispatch,
props,
// extra: {...pass something extra to buildLoad here if you need},
}));
},
onCreate() {
history.push('/admin/tags/create')
},
onEdit(id) {
history.push('/admin/tags/edit/' + id)
},
deleteItem(id) {
dispatch(actions.tagsDelete(id))
}
};
};
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(Header));
Extra tools
I've separated some tools because they are doing some more universal things that can be used beyound Roderic project.