Comparing version 0.9.2 to 1.0.0
650
lib/cache.js
// cache functions | ||
'use strict' | ||
// node | ||
import fs from 'node:fs' | ||
// citizen | ||
import helpers from './helpers.js' | ||
const | ||
fs = require('fs'), | ||
helpers = require('./helpers') | ||
module.exports = { | ||
public: { | ||
clear : clear, | ||
exists : exists, | ||
get : get, | ||
set : set | ||
}, | ||
citizen: { | ||
getController : getController, | ||
setController : setController, | ||
setRoute : setRoute | ||
} | ||
} | ||
const newTimer = (args) => { | ||
let extendBy = CTZN.cache[args.scope]?.[args.key] ? CTZN.cache[args.scope][args.key].lastAccessed + CTZN.cache[args.scope][args.key].lifespan - Date.now() : false | ||
function newTimer(args) { | ||
var extendBy = CTZN.cache[args.scope] && CTZN.cache[args.scope][args.key] ? CTZN.cache[args.scope][args.key].lastAccessed + CTZN.cache[args.scope][args.key].lifespan - Date.now() : false, | ||
log = '' | ||
if ( extendBy !== false ) { | ||
if ( extendBy > 0 ) { | ||
CTZN.cache[args.scope][args.key].timer = setTimeout( function () { | ||
newTimer({ | ||
scope: args.scope, | ||
key: args.key | ||
}) | ||
newTimer(args) | ||
}, extendBy) | ||
helpers.log({ | ||
label: args.scope + ' cache item extended by ' + ( extendBy / 60000 ) + ' minutes', | ||
label: 'Cache extended: ' + args.key, | ||
content: { | ||
scope: args.scope, | ||
key: args.key | ||
key: args.key, | ||
extension: ( extendBy / 60000 ) + ' minutes' | ||
} | ||
@@ -45,12 +27,7 @@ }) | ||
helpers.log({ | ||
label: args.scope + ' cache item expired', | ||
content: { | ||
scope: args.scope, | ||
key: args.key | ||
} | ||
label: 'Cache expired: ' + args.key, | ||
content: args | ||
}) | ||
clear({ | ||
scope: args.scope, | ||
key: args.key | ||
}) | ||
args.log = false | ||
clear(args) | ||
} | ||
@@ -61,59 +38,15 @@ } | ||
function newControllerTimer(args) { | ||
var extendBy = CTZN.cache.controllers ? CTZN.cache.controllers[args.controller][args.action][args.view][args.route].lastAccessed + CTZN.cache.controllers[args.controller][args.action][args.view][args.route].lifespan - Date.now() : false, | ||
log = '' | ||
const newRouteTimer = (args) => { | ||
let extendBy = CTZN.cache.routes[args.route][args.contentType].lastAccessed + CTZN.cache.routes[args.route][args.contentType].lifespan - Date.now() | ||
if ( extendBy !== false ) { | ||
if ( extendBy > 0 ) { | ||
CTZN.cache.controllers[args.controller][args.action][args.view][args.route].timer = setTimeout( function () { | ||
newControllerTimer({ | ||
controller: args.controller, | ||
action: args.action, | ||
view: args.view, | ||
route: args.route | ||
}) | ||
}, extendBy) | ||
helpers.log({ | ||
label: 'controller cache item extended ' + ( extendBy / 60000 ) + ' minutes', | ||
content: { | ||
controller: args.controller, | ||
action: args.action, | ||
view: args.view, | ||
route: args.route | ||
} | ||
}) | ||
} else { | ||
helpers.log({ | ||
label: 'controller cache item expired', | ||
content: { | ||
controller: args.controller, | ||
action: args.action, | ||
view: args.view, | ||
route: args.route | ||
} | ||
}) | ||
clear({ | ||
controller: args.controller, | ||
action: args.action, | ||
view: args.view, | ||
route: args.route | ||
}) | ||
} | ||
} | ||
} | ||
function newRouteTimer(args) { | ||
var extendBy = CTZN.cache.routes[args.route].lastAccessed + CTZN.cache.routes[args.route].lifespan - Date.now() | ||
if ( extendBy > 0 ) { | ||
CTZN.cache.routes[args.route].timer = setTimeout( function () { | ||
newRouteTimer({ | ||
route: args.route | ||
}) | ||
CTZN.cache.routes[args.route][args.contentType].timer = setTimeout( function () { | ||
newRouteTimer(args) | ||
}, extendBy) | ||
helpers.log({ | ||
label: 'route cache item extended ' + ( extendBy / 60000 ) + ' minutes', | ||
label: 'Route cache extended: ' + args.route, | ||
content: { | ||
route: args.route | ||
route: args.route, | ||
contentType: args.contentType, | ||
extension: ( extendBy / 60000 ) + ' minutes' | ||
} | ||
@@ -123,10 +56,7 @@ }) | ||
helpers.log({ | ||
label: 'route cache item expired', | ||
content: { | ||
route: args.route | ||
} | ||
label: 'Route cache expired: ' + args.route, | ||
content: args | ||
}) | ||
clear({ | ||
route: args.route | ||
}) | ||
args.log = false | ||
clear(args) | ||
} | ||
@@ -136,13 +66,12 @@ } | ||
function set(options) { | ||
var timer, | ||
scope = options.scope || 'app', | ||
key = options.key || options.file, | ||
value, | ||
stats, | ||
lifespan = options.lifespan || CTZN.config.citizen.cache.application.lifespan, | ||
enableCache = ( CTZN.config.citizen.mode !== 'development' && CTZN.config.citizen.cache.application.enable ) || ( CTZN.config.citizen.mode === 'development' && CTZN.config.citizen.development.enableCache && CTZN.config.citizen.cache.application.enable ) ? true : false | ||
const set = (options) => { | ||
// If caching is enabled, proceed. | ||
if ( options.file ? CTZN.config.citizen.cache.static.enabled : CTZN.config.citizen.cache.application.enabled ) { | ||
let timer = false, | ||
scope = options.scope || 'app', | ||
key = options.key || options.file, | ||
value, | ||
stats, | ||
lifespan = options.lifespan || options.file ? CTZN.config.citizen.cache.static.lifespan : CTZN.config.citizen.cache.application.lifespan | ||
if ( enableCache ) { | ||
if ( !isNaN(lifespan) ) { | ||
@@ -153,3 +82,2 @@ // Convert minutes to milliseconds | ||
options.overwrite = options.overwrite || CTZN.config.citizen.cache.application.overwrite | ||
options.resetOnAccess = options.resetOnAccess || CTZN.config.citizen.cache.application.resetOnAccess | ||
@@ -159,6 +87,6 @@ options.encoding = options.encoding || CTZN.config.citizen.cache.application.encoding | ||
if ( scope !== 'controllers' && scope !== 'routes' && scope !== 'files' ) { | ||
if ( scope !== 'routes' && scope !== 'files' ) { | ||
CTZN.cache[scope] = CTZN.cache[scope] || {} | ||
} else { | ||
throw new Error('cache.set(): The terms "controllers", "routes", and "files" are reserved cache scope names. Please choose a different name for your custom cache scope.') | ||
throw new Error('cache.set(): The terms "routes" and "files" are reserved cache scope names. Please choose a different name for your custom cache scope.') | ||
} | ||
@@ -172,3 +100,3 @@ | ||
if ( options.value && !options.file ) { | ||
if ( !CTZN.cache[scope][key] || ( CTZN.cache[scope][key] && options.overwrite ) ) { | ||
if ( !CTZN.cache[scope][key] ) { | ||
if ( lifespan !== 'application' ) { | ||
@@ -203,3 +131,5 @@ timer = setTimeout( function () { | ||
} else { | ||
throw new Error('cache.set(): An cache item using the specified key (\'' + options.key + '\') already exists. If your intention is to overwrite the existing cache, you have to pass the overwrite flag explicitly.') | ||
options.log = false | ||
clear(options) | ||
set(options) | ||
} | ||
@@ -210,3 +140,3 @@ // If a file path is provided, we need to read the file and perhaps parse it | ||
if ( !CTZN.cache.files[key] || ( CTZN.cache.files[key] && options.overwrite ) ) { | ||
if ( !CTZN.cache.files[key] ) { | ||
if ( lifespan !== 'application' ) { | ||
@@ -316,3 +246,5 @@ timer = setTimeout( function () { | ||
} else { | ||
throw new Error('cache.set(): A cache item containing the specified file (\'' + options.file + '\') already exists. If your intention is to overwrite the existing cache, you have to pass the overwrite option explicitly.') | ||
options.log = false | ||
clear(options) | ||
set(options) | ||
} | ||
@@ -324,159 +256,37 @@ } | ||
function setController(options) { | ||
var timer, | ||
enableCache = ( CTZN.config.citizen.mode !== 'development' && CTZN.config.citizen.cache.application.enable ) || ( CTZN.config.citizen.mode === 'development' && CTZN.config.citizen.development.enableCache && CTZN.config.citizen.cache.application.enable ) ? true : false | ||
if ( enableCache ) { | ||
CTZN.cache.controllers = CTZN.cache.controllers || {} | ||
CTZN.cache.controllers[options.controller] = CTZN.cache.controllers[options.controller] || {} | ||
CTZN.cache.controllers[options.controller][options.action] = CTZN.cache.controllers[options.controller][options.action] || {} | ||
CTZN.cache.controllers[options.controller][options.action][options.view] = CTZN.cache.controllers[options.controller][options.action][options.view] || {} | ||
if ( !CTZN.cache.controllers[options.controller][options.action][options.view][options.route] || options.overwrite ) { | ||
if ( options.lifespan !== 'application' ) { | ||
timer = setTimeout( function () { | ||
newControllerTimer({ | ||
controller: options.controller, | ||
action: options.action, | ||
view: options.view, | ||
route: options.route, | ||
lifespan: options.lifespan * 60000 | ||
}) | ||
}, options.lifespan * 60000) | ||
} | ||
CTZN.cache.controllers[options.controller][options.action][options.view][options.route] = { | ||
controller: options.controller, | ||
action: options.action, | ||
view: options.view, | ||
route: options.route, | ||
context: options.context, | ||
render: options.render, | ||
timer: timer, | ||
lifespan: options.lifespan * 60000 || 'application', | ||
resetOnAccess: options.resetOnAccess | ||
} | ||
helpers.log({ | ||
label: 'Controller cached', | ||
content: { | ||
controller: options.controller, | ||
action: options.action, | ||
view: options.view, | ||
const setRoute = (options) => { | ||
CTZN.cache.routes = CTZN.cache.routes || {} | ||
// If the route isn't already cached, cache it. | ||
if ( !CTZN.cache.routes[options.route] || !CTZN.cache.routes[options.route][options.contentType] ) { | ||
options.timer = false | ||
if ( options.lifespan !== 'application' ) { | ||
options.lifespan = options.lifespan * 60000 | ||
options.timer = setTimeout( function () { | ||
newRouteTimer({ | ||
route: options.route, | ||
lifespan: options.lifespan, | ||
resetOnAccess: options.resetOnAccess | ||
} | ||
}) | ||
} else { | ||
throw new Error('cache.set(): A cache item containing the specified controller/action/view/route combination already exists. If your intention is to overwrite the existing cache, you have to pass the overwrite option explicitly.\n controller: ' + options.controller + '\n action: ' + options.action + '\n view: ' + options.view + '\n route: ' + options.route) | ||
contentType: options.contentType | ||
}) | ||
}, options.lifespan) | ||
} | ||
} | ||
} | ||
CTZN.cache.routes[options.route] = CTZN.cache.routes[options.route] || {} | ||
CTZN.cache.routes[options.route][options.contentType] = options | ||
function setRoute(options) { | ||
var timer, | ||
enableCache = ( CTZN.config.citizen.mode !== 'development' && CTZN.config.citizen.cache.application.enable ) || ( CTZN.config.citizen.mode === 'development' && CTZN.config.citizen.development.enableCache && CTZN.config.citizen.cache.application.enable ) ? true : false | ||
if ( enableCache ) { | ||
CTZN.cache.routes = CTZN.cache.routes || {} | ||
if ( !CTZN.cache.routes[options.route] || ( CTZN.cache.routes[options.route] && options.overwrite ) ) { | ||
if ( options.lifespan && options.lifespan !== 'application' ) { | ||
timer = setTimeout( function () { | ||
newRouteTimer({ | ||
route: options.route, | ||
lifespan: options.lifespan * 60000 | ||
}) | ||
}, options.lifespan * 60000) | ||
} | ||
CTZN.cache.routes[options.route] = { | ||
route: options.route, | ||
contentType: options.contentType, | ||
render: { | ||
identity: options.render.identity, | ||
gzip: options.render.gzip, | ||
deflate: options.render.deflate | ||
}, | ||
timer: timer, | ||
context: options.context, | ||
lastModified: options.lastModified, | ||
lifespan: options.lifespan * 60000 || 'application', | ||
resetOnAccess: options.resetOnAccess | ||
} | ||
helpers.log({ | ||
label: 'Route cached', | ||
content: { | ||
route: options.route, | ||
contentType: options.contentType, | ||
lifespan: options.lifespan, | ||
resetOnAccess: options.resetOnAccess | ||
} | ||
}) | ||
} else { | ||
throw new Error('cache.set(): A cache containing the specified route (\'' + options.route + '\') already exists. If your intention is to overwrite the existing cache, you have to pass the overwrite option explicitly.') | ||
} | ||
} | ||
} | ||
function exists(options) { | ||
// If both a scope and key are provided, check the scope for the specified key | ||
if ( options.key && options.scope ) { | ||
if ( CTZN.cache[options.scope] && CTZN.cache[options.scope][options.key] ) { | ||
return true | ||
} | ||
// If only a key is provided, check the app scope (default) for the specified key | ||
} else if ( options.key ) { | ||
if ( CTZN.cache.app[options.key] ) { | ||
return true | ||
} | ||
// If a file path is provided, check the files scope using the path as the key | ||
} else if ( options.file ) { | ||
if ( CTZN.cache.files[options.key || options.file] ) { | ||
return true | ||
} | ||
// If only a scope is provided, check if the specified scope has members | ||
} else if ( options.scope ) { | ||
if ( CTZN.cache[options.scope] && helpers.size(CTZN.cache[options.scope]) ) { | ||
return true | ||
} | ||
// If a controller, action, and view are provided, check if the controllers scope | ||
// has that view | ||
} else if ( options.controller && options.action && options.view && options.route ) { | ||
if ( CTZN.cache.controllers && CTZN.cache.controllers[options.controller] && CTZN.cache.controllers[options.controller][options.action] && CTZN.cache.controllers[options.controller][options.action][options.view] && CTZN.cache.controllers[options.controller][options.action][options.view][options.route] ) { | ||
return true | ||
} | ||
// If a controller and action are provided, check if the controllers scope has them | ||
} else if ( options.controller && options.action ) { | ||
if ( CTZN.cache.controllers[options.controller] && CTZN.cache.controllers[options.controller][options.action] && helpers.size(CTZN.cache.controllers[options.controller][options.action]) ) { | ||
return true | ||
} | ||
// If only a controller is provided, check if the controllers scope has it | ||
} else if ( options.controller ) { | ||
if ( CTZN.cache.controllers[options.controller] && helpers.size(CTZN.cache.controllers[options.controller]) ) { | ||
return true | ||
} | ||
// If only a route is provided, check if the controllers scope has it | ||
} else if ( options.route ) { | ||
if ( CTZN.cache.routes[options.route] && helpers.size(CTZN.cache.routes[options.route]) ) { | ||
return true | ||
} | ||
// Throw an error if the required arguments aren't provided | ||
helpers.log({ | ||
label: 'Route cached', | ||
content: options | ||
}) | ||
// If the route is already cached, clear and set the new cache. | ||
} else { | ||
throw new Error('cache.exists(): Missing arguments. You must provide a cache key, cache scope, or both options.') | ||
clear(options) | ||
setRoute(options) | ||
} | ||
return false | ||
} | ||
function get(options) { | ||
var scope = options.scope || 'app', | ||
const get = (options) => { | ||
let scope = options.scope || 'app', | ||
resetOnAccess, | ||
matchingKeys = {}, | ||
output = options.output || 'value' | ||
matchingKeys = {} | ||
@@ -499,16 +309,14 @@ // Return the provided key from the specified scope. This first condition matches | ||
helpers.log({ | ||
label: 'Retrieving from cache', | ||
label: 'Retrieved key from cache: ' + options.key, | ||
content: options | ||
}) | ||
if ( scope === 'routes' ) { | ||
return CTZN.cache[scope][options.key] | ||
} else { | ||
return helpers.copy(CTZN.cache[scope][options.key].value) | ||
} | ||
return helpers.copy(CTZN.cache[scope][options.key].value) | ||
} else { | ||
return false | ||
} | ||
// If a file path is provided, check the files scope | ||
} else if ( options.file ) { | ||
if ( CTZN.cache.files && CTZN.cache.files[options.file] ) { | ||
options.output = options.output || 'value' | ||
@@ -521,19 +329,16 @@ // Reset the timer (or don't) based on the provided option. If the option isn't | ||
CTZN.cache.files[options.file].lastAccessed = Date.now() | ||
helpers.log({ | ||
label: 'File cache accessed', | ||
content: options | ||
}) | ||
} | ||
helpers.log({ | ||
label: 'Retrieving file from cache', | ||
label: 'Retrieved file from cache: ' + options.file, | ||
content: options | ||
}) | ||
if ( output !== 'all' ) { | ||
return CTZN.cache.files[options.file][output] | ||
if ( options.output !== 'all' ) { | ||
return CTZN.cache.files[options.file][options.output] | ||
} else { | ||
return CTZN.cache.files[options.file] | ||
} | ||
} else { | ||
return false | ||
} | ||
@@ -543,22 +348,22 @@ // If only a scope is provided, return the entire scope if it exists and it has | ||
} else if ( options.scope ) { | ||
if ( CTZN.cache[options.scope] && helpers.size(CTZN.cache[options.scope]) ) { | ||
if ( CTZN.cache[options.scope] && Object.keys(CTZN.cache[options.scope]).length ) { | ||
matchingKeys[options.scope] = {} | ||
for ( var key in CTZN.cache[options.scope] ) { | ||
if ( CTZN.cache[options.scope].hasOwnProperty(key) ) { | ||
// Reset the timer (or don't) based on the provided option. If the option isn't | ||
// provided, use the stored resetOnAccess value. | ||
resetOnAccess = options.resetOnAccess || CTZN.cache[scope][key].resetOnAccess | ||
Object.keys(CTZN.cache[options.scope]).forEach( item => { | ||
// Reset the timer (or don't) based on the provided option. If the option isn't | ||
// provided, use the stored resetOnAccess value. | ||
resetOnAccess = options.resetOnAccess || CTZN.cache[scope][item].resetOnAccess | ||
matchingKeys[options.scope][key] = get({ scope: options.scope, key: key, resetOnAccess: resetOnAccess }) | ||
} | ||
} | ||
matchingKeys[options.scope][item] = get({ scope: options.scope, key: item, resetOnAccess: resetOnAccess }) | ||
}) | ||
} | ||
if ( helpers.size(matchingKeys) ) { | ||
if ( Object.keys(matchingKeys).length ) { | ||
helpers.log({ | ||
label: 'Retrieving from cache', | ||
label: 'Retrieved scope from cache: ' + options.scope, | ||
content: options | ||
}) | ||
return helpers.copy(matchingKeys[options.scope]) | ||
} else { | ||
return false | ||
} | ||
@@ -569,43 +374,19 @@ // Throw an error if the required arguments aren't provided | ||
} | ||
return false | ||
} | ||
function getController(options) { | ||
var lifespan, | ||
resetOnAccess | ||
if ( CTZN.cache.controllers && CTZN.cache.controllers[options.controller] && CTZN.cache.controllers[options.controller][options.action] && CTZN.cache.controllers[options.controller][options.action][options.view] && CTZN.cache.controllers[options.controller][options.action][options.view][options.route] ) { | ||
lifespan = options.lifespan || CTZN.cache.controllers[options.controller][options.action][options.view][options.route].lifespan | ||
resetOnAccess = options.resetOnAccess || CTZN.cache.controllers[options.controller][options.action][options.view][options.route].resetOnAccess | ||
if ( CTZN.cache.controllers[options.controller][options.action][options.view][options.route].timer && resetOnAccess ) { | ||
CTZN.cache.controllers[options.controller][options.action][options.view][options.route].lastAccessed = Date.now() | ||
helpers.log({ | ||
label: 'Cache timer accessed', | ||
content: { | ||
controller: options.controller, | ||
action: options.action, | ||
view: options.view, | ||
route: options.route, | ||
lifespan: lifespan, | ||
resetOnAccess: resetOnAccess | ||
} | ||
}) | ||
const getRoute = (options) => { | ||
if ( CTZN.cache.routes?.[options.route]?.[options.contentType] ) { | ||
if ( CTZN.cache.routes[options.route][options.contentType].timer && CTZN.cache.routes[options.route][options.contentType].resetOnAccess ) { | ||
CTZN.cache.routes[options.route][options.contentType].lastAccessed = Date.now() | ||
} | ||
helpers.log({ | ||
label: 'Retrieving controller from cache', | ||
content: { | ||
controller: options.controller, | ||
action: options.action, | ||
view: options.view, | ||
route: options.route, | ||
lifespan: lifespan, | ||
resetOnAccess: resetOnAccess | ||
} | ||
label: 'Retrieved route from cache: ' + options.route, | ||
content: options | ||
}) | ||
return CTZN.cache.controllers[options.controller][options.action][options.view][options.route] | ||
return CTZN.cache.routes[options.route][options.contentType] | ||
} else { | ||
return false | ||
} | ||
@@ -615,15 +396,59 @@ } | ||
function clear(options) { | ||
var scope = options ? options.scope : 'app' | ||
const clear = (options) => { | ||
let scope = options?.scope || 'app', | ||
log = options?.log === false ? options.log : true | ||
// If no options are provided, nuke it from orbit. It's the only way to be sure. | ||
if ( !options ) { | ||
CTZN.cache = {} | ||
Object.keys(CTZN.cache).forEach( item => { | ||
clear({ scope: item, log: false }) | ||
}) | ||
helpers.log({ | ||
content: 'App cache cleared' | ||
label: 'Cache cleared' | ||
}) | ||
// If a file attribute is provided, clear it from the files scope | ||
} else if ( options.file ) { | ||
if ( CTZN.cache.files?.[options.file] ) { | ||
if ( CTZN.cache.files[options.file].timer ) { | ||
clearTimeout(CTZN.cache.files[options.file].timer) | ||
} | ||
delete CTZN.cache.files[options.file] | ||
if ( log ) { | ||
helpers.log({ | ||
label: 'Cached file cleared', | ||
content: options | ||
}) | ||
} | ||
} | ||
// If only a route is provided, clear that route from the route cache | ||
} else if ( options.route ) { | ||
if ( CTZN.cache.routes?.[options.route] ) { | ||
if ( options.contentType && CTZN.cache.routes[options.route][options.contentType]?.timer ) { | ||
clearTimeout(CTZN.cache.routes[options.route][options.contentType].timer) | ||
delete CTZN.cache.routes[options.route][options.contentType] | ||
} else { | ||
Object.keys(CTZN.cache.routes[options.route]).map( contentType => { | ||
clearTimeout(CTZN.cache.routes[options.route][contentType].timer) | ||
delete CTZN.cache.routes[options.route][contentType] | ||
}) | ||
} | ||
if ( !Object.keys(CTZN.cache.routes[options.route]).length ) { | ||
delete CTZN.cache.routes[options.route] | ||
} | ||
if ( log ) { | ||
helpers.log({ | ||
label: 'Route cache cleared', | ||
content: { | ||
route: options.route, | ||
contentType: options.contentType | ||
} | ||
}) | ||
} | ||
} | ||
// If only a key is provided, remove that key from the app scope. | ||
// If a scope is also provided, remove the key from that scope. | ||
} else if ( options.key && ( options.scope || scope === 'app' ) ) { | ||
if ( CTZN.cache[scope] && CTZN.cache[scope][options.key] ) { | ||
if ( CTZN.cache[scope]?.[options.key] ) { | ||
if ( CTZN.cache[scope][options.key].timer ) { | ||
@@ -634,90 +459,8 @@ clearTimeout(CTZN.cache[scope][options.key].timer) | ||
// Delete the scope if it's empty | ||
if ( !helpers.size(CTZN.cache[scope]) ) { | ||
if ( !Object.keys(CTZN.cache[scope]).length ) { | ||
delete CTZN.cache[scope] | ||
} | ||
helpers.log({ | ||
label: scope + ' cache item cleared', | ||
content: options | ||
}) | ||
} | ||
// If a file attribute is provided, clear it from the files scope | ||
} else if ( options.file ) { | ||
if ( CTZN.cache.files && CTZN.cache.files[options.file] ) { | ||
if ( CTZN.cache.files[options.file].timer ) { | ||
clearTimeout(CTZN.cache.files[options.file].timer) | ||
} | ||
delete CTZN.cache.files[options.file] | ||
helpers.log({ | ||
label: 'Cached file cleared', | ||
content: options | ||
}) | ||
} | ||
// If a controller name is provided, clear the controller scope based on the | ||
// optionally provided action and view | ||
} else if ( options.controller ) { | ||
if ( CTZN.cache.controllers && CTZN.cache.controllers[options.controller] ) { | ||
if ( options.action && options.view && options.route ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action] && CTZN.cache.controllers[options.controller][options.action][options.view] && CTZN.cache.controllers[options.controller][options.action][options.view][options.route] ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action][options.view][options.route].timer ) { | ||
clearTimeout(CTZN.cache.controllers[options.controller][options.action][options.view][options.route].timer) | ||
} | ||
delete CTZN.cache.controllers[options.controller][options.action][options.view][options.route] | ||
helpers.log({ | ||
label: 'Controller/action/view/route cache cleared', | ||
content: options | ||
}) | ||
} | ||
} else if ( options.action && options.view ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action] && CTZN.cache.controllers[options.controller][options.action][options.view] ) { | ||
for ( var route in CTZN.cache.controllers[options.controller][options.action][options.view] ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action][options.view].hasOwnProperty(route) ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action][options.view][route].timer ) { | ||
clearTimeout(CTZN.cache.controllers[options.controller][options.action][options.view][route].timer) | ||
} | ||
} | ||
} | ||
delete CTZN.cache.controllers[options.controller][options.action][options.view] | ||
helpers.log({ | ||
label: 'Controller/action/view cache cleared', | ||
content: options | ||
}) | ||
} | ||
} else if ( options.action ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action] ) { | ||
for ( var view in CTZN.cache.controllers[options.controller][options.action] ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action].hasOwnProperty(view) ) { | ||
for ( var viewRoute in CTZN.cache.controllers[options.controller][options.action][view] ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action][view].hasOwnProperty(viewRoute) ) { | ||
if ( CTZN.cache.controllers[options.controller][options.action][view][viewRoute].timer ) { | ||
clearTimeout(CTZN.cache.controllers[options.controller][options.action][view][viewRoute].timer) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
delete CTZN.cache.controllers[options.controller][options.action] | ||
helpers.log({ | ||
label: 'Controller/action cache cleared', | ||
content: options | ||
}) | ||
} | ||
} else { | ||
for ( var action in CTZN.cache.controllers[options.controller] ) { | ||
if ( CTZN.cache.controllers[options.controller].hasOwnProperty(action) ) { | ||
for ( var actionView in CTZN.cache.controllers[options.controller][action] ) { | ||
if ( CTZN.cache.controllers[options.controller][action].hasOwnProperty(actionView) ) { | ||
for ( var viewRouteB in CTZN.cache.controllers[options.controller][action][actionView] ) { | ||
if ( CTZN.cache.controllers[options.controller][action][actionView].hasOwnProperty(viewRouteB) ) { | ||
if ( CTZN.cache.controllers[options.controller][action][actionView][viewRouteB].timer ) { | ||
clearTimeout(CTZN.cache.controllers[options.controller][action][actionView][viewRouteB].timer) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
delete CTZN.cache.controllers[options.controller] | ||
if ( log ) { | ||
helpers.log({ | ||
label: 'Controller cache item cleared', | ||
label: 'Cache cleared', | ||
content: options | ||
@@ -730,34 +473,24 @@ }) | ||
if ( CTZN.cache[options.scope] ) { | ||
if ( options.scope !== 'controllers' ) { | ||
for ( var property in CTZN.cache[options.scope] ) { | ||
if ( CTZN.cache[options.scope].hasOwnProperty(property) ) { | ||
if ( CTZN.cache[options.scope][property].timer ) { | ||
clearTimeout(CTZN.cache[options.scope][property].timer) | ||
switch ( options.scope ) { | ||
case 'routes': | ||
Object.keys(CTZN.cache.routes).forEach( route => { | ||
Object.keys(CTZN.cache.routes[route]).forEach( contentType => { | ||
clear({ route: route, contentType: contentType, log: false }) | ||
}) | ||
}) | ||
break | ||
default: | ||
Object.keys(CTZN.cache[options.scope]).forEach( item => { | ||
if ( CTZN.cache[options.scope][item].timer ) { | ||
clearTimeout(CTZN.cache[options.scope][item].timer) | ||
} | ||
} | ||
} | ||
} else { | ||
for ( var controller in CTZN.cache.controllers ) { | ||
if ( CTZN.cache.controllers.hasOwnProperty(controller) ) { | ||
clear({ controller: controller }) | ||
} | ||
} | ||
}) | ||
} | ||
delete CTZN.cache[options.scope] | ||
helpers.log({ | ||
label: options.scope + ' scope cache cleared', | ||
content: options | ||
}) | ||
} | ||
// If only a route is provided, clear that route from the route cache | ||
} else if ( options.route ) { | ||
if ( CTZN.cache.routes && CTZN.cache.routes[options.route] ) { | ||
if ( CTZN.cache.routes[options.route].timer ) { | ||
clearTimeout(CTZN.cache.routes[options.route].timer) | ||
if ( log ) { | ||
helpers.log({ | ||
label: 'Scope cache cleared', | ||
content: options | ||
}) | ||
} | ||
delete CTZN.cache.routes[options.route] | ||
helpers.log({ | ||
label: 'Route cache item cleared', | ||
content: options | ||
}) | ||
} | ||
@@ -769,1 +502,40 @@ // Throw an error if the required arguments aren't provided | ||
} | ||
const exists = (options) => { | ||
// If both a scope and key are provided, check the scope for the specified key | ||
if ( options.key && options.scope ) { | ||
if ( CTZN.cache[options.scope]?.[options.key] ) { | ||
return true | ||
} | ||
// If only a key is provided, check the app scope (default) for the specified key | ||
} else if ( options.key ) { | ||
if ( CTZN.cache.app[options.key] ) { | ||
return true | ||
} | ||
// If a file path is provided, check the files scope using the path as the key | ||
} else if ( options.file ) { | ||
if ( CTZN.cache.files[options.key || options.file] ) { | ||
return true | ||
} | ||
// If only a scope is provided, check if the specified scope has members | ||
} else if ( options.scope ) { | ||
if ( CTZN.cache[options.scope] && Object.keys(CTZN.cache[options.scope]).length ) { | ||
return true | ||
} | ||
// If only a route is provided, check if the route scope has it | ||
} else if ( options.route && options.contentType ) { | ||
if ( CTZN.cache.routes?.[options.route]?.[options.contentType] ) { | ||
return true | ||
} | ||
// Throw an error if the required arguments aren't provided | ||
} else { | ||
throw new Error('cache.exists(): Missing arguments. You must provide a cache key, cache scope, or both options.') | ||
} | ||
return false | ||
} | ||
export default { clear, exists, get, getRoute, set, setRoute } | ||
export { clear, exists, get, set } |
// core framework functions that might also be of use in the app | ||
'use strict' | ||
// node | ||
import fs from 'node:fs' | ||
import http from 'node:http' | ||
import util from 'node:util' | ||
const | ||
fs = require('fs'), | ||
util = require('util') | ||
module.exports = { | ||
copy : copy, | ||
extend : extend, | ||
log : log, | ||
size : size | ||
} | ||
function copy(object) { | ||
const copy = (object) => { | ||
var objectCopy | ||
if ( !object || typeof object === 'number' || typeof object === 'string' || typeof object === 'boolean' || typeof object === 'symbol' || typeof object === 'function' || object.constructor === Date ) { | ||
if ( !object || typeof object === 'number' || typeof object === 'string' || typeof object === 'boolean' || typeof object === 'symbol' || typeof object === 'function' || object.constructor === Date || object._onTimeout ) { // Node returns typeof === 'object' for setTimeout() | ||
objectCopy = object | ||
@@ -32,5 +24,3 @@ } else if ( Array.isArray(object) ) { | ||
for ( var property in objectCopy ) { | ||
if ( object.constructor === Object || Object(object) === object ) { | ||
objectCopy[property] = copy(object[property]) | ||
} | ||
objectCopy[property] = copy(object[property]) | ||
} | ||
@@ -45,3 +35,3 @@ } else { | ||
function extend(original, extension) { | ||
const extend = (original, extension) => { | ||
var mergedObject = Object.assign({}, original) || {} | ||
@@ -51,11 +41,9 @@ | ||
for ( var property in extension ) { | ||
if ( extension.hasOwnProperty(property) ) { | ||
if ( extension[property] && extension[property].constructor === Object ) { | ||
mergedObject[property] = extend(mergedObject[property], extension[property]) | ||
} else { | ||
mergedObject[property] = extension[property] | ||
} | ||
Object.keys(extension).forEach( item => { | ||
if ( extension[item] && extension[item].constructor === Object ) { | ||
mergedObject[item] = extend(mergedObject[item], extension[item]) | ||
} else { | ||
mergedObject[item] = copy(extension[item]) | ||
} | ||
} | ||
}) | ||
@@ -66,6 +54,6 @@ return mergedObject | ||
function log(options) { | ||
let type = options.type || 'status', | ||
toConsole = options.console || CTZN.config.citizen.mode === 'development' || ( type === 'request' && CTZN.config.citizen.log.console.request ) || ( type === 'error' && CTZN.config.citizen.log.console.error ) || ( type === 'status' && CTZN.config.citizen.log.console.status ), | ||
toFile = options.file || ( type === 'request' && CTZN.config.citizen.log.file.request ) || ( type === 'status' && CTZN.config.citizen.log.file.status ) || ( type === 'error' && CTZN.config.citizen.log.file.error ), | ||
const log = (options) => { | ||
let type = options.type || 'debug', | ||
toConsole = options.console || CTZN.config.citizen.mode === 'development', | ||
toFile = options.file || ( type === 'access' && CTZN.config.citizen.logs.access ) || ( type === 'error:client' && CTZN.config.citizen.logs.error?.client ) || ( type === 'error:server' && CTZN.config.citizen.logs.error?.server ) || ( type === 'debug' && CTZN.config.citizen.logs.debug ), | ||
depth = options.depth || CTZN.config.citizen.development.debug.depth, | ||
@@ -101,3 +89,3 @@ showHidden = options.showHidden || CTZN.config.citizen.development.debug.showHidden | ||
let content = type === 'request' || type === 'error' ? '' : '\n' | ||
let content = '\n' | ||
if ( options.content ) { | ||
@@ -107,9 +95,9 @@ switch ( typeof options.content ) { | ||
if ( options.content.length ) { | ||
content = options.content + '\n' | ||
content = '\n' + options.content + '\n' | ||
} else { | ||
content = '(empty string)\n' | ||
content = '\n(empty string)\n' | ||
} | ||
break | ||
case 'number': | ||
content = options.content + '\n' | ||
content = '\n' + options.content + '\n' | ||
break | ||
@@ -170,12 +158,12 @@ default: | ||
if ( options.content.length ) { | ||
content = options.content + '\n' | ||
content = '\n ' + options.content + '\n' | ||
} else { | ||
content = '(empty string)\n' | ||
content = '\n(empty string)\n' | ||
} | ||
break | ||
case 'number': | ||
content = options.content + '\n' | ||
content = '\n ' + options.content + '\n' | ||
break | ||
default: | ||
content = util.inspect(options.content, { depth: depth, colors: false, showHidden: showHidden }) + '\n' | ||
content = '\n ' + util.inspect(options.content, { depth: depth, colors: false, showHidden: showHidden }) + '\n' | ||
break | ||
@@ -196,3 +184,3 @@ } | ||
let file = options.file || 'citizen.log', | ||
let file = options.file || ( type === 'access' ? 'access.log' : 'error.log' ), | ||
log = dividerTop + label + content + dividerBottom | ||
@@ -203,6 +191,9 @@ fs.appendFile(CTZN.config.citizen.directories.logs + '/' + file, log, function (err) { | ||
case 'ENOENT': | ||
console.log('Error in app.log(): Unable to write to the log file because the specified log file path (' + CTZN.config.citizen.directories.logs + ') doesn\'t exist.') | ||
console.log('Error in app.log(): Unable to write to the log file because the specified log file path doesn\'t exist:\n\n') | ||
console.log(' ' + CTZN.config.citizen.directories.logs + '\n\n') | ||
console.log('Please set a valid file path in your citizen configuration.') | ||
break | ||
default: | ||
console.log('Error in app.log(): There was a problem writing to the log file (' + CTZN.config.citizen.directories.logs + '/' + file + ')') | ||
console.log('Error in app.log(): There was a problem writing to the log file:\n\n') | ||
console.log(' ' + CTZN.config.citizen.directories.logs + '/' + file + '\n\n') | ||
console.log(err) | ||
@@ -217,15 +208,8 @@ break | ||
function size(object) { | ||
var count = 0 | ||
const serverLogLabel = (statusCode, params, request) => { | ||
return statusCode + ' ' + http.STATUS_CODES[statusCode] + ' ' + request.method + ' ' + params.route.url + ' ' + request.remoteAddress + ' "' + request.headers['user-agent'] + '"' | ||
} | ||
if ( object === Object(object) ) { | ||
for ( var property in object ) { | ||
if ( object.hasOwnProperty(property) ) { | ||
count += 1 | ||
} | ||
} | ||
return count | ||
} else { | ||
throw new Error('app.size(): The supplied argument is not an object. size() only accepts objects as arguments.') | ||
} | ||
} | ||
export default { copy, extend, log, serverLogLabel } | ||
export { log } |
// router | ||
'use strict' | ||
// node | ||
import fs from 'node:fs/promises' | ||
const | ||
url = require('url') | ||
module.exports = { | ||
getRoute : getRoute, | ||
getUrlParams : getUrlParams | ||
} | ||
const staticMimeTypes = JSON.parse( | ||
await fs.readFile( | ||
new URL('../config/mimetypes.json', import.meta.url) | ||
) | ||
) | ||
function getRoute(urlToParse) { | ||
var parsed = url.parse(urlToParse), | ||
pathToParse = url.parse(urlToParse).pathname.replace(/\/\//g, '/'), | ||
publicControllerRegex = /^\/([A-Za-z0-9-_]+)\/?.*/, | ||
staticRegex = /^\/.*\.([A-Za-z0-9-_]+)$/, | ||
route = { | ||
parsed : parsed, | ||
url : parsed.href, | ||
pathname : pathToParse, | ||
controller : 'index', | ||
action : 'handler', | ||
chain : [{ controller: 'index', action: 'handler', view: 'index'}], | ||
renderer : 'index', | ||
descriptor : '', | ||
view : 'index', | ||
renderedView : 'index', | ||
ajax : false, | ||
format : 'html', | ||
show : 'default', | ||
task : 'default', | ||
type : 'default', | ||
isStatic : false | ||
} | ||
// pathname is necessary for citizen includes, which can be invoked using a URL-compliant route | ||
const parseRoute = (request, protocol, pathname) => { | ||
const url = new URL( ( ( request.headers.forwardedParsed?.proto || request.headers['x-forwarded-proto'] || protocol ) + '://' ) + ( request.headers.forwardedParsed?.host || request.headers['x-forwarded-host'] || request.headers.host ) + ( pathname || request.url ) ), | ||
publicControllerRegex = /^\/([A-Za-z0-9-_]+)\/?.*/, | ||
directRequestRegex = /^\/_([A-Za-z0-9-_]+)\/?.*/, | ||
staticRegex = /^\/.*\.([A-Za-z0-9-_]+)$/ | ||
let route = {} | ||
if ( CTZN.config.citizen.mimetypes[pathToParse.replace(staticRegex, '$1')] ) { | ||
if ( !staticMimeTypes[url.pathname.replace(staticRegex, '$1')] ) { | ||
route = { | ||
url : parsed.href, | ||
pathname : pathToParse, | ||
filePath : url.parse(urlToParse).pathname, | ||
extension : pathToParse.replace(staticRegex, '$1'), | ||
isStatic : true | ||
url : url.href, | ||
parsed : url, | ||
base : url.protocol + '//' + url.host, | ||
pathname : url.pathname, | ||
protocol : url.protocol.replace(':', ''), | ||
urlParams : getUrlParams(url.pathname), | ||
chain : {} | ||
} | ||
if ( CTZN.config.citizen.urlPaths.app !== '/' && route.filePath.indexOf(CTZN.config.citizen.urlPaths.app) === 0 ) { | ||
route.filePath = route.filePath.replace(CTZN.config.citizen.urlPaths.app, '') | ||
if ( publicControllerRegex.test(url.pathname) ) { | ||
route.controller = url.pathname.replace(/^\/([A-Za-z0-9-_]+)\/?.*/, '$1') | ||
} else { | ||
route.controller = 'index' | ||
} | ||
route.action = route.urlParams.action || 'handler' | ||
route.descriptor = route.urlParams[route.controller] || '' | ||
route.direct = directRequestRegex.test(url.pathname) || route.urlParams.direct || false | ||
} else { | ||
if ( CTZN.config.citizen.urlPaths.app !== '/' ) { | ||
pathToParse = pathToParse.replace(CTZN.config.citizen.urlPaths.app, '') | ||
route = { | ||
url : url.href, | ||
pathname : url.pathname, | ||
filePath : url.pathname, | ||
extension : url.pathname.replace(staticRegex, '$1'), | ||
isStatic : true | ||
} | ||
if ( publicControllerRegex.test(pathToParse) ) { | ||
route.controller = pathToParse.replace(/^\/([A-Za-z0-9-_]+)\/?.*/, '$1') | ||
} | ||
if ( !CTZN.patterns.controllers[route.controller] && CTZN.config.citizen.fallbackController.length ) { | ||
route.controller = CTZN.config.citizen.fallbackController | ||
} | ||
route.chain[0].controller = route.controller | ||
route.chain[0].action = route.action | ||
route.chain[0].view = route.controller | ||
route.renderer = route.controller | ||
route.view = route.controller | ||
route.renderedView = route.controller | ||
route.descriptor = pathToParse.replace(/^\/[A-Za-z0-9-_]+\/([A-Za-z0-9-_.~]+)\/?.*/, '$1').replace(/\//g, '') | ||
} | ||
@@ -76,5 +57,4 @@ | ||
function getUrlParams(urlToParse) { | ||
var pathToParse = url.parse(urlToParse).pathname.replace(/\/\//g, '/'), | ||
paramsRegex = /\/[A-Za-z-_]+[A-Za-z0-9-_]*\/[^/]+\/?$/, | ||
const getUrlParams = (pathName) => { | ||
var paramsRegex = /\/[A-Za-z-_]+[A-Za-z0-9-_]*\/[^/]+\/?$/, | ||
parameterNames = [], | ||
@@ -84,12 +64,8 @@ parameterValues = [], | ||
if ( CTZN.config.citizen.urlPaths.app !== '/' ) { | ||
pathToParse = pathToParse.replace(CTZN.config.citizen.urlPaths.app, '') | ||
while ( paramsRegex.test(pathName) ) { | ||
parameterNames.unshift(pathName.replace(/.*\/([A-Za-z-_]+[A-Za-z0-9-_]*)\/[^/]+\/?$/, '$1')) | ||
parameterValues.unshift(pathName.replace(/.*\/[A-Za-z-_]+[A-Za-z0-9-_]*\/([^/]+)\/?$/, '$1')) | ||
pathName = pathName.replace(/(.*)\/[A-Za-z-_]+[A-Za-z0-9-_]*\/[^/]+\/?$/, '$1') | ||
} | ||
while ( paramsRegex.test(pathToParse) ) { | ||
parameterNames.unshift(pathToParse.replace(/.*\/([A-Za-z-_]+[A-Za-z0-9-_]*)\/[^/]+\/?$/, '$1')) | ||
parameterValues.unshift(pathToParse.replace(/.*\/[A-Za-z-_]+[A-Za-z0-9-_]*\/([^/]+)\/?$/, '$1')) | ||
pathToParse = pathToParse.replace(/(.*)\/[A-Za-z-_]+[A-Za-z0-9-_]*\/[^/]+\/?$/, '$1') | ||
} | ||
for ( var i = 0; i < parameterNames.length; i++ ) { | ||
@@ -101,1 +77,4 @@ urlParams[parameterNames[i]] = parameterValues[i] | ||
} | ||
export default { parseRoute, getUrlParams, staticMimeTypes } |
// session management | ||
'use strict' | ||
// citizen | ||
import helpers from './helpers.js' | ||
// event hooks | ||
import sessionHooks from './hooks/session.js' | ||
const helpers = require('./helpers') | ||
module.exports = { | ||
public: { | ||
end: end | ||
}, | ||
citizen: { | ||
create: create, | ||
extend: extend | ||
} | ||
} | ||
function create() { | ||
var sessionID = '', | ||
const create = (request) => { | ||
let sessionID = '', | ||
started = Date.now(), | ||
expires = started + CTZN.config.citizen.sessionTimeout | ||
expires = started + ( CTZN.config.citizen.sessions.lifespan * 60000 ) | ||
@@ -27,8 +18,14 @@ while ( !CTZN.sessions[sessionID] ) { | ||
CTZN.sessions[sessionID] = { | ||
id: sessionID, | ||
started: started, | ||
expires: expires, | ||
timer: setTimeout( function () { | ||
checkExpiration(sessionID) | ||
}, CTZN.config.citizen.sessionTimeout) | ||
properties: { | ||
id: sessionID, | ||
started: started, | ||
expires: expires, | ||
cors: request.cors || false, | ||
timer: setTimeout( function () { | ||
checkExpiration(sessionID) | ||
}, CTZN.config.citizen.sessions.lifespan * 60000) | ||
}, | ||
app: { | ||
ctzn_session_id: sessionID | ||
} | ||
} | ||
@@ -38,3 +35,3 @@ | ||
label: 'Session started', | ||
content: CTZN.sessions[sessionID] | ||
content: CTZN.sessions[sessionID].properties | ||
}) | ||
@@ -48,12 +45,12 @@ } | ||
function checkExpiration(sessionID) { | ||
var now = Date.now() | ||
const checkExpiration = (sessionID) => { | ||
let now = Date.now() | ||
if ( CTZN.sessions[sessionID] ) { | ||
if ( CTZN.sessions[sessionID].expires < now ) { | ||
if ( CTZN.sessions[sessionID].properties.expires < now ) { | ||
onEnd(sessionID) | ||
} else { | ||
CTZN.sessions[sessionID].timer = setTimeout( function () { | ||
CTZN.sessions[sessionID].properties.timer = setTimeout( function () { | ||
checkExpiration(sessionID) | ||
}, CTZN.sessions[sessionID].expires - now) | ||
}, CTZN.sessions[sessionID].properties.expires - now) | ||
} | ||
@@ -64,16 +61,16 @@ } | ||
function end(key, value) { | ||
if ( arguments.length === 1 ) { | ||
if ( CTZN.sessions[key] ) { | ||
clearTimeout(CTZN.sessions[key].timer) | ||
onEnd(key) | ||
const end = (session) => { | ||
if ( typeof session === 'string' ) { | ||
if ( CTZN.sessions[session] ) { | ||
clearTimeout(CTZN.sessions[session].properties.timer) | ||
onEnd(session) | ||
} | ||
} else { | ||
for ( var property in CTZN.sessions ) { | ||
if ( CTZN.sessions[property][key] && CTZN.sessions[property][key] === value ) { | ||
clearTimeout(CTZN.sessions[property].timer) | ||
onEnd(property) | ||
break | ||
Object.keys(CTZN.sessions).forEach( item => { | ||
if ( CTZN.sessions[item].app[session.key] && CTZN.sessions[item].app[session.key] === session.value ) { | ||
clearTimeout(CTZN.sessions[item].properties.timer) | ||
onEnd(item) | ||
return false | ||
} | ||
} | ||
}) | ||
} | ||
@@ -83,21 +80,16 @@ } | ||
async function onEnd(sessionID) { | ||
let expiredSession | ||
delete CTZN.sessions[sessionID].timer | ||
expiredSession = helpers.copy(CTZN.sessions[sessionID]) | ||
const onEnd = async (sessionID) => { | ||
delete CTZN.sessions[sessionID].properties.timer | ||
let expiredSession = helpers.copy(CTZN.sessions[sessionID].app) | ||
delete CTZN.sessions[sessionID] | ||
try { | ||
let context = await CTZN.on.session.end(expiredSession) | ||
if ( CTZN.appOn.session && CTZN.appOn.session.end ) { | ||
CTZN.appOn.session.end(expiredSession, context) | ||
} | ||
helpers.log({ | ||
label: 'Session ended', | ||
content: expiredSession | ||
content: await sessionHooks.end(expiredSession) | ||
}) | ||
} catch (err) { | ||
throw new Error('An error occurred while processing session end') | ||
} catch ( err ) { | ||
err.message = 'An error occurred during a session end event' | ||
err.session = expiredSession | ||
throw err | ||
} | ||
@@ -107,5 +99,5 @@ } | ||
function extend(sessionID) { | ||
const extend = (sessionID) => { | ||
if ( CTZN.sessions[sessionID] ) { | ||
CTZN.sessions[sessionID].expires = Date.now() + CTZN.config.citizen.sessionTimeout | ||
CTZN.sessions[sessionID].properties.expires = Date.now() + ( CTZN.config.citizen.sessions.lifespan * 60000 ) | ||
} | ||
@@ -115,4 +107,8 @@ } | ||
function generateSessionID() { | ||
const generateSessionID = () => { | ||
return Math.random().toString().replace('0.', '') + Math.random().toString().replace('0.', '') | ||
} | ||
export default { create, end, extend } | ||
export { end } |
{ | ||
"name": "citizen", | ||
"version": "0.9.2", | ||
"description": "An MVC-based web application framework. Includes routing, serving, caching, and other helpful tools.", | ||
"keywords": [ | ||
"api server", | ||
"application server", | ||
"cache", | ||
"caching", | ||
"citizen", | ||
"framework", | ||
"mvc", | ||
"server side", | ||
"router", | ||
"routing", | ||
"view rendering", | ||
"web application server", | ||
"web server" | ||
], | ||
"name": "citizen", | ||
"version": "1.0.0", | ||
"description": "Node.js MVC web application framework. Includes routing, serving, caching, session management, and other helpful tools.", | ||
"keywords": [ | ||
"api server", | ||
"application server", | ||
"cache", | ||
"caching", | ||
"citizen", | ||
"framework", | ||
"mvc", | ||
"server side", | ||
"router", | ||
"routing", | ||
"view rendering", | ||
"web application server", | ||
"web server" | ||
], | ||
"author": { | ||
"name": "Jay Sylvester", | ||
"email": "jay@jaysylvester.com", | ||
"url": "https://jaysylvester.com" | ||
"name": "Jay Sylvester", | ||
"email": "jay@jaysylvester.com", | ||
"url": "https://jaysylvester.com" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/jaysylvester/citizen" | ||
"type": "git", | ||
"url": "git+https://github.com/jaysylvester/citizen.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/jaysylvester/citizen/issues" | ||
"url": "https://github.com/jaysylvester/citizen/issues" | ||
}, | ||
"main": "./lib/citizen.js", | ||
"main": "./index.js", | ||
"type": "module", | ||
"dependencies": { | ||
"chokidar": "^3.4.x", | ||
"commander": "^7.2.x", | ||
"consolidate": "^0.16.x", | ||
"formidable": "^1.2.x", | ||
"handlebars": "^4.7.x" | ||
"chokidar": "^3.6.x", | ||
"commander": "^12.0.x" | ||
}, | ||
"devDependencies": { | ||
"@eslint/js": "^9.0.0", | ||
"eslint": "^9.0.0", | ||
"globals": "^15.0.0" | ||
}, | ||
"engines": { | ||
"node": ">=12.9.x" | ||
"node": ">=16.0.0" | ||
}, | ||
"license": "MIT" | ||
"license": "MIT" | ||
} |
// Generates files and directories needed for citizen apps | ||
'use strict' | ||
import { program } from 'commander' | ||
import fs from 'node:fs' | ||
import path from 'node:path' | ||
const | ||
program = require('commander'), | ||
fs = require('fs'), | ||
path = require('path'), | ||
scaffoldPath = path.dirname(module.filename), | ||
appPath = path.resolve(scaffoldPath, '../../../app') | ||
const scaffoldPath = new URL('../util/', import.meta.url).pathname, | ||
appPath = path.resolve(scaffoldPath, '../../../app') | ||
const buildController = (options) => { | ||
var template = fs.readFileSync(scaffoldPath + '/templates/controller.js'), | ||
pattern = options.pattern, | ||
appName = options.appName, | ||
name = pattern + '.js' | ||
template = template.toString() | ||
template = template.replace(/\[pattern\]/g, pattern) | ||
template = template.replace(/\[appName\]/g, appName) | ||
return { | ||
name : name, | ||
contents : template | ||
} | ||
} | ||
const buildModel = (options) => { | ||
var template = fs.readFileSync(scaffoldPath + '/templates/model.js'), | ||
pattern = options.pattern, | ||
header = options.main && options.main.header ? options.main.header : pattern + ' pattern template', | ||
text = options.main && options.main.text ? options.main.text : 'This is a template for the ' + pattern + ' pattern.' | ||
template = template.toString() | ||
template = template.replace(/\[pattern\]/g, pattern) | ||
template = template.replace(/\[header\]/g, header) | ||
template = template.replace(/\[text\]/g, text) | ||
return { | ||
name : pattern + '.js', | ||
contents : template | ||
} | ||
} | ||
const buildView = (options) => { | ||
var pattern = options.pattern, | ||
template = fs.readFileSync(scaffoldPath + '/templates/view.html'), | ||
directory = pattern, | ||
name = pattern + '.html' | ||
return { | ||
directory : directory, | ||
name : name, | ||
contents : template.toString() | ||
} | ||
} | ||
const buildConfig = (options) => { | ||
var template = fs.readFileSync(scaffoldPath + '/templates/config.json'), | ||
mode = options.mode || 'development', | ||
port = options.port || 3000, | ||
name = options.name || 'citizen' | ||
template = template.toString() | ||
template = template.replace(/\[mode\]/g, mode) | ||
template = template.replace(/\[port\]/g, port) | ||
return { | ||
name : name + '.json', | ||
contents : template | ||
} | ||
} | ||
program | ||
.version('0.0.3') | ||
.version('1.0.0') | ||
.on('--help', function () { | ||
@@ -25,25 +90,22 @@ console.log('') | ||
.command('skeleton') | ||
.option('-n, --network-port [port number]', 'Default HTTP port is 80, but if that\'s taken, use this option to set your config') | ||
.option('-m, --mode [mode]', 'Set the config mode to production (default) or development') | ||
.option('-U, --no-use-strict', 'Don\'t include the \'use strict\' statement in any of the modules') | ||
.option('-n, --network-port [port number]', 'Default HTTP port is 3000, but if that\'s taken, use this option to set your config') | ||
.option('-m, --mode [mode]', 'Set the config mode to development (default) or production') | ||
.action( function (options) { | ||
var webPath = path.resolve(appPath, '../web'), | ||
templates = { | ||
application: fs.readFileSync(scaffoldPath + '/templates/hooks/application.js'), | ||
request: fs.readFileSync(scaffoldPath + '/templates/hooks/request.js'), | ||
response: fs.readFileSync(scaffoldPath + '/templates/hooks/response.js'), | ||
session: fs.readFileSync(scaffoldPath + '/templates/hooks/session.js'), | ||
start: fs.readFileSync(scaffoldPath + '/templates/start.js'), | ||
error: fs.readdirSync(scaffoldPath + '/templates/error') | ||
application : fs.readFileSync(scaffoldPath + '/templates/hooks/application.js'), | ||
package : fs.readFileSync(scaffoldPath + '/templates/package.json'), | ||
request : fs.readFileSync(scaffoldPath + '/templates/hooks/request.js'), | ||
response : fs.readFileSync(scaffoldPath + '/templates/hooks/response.js'), | ||
session : fs.readFileSync(scaffoldPath + '/templates/hooks/session.js'), | ||
start : fs.readFileSync(scaffoldPath + '/templates/start.js'), | ||
error : fs.readdirSync(scaffoldPath + '/templates/error') | ||
}, | ||
useStrict = options.useStrict ? '\'use strict\'\n' : '', | ||
controller = buildController({ | ||
pattern: 'index', | ||
appName: 'app', | ||
useStrict: useStrict | ||
pattern: 'index', | ||
appName: 'app' | ||
}), | ||
model = buildModel({ | ||
pattern: 'index', | ||
appName: 'app', | ||
useStrict: useStrict, | ||
pattern: 'index', | ||
appName: 'app', | ||
main: { | ||
@@ -62,2 +124,3 @@ header: 'Hello, world!', | ||
application = templates.application.toString(), | ||
packageJSON = templates.package.toString(), | ||
request = templates.request.toString(), | ||
@@ -68,30 +131,25 @@ response = templates.response.toString(), | ||
application = application.replace(/\[useStrict\]/g, useStrict) | ||
request = request.replace(/\[useStrict\]/g, useStrict) | ||
response = response.replace(/\[useStrict\]/g, useStrict) | ||
session = session.replace(/\[useStrict\]/g, useStrict) | ||
start = start.replace(/\[useStrict\]/g, useStrict) | ||
fs.mkdirSync(appPath) | ||
fs.writeFileSync(appPath + '/package.json', packageJSON) | ||
fs.writeFileSync(appPath + '/start.js', start) | ||
fs.mkdirSync(appPath + '/config') | ||
fs.mkdirSync(appPath + '/config') | ||
fs.writeFileSync(appPath + '/config/' + config.name, config.contents) | ||
fs.mkdirSync(appPath + '/logs') | ||
fs.mkdirSync(appPath + '/hooks') | ||
fs.writeFileSync(appPath + '/hooks/application.js', application) | ||
fs.writeFileSync(appPath + '/hooks/request.js', request) | ||
fs.writeFileSync(appPath + '/hooks/response.js', response) | ||
fs.writeFileSync(appPath + '/hooks/session.js', session) | ||
fs.mkdirSync(appPath + '/patterns') | ||
fs.mkdirSync(appPath + '/patterns/controllers') | ||
fs.writeFileSync(appPath + '/patterns/controllers/' + controller.name, controller.contents) | ||
fs.mkdirSync(appPath + '/patterns/models') | ||
fs.writeFileSync(appPath + '/patterns/models/' + model.name, model.contents) | ||
fs.mkdirSync(appPath + '/patterns/views') | ||
fs.mkdirSync(appPath + '/patterns/views/' + view.directory) | ||
fs.writeFileSync(appPath + '/patterns/views/' + view.directory + '/' + view.name, view.contents) | ||
fs.mkdirSync(appPath + '/patterns/views/error') | ||
fs.mkdirSync(appPath + '/controllers') | ||
fs.mkdirSync(appPath + '/controllers/hooks') | ||
fs.writeFileSync(appPath + '/controllers/hooks/application.js', application) | ||
fs.writeFileSync(appPath + '/controllers/hooks/request.js', request) | ||
fs.writeFileSync(appPath + '/controllers/hooks/response.js', response) | ||
fs.writeFileSync(appPath + '/controllers/hooks/session.js', session) | ||
fs.mkdirSync(appPath + '/controllers/routes') | ||
fs.writeFileSync(appPath + '/controllers/routes/' + controller.name, controller.contents) | ||
fs.mkdirSync(appPath + '/helpers') | ||
fs.mkdirSync(appPath + '/models') | ||
fs.writeFileSync(appPath + '/models/' + model.name, model.contents) | ||
fs.mkdirSync(appPath + '/views') | ||
fs.mkdirSync(appPath + '/views/' + view.directory) | ||
fs.writeFileSync(appPath + '/views/' + view.directory + '/' + view.name, view.contents) | ||
fs.mkdirSync(appPath + '/views/error') | ||
templates.error.forEach( function (file) { | ||
var template, | ||
viewRegex = new RegExp(/.+\.hbs$/) | ||
viewRegex = new RegExp(/.+\.html$/) | ||
@@ -102,3 +160,3 @@ if ( viewRegex.test(file) ) { | ||
fs.writeFileSync(appPath + '/patterns/views/error/' + file, template) | ||
fs.writeFileSync(appPath + '/views/error/' + file, template) | ||
}) | ||
@@ -128,21 +186,20 @@ fs.mkdirSync(webPath) | ||
console.log(' citizen.json') | ||
console.log(' logs/') | ||
console.log(' hooks/') | ||
console.log(' application.js') | ||
console.log(' request.js') | ||
console.log(' response.js') | ||
console.log(' session.js') | ||
console.log(' patterns/') | ||
console.log(' controllers/') | ||
console.log(' controllers/') | ||
console.log(' hooks/') | ||
console.log(' application.js') | ||
console.log(' request.js') | ||
console.log(' response.js') | ||
console.log(' session.js') | ||
console.log(' routes/') | ||
console.log(' index.js') | ||
console.log(' models/') | ||
console.log(' index.js') | ||
console.log(' views/') | ||
console.log(' error/') | ||
console.log(' 404.hbs') | ||
console.log(' 500.hbs') | ||
console.log(' ENOENT.hbs') | ||
console.log(' error.hbs') | ||
console.log(' index/') | ||
console.log(' index.hbs') | ||
console.log(' helpers/') | ||
console.log(' models/') | ||
console.log(' index.js') | ||
console.log(' views/') | ||
console.log(' error/') | ||
console.log(' 404.html') | ||
console.log(' 500.html') | ||
console.log(' ENOENT.html') | ||
console.log(' error.html') | ||
console.log(' index.html') | ||
console.log(' start.js') | ||
@@ -161,33 +218,24 @@ console.log(' web/') | ||
.option('-a, --app-name [name]', 'Specify a custom global app variable name (default is "app")') | ||
.option('-p, --private', 'Make the controller private (inaccessible via HTTP)') | ||
.option('-U, --no-use-strict', 'Don\'t include the \'use strict\' statement in the controller and model') | ||
.option('-M, --no-model', 'Skip creation of the model') | ||
.option('-T, --no-view-template', 'Skip creation of the view') | ||
.option('-T, --no-view', 'Skip creation of the view') | ||
.action( function (pattern, options) { | ||
var appName = options.appName || 'app', | ||
useStrict = options.useStrict ? '\'use strict\'\n' : '', | ||
controller = buildController({ | ||
pattern: pattern, | ||
appName: appName, | ||
useStrict: useStrict, | ||
private: options.private | ||
appName: appName | ||
}), | ||
model = buildModel({ | ||
pattern: pattern, | ||
appName: appName, | ||
useStrict: useStrict, | ||
private: options.private | ||
appName: appName | ||
}), | ||
view = buildView({ | ||
pattern: pattern, | ||
private: options.private | ||
pattern: pattern | ||
}) | ||
fs.writeFileSync(appPath + '/patterns/controllers/' + controller.name, controller.contents) | ||
fs.writeFileSync(appPath + '/controllers/routes/' + controller.name, controller.contents) | ||
if ( options.model ) { | ||
fs.writeFileSync(appPath + '/patterns/models/' + model.name, model.contents) | ||
fs.writeFileSync(appPath + '/models/' + model.name, model.contents) | ||
} | ||
if ( options.viewTemplate ) { | ||
fs.mkdirSync(appPath + '/patterns/views/' + view.directory) | ||
fs.writeFileSync(appPath + '/patterns/views/' + view.directory + '/' + view.name, view.contents) | ||
if ( options.view ) { | ||
fs.writeFileSync(appPath + '/views/' + view.name, view.contents) | ||
} | ||
@@ -208,10 +256,9 @@ | ||
console.log(' app/') | ||
console.log(' patterns/') | ||
console.log(' controllers/') | ||
console.log(' controllers/') | ||
console.log(' routes/') | ||
console.log(' foo.js') | ||
console.log(' models/') | ||
console.log(' foo.js') | ||
console.log(' views/') | ||
console.log(' foo/') | ||
console.log(' foo.hbs') | ||
console.log(' models/') | ||
console.log(' foo.js') | ||
console.log(' views/') | ||
console.log(' foo.html') | ||
console.log('') | ||
@@ -221,82 +268,1 @@ }) | ||
program.parse(process.argv) | ||
function buildController(options) { | ||
var template = fs.readFileSync(scaffoldPath + '/templates/controller.js'), | ||
pattern = options.pattern, | ||
appName = options.appName, | ||
isPrivate = options.private || false, | ||
useStrict = options.useStrict, | ||
name = pattern + '.js' | ||
if ( isPrivate ) { | ||
name = '+' + name | ||
} | ||
template = template.toString() | ||
template = template.replace(/\[pattern\]/g, pattern) | ||
template = template.replace(/\[useStrict\]/g, useStrict) | ||
template = template.replace(/\[appName\]/g, appName) | ||
return { | ||
name : name, | ||
contents : template | ||
} | ||
} | ||
function buildModel(options) { | ||
var template = fs.readFileSync(scaffoldPath + '/templates/model.js'), | ||
pattern = options.pattern, | ||
useStrict = options.useStrict, | ||
header = options.main && options.main.header ? options.main.header : pattern + ' pattern template', | ||
text = options.main && options.main.text ? options.main.text : 'This is a template for the ' + pattern + ' pattern.' | ||
template = template.toString() | ||
template = template.replace(/\[pattern\]/g, pattern) | ||
template = template.replace(/\[useStrict\]/g, useStrict) | ||
template = template.replace(/\[header\]/g, header) | ||
template = template.replace(/\[text\]/g, text) | ||
return { | ||
name : pattern + '.js', | ||
contents : template | ||
} | ||
} | ||
function buildView(options) { | ||
var pattern = options.pattern, | ||
isPrivate = options.private || false, | ||
template = fs.readFileSync(scaffoldPath + '/templates/view.hbs'), | ||
directory = pattern, | ||
name = pattern + '.hbs' | ||
if ( isPrivate ) { | ||
directory = '+' + directory | ||
name = '+' + name | ||
} | ||
return { | ||
directory : directory, | ||
name : name, | ||
contents : template.toString() | ||
} | ||
} | ||
function buildConfig(options) { | ||
var template = fs.readFileSync(scaffoldPath + '/templates/config.json'), | ||
mode = options.mode || 'production', | ||
port = options.port || 80, | ||
name = options.name || 'citizen' | ||
template = template.toString() | ||
template = template.replace(/\[mode\]/g, mode) | ||
template = template.replace(/\[port\]/g, port) | ||
return { | ||
name : name + '.json', | ||
contents : template | ||
} | ||
} |
{ | ||
"citizen": { | ||
"mode": "[mode]", | ||
"mode": "[mode]", | ||
"http": { | ||
"port": [port] | ||
"port": [port] | ||
} | ||
} | ||
} |
// [pattern] controller | ||
[useStrict] | ||
module.exports = { | ||
handler: handler | ||
} | ||
// default action | ||
async function handler(params, context) { | ||
export const handler = async (params, context) => { | ||
let content = await [appName].models.[pattern].content() | ||
return { | ||
content: content | ||
local: content | ||
} | ||
} |
// application events | ||
// This module optionally exports the following methods: | ||
// start(context, emitter) - Called when the application starts | ||
// error(err, context, emitter) - Called on every application error | ||
// start(params, request, response, context) - Called when the application starts | ||
// error(params, request, response, context, err) - Called on every application error (500-level) | ||
// If you have no use for this file, you can delete it. | ||
[useStrict] | ||
module.exports = { | ||
start: start, | ||
error: error | ||
} | ||
async function start(context) { | ||
return | ||
export const start = async (config) => { | ||
// Anything you want to happen when the application starts | ||
} | ||
async function error(err, params, context) { | ||
return | ||
export const error = async (params, request, response, context, err) => { | ||
// Anything you want to happen when the application throws an error | ||
} |
// request events | ||
// This module optionally exports the following methods: | ||
// start(params, context, emitter) - Called at the beginning of every request | ||
// end(params, context, emitter) - Called at the end of every request | ||
// start(params, request, response, context) - Called at the beginning of every request | ||
// end(params, request, response, context) - Called at the end of every request | ||
// If you have no use for this file, you can delete it. | ||
[useStrict] | ||
module.exports = { | ||
start: start, | ||
end: end | ||
} | ||
async function start(params, context) { | ||
return | ||
export const start = (params, request, response, context) => { | ||
// Anything you want to happen at the beginning of a request | ||
} | ||
async function end(params, context) { | ||
return | ||
export const end = (params, request, response, context) => { | ||
// Anything you want to happen at the end of a request | ||
} |
// response events | ||
// This module optionally exports the following methods: | ||
// start(params, context, emitter) - Called at the beginning of every response | ||
// end(params, context, emitter) - Called at the end of every response (after the response has been sent to the client) | ||
// start(params, request, response, context) - Called at the beginning of every response | ||
// end(params, request, response, context) - Called at the end of every response (after the response has been sent to the client) | ||
// If you have no use for this file, you can delete it. | ||
[useStrict] | ||
module.exports = { | ||
start: start, | ||
end: end | ||
} | ||
async function start(params, context) { | ||
return | ||
export const start = (params, request, response, context) => { | ||
// Anything you want to happen at the beginning of a response | ||
} | ||
async function end(params, context) { | ||
return | ||
export const end = (params, request, response, context) => { | ||
// Anything you want to happen at the end of a response | ||
} |
// session events | ||
// This module optionally exports the following methods: | ||
// start(params, context, emitter) - Called at the beginning of every user session | ||
// end(params, context, emitter) - Called at the end of every user session | ||
// start(params, request, response, context) - Called at the start of every user session | ||
// end(session, context) - Called when any user session expires | ||
// If you have no use for this file, you can delete it. | ||
[useStrict] | ||
module.exports = { | ||
start: start, | ||
end: end | ||
} | ||
async function start(params, context) { | ||
return | ||
export const start = (params, request, response, context) => { | ||
// Anything you want to happen when a new session starts | ||
} | ||
async function end(params, context) { | ||
return | ||
export const end = (session) => { | ||
// Anything you want to happen when a session ends. The "session" argument contains the properties of the expired session. | ||
} |
// [pattern] model | ||
[useStrict] | ||
module.exports = { | ||
content: content | ||
} | ||
function content() { | ||
export const content = () => { | ||
return { | ||
@@ -10,0 +5,0 @@ metaData: { |
// app start | ||
[useStrict] | ||
global.app = require('citizen') | ||
import citizen from 'citizen' | ||
global.app = citizen | ||
app.start() |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
2
33
1
7
3
Yes
283873
3
3536
2780
+ Addedcommander@12.1.0(transitive)
- Removedconsolidate@^0.16.x
- Removedformidable@^1.2.x
- Removedhandlebars@^4.7.x
- Removedbluebird@3.7.2(transitive)
- Removedcommander@7.2.0(transitive)
- Removedconsolidate@0.16.0(transitive)
- Removedformidable@1.2.6(transitive)
- Removedhandlebars@4.7.8(transitive)
- Removedminimist@1.2.8(transitive)
- Removedneo-async@2.6.2(transitive)
- Removedsource-map@0.6.1(transitive)
- Removeduglify-js@3.19.3(transitive)
- Removedwordwrap@1.0.0(transitive)
Updatedchokidar@^3.6.x
Updatedcommander@^12.0.x