1 | <img src="https://raw.githubusercontent.com/gladjs/glad/master/.assets/glad-banner.png" height="160px" width="712px" align="center">
|
2 |
|
3 | [![Code Climate](https://codeclimate.com/github/gladjs/glad/badges/gpa.svg)](https://codeclimate.com/github/gladjs/glad)
|
4 | [![Build Status](https://travis-ci.org/gladjs/glad.svg?branch=master)](https://travis-ci.org/gladjs/glad)
|
5 | [![Coverage Status](https://coveralls.io/repos/github/gladjs/glad/badge.svg?branch=master)](https://coveralls.io/github/gladjs/glad?branch=master)
|
6 |
|
7 | [![NPM](https://nodei.co/npm/glad.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/glad/)
|
8 |
|
9 |
|
10 | <div style="clear:both;"></div>
|
11 |
|
12 | <h2>Required</h2>
|
13 |
|
14 | * [Node >= 6.5](http://nodejs.org/) *(with NPM)*
|
15 | * [Redis >= 3.0.2](http://redis.io/)
|
16 |
|
17 | *Requires Redis 3.0.2 or greater because caching uses the [XX option of ZADD](https://redis.io/commands/zadd#zadd-options-redis-302-or-greater).*
|
18 | <br>
|
19 | *Requires Node 6.5 or greater because many features of ES6 are used.*
|
20 | <br>
|
21 | *You can still use Glad versions less than 1.0.0 with older versions of Node/IOJS*
|
22 |
|
23 | <br>
|
24 |
|
25 | ## What is Glad?
|
26 | Glad is a Node JS (MC / MVC) framework built on top of Express that aims to make common things that developers need available in an easy and intuitive manner.
|
27 |
|
28 | #### Main features.
|
29 | - REST-MC or MVC Workflow
|
30 | - Endpoint access policies
|
31 | - Class based controllers
|
32 | - Intuitive routing with route based body parsing and policies
|
33 | - Socket.io integration & Websocket routing with route based policies available
|
34 | - Built in session management
|
35 | - LRU/LFU controller action caching
|
36 | - General caching
|
37 | - Sane project directory structure
|
38 | - Works with any database
|
39 | - High test coverage
|
40 | - Leverages the Express ecosystem so you can too.
|
41 | - REST endpoint generation with Glad CLI
|
42 | - Utilities such as tokenization, number conversions, object tools, array tools, date tools and other various utilities.
|
43 | - Console Access to the server process with Glad CLI
|
44 |
|
45 |
|
46 | Out of the box, you get a Model/Controller framework which is great for building REST APIs for either client rendered apps
|
47 | such as Ember, Angular, React/Redux etc... or API Service development.
|
48 | Furthermore, Glad supports MVC and comes bundled with Pug (formerly Jade). Under the hood, view rendering is handled by Express. This gives you the utility of the Express eco-system right at your fingertips.
|
49 |
|
50 | If you are familiar with Node JS, then getting started with Glad is absolutely painless. If you are new to Node JS, then you should move along quite nicely and Glad will kick start your project in superhero fashion. In fact, you can get a REST API up and running in a matter of minutes after initializing your project.
|
51 |
|
52 | <br>
|
53 |
|
54 | ## Installation
|
55 |
|
56 | The recommended way to install Glad is using Glad CLI. However, it is not necessary, but again highly reccomended. Previous versions of glad included the CLI. In versions >= 1.0.0, the CLI has been abstracted out to a separate package.
|
57 |
|
58 | #### Installing Glad CLI
|
59 |
|
60 | `npm install -g glad-cli`
|
61 |
|
62 | #### Installing Glad without the CLI
|
63 | `npm install glad --save`
|
64 |
|
65 | <br/>
|
66 |
|
67 | #### Creating a new project using Glad CLI
|
68 |
|
69 | Glad CLI has many options for initializing a project. The CLI supports several different databases including MongoDB, Postgresql, MySQL. In addition, Glad CLI supports either Mongoose, Waterline, or usage without an ORM/ODM. You can read the documentation for Glad-CLI [here.](https://github.com/gladjs/glad-cli/blob/master/README.md)
|
70 |
|
71 | **Example using MongoDB & Mongoose**
|
72 | - Create a new folder for your project
|
73 | - `cd /path/to/new-folder`
|
74 | - `glad init --odm=mongoose`
|
75 |
|
76 | **Example using Postgresql & Waterline**
|
77 | - Create a new folder for your project
|
78 | - `cd /path/to/new-folder`
|
79 | - `glad init --odm=waterline --adapter=sails-postgresql`
|
80 |
|
81 | #### Creating a project without Glad-CLI
|
82 |
|
83 | - Create a new folder for your project
|
84 | - `cd /path/to/new-folder`
|
85 | - `npm install glad --save`
|
86 |
|
87 | <br/>
|
88 |
|
89 | # Running / Development
|
90 |
|
91 | Let us begin with a brief overview of how Glad.JS works by taking a high level look at the request lifecycle.
|
92 |
|
93 | - A request comes in to `/foo`
|
94 | - The router locates the `/foo` route.
|
95 | - The router checks the route entry to see if there is a specific body parser it should use for this request. If not, it uses the default body parser defined in your `config.js` file. Either way, the body gets parsed.
|
96 | - The router then checks to see if there is a policy specified on the route entry.
|
97 | - If there is a policy, it routes the request to the policy and if it is accepted, the policy hands the request off to the correct controller/action combo.
|
98 | - If there is no policy, The router routes the request off to the correct controller/action combo.
|
99 |
|
100 | There's quite a lot happening behind the scenes to facilitate this flow, but that's basically the gist of it. You have routes, models, controllers, and (if you'd like) views. Routes point to controllers, controllers pull in models and complete the request with various types of data/views.
|
101 |
|
102 | Now lets take a look at getting set up. After installation, it's a good idea to take a look at the config file and make any neccesary adjustments. The config file contains all of the settings you'll need to change when either developing or deploying your application. It's safe to assume that you might want to read environment variables or dynamically change different configuration options based on any number of factors. You'll most likely want to do that here.
|
103 |
|
104 |
|
105 | ### An overview of the config file
|
106 | ```js
|
107 | {
|
108 |
|
109 | port : 4242, // <-- The port that the http server will listen to
|
110 |
|
111 | host : '0.0.0.0', // <-- The host that the http server will listen on, (Can be /tmp/sock)
|
112 |
|
113 | logHTTP: true, // <-- Log HTTP request info to stdout
|
114 |
|
115 | defaultBodyParser : { // <-- The default body parser
|
116 | limit : '0.3mb',
|
117 | type : 'json'
|
118 | },
|
119 |
|
120 | exposeModelsGlobally : true, // <-- Makes your models available in the global namespace
|
121 |
|
122 | redis : {
|
123 | host: "localhost", // <-- The Redis Host
|
124 | port: 6379 // <-- The Redis Port
|
125 | },
|
126 |
|
127 | cookie : {
|
128 | name : 'glad.sid', // <-- The name of your cookie
|
129 | secret: 'change-this', // <-- The cookie secret
|
130 | maxAge : 3600000 * 24, // <-- Cookie expiration date
|
131 | },
|
132 |
|
133 | session : {
|
134 | storage: 'redis' // <-- Where sessions are stored. See the section on Sessions for more options
|
135 | },
|
136 |
|
137 | defaultViewEngine : 'pug', // <-- The default view engine to use
|
138 |
|
139 | orm: "mongoose" // <-- This tells Glad CLI to use the mongoose blueprint when generating APIs
|
140 |
|
141 | }
|
142 | ```
|
143 |
|
144 | <br>
|
145 |
|
146 | Next we'll have a look at policies. A policy is an asynchronous function that receives the request & response objects as well as accept and reject methods.
|
147 | If accept is invoked, the request gets routed to the controller action. If reject is invoked, the request is terminated and the **onFailure** method of the policies object is called. The reject method takes an optional argument. This argument can be anything, but it can be useful to extend the onFailure method.
|
148 | Maybe you want to pass functions into the **onFailure** method and have a default responder combined with custom responders. See `fig.p-1` for an example of this.
|
149 |
|
150 | ### Policies
|
151 |
|
152 | ```js
|
153 | module.exports = {
|
154 |
|
155 | /*
|
156 | The onFailure method is a default that you can set for denying requests.
|
157 | This method receives the req & res objects, as well as an optional rejection reason that you pass to the reject method.
|
158 | */
|
159 | onFailure (req, res, rejectMessage) {
|
160 | res.status(403).json({error: rejectMessage});
|
161 | },
|
162 |
|
163 | /*
|
164 | This method is intentionally overly verbose
|
165 | to make it clear what is happening.
|
166 | */
|
167 | authenticated (req, res, accept, reject) {
|
168 | if (req.session) {
|
169 | if (req.session.authenticated) { // <--- What value on the session says they are logged in?
|
170 | accept(); // Accept the request. All is good
|
171 | } else {
|
172 | reject("You must be logged in to do that"); // Reject the request. This will end up calling the above on failure method.
|
173 | }
|
174 | } else {
|
175 | reject();
|
176 | }
|
177 | },
|
178 |
|
179 | // <--- Add additional policies if needed. Examples below....
|
180 | isDeveloper (req, res, accept, reject) {
|
181 | if (req.session && req.session.developer) {
|
182 | accept();
|
183 | } else {
|
184 | reject("You are not allowed to access this API");
|
185 | }
|
186 | },
|
187 |
|
188 | // Note: This policy requires you to follow the convention /api/resource/:id
|
189 | resourceOwnerOrAdmin (req, res, accept, reject) {
|
190 |
|
191 | if (!req.params) {
|
192 | return reject("Incorrect Parameters: Missing Parameters");
|
193 | } else if (!req.params.id) {
|
194 | return reject("Incorrect Parameters: Missing ID");
|
195 | }
|
196 |
|
197 | if (req.session && req.session.authenticated) {
|
198 | if (req.session.user.admin) {
|
199 | accept();
|
200 | } else if (req.session.user.id === req.params.id) {
|
201 | accept();
|
202 | } else {
|
203 | reject("You don't have access to this content");
|
204 | }
|
205 | } else {
|
206 | reject("You must be logged in to do that.");
|
207 | }
|
208 | }
|
209 | };
|
210 | ```
|
211 |
|
212 | `fig.p-1` An example of a flexible approach:
|
213 | ```js
|
214 | module.exports = {
|
215 | onFailure (req, res, custom = false) {
|
216 | if (custom && typeof custom === 'function') {
|
217 | custom();
|
218 | } else {
|
219 | res.status(403).end()
|
220 | }
|
221 | },
|
222 |
|
223 | authenticated (req, res, accept, reject) {
|
224 | if (req.session.authenticated) {
|
225 | accept();
|
226 | } else {
|
227 | reject(() => res.status(403).json({error: 'You must be logged in to do that'}));
|
228 | }
|
229 | },
|
230 |
|
231 | never (req, res, accept, reject) {
|
232 | reject();
|
233 | }
|
234 | }
|
235 | ```
|
236 |
|
237 | <br>
|
238 |
|
239 | Moving right along to routing. A route file is an object and the keys consist of HTTP verbs. (Any valid HTTP verb can be used) Each verb is an array route entries. This is how the router knows what controller action to route the request to, what policy to check, and if there is a specific body parser to use or the default. Each controller in your app will have it's own route file. You can think of it as resource based routing. Given a resource called `user` you should have the following files.
|
240 |
|
241 | - controllers/user.js
|
242 | - models/user.js
|
243 | - routes/user.js
|
244 |
|
245 | If you plan to render views, you should also have `views/user/some-view.pug`
|
246 |
|
247 | **A route entry**
|
248 | * path : matching url
|
249 | * action : the controller method to call when this route is matched
|
250 | * policy : the policy method to call in order to determine if the action is allowed. * see policies.js
|
251 | * bodyParser: A body parser configuration *(See Below)*
|
252 |
|
253 | **A resource**
|
254 | - model
|
255 | - route
|
256 | - controller
|
257 | - view folder
|
258 |
|
259 | <br>
|
260 |
|
261 | ## Sessions
|
262 |
|
263 | Glad will automatically handle sessions for you.
|
264 | However, sometimes you may not want this, or you have a different use case.
|
265 | If you remove the session object key from `config.js`, Glad will not handle sessions for you.
|
266 | It will be up to you to implement it on your own.
|
267 | You would likely want to do this in `middleware.js`.
|
268 | You can also do a hybrid implementation where glad will handle the session in some cases, and not in others.
|
269 | In order to accomplish this, you will need to create a file in your App's root directory called session.js.
|
270 | This file should export a method that receives `req, res, next` and returns a Promise. If you resolve the promise with true, Glad will use it's session middleware, if you resolve with false, Glad will not use session middleware for the request. This allows you to choose whether or not you want to use a session on a request by request basis. It's important that if you do resolve with false, that you invoke `next()`, otherwise the middleware chain will halt and the request will stall then timeout. Futhermore, if you resolve with true, do not invoke `next()`.
|
271 |
|
272 | #### TLDR:
|
273 |
|
274 | - if you create a file named session.js, Glad will route the middleware through it first.
|
275 | - session.js must return a Promise
|
276 | - resolving true tells Glad to implement the session.
|
277 | - resolving false tells Glad not to implement the session.
|
278 | - invoking `next()` and resolving with `true` is a bad idea.
|
279 | - not invoking `next()` and resolving with `false` will break your App.
|
280 | - if you don't create the session.js file, Glad will handle sessions normally.
|
281 | - If you don't want to use sessions at all, remove the session object key from `config.js`
|
282 |
|
283 | <br>
|
284 |
|
285 | ## Routing
|
286 |
|
287 | In the routes folder file you will find your routes. The routes object is organized by request method.
|
288 |
|
289 | ```
|
290 | module.exports = {
|
291 | GET: [{
|
292 | path: '/users', // <--- what url does this entry match?
|
293 | action: 'GET', // <--- what controller method should handle this request?
|
294 | policy: 'authenticated' // <--- what policy applies to this route?
|
295 | },{
|
296 | path: '/users/:id',
|
297 | action: 'findOne',
|
298 | policy: 'resourceOwnerOrAdmin' // <--- Not built in, but in the policies example above.
|
299 | }],
|
300 |
|
301 | POST: [{
|
302 | path: '/users',
|
303 | action: 'POST',
|
304 | policy: 'authenticated'
|
305 | }],
|
306 |
|
307 | PUT: [{
|
308 | path: '/users/:id',
|
309 | action: 'PUT',
|
310 | policy: 'authenticated'
|
311 | }],
|
312 |
|
313 | DELETE: [{
|
314 | path: '/users/:id',
|
315 | action: 'DELETE',
|
316 | policy: 'authenticated'
|
317 | }]
|
318 | }
|
319 | ```
|
320 |
|
321 | If you need to override the default body parser or the max body limit you can do this per endpoint.
|
322 |
|
323 | ```
|
324 | bodyParser : {
|
325 | limit : '10kb',
|
326 | parser : 'json'
|
327 | }
|
328 | ```
|
329 |
|
330 | You can also use a custom body parser.
|
331 |
|
332 | As an example, maybe you're using multer for file uploads etc...
|
333 |
|
334 | ```
|
335 | bodyParser : {
|
336 | custom: multer({ dest: '/tmp/uploads/' }).single('file')
|
337 | }
|
338 | ```
|
339 |
|
340 | Or not...
|
341 | ```
|
342 | bodyParser : {
|
343 | custom (req, res, next) {
|
344 | ... do stuff
|
345 | next();
|
346 | }
|
347 | }
|
348 | ```
|
349 |
|
350 | <br><br>
|
351 |
|
352 | # The Controller
|
353 |
|
354 | Each request creates a new instance of your controller. Your controller should always extend the **GladController**.
|
355 |
|
356 | Methods / Objects available using `this` from within an action.
|
357 |
|
358 | | method / object | Short Description |
|
359 | | :-------------- |:------------------|
|
360 | | cache | Caching class |
|
361 | | actionCache | Action caching store |
|
362 | | cacheStore | Current request's cache store |
|
363 | | req | Express request object |
|
364 | | res | Express response object |
|
365 | | params | Alias for `this.req.params` |
|
366 | | body | Alias for `this.req.body` |
|
367 | | query | Alias for `this.req.query` |
|
368 | | redisClient | Redis Connection |
|
369 | | permit | A method that removes non-whitelisted objects. Nested Objects are not allowed without using dot notation to specify the nested keys. |
|
370 | | deepPermit | A method that removes non-whitelisted objects. Nested Objects are allowed. |
|
371 | | permitted | Boolean, has this action called permit. This may be useful to enforce permitting. |
|
372 | | render | Renders a view. |
|
373 |
|
374 | ## Methods / Objects in detail
|
375 |
|
376 | ### cache
|
377 |
|
378 | Actions can be cached in two ways. Using the callback method or the chainable method.
|
379 |
|
380 | **The callback method**
|
381 |
|
382 | When using the callback method you tell the controller that you want to cache the result of this method for next time.
|
383 | You will provide some configuration regarding what type of strategy you'd like to use, as well as the max number of items that this particular
|
384 | method is allowed to store in cache. This allows you to better understand how much data you'll be putting into your caches, and allows you to
|
385 | favor certain methods over other (maybe less important) methods. The available named strategies are `"LRU"` and `"LFU"` for controller actions.
|
386 | If you would prefer to implement your own strategy, you can specify that by providing a additional parameters called score and increment. For LRU cache the scoring function is simply `() => new Date().getTime()` with `increment : false`. For LFU it is `() => 1` with `increment : true`.
|
387 |
|
388 | Now to the callback. Below you can see that we are sending in a callback that contains our code to generate the data we need to send back to the client.
|
389 | It will receive a caching function that you should call once your final data is generated.
|
390 | The first time this request happens, it runs your code to generate the response. The next time, it will not run the callback. Instead it will get the data from redis.
|
391 |
|
392 | ```javascript
|
393 | GET () {
|
394 | this.cache({ max: 200, strategy: 'LFU' }, cache => {
|
395 | SomeModel.find()
|
396 | .limit(15)
|
397 | .then(users => this.res.json(users) && cache(users))
|
398 | .catch(err => this.error(err))
|
399 | });
|
400 | }
|
401 | ```
|
402 |
|
403 | It is worth noting that a cache hit is defined by the url including any query parameters. So '/api/widgets?foo=1&bar=2' !== `/api/widgets?foo=1`.
|
404 |
|
405 | ##### The chain-able method.
|
406 |
|
407 | Just like the callback method, the code that runs to generate the data you need to send back to the client only gets executed on a cache:miss. In this case, it's a callback sent into the miss method. You also have an optional hit method that receives the cached data so that you can determine how you'd like to respond. (This could be useful when you'll be rendering a view with the data)
|
408 |
|
409 | ```javascript
|
410 | this.cache({ max: 100, strategy: 'LRU' })
|
411 | .miss(cache => {
|
412 | Products.find({category: 'widgets'}).exec( (err, widgets) => {
|
413 | this.res.status(200).json(widgets);
|
414 | cache(widgets);
|
415 | });
|
416 | })
|
417 | .hit(data => this.res.status(200).json(data))
|
418 | .exec();
|
419 | ```
|
420 |
|
421 |
|
422 | ##### How To?
|
423 |
|
424 | What if i'm not using json? `this.cache({ max: 2, strategy: 'LFU', type: 'html' })` you can pass in an additional parameter called type. This can be any type that express understands.
|
425 |
|
426 | How do I enable caching in development? You can enable caching in development by either using the cache flags `--enable-cache` when starting up your server or console. Or you can do it in an initializer by setting `Glad.cache.disabled = false`
|
427 |
|
428 | How do I disable caching in non-development environments? You can disable caching in development by either using the cache flags `--disable-cache` when starting up your server or console. Or you can do it in an initializer by setting `Glad.cache.disabled = true`
|
429 |
|
430 | How do I progromatically enable or disable Glad caching based on something else, like an environment variable? You can set `Glad.cache.disabled` anywhere you want based off of anything. (The preferred way is to do this in an initializer)
|
431 |
|
432 | <br/>
|
433 |
|
434 | ### actionCache
|
435 |
|
436 | The **actionCache** method retrieves the cache store used for a specific method on your controller. Since controller methods have their own separate namespaced cache, you will need to look up an action's cache namespace if you want to operate on it from a different action. This is especially useful if you need to drop/re-warm a cache when data changes. As an example, let's say that you have a controller setup for widgets, and when you HTTP PUT a widget, you want to remove that item from the cache. Let's also assume that `FindOne` handles the GET requests for a specific widget, and the GET method handles GET requests for an array of widgets.
|
437 |
|
438 | From the PUT action on the controller
|
439 |
|
440 | ```js
|
441 | this.actionCache('FindOne').del('/widgets/12').then(..do stuff);
|
442 | this.actionCache('GET').del('/widgets').then(..do stuff);
|
443 | ```
|
444 |
|
445 | ### cacheStore
|
446 |
|
447 | The **cacheStore** object is an instance of **ControllerCache** delegated to the current controller instance.
|
448 | This means that `this.cacheStore` only operates on the current action's cache namespace.
|
449 |
|
450 | API: All methods return a **Promise**.
|
451 |
|
452 | | method | Description |
|
453 | | --- | ---|
|
454 | | `set(key, value, maxAge)` | set value for the given key, marking it as the most recently accessed one. Keys should be strings, values will be - - - JSON.stringified. The optional maxAge overrides for this specific key the global expiration of the cache. |
|
455 | |`get(key)` | resolve to the value stored in the cache for the given key or null if not present. If present, the key will be marked as the most recently accessed one.|
|
456 | | `getOrSet(key, fn, maxAge)` | resolve to the value stored in the cache for the given key. If not present, execute fn, save the result in the cache and return it. fn should be a no args function that returns a value or a promise. If maxAge is passed, it will be used only if the key is not already in the cache.|
|
457 | | `peek(key)` | resolve to the value stored in the cache for the given key, without changing its last accessed time.|
|
458 | | `del(key)` | removes the item from the cache. |
|
459 | | `reset()` | empties the cache. |
|
460 | | `has(key)` | resolves to true if the given key is present in the cache. |
|
461 | | `keys()` | resolves to an array of keys in the cache, sorted from most to least recently accessed. |
|
462 | | `values()` | resolves to an array of values in the cache, sorted from most to least recently accessed. |
|
463 | | `count()` | resolves to the number of items currently in the cache. |
|
464 |
|
465 |
|
466 | ### req / res / params / query / body
|
467 |
|
468 | See [Express Request](https://expressjs.com/en/api.html#req)
|
469 | <br/>
|
470 | See [Express Response](https://expressjs.com/en/api.html#res)
|
471 | <br/>
|
472 | See [req.params](https://expressjs.com/en/api.html#req.params)
|
473 | <br/>
|
474 | See [req.query](https://expressjs.com/en/api.html#req.query)
|
475 | <br/>
|
476 | See [req.body](https://expressjs.com/en/api.html#req.body)
|
477 |
|
478 | ### redisClient
|
479 |
|
480 | Initialized redis client. See [redis package on NPM](https://www.npmjs.com/package/redis)
|
481 |
|
482 | ### permit / deepPermit / permitted
|
483 |
|
484 | Whitelist allowable data in a request body. This is a good idea if you are mass assigning things.
|
485 |
|
486 | Ex: Shallow
|
487 |
|
488 | ```js
|
489 | this.req.body = { name: 'Fooze', admin: true };
|
490 | this.permit('name', 'email', 'phone');
|
491 | // this.req.body === { name: 'Fooze' }
|
492 | ```
|
493 |
|
494 | Ex: Deep
|
495 | ```js
|
496 | this.req.body = { user: { name: 'Fooze', admin: true } };
|
497 | this.permit('user.name', 'user.email', 'user.phone');
|
498 | // this.req.body === { user: { name: 'Fooze' } }
|
499 | ```
|
500 |
|
501 | - You must be specific when the key contains an object.
|
502 | - You cannot permit the whole user object at once. In order to permit "sub documents" you need to use the **deepPermit** method instead. This is intentional because it can defeat the purpose of **permit** when you permit a subdocument that could potentially contain things that shouldn't be allowed.
|
503 |
|
504 | After calling permit, `this.permitted` will be true.
|
505 |
|
506 |
|
507 | ### render
|
508 |
|
509 | The render method is an enhanced version of Express' **res.render**. Calling `this.render` from a controller automatically looks in the correct folder for the view file. As an example, let's say you have a controller for a resource called widgets. This means that there should be a folder in the views directory called widgets. Invoking `this.render('my-widget', {data: 'stuff'})` will render the view located at `views/widgets/my-widget.pug`.
|
510 |
|
511 |
|
512 | <br><br>
|
513 |
|
514 |
|
515 | ## Web Sockets / Real Time API
|
516 | <img src="https://socket.io/assets/img/logo.svg" height="35px">
|
517 |
|
518 | Glad comes with full support for websockets and an intuitive convention in dealing with it.
|
519 |
|
520 | **Features**
|
521 | - Session integration
|
522 | - Routing
|
523 | - Policies
|
524 |
|
525 | There should be a directory in your project root called websockets. The websockets directory consists of a few files.
|
526 | - router.js
|
527 | - policies.js
|
528 |
|
529 | The router works very much like the Glad HTTP router. Each entry must specify an event and an action. The policy is optional.
|
530 | The action is a function that will be invoked when a connection sends a message to the given event. Furthermore the value of `this`
|
531 | in the action will be the instance of SocketIo.
|
532 |
|
533 | ```javascript
|
534 | module.exports = [{
|
535 | event: 'hello',
|
536 | action (message, connection) {
|
537 | connection.emit('hello', message);
|
538 | },
|
539 | policy : 'canSayHello'
|
540 | },{
|
541 | event: 'subscribeToRomm',
|
542 | action (room, connection) {
|
543 | socket.join(room);
|
544 | },
|
545 | policy: 'loggedIn'
|
546 | }];
|
547 | ```
|
548 | For illustrative purposes, the action method is defined in the example above. However, you may find that it's good to create another file(s) in the sockets directory that will define your handlers, like below.
|
549 |
|
550 | ```javascript
|
551 |
|
552 | let chats = require('./chats');
|
553 |
|
554 | module.exports = [{
|
555 | event: 'hello',
|
556 | action: chats.hello,
|
557 | policy : 'canSayHello'
|
558 | },{
|
559 | event: 'subscribeToRomm',
|
560 | action: chats.joinRoom,
|
561 | policy: 'loggedIn'
|
562 | }];
|
563 | ```
|
564 |
|
565 |
|
566 | Policies are also similar to the HTTP policies. They receive two arguments: the connection that sent the message and an accept method.
|
567 | If you choose not to call the accept method, then it is rejected by default.
|
568 |
|
569 | ```javascript
|
570 | module.exports = {
|
571 | canSayHello (connection, accept) {
|
572 | if (connection.request.session.loggedIn) { // By default glad handles syncing up the session to the socket connection.
|
573 | accept();
|
574 | }
|
575 | }
|
576 | };
|
577 | ```
|
578 |
|
579 | ## Flags
|
580 |
|
581 | #### Only
|
582 |
|
583 | `--only=[controller|controllers,controllers]`
|
584 | Using the only flag, you can spin up a server that only binds routes for a specific controller or a group of controllers.
|
585 |
|
586 | As an example, `glad s --only=posts` would launch your app and allow traffic to routes handled by the posts controller.
|
587 |
|
588 | If you would like to launch a group of controllers, you would simply comma separate the controllers such as `glad s --only=posts,users`.
|
589 |
|
590 | The convention is such that you provide the lowercase filename of your controller(s) that should be enabled.
|
591 |
|
592 | If you are not using Glad CLI, this will still work with the node binary.
|
593 |
|
594 | `node index.js --only=posts`
|
595 |
|
596 | #### Server Flags
|
597 |
|
598 | `--port` | `--host` | `--sock` | `--backlog`
|
599 | Using the server flags you can over ride the values in your config.js file.
|
600 |
|
601 | #### Cache Flags
|
602 | `--enable-cache` | `disable-cache`
|
603 | Using the caching flags you can disable caching for non-development environments or enable cache on development environments
|
604 |
|
605 |
|
606 | ## Classes / Helpers / Tools
|
607 |
|
608 | Glad comes with lots of helpful utilities.
|
609 |
|
610 | ### String
|
611 |
|
612 | Assume `string === Glad.string` for the examples below.
|
613 |
|
614 | - color
|
615 | - Produces a colorized string for stdout. `string.color('Oh No', 'red')`
|
616 | - Can be any color name supported by chalk.js
|
617 |
|
618 | - deburr
|
619 | - converts unicode
|
620 |
|
621 | `string.deburr('♥') === 'love'`
|
622 |
|
623 | - converts short symbols
|
624 |
|
625 | `string.deburr('<3') === 'love'`
|
626 |
|
627 | `string.deburr('released w/ stuff') === 'released with stuff'`
|
628 |
|
629 | `string.deburr('live && loud') === live and loud'`
|
630 |
|
631 | `string.deburr(':) || sad') === 'smile or sad'`
|
632 |
|
633 | - converts Latin characters
|
634 |
|
635 | `string.deburr('À') === 'A'` <i>See fig s1 at the bottom of the page for a list of all supported characters</i>
|
636 |
|
637 | - converts Greek characters
|
638 |
|
639 | `string.deburr('α') === 'a'` <i>See fig s2 at the bottom of the page for a list of all supported characters</i>
|
640 |
|
641 | - converts Turkish characters
|
642 |
|
643 | `string.deburr('ş') === 's'` <i>See fig s3 at the bottom of the page for a list of all supported characters</i>
|
644 |
|
645 | - converts Russian characters
|
646 |
|
647 | `string.deburr('ф') === 'f'` <i>See fig s4 at the bottom of the page for a list of all supported characters</i>
|
648 |
|
649 | - slugify
|
650 |
|
651 | Creates a normalized slug from a string
|
652 |
|
653 | `string.slugify('San Francisco, CA') === 'san-francisco-ca'`
|
654 |
|
655 | `string.slugify('The brown fox w/ βeta') === 'the-brown-fox-with-beta'`
|
656 |
|
657 | `string.slugify('good news so you can :)') === 'good-news-so-you-can-smile'`
|
658 |
|
659 | If you need to create a multi-variable slug such as `first_name + " " + last_name` etc... consider allowing slugify to do the concatenation for you. `slugify` can receive "n" arguments which become concatenated to form additional segments of the slug.
|
660 |
|
661 | `string.slugify('how to create stuff', 'June 17 2017') === 'how-to-create-stuff-june-17-2017'`
|
662 |
|
663 | - camelize
|
664 |
|
665 | `string.camelize("fooze-barz") === 'foozeBarz'`
|
666 |
|
667 |
|
668 | - titleize
|
669 |
|
670 | `string.titelize("fooze barz") === 'Fooze Barz'`
|
671 |
|
672 | - slasherize
|
673 |
|
674 | `string.slasherize("fooze barz") === "fooze/barz"`
|
675 |
|
676 | - reverseSlasherize
|
677 |
|
678 | `string.reverseSlasherize("fooze barz") === "barz/fooze"`
|
679 |
|
680 | - underscore
|
681 |
|
682 | `string.underscore("fooze barz") === "fooze_barz"`
|
683 |
|
684 |
|
685 | - cleanSpaces
|
686 |
|
687 | `string.cleanSpaces("fooze barz") === "fooze barz"
|
688 |
|
689 | - endsWith
|
690 |
|
691 | `string.endsWith("fooze barz fooze", "fooze") === true`
|
692 |
|
693 | `string.endsWith("fooze barz", "fooze") === false`
|
694 |
|
695 |
|
696 | - escape
|
697 |
|
698 | `string.escape("fred, barney, & pebbles") === "fred, barney, & pebbles"
|
699 |
|
700 |
|
701 | - unescape
|
702 |
|
703 | `string.unescape("fred, barney, & pebbles") === "fred, barney, & pebbles"`
|
704 |
|
705 | - escapeRegExp
|
706 |
|
707 | escapes a string for use in regexp
|
708 |
|
709 | `string.escapeRegExp('[lodash](https://lodash.com/)') === "\\[lodash\\]\\(https://lodash\\.com/\\)"`
|
710 |
|
711 |
|
712 | - repeat
|
713 |
|
714 | `string.repeat("glad", 2) === "gladglad"`
|
715 |
|
716 |
|
717 | - startsWith
|
718 |
|
719 | `string.startsWith("fooze barz", "fooze") === true`
|
720 |
|
721 | `string.startsWith("x fooze barz", "fooze") === false`
|
722 |
|
723 |
|
724 | - words
|
725 |
|
726 | split a string based on words
|
727 |
|
728 | `string.words('fred, barney, & pebbles') === ['fred', 'barney', 'pebbles']`
|
729 |
|
730 | split a string based on words and keep elements by regexp
|
731 |
|
732 | `string.words('fred, barney, & pebbles', /[^, ]+/g) === ['fred', 'barney', '&', 'pebbles']`
|
733 |
|
734 | - sentenceCase
|
735 |
|
736 | Capitalizes letters at the beginning of a string, and after periods.
|
737 |
|
738 | ### Objects
|
739 |
|
740 | Assume object refences Glad.object
|
741 |
|
742 | - get
|
743 |
|
744 | Pick a value from an object
|
745 | ```
|
746 | let o = {foo: {bar : 'foobar' }};
|
747 | object.get(o, 'foo.bar') => 'foobar'
|
748 | ```
|
749 |
|
750 | - extend
|
751 |
|
752 | Extend `object at argument 1` with `object at argument 2,3,4,n`. The order of extension is from left to right. Duplicate values will be taken from the right-most arguments.
|
753 |
|
754 | ```
|
755 | let src = {one: 1};
|
756 | let ext = {two: 2};
|
757 | assert.deepEqual(object.extend(src, ext), {one: 1, two: 2})
|
758 | ```
|
759 |
|
760 | ```
|
761 | let src = {one: 1};
|
762 | let ext = {two: 2};
|
763 | let ext2 = {three: 3};
|
764 | assert.deepEqual(object.extend(src, ext, ext2), {one: 1, two: 2, three: 3});
|
765 | ```
|
766 |
|
767 | ```
|
768 | let src = {one: 1};
|
769 | let ext = {one: 2};
|
770 | let ext2 = {three: 3};
|
771 | assert.deepEqual(object.extend(src, ext, ext2), {one: 2, three: 3});
|
772 | ```
|
773 |
|
774 | ```
|
775 | let src = {one: 1};
|
776 | let ext = {two: 2};
|
777 | let ext2 = {two: 3};
|
778 | let ext3 = {two: 4};
|
779 | assert.deepEqual(object.extend(src, ext, ext2, ext3), {one: 1, two: 4});
|
780 | ```
|
781 |
|
782 | - hasPath
|
783 |
|
784 | return a boolean value if an object contains a path
|
785 |
|
786 | ```
|
787 | let src = {one: 1};
|
788 | assert.equal(object.hasPath(src, 'one'), true);
|
789 | assert.equal(object.hasPath(src, 'one.value.at.nowhere'), false);
|
790 | ```
|
791 |
|
792 | - clone
|
793 |
|
794 | return a new object
|
795 |
|
796 | ```
|
797 | let src = {one: 1, two: {a: 1}};
|
798 | assert.deepEqual(object.clone(src), {one: 1, two: {a: 1}});
|
799 | ```
|
800 |
|
801 | new object should not be passed by reference
|
802 |
|
803 | ```
|
804 | let src = {one: 1, two: {a: 1} };
|
805 | let src2 = object.clone(src);
|
806 | src2.two.a = 3;
|
807 | assert.equal(src.two.a, 1);
|
808 | assert.equal(src2.two.a, 3);
|
809 | ```
|
810 |
|
811 | - set
|
812 |
|
813 | set a value at the given path (null/undefined safe)
|
814 |
|
815 | ```
|
816 | let src = {};
|
817 | object.set(src, 'foo', 1);
|
818 | assert.equal(src.foo, 1);
|
819 | ```
|
820 |
|
821 | ```
|
822 | let src = {};
|
823 | object.set(src, 'foo.bar.baz.x', 1);
|
824 | assert.equal(src.foo.bar.baz.x, 1);
|
825 | ```
|
826 |
|
827 | - arrayToObject
|
828 |
|
829 | convert an array of arrays to an object
|
830 |
|
831 | ```
|
832 | let arr = [['a', 1], ['b', 2]];
|
833 | let o = object.arrayToObject(arr);
|
834 | assert.equal(o.a, 1);
|
835 | assert.equal(o.b, 2);
|
836 | ```
|
837 |
|
838 | - invert
|
839 |
|
840 | invert an object
|
841 |
|
842 | ```
|
843 | let o = {a: 'A', b: 'B'};
|
844 | object.invert(o);
|
845 | assert.deepEqual(o, {A: 'a', B: 'b'});
|
846 | assert.equal(o.a, undefined);
|
847 | ```
|
848 |
|
849 | - createShallowInvertedClone
|
850 |
|
851 | Creates an inverted shallow clone of an object. This is particulary useful when creating data maps. As an example say that you want an Object called usStatesToAbbreviations that acts as a map of U.S States to their respective abbreviation. It would be easy to create the inverse object from it via `createShallowInvertedClone`, which would generate a map of U.S State Abbreviations to their respective State.
|
852 |
|
853 | ```
|
854 | let o = {a: 'A', b: 'B'};
|
855 | let x = createShallowInvertedClone(o); // Glad.object.createShallowInvertedClone
|
856 | assert.deepEqual(x, {A: 'a', B: 'b'});
|
857 | assert.equal(x.a, undefined); // Non-inverted keys should not be present
|
858 | assert.deepEqual(o, {a: 'A', b: 'B'}); // Original should be non-inverted
|
859 | ```
|
860 |
|
861 | - select
|
862 |
|
863 | select keys from an object (creates a new object)
|
864 |
|
865 | ```
|
866 | let o = {name: 'fred', email: 'me@mail.com', password: 'secretSquirrel'};
|
867 | let fields = object.select(o, 'name', 'email');
|
868 | assert.deepEqual(fields, {name: 'fred', email: 'me@mail.com'});
|
869 | ```
|
870 |
|
871 | select keys from an object via Array
|
872 |
|
873 | ```
|
874 | let o = {name: 'fred', email: 'me@mail.com', password: 'secretSquirrel'};
|
875 | let fields = object.select(o, ['name', 'email']);
|
876 | assert.deepEqual(fields, {name: 'fred', email: 'me@mail.com'});
|
877 | ```
|
878 |
|
879 | - drop
|
880 |
|
881 | drop keys from an object
|
882 |
|
883 | ```
|
884 | let o = { name: 'fred', email: 'me@mail.com', password: 'secretSquirrel' };
|
885 | object.drop(o, 'password');
|
886 | assert.deepEqual(o, {name: 'fred', email: 'me@mail.com'});
|
887 | ```
|
888 |
|
889 | drop keys from an object via Array
|
890 |
|
891 | ```
|
892 | let o = { name: 'fred', email: 'me@mail.com', password: 'secretSquirrel'
|
893 | object.drop(o, ['password']);
|
894 | assert.deepEqual(o, {name: 'fred', email: 'me@mail.com'});
|
895 | });
|
896 | ```
|
897 |
|
898 | - selectCombination
|
899 |
|
900 | select values from multiple objects and create a new one
|
901 |
|
902 | ```
|
903 | let a = {
|
904 | name: 'fred',
|
905 | email: 'me@mail.com',
|
906 | password: 'secretSquirrel'
|
907 | };
|
908 | let b = { sid: '8372487234', last_visit: new Date()};
|
909 | let c = { likes : 'stuff', knows: 'things'};
|
910 | let o = object.selectCombination([a, b, c], 'name', 'email', 'last_visit', 'likes', 'knows');
|
911 | assert.deepEqual(o, { name: 'fred', email: 'me@mail.com', last_visit: b.last_visit, likes : 'stuff', knows: 'things'});
|
912 | });
|
913 | ```
|
914 |
|
915 | - format
|
916 |
|
917 | Create a new object from an existing one, but reformat the keys.
|
918 | Individual arguments such as 'email' or 'name' copy the value for the respective key to the new object maintaining the same key name. In order to map a value from the existing array to a new key on the new object, you use an array. [`destination path`, `source path`].
|
919 |
|
920 | ```
|
921 | let a = {
|
922 | name: 'fred',
|
923 | email: 'me@mail.com',
|
924 | data: {
|
925 | stuff: { a: 'a', b: 'b'},
|
926 | more : { c: 'value'}
|
927 | }
|
928 | };
|
929 |
|
930 | let o = object.format(a, 'name', 'email', ['stuff', 'data.stuff.a' ], ['value', 'data.more.c']);
|
931 |
|
932 | assert.deepEqual(o, {
|
933 | name: 'fred',
|
934 | email: 'me@mail.com',
|
935 | stuff: 'a',
|
936 | value : 'value'
|
937 | });
|
938 |
|
939 | });
|
940 | ```
|
941 |
|
942 | Format the keys using an array
|
943 |
|
944 | ```
|
945 | let a = {
|
946 | name: 'fred',
|
947 | email: 'me@mail.com',
|
948 | data: {
|
949 | stuff: { a: 'a', b: 'b'},
|
950 | more : { c: 'value'}
|
951 | }
|
952 | };
|
953 |
|
954 | let o = object.format(a, [
|
955 | 'name',
|
956 | 'email',
|
957 | ['stuff', 'data.stuff.a' ],
|
958 | ['value', 'data.more.c']
|
959 | ]);
|
960 |
|
961 | assert.deepEqual(o, {
|
962 | name: 'fred',
|
963 | email: 'me@mail.com',
|
964 | stuff: 'a',
|
965 | value : 'value'
|
966 | });
|
967 |
|
968 | });
|
969 | ```
|
970 |
|
971 | - explode
|
972 |
|
973 | Explode an object
|
974 |
|
975 | ```
|
976 |
|
977 | let row = {
|
978 | 'id': 2,
|
979 | 'contact.name.first': 'John',
|
980 | 'contact.name.last': 'Doe',
|
981 | 'contact.email': 'example@gmail.com',
|
982 | 'contact.info.about.me': 'classified',
|
983 | 'devices.0': 'mobile',
|
984 | 'devices.1': 'laptop',
|
985 | 'some.other.things.0': 'this',
|
986 | 'some.other.things.1': 'that',
|
987 | 'some.other.stuff.0.key': 'stuff'
|
988 | };
|
989 |
|
990 | object.explode(row);
|
991 |
|
992 | assert.deepEqual(row, {
|
993 | "id": 2,
|
994 | "contact": {
|
995 | "name": {
|
996 | "first": "John",
|
997 | "last": "Doe"
|
998 | },
|
999 | "email": "example@gmail.com",
|
1000 | "info": {
|
1001 | "about": {
|
1002 | "me": "classified"
|
1003 | }
|
1004 | }
|
1005 | },
|
1006 | "devices": [
|
1007 | "mobile",
|
1008 | "laptop"
|
1009 | ],
|
1010 | "some": {
|
1011 | "other": {
|
1012 | "things": [
|
1013 | "this",
|
1014 | "that"
|
1015 | ],
|
1016 | stuff : [{
|
1017 | key : "stuff"
|
1018 | }]
|
1019 | }
|
1020 | }
|
1021 | });
|
1022 | });
|
1023 | ```
|
1024 |
|
1025 | ### Number
|
1026 |
|
1027 | Assume number is Glad.number
|
1028 |
|
1029 | - Time Constants
|
1030 | ```
|
1031 | assert.equal(number.SECOND, 1000);
|
1032 | assert.equal(number.MINUTE, 1000 * 60);
|
1033 | assert.equal(number.HOUR, 1000 * 60 * 60);
|
1034 | assert.equal(number.DAY, 1000 * 60 * 60 * 24);
|
1035 | ```
|
1036 |
|
1037 | - parse
|
1038 |
|
1039 | Strip off weird stuff
|
1040 |
|
1041 | ```
|
1042 | assert.equal(number.parse("$34.72"), 34.72);
|
1043 | assert.equal(number.parse("65.323%"), 65.323);
|
1044 | assert.equal(number.parse("65%"), 65);
|
1045 |
|
1046 | // RESPECTS NEGATIVE NUMBERS
|
1047 | assert.equal(number.parse("-65%"), -65);
|
1048 | assert.equal(number.parse("-$65.34"), -65.34);
|
1049 | assert.equal(number.parse("-78.32-"), -78.32);
|
1050 | ```
|
1051 |
|
1052 |
|
1053 | - random
|
1054 |
|
1055 | generate a random number between x,y
|
1056 |
|
1057 | ```
|
1058 | let rand = number.random(5,10);
|
1059 | assert.equal(rand <= 10, true);
|
1060 | assert.equal(rand >= 5, true);
|
1061 | ```
|
1062 |
|
1063 | - withDelimiter
|
1064 |
|
1065 | Simply format a number with commas and decimals
|
1066 |
|
1067 | ```
|
1068 | assert.equal(number.withDelimiter(4), '4.00');
|
1069 | assert.equal(number.withDelimiter(45), '45.00');
|
1070 | assert.equal(number.withDelimiter(450), '450.00');
|
1071 | assert.equal(number.withDelimiter(4500), '4,500.00');
|
1072 | assert.equal(number.withDelimiter(45000), '45,000.00');
|
1073 | assert.equal(number.withDelimiter(450000), '450,000.00');
|
1074 | assert.equal(number.withDelimiter(4500000), '4,500,000.00');
|
1075 | assert.equal(number.withDelimiter(45000000), '45,000,000.00');
|
1076 | assert.equal(number.withDelimiter(450000000), '450,000,000.00');
|
1077 | assert.equal(number.withDelimiter(4500000000), '4,500,000,000.00');
|
1078 | assert.equal(number.withDelimiter(45000000000), '45,000,000,000.00');
|
1079 | assert.equal(number.withDelimiter(450000000000), '450,000,000,000.00');
|
1080 | assert.equal(number.withDelimiter(4500000000000), '4,500,000,000,000.00');
|
1081 | assert.equal(number.withDelimiter(45000000000000), '45,000,000,000,000.00');
|
1082 | assert.equal(number.withDelimiter(450000000000000), '450,000,000,000,000.00');
|
1083 | assert.equal(number.withDelimiter(99e19), '990,000,000,000,000,000,000.00');
|
1084 | ```
|
1085 |
|
1086 | - toAbbr
|
1087 |
|
1088 | Abbreviate a number
|
1089 |
|
1090 | ```
|
1091 | assert.equal(number.toAbbr(45000), '45k');
|
1092 | assert.equal(number.toAbbr(450000), '450k');
|
1093 | assert.equal(number.toAbbr(4500000), '4.5m');
|
1094 | assert.equal(number.toAbbr(45000000), '45m');
|
1095 | assert.equal(number.toAbbr(450000000), '450m');
|
1096 | assert.equal(number.toAbbr(4500000000), '4.5b');
|
1097 | assert.equal(number.toAbbr(45000000000), '45b');
|
1098 | assert.equal(number.toAbbr(450000000000), '450b');
|
1099 | assert.equal(number.toAbbr(450), '450');
|
1100 | assert.equal(number.toAbbr(4500), '4.5k');
|
1101 | ```
|
1102 |
|
1103 | - `toData(bytes)`
|
1104 |
|
1105 | format a number in data units
|
1106 |
|
1107 | ```
|
1108 | assert.equal(number.toData(126.02 * 1000), '126.0 kB');
|
1109 | assert.equal(number.toData(126.32 * 1000), '126.3 kB');
|
1110 | assert.equal(number.toData(126.32 * 1000 * 1000), '126.3 MB');
|
1111 | assert.equal(number.toData(126.32 * Math.pow(1000, 3)), '126.3 GB');
|
1112 | assert.equal(number.toData(126.32 * Math.pow(1000, 4)), '126.3 TB');
|
1113 | assert.equal(number.toData(126.32 * Math.pow(1000, 5)), '126.3 PB');
|
1114 | assert.equal(number.toData(126.32 * Math.pow(1000, 6)), '126.3 EB');
|
1115 | assert.equal(number.toData(126.32 * Math.pow(1000, 7)), '126.3 ZB');
|
1116 | assert.equal(number.toData(126.32 * Math.pow(1000, 8)), '126.3 YB');
|
1117 | ```
|
1118 |
|
1119 | - `toTime(seconds, returnArray = false)`
|
1120 |
|
1121 | format a number in time
|
1122 |
|
1123 | ```
|
1124 | const HOUR = 60 * 60;
|
1125 | const DAY = 24 * HOUR;
|
1126 |
|
1127 | assert.equal(number.toTime(50), '50 sec');
|
1128 | assert.equal(number.toTime(60), '1 min');
|
1129 | assert.equal(number.toTime(HOUR), '1 hr');
|
1130 | assert.equal(number.toTime(DAY), '1 day');
|
1131 | assert.equal(number.toTime(DAY * 30), '30 days');
|
1132 | assert.equal(number.toTime( (DAY * 2) + 10), '2 days 10 sec');
|
1133 | assert.equal(number.toTime( (DAY * 2) + (HOUR * 2) + 32), '2 days 2 hr 32 sec');
|
1134 | ```
|
1135 |
|
1136 | format a number in time and return an array
|
1137 |
|
1138 | ```
|
1139 | assert.deepEqual(number.toTime(50, true), [0, 0, 0, 50]);
|
1140 | assert.deepEqual(number.toTime(60, true), [0, 0, 1, 0]);
|
1141 | assert.deepEqual(number.toTime(HOUR, true), [0, 1, 0, 0]);
|
1142 | assert.deepEqual(number.toTime(DAY, true), [1, 0, 0, 0]);
|
1143 | assert.deepEqual(number.toTime(DAY * 30, true), [30, 0, 0, 0]);
|
1144 | assert.deepEqual(number.toTime( (DAY * 2) + 10, true), [2, 0, 0, 10]);
|
1145 | assert.deepEqual(number.toTime( (DAY * 2) + (HOUR * 2) + 32, true), [2, 2, 0, 32]);
|
1146 | ```
|
1147 |
|
1148 | - `toCurrency(number, precision, decimal, comma)`
|
1149 |
|
1150 | format a number in USD currency (convienience method)
|
1151 |
|
1152 | ```
|
1153 | assert.equal(number.toCurrency(240.658), '$240.66');
|
1154 | assert.equal(number.toCurrency(-376240.658), '$-376,240.66');
|
1155 | ```
|
1156 |
|
1157 | If you need to format a number in other currencies, use the NumberFormatter Class.
|
1158 |
|
1159 | ```
|
1160 | let toGBP = new Glad.number.NumberFormatter("£", false, 2, ',', '.');
|
1161 | assert.equal(toGBP(1234567.89), '£1.234.567,89');
|
1162 | ```
|
1163 |
|
1164 | - `toPercent(number, comma = ",", decimal = ".")`
|
1165 |
|
1166 | format a number as a percentage
|
1167 |
|
1168 | ```
|
1169 | assert.equal(number.toPercent(43.47576353), '43.48%');
|
1170 | assert.equal(number.toPercent(43.47576353, 4), '43.4758%');
|
1171 | assert.equal(number.toPercent(43873.47581765327, 4, '*', '\''), "43'873*4758%");
|
1172 | assert.equal(number.toPercent(-43.47576353), '-43.48%');
|
1173 | assert.equal(number.toPercent(-43.47576353, 4), '-43.4758%');
|
1174 | ```
|
1175 |
|
1176 |
|
1177 | - `toPhone(number = "", formatAreaCode = false, extension = false)`
|
1178 |
|
1179 | format a number as a phone number
|
1180 | ```
|
1181 | assert.equal(number.toPhone(9255551212), "925-555-1212");
|
1182 | assert.equal(number.toPhone('9255551212'), "925-555-1212");
|
1183 | assert.equal(number.toPhone(9255551212, true), "(925) 555-1212");
|
1184 | assert.equal(number.toPhone(9255551212, true, 4528), "(925) 555-1212 x4528");
|
1185 | ```
|
1186 |
|
1187 | ### imports
|
1188 | Imports is a require alias with some nifty features specifically for Glad. When using the imports method, paths are imported relative to your working directory.
|
1189 |
|
1190 | Examples: (Assume imports is Glad.imports)
|
1191 |
|
1192 | Both controllers and models can be imported without pluralization
|
1193 |
|
1194 | `imports('UserModel')` => `require('./models/user')`
|
1195 |
|
1196 | Importing the widget model from within the directory `queries/widgets/find-by-name`
|
1197 |
|
1198 | `imports('UserModel')` => `require('../../models/user')`
|
1199 |
|
1200 | Importing using reverse camelize
|
1201 |
|
1202 | `imports('UserQueryMocksTest')` => `require('./test/mocks/query/user')`
|
1203 |
|
1204 | Importing normally from within a sub-directory
|
1205 |
|
1206 | `imports('classes/fooze')` => `require('../../classes/fooze')`
|
1207 |
|
1208 | ### Dates
|
1209 |
|
1210 | Glad's Date class allows you to create different instances to use for different timezone offsets. To use Glad's Date class you'll need to install moment. `npm install moment --save`
|
1211 |
|
1212 | For each example below assume these variables are created.
|
1213 |
|
1214 | ```
|
1215 | let moment = require('moment');
|
1216 | let utcDate = new Glad.Date(moment);
|
1217 | let pstDate = new Glad.Date(moment, -8);
|
1218 | ```
|
1219 | When creating an inistance of Glad.Date, the first argument is moment, and the 2nd is the offset to use. All methods return a Javascript Date Object.
|
1220 |
|
1221 | Also assume that the current Date is January 1st 1970 12:00am
|
1222 |
|
1223 | - monthsFromNow
|
1224 | ```
|
1225 | utcDate.monthsFromNow(1) => 1970-02-01T00:00:00.000Z
|
1226 | utcDate.monthsFromNow(12) => 1971-01-01T00:00:00.000Z
|
1227 | utcDate.monthsFromNow(24) => 1972-01-01T00:00:00.000Z
|
1228 | ```
|
1229 |
|
1230 |
|
1231 | - weeksFromNow
|
1232 |
|
1233 | ```
|
1234 | utcDate.weeksFromNow(1) => 1970-01-08T00:00:00.000Z
|
1235 | ```
|
1236 |
|
1237 | - daysFromNow
|
1238 |
|
1239 | ```
|
1240 | utcDate.daysFromNow(1) => 1970-01-02T00:00:00.000Z
|
1241 | utcDate.daysFromNow(10) => 1970-01-11T00:00:00.000Z
|
1242 | ```
|
1243 |
|
1244 |
|
1245 | - hoursFromNow
|
1246 |
|
1247 | ```
|
1248 | utcDate.hoursFromNow(1) => 1970-01-01T01:00:00.000Z')
|
1249 | ```
|
1250 |
|
1251 | - minutesFromNow
|
1252 |
|
1253 | ```
|
1254 | utcDate.minutesFromNow(1) => 1970-01-01T00:01:00.000Z
|
1255 | utcDate.minutesFromNow(14) => 1970-01-01T00:14:00.000Z
|
1256 | ```
|
1257 |
|
1258 | - secondsFromNow
|
1259 |
|
1260 | ```
|
1261 | utcDate.secondsFromNow(1) => 1970-01-01T00:00:01.000Z
|
1262 | utcDate.secondsFromNow(10) => 1970-01-01T00:00:10.000Z
|
1263 | utcDate.secondsFromNow(61) => 1970-01-01T00:01:01.000Z
|
1264 | ```
|
1265 |
|
1266 | - startOfSecond
|
1267 |
|
1268 | Supply a Date Object and this will return a new date object with milliseconds zeroed out.
|
1269 |
|
1270 |
|
1271 | - startOfMinute
|
1272 |
|
1273 | Supply a Date Object and this will return a new date object with milliseconds and seconds zeroed out.
|
1274 |
|
1275 | - startOfHour
|
1276 |
|
1277 | Supply a Date Object and this will return a new date object with milliseconds, seconds and minutes zeroed out.
|
1278 |
|
1279 | - startOfDay
|
1280 |
|
1281 | Supply a Date Object and this will return a new date object with milliseconds, seconds, minutes, and hours zeroed out.
|
1282 |
|
1283 |
|
1284 |
|
1285 | - startOfWeek
|
1286 |
|
1287 | Supply a Date Object and this will return a new date object set to the beginning of the week that the supplied date landed in.
|
1288 | ```
|
1289 | let testDate = utcDate.daysFromNow(22);
|
1290 | let startOfWeek = utcDate.startOfWeek(testDate);
|
1291 | startOfWeek.toISOString() => 1970-01-18T00:00:00.000Z
|
1292 | ```
|
1293 |
|
1294 | - startOfMonth
|
1295 |
|
1296 | Supply a Date Object and this will return a new date object set to the beginning of the month that the supplied date landed in.
|
1297 | ```
|
1298 | let testDate = utcDate.daysFromNow(22);
|
1299 | utcDate.startOfMonth(testDate) => 1970-01-01T00:00:00.000Z
|
1300 | ```
|
1301 |
|
1302 | - startOfQuarter
|
1303 |
|
1304 | Supply a Date Object and this will return a new date object set to the beginning of the quarter that the supplied date landed in.
|
1305 |
|
1306 | ```
|
1307 | let testDate = utcDate.daysFromNow(2);
|
1308 | utcDate.startOfQuarter(testDate) => 1970-01-01T00:00:00.000Z
|
1309 | ```
|
1310 |
|
1311 | ```
|
1312 | let testDate = utcDate.daysFromNow(100);
|
1313 | utcDate.startOfQuarter(testDate) => 1970-04-01T00:00:00.000Z
|
1314 | ```
|
1315 |
|
1316 | ```
|
1317 | let testDate = utcDate.daysFromNow(270);
|
1318 | utcDate.startOfQuarter(testDate) => 1970-07-01T00:00:00.000Z
|
1319 | ```
|
1320 |
|
1321 | ```
|
1322 | let testDate = utcDate.daysFromNow(300);
|
1323 | utcDate.startOfQuarter(testDate) => 1970-10-01T00:00:00.000Z
|
1324 | ```
|
1325 |
|
1326 | - startOfYear
|
1327 |
|
1328 | Supply a Date Object and this will return a new date object set to the beginning of the year that the supplied date landed in.
|
1329 |
|
1330 | ```
|
1331 | let testDate = utcDate.daysFromNow(300);
|
1332 | utcDate.startOfYear(testDate) => 1970-01-01T00:00:00.000Z
|
1333 | ```
|
1334 |
|
1335 | ```
|
1336 | let testDate = utcDate.daysFromNow(366);
|
1337 | utcDate.startOfYear(testDate) => 1971-01-01T00:00:00.000Z
|
1338 | ```
|
1339 |
|
1340 | ### Tokenization
|
1341 |
|
1342 | The examples below assume the following constants are declared.
|
1343 |
|
1344 | ```
|
1345 | const { generate, create, timeCoded, timeDecoded } = Glad.token;
|
1346 | ```
|
1347 |
|
1348 | ** Uniqueness **
|
1349 |
|
1350 | The probability is extremely low that duplicate tokens will be created if you are generating long enough tokens. However, it can happen.
|
1351 |
|
1352 | Uniqueness is not built in. It's up to you to ensure uniqueness. If uniqueness is crucial to your use case, please use something more robust or add to the token using unique things such as a database record id.
|
1353 |
|
1354 | ** Radix **
|
1355 |
|
1356 | The default Radix is url safe so your tokens can safely be used in URLs.
|
1357 | `ABCDEFGHIJKLMNOPQRSTUVWXYZ-0987654321_abcdefghijklmnopqrstuvwxyz`
|
1358 |
|
1359 | You can use your own radix or view the ones available under `Glad.token.radixes`.
|
1360 |
|
1361 | ** create a 6 character token **
|
1362 |
|
1363 | `generate(6);`
|
1364 |
|
1365 | ** create a 12 character token **
|
1366 |
|
1367 | `generate(12);`
|
1368 |
|
1369 |
|
1370 | ** create a new tokenizer from a provided radix **
|
1371 |
|
1372 | ```
|
1373 | let myTokenizer = create('0123456789');
|
1374 | myTokenizer.generate(6) => 6 characters, only digits
|
1375 | ```
|
1376 |
|
1377 | ** create a TimeEncoded Token **
|
1378 |
|
1379 | `let timeToken = timeCoded()`
|
1380 |
|
1381 | ** decode a TimeEncoded Token **
|
1382 |
|
1383 | `let tokenCreatedAt = timeDecoded(timeToken)`
|
1384 |
|
1385 |
|
1386 |
|
1387 | ---
|
1388 |
|
1389 | ** fig s1 ** - Supported Latin Chars
|
1390 | ```
|
1391 | 'À' => 'A'
|
1392 | 'Á' => 'A'
|
1393 | 'Â' => 'A'
|
1394 | 'Ã' => 'A'
|
1395 | 'Ä' => 'A'
|
1396 | 'Å' => 'A'
|
1397 | 'Æ' => 'Ae'
|
1398 | 'Ç' => 'C'
|
1399 | 'È' => 'E'
|
1400 | 'É' => 'E'
|
1401 | 'Ê' => 'E'
|
1402 | 'Ë' => 'E'
|
1403 | 'Ì' => 'I'
|
1404 | 'Í' => 'I'
|
1405 | 'Î' => 'I'
|
1406 | 'Ï' => 'I'
|
1407 | 'Ð' => 'D'
|
1408 | 'Ñ' => 'N'
|
1409 | 'Ò' => 'O'
|
1410 | 'Ó' => 'O'
|
1411 | 'Ô' => 'O'
|
1412 | 'Õ' => 'O'
|
1413 | 'Ö' => 'O'
|
1414 | 'Ő' => 'O'
|
1415 | 'Ø' => 'O'
|
1416 | 'Ù' => 'U'
|
1417 | 'Ú' => 'U'
|
1418 | 'Û' => 'U'
|
1419 | 'Ü' => 'U'
|
1420 | 'Ű' => 'U'
|
1421 | 'Ý' => 'Y'
|
1422 | 'Þ' => 'Th'
|
1423 | 'ß' => 'ss'
|
1424 | 'à' => 'a'
|
1425 | 'á' => 'a',
|
1426 | 'â' => 'a'
|
1427 | 'ã' => 'a'
|
1428 | 'ä' => 'a'
|
1429 | 'å' => 'a'
|
1430 | 'æ' => 'ae'
|
1431 | 'ç' => 'c'
|
1432 | 'è' => 'e',
|
1433 | 'é' => 'e'
|
1434 | 'ê' => 'e'
|
1435 | 'ë' => 'e'
|
1436 | 'ì' => 'i'
|
1437 | 'í' => 'i'
|
1438 | 'î' => 'i'
|
1439 | 'ï' => 'i'
|
1440 | 'ð' => 'd'
|
1441 | 'ñ' => 'n'
|
1442 | 'ò' => 'o'
|
1443 | 'ó' => 'o'
|
1444 | 'ô' => 'o'
|
1445 | 'õ' => 'o'
|
1446 | 'ö' => 'o'
|
1447 | 'ő' => 'o'
|
1448 | 'ø' => 'o'
|
1449 | 'ù' => 'u'
|
1450 | 'ú' => 'u'
|
1451 | 'û' => 'u'
|
1452 | 'ü' => 'u'
|
1453 | 'ű' => 'u'
|
1454 | 'ý' => 'y'
|
1455 | 'þ' => 'th'
|
1456 | 'ÿ' => 'y'
|
1457 | ```
|
1458 |
|
1459 | fig s2 Greek character conversion
|
1460 | ```
|
1461 | 'α' => 'a'
|
1462 | 'β' => 'b'
|
1463 | 'γ' => 'g'
|
1464 | 'δ' => 'd'
|
1465 | 'ε' => 'e'
|
1466 | 'ζ' => 'z'
|
1467 | 'η' => 'h'
|
1468 | 'θ' => '8'
|
1469 | 'ι' => 'i'
|
1470 | 'κ' => 'k'
|
1471 | 'λ' => 'l'
|
1472 | 'μ' => 'm'
|
1473 | 'ν' => 'n'
|
1474 | 'ξ' => '3'
|
1475 | 'ο' => 'o'
|
1476 | 'π' => 'p'
|
1477 | 'ρ' => 'r'
|
1478 | 'σ' => 's'
|
1479 | 'τ' => 't'
|
1480 | 'υ' => 'y'
|
1481 | 'φ' => 'f'
|
1482 | 'χ' => 'x'
|
1483 | 'ψ' => 'ps'
|
1484 | 'ω' => 'w'
|
1485 | 'ά' => 'a'
|
1486 | 'έ' => 'e'
|
1487 | 'ί' => 'i'
|
1488 | 'ό' => 'o'
|
1489 | 'ύ' => 'y'
|
1490 | 'ή' => 'h'
|
1491 | 'ώ' => 'w'
|
1492 | 'ς' => 's'
|
1493 | 'ϊ' => 'i'
|
1494 | 'ΰ' => 'y'
|
1495 | 'ϋ' => 'y'
|
1496 | 'ΐ' => 'i'
|
1497 | 'Α' => 'A'
|
1498 | 'Β' => 'B'
|
1499 | 'Γ' => 'G'
|
1500 | 'Δ' => 'D'
|
1501 | 'Ε' => 'E'
|
1502 | 'Ζ' => 'Z'
|
1503 | 'Η' => 'H'
|
1504 | 'Θ' => '8'
|
1505 | 'Ι' => 'I'
|
1506 | 'Κ' => 'K'
|
1507 | 'Λ' => 'L'
|
1508 | 'Μ' => 'M'
|
1509 | 'Ν' => 'N'
|
1510 | 'Ξ' => '3'
|
1511 | 'Ο' => 'O'
|
1512 | 'Π' => 'P'
|
1513 | 'Ρ' => 'R'
|
1514 | 'Σ' => 'S'
|
1515 | 'Τ' => 'T'
|
1516 | 'Υ' => 'Y'
|
1517 | 'Φ' => 'F'
|
1518 | 'Χ' => 'X'
|
1519 | 'Ψ' => 'PS'
|
1520 | 'Ω' => 'W'
|
1521 | 'Ά' => 'A'
|
1522 | 'Έ' => 'E'
|
1523 | 'Ί' => 'I'
|
1524 | 'Ό' => 'O'
|
1525 | 'Ύ' => 'Y'
|
1526 | 'Ή' => 'H'
|
1527 | 'Ώ' => 'W'
|
1528 | 'Ϊ' => 'I'
|
1529 | 'Ϋ' => 'Y
|
1530 | ```
|
1531 |
|
1532 | fig s3 Turkish character conversion
|
1533 | ```
|
1534 | 'ş' => 's'
|
1535 | 'Ş' => 'S'
|
1536 | 'ı' => 'i'
|
1537 | 'İ' => 'I'
|
1538 | 'ğ' => 'g'
|
1539 | 'Ğ' => 'G'
|
1540 | ```
|
1541 |
|
1542 | fig s4 Russian character conversion
|
1543 | ```
|
1544 | 'а' => 'a'
|
1545 | 'б' => 'b'
|
1546 | 'в' => 'v'
|
1547 | 'г' => 'g'
|
1548 | 'д' => 'd'
|
1549 | 'е' => 'e'
|
1550 | 'ё' => 'yo'
|
1551 | 'ж' => 'zh'
|
1552 | 'з' => 'z'
|
1553 | 'и' => 'i'
|
1554 | 'й' => 'j'
|
1555 | 'к' => 'k'
|
1556 | 'л' => 'l'
|
1557 | 'м' => 'm'
|
1558 | 'н' => 'n'
|
1559 | 'о' => 'o'
|
1560 | 'п' => 'p'
|
1561 | 'р' => 'r'
|
1562 | 'с' => 's'
|
1563 | 'т' => 't'
|
1564 | 'у' => 'u'
|
1565 | 'ф' => 'f'
|
1566 | 'х' => 'h'
|
1567 | 'ц' => 'c',
|
1568 | 'ч' => 'ch'
|
1569 | 'ш' => 'sh'
|
1570 | 'щ' => 'sh'
|
1571 | 'ъ' => 'u'
|
1572 | 'ы' => 'y'
|
1573 | 'э' => 'e'
|
1574 | 'ю' => 'yu'
|
1575 | 'я' => 'ya'
|
1576 | 'А' => 'A'
|
1577 | 'Б' => 'B'
|
1578 | 'В' => 'V'
|
1579 | 'Г' => 'G'
|
1580 | 'Д' => 'D'
|
1581 | 'Е' => 'E'
|
1582 | 'Ё' => 'Yo'
|
1583 | 'Ж' => 'Zh'
|
1584 | 'З' => 'Z'
|
1585 | 'И' => 'I'
|
1586 | 'Й' => 'J'
|
1587 | 'К' => 'K'
|
1588 | 'Л' => 'L'
|
1589 | 'М' => 'M'
|
1590 | 'Н' => 'N'
|
1591 | 'О' => 'O'
|
1592 | 'П' => 'P'
|
1593 | 'Р' => 'R'
|
1594 | 'С' => 'S'
|
1595 | 'Т' => 'T'
|
1596 | 'У' => 'U'
|
1597 | 'Ф' => 'F'
|
1598 | 'Х' => 'H'
|
1599 | 'Ц' => 'C'
|
1600 | 'Ч' => 'Ch'
|
1601 | 'Ш' => 'Sh'
|
1602 | 'Щ' => 'Sh'
|
1603 | 'Ъ' => 'U'
|
1604 | 'Ы' => 'Y'
|
1605 | 'Ь' => 'Ь'
|
1606 | 'Э' => 'E'
|
1607 | 'Ю' => 'Yu'
|
1608 | 'Я' => 'Ya'
|
1609 | ```
|
1610 | <br>
|
1611 |
|
1612 | ## Using Debug.js
|
1613 |
|
1614 | Running `DEBUG=glad* node index.js` will log most of the Glad.js internals.
|
1615 |
|
1616 | ## Other
|
1617 |
|
1618 | #### How should I handle Cache Warming?
|
1619 |
|
1620 | Sometimes you'll want to warm your controller#action caches.
|
1621 | This can be achieved a few ways.
|
1622 |
|
1623 | #### Using The API Itself
|
1624 |
|
1625 | Start your server, then automate requests to it by using a library such as unirest in conjunction with glad-cli's run command. Below is an example of what warming some popular city metrics might look like.
|
1626 |
|
1627 | ```js
|
1628 | const unirest = require('unirest');
|
1629 |
|
1630 | module.exports = function () {
|
1631 | CityMetric.find()
|
1632 | .sort('popularity')
|
1633 | .select({slug: 1})
|
1634 | .limit(500)
|
1635 | .then(cities => {
|
1636 | const promises = cities.map(city => {
|
1637 | return new Promise( (resolve, reject) => {
|
1638 | return unirest.get(`http://some-url-or-localhost/api/city-metrics/${city.slug}`).end(resolve);
|
1639 | });
|
1640 | });
|
1641 | Promise.all(promises).then(() => process.exit(0));
|
1642 | });
|
1643 | };
|
1644 | ```
|
1645 |
|
1646 | ```sh
|
1647 | # Execute the run command with the path to the warming script.
|
1648 | glad run warming/channel-metrics
|
1649 | ```
|
1650 |
|
1651 | #### Working directly with redis or Glad.cache
|
1652 |
|
1653 | If you would prefer to warm your caches via working directly with Redis, or `Glad.cache`, you can do that as well. However, we won't cover this topic right here. If you want to work directly with redis, please read about this in the redis documentation. If you would like to read about how to work with Glad.cache, please read the section pertaining to this. |
\ | No newline at end of file |