Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

citizen

Package Overview
Dependencies
Maintainers
0
Versions
122
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

citizen - npm Package Compare versions

Comparing version 0.9.2 to 1.0.0

CHANGELOG.md

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc