scimgateway
Version:
Using SCIM protocol as a gateway for user provisioning to other endpoints
646 lines (588 loc) • 27.6 kB
text/typescript
// =================================================================================
// File: plugin-loki.js
//
// Authors: Jarle Elshaug
// Jeffrey Gilbert (visualjeff)
//
// Purpose: SCIM Gateway becomes a standalone SCIM endpoint
// - Demonstrate userprovisioning towards a document-oriented database
// - Using LokiJS (http://lokijs.org) for a fast, in-memory document-oriented database with persistence
// - Two predefined test users loaded when using in-memory only (no persistence)
// - Supporting explore, create, delete, modify and list users (including groups)
//
// Supported attributes:
//
// GlobalUser Template Scim Endpoint
// ------------------------------------------------------
// All attributes are supported, note multivalue "type" must be unique
//
// NOTE: Default configuration file setting {"persistence": false} gives an inMemory adapter for testing purposes
// having two predifiend users loaded. Using {"persistence": true} gives an persistence file store located in
// config directory with name according to configuration setting {"dbname": "loki.db"} and no no testusers loaded.
//
// LokiJS are well suited for handling large dataloads
//
// =================================================================================
import Loki from 'lokijs'
import path from 'node:path'
// for supporting nodejs running scimgateway package directly, using dynamic import instead of: import { ScimGateway } from 'scimgateway'
// scimgateway also inclues HelperRest: import { ScimGateway, HelperRest } from 'scimgateway'
// start - mandatory plugin initialization
const ScimGateway: typeof import('scimgateway').ScimGateway = await (async () => {
try {
return (await import('scimgateway')).ScimGateway
} catch (err) {
const source = './scimgateway.ts'
return (await import(source)).ScimGateway
}
})()
const scimgateway = new ScimGateway()
const config = scimgateway.getConfig()
scimgateway.authPassThroughAllowed = false
// end - mandatory plugin initialization
const configDir = scimgateway.configDir
const validFilterOperators = ['eq', 'ne', 'aeq', 'dteq', 'gt', 'gte', 'lt', 'lte', 'between', 'jgt', 'jgte', 'jlt', 'jlte', 'jbetween', 'regex', 'in', 'nin', 'keyin', 'nkeyin', 'definedin', 'undefinedin', 'contains', 'containsAny', 'type', 'finite', 'size', 'len', 'exists']
const dbNames: string[] = []
for (const baseEntity in config.entity) {
let dbname = config.entity[baseEntity].dbname || 'loki.db'
if (dbNames.includes(dbname)) {
scimgateway.logError(baseEntity, `initialization error: database '${dbname}' is already used by another baseEntity configuration`)
continue
}
dbNames.push(dbname)
dbname = path.join(`${configDir}`, `${dbname}`)
const isPersisence = config.entity[baseEntity].persistence !== false
const loadHandler = () => {
let users = db.getCollection('users')
if (users === null) { // if database do not exist it will be empty so intitialize here
users = db.addCollection('users', {
unique: ['id', 'userName'],
})
}
let groups = db.getCollection('groups')
if (groups === null) {
groups = db.addCollection('groups', {
unique: ['displayName'],
})
}
if (!isPersisence) { // load testusers
scimgateway.getTestModeUsers().forEach((record) => {
const r: any = scimgateway.copyObj(record)
if (r.meta) delete r.meta
users.insert(r)
})
scimgateway.getTestModeGroups().forEach((record) => {
const r: any = scimgateway.copyObj(record)
if (r.meta) delete r.meta
groups.insert(r)
})
}
config.entity[baseEntity].users = users
config.entity[baseEntity].groups = groups
}
const db = new Loki(dbname, {
env: 'NA', // avoid default NODEJS
autoload: isPersisence,
autoloadCallback: isPersisence ? loadHandler : undefined,
autosave: isPersisence,
autosaveInterval: 10000, // 10 seconds
adapter: isPersisence ? new Loki.LokiFsAdapter() : new Loki.LokiMemoryAdapter(),
})
config.entity[baseEntity].db = db
if (!isPersisence) loadHandler()
}
// =================================================
// getUsers
// =================================================
scimgateway.getUsers = async (baseEntity, getObj, attributes) => {
const action = 'getUsers'
scimgateway.logDebug(baseEntity, `handling ${action} getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const users = config.entity[baseEntity].users
if (getObj.operator) { // convert to plugin supported syntax
switch (getObj.operator) {
case 'co':
getObj.operator = '$contains'
break
case 'ge':
getObj.operator = '$gte'
break
case 'le':
getObj.operator = '$lte'
break
case 'sw':
getObj.operator = '$regex'
getObj.value = new RegExp(`^${getObj.value}.*`)
break
case 'ew':
getObj.operator = '$regex'
getObj.value = new RegExp(`.*${getObj.value}$`)
break
default:
if (!validFilterOperators.includes(getObj.operator)) {
const err = new Error(`${action} error: filter operator '${getObj.operator}' is not valid, valid operators for this endpoint are: ${validFilterOperators}` + ',co,ge,le,sw,ew')
err.name = 'invalidFilter' // maps to scimType error handling
throw err
}
getObj.operator = '$' + getObj.operator
}
}
let usersArr: Record<string, any>[] | undefined
// mandatory if-else logic - start
if (getObj.operator) { // note, loki using prefix '$'
if (getObj.operator === '$eq' && ['id', 'userName', 'externalId'].includes(getObj.attribute)) {
// mandatory - unique filtering - single unique user to be returned - correspond to getUser() in versions < 4.x.x
const queryObj: any = {}
if (getObj.attribute === 'id') queryObj[getObj.attribute] = getObj.value
else queryObj[getObj.attribute] = { $regex: [`^${getObj.value}$`, 'i'] } // case insensitive
// new RegExp(`^${getObj.value}$`, 'i')
usersArr = users.find(queryObj)
} else if (getObj.operator === '$eq' && getObj.attribute === 'group.value') {
// optional - only used when groups are member of users, not default behavior - correspond to getGroupUsers() in versions < 4.x.x
const queryObj: any = {}
queryObj[getObj.attribute] = getObj.value
usersArr = users.chain().find(queryObj).data()
} else {
// optional - simpel filtering
const dt = Date.parse(getObj.value)
if (!isNaN(dt)) { // date string to timestamp
getObj.value = dt
}
const queryObj: any = {}
queryObj[getObj.attribute] = {}
queryObj[getObj.attribute][getObj.operator] = getObj.value
usersArr = users.chain().find(queryObj).data() // {name.familyName: { $eq: "Jensen" } }
}
} else if (getObj.rawFilter) {
// optional - advanced filtering having and/or/not - use getObj.rawFilter
//
// support "or" filter using "eq"
// e.g.: (id eq "bjensen") or (id eq "jsmith")
//
const arr = getObj.rawFilter.split(' or ')
const getObjArr: any = []
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i].replace(/\(/g, '').replace(/\)/g, '').trim()
const arrFilter = arr[i].split(' ')
if (arrFilter.length === 3 || (arrFilter.length > 2 && arrFilter[2].startsWith('"') && arrFilter[arrFilter.length - 1].endsWith('"'))) {
const o: any = {}
o.attribute = arrFilter[0] // id
o.operator = arrFilter[1].toLowerCase() // eq
o.value = decodeURIComponent(arrFilter.slice(2).join(' ').replace(/"/g, '')) // bjensen
getObjArr.push(o)
}
}
const o: any = {}
for (let i = 0; i < getObjArr.length; i++) {
if (getObjArr[i].operator === 'eq') {
if (!o[getObjArr[i].attribute]) o[getObjArr[i].attribute] = []
o[getObjArr[i].attribute].push(getObjArr[i].value)
} else {
throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
}
} // { id: [ 'bjensen', 'jsmith' ] }
for (const k in o) {
const f: any = {}
f[k] = { $in: o[k] } // { id: { $in: ['bjensen', 'jsmith'] } }
const u = users.chain().find(f).data()
if (!usersArr) usersArr = []
Array.prototype.push.apply(usersArr, u)
}
if (!usersArr) throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
} else {
// mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all users to be returned - correspond to exploreUsers() in versions < 4.x.x
usersArr = users.chain().data()
}
// mandatory if-else logic - end
if (!usersArr) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
if (!getObj.startIndex) getObj.startIndex = 1
if (!getObj.count) getObj.count = 200
const ret: any = {
Resources: [],
totalResults: null, // total number of objects when using paging (ref. startIndex/count)
}
const arr = usersArr.map((obj) => { return stripLoki(obj) }) // all attributes included - virtual attribute groups automatically handled by scimgateway
const delta = arr.slice(getObj.startIndex - 1, getObj.startIndex - 1 + getObj.count) // supporting paging "light"
Array.prototype.push.apply(ret.Resources, delta)
ret.totalResults = arr.length // set to maximum, will be corrected if needed by scimgateway
return ret
}
// =================================================
// createUser
// =================================================
scimgateway.createUser = async (baseEntity, userObj) => {
const action = 'createUser'
scimgateway.logDebug(baseEntity, `handling ${action} userObj=${JSON.stringify(userObj)}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const users = config.entity[baseEntity].users
if (userObj.password) delete userObj.password // exclude password db not ecrypted
for (const key in userObj) {
if (!Array.isArray(userObj[key]) && scimgateway.isMultiValueTypes(key)) { // true if attribute is "type converted object" => convert to standard array
const arr: string[] = []
for (const el in userObj[key]) {
userObj[key][el].type = el
if (el === 'undefined') delete userObj[key][el].type // type "undefined" reverted back to original blank
arr.push(userObj[key][el]) // create
}
userObj[key] = arr
}
}
if (userObj.userName) userObj.id = userObj.userName // id set to userName or externalId
else if (userObj.externalId) userObj.id = userObj.externalId
else throw new Error(`${action} error: missing mandatory userName or externalId`)
try {
await users.insert(userObj)
return null
} catch (err: any) {
const newErr = new Error(`${action} error: ${err.message}`)
if (err.message && err.message.startsWith('Duplicate key')) {
newErr.name += '#409' // customErrorCode
}
throw newErr
}
}
// =================================================
// deleteUser
// =================================================
scimgateway.deleteUser = async (baseEntity, id) => {
const action = 'deleteUser'
scimgateway.logDebug(baseEntity, `handling ${action} id=${id}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const users = config.entity[baseEntity].users
const res = users.find({ id: id })
if (res.length !== 1) throw new Error(`${action} error: failed for user id=${id}`)
const userObj = res[0]
await users.remove(userObj)
return null
}
// =================================================
// modifyUser
// =================================================
scimgateway.modifyUser = async (baseEntity, id, attrObj) => {
const action = 'modifyUser'
scimgateway.logDebug(baseEntity, `handling ${action} id=${id} attrObj=${JSON.stringify(attrObj)}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const users = config.entity[baseEntity].users
if (attrObj.password) delete attrObj.password // exclude password db not ecrypted
const res = users.find({ id: id })
if (res.length === 0) throw new Error(`${action} error: user id=${id} - user does not exist`)
if (res.length > 1) throw new Error(`${action} error: user id=${id} - user is not unique, more than one have been found`)
const userObj: any = res[0]
for (const key in attrObj) {
if (Array.isArray(attrObj[key])) { // standard, not using type (e.g roles/groups) or skipTypeConvert=true
const delArr = attrObj[key].filter(el => el.operation === 'delete')
const addArr = attrObj[key].filter(el => (!el.operation || el.operation !== 'delete'))
if (!userObj[key] || !Array.isArray(userObj[key])) userObj[key] = []
// delete
userObj[key] = userObj[key].filter((el: Record<string, any>) => {
const index = delArr.findIndex((e) => {
let elExist = false
for (const k in el) {
if (k === 'primary') continue
if (el[k] !== e[k]) {
elExist = false
break
}
elExist = true
}
return elExist
})
if (index >= 0) return false
else return true
})
// add
addArr.forEach((el) => {
if (Object.prototype.hasOwnProperty.call(el, 'primary')) {
if (el.primary === true || (typeof el.primary === 'string' && el.primary.toLowerCase() === 'true')) {
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === el.primary)
if (index >= 0) {
if (key === 'roles') userObj[key].splice(index, 1) // roles, delete existing role having primary attribute true (new role with primary will be added)
else userObj[key][index].primary = undefined // remove primary attribute, only one primary
}
}
}
const index = userObj[key].findIndex((e: Record<string, any>) => { // avoid adding existing
let elExist = false
for (const k in el) {
if (el[k] !== e[k]) {
elExist = false
break
}
elExist = true
}
return elExist
})
if (index < 0) userObj[key].push(el)
})
} else if (scimgateway.isMultiValueTypes(key)) { // "type converted object" logic and original blank type having type "undefined"
if (!attrObj[key]) delete userObj[key] // blank or null
for (const el in attrObj[key]) {
attrObj[key][el].type = el
if (attrObj[key][el].operation && attrObj[key][el].operation === 'delete') { // delete multivalue
let type: any = el
if (type === 'undefined') type = undefined
userObj[key] = userObj[key].filter((e: Record<string, any>) => e.type !== type)
if (userObj[key].length < 1) delete userObj[key]
} else { // modify/create multivalue
if (!userObj[key]) userObj[key] = []
if (attrObj[key][el].primary) { // remove any existing primary attribute, should only have one primary set
const primVal = attrObj[key][el].primary
if (primVal === true || (typeof primVal === 'string' && primVal.toLowerCase() === 'true')) {
const index = userObj[key].findIndex((e: Record<string, any>) => e.primary === primVal)
if (index >= 0) {
userObj[key][index].primary = undefined
}
}
}
const found = userObj[key].find((e: Record<string, any>, i: any) => {
if (e.type === el || (!e.type && el === 'undefined')) {
for (const k in attrObj[key][el]) {
userObj[key][i][k] = attrObj[key][el][k]
if (k === 'type' && attrObj[key][el][k] === 'undefined') delete userObj[key][i][k] // don't store with type "undefined"
}
return true
} else return false
})
if (attrObj[key][el].type && attrObj[key][el].type === 'undefined') delete attrObj[key][el].type // don't store with type "undefined"
if (!found) userObj[key].push(attrObj[key][el]) // create
}
}
} else {
// None multi value attribute, blank will be deleted
if (typeof (attrObj[key]) === 'object' && attrObj[key] !== null) {
// name.familyName=Bianchi
if (!userObj[key]) userObj[key] = {} // e.g name object does not exist
for (const sub in attrObj[key]) {
if (!userObj[key]) userObj[key] = {}
if (Object.prototype.hasOwnProperty.call(attrObj[key][sub], 'value')
&& attrObj[key][sub].value === '') delete userObj[key][sub] // object having blank value attribute e.g. {"manager": {"value": "",...}}
else if (attrObj[key][sub] === '') delete userObj[key][sub]
else {
if (!userObj[key]) userObj[key] = {} // may have been deleted by length check below
userObj[key][sub] = attrObj[key][sub]
}
if (Object.keys(userObj[key]).length < 1) delete userObj[key]
}
} else {
if (attrObj[key] === '') delete userObj[key]
else userObj[key] = attrObj[key]
}
}
}
await users.update(userObj) // needed for persistence
return null
}
// =================================================
// getGroups
// =================================================
scimgateway.getGroups = async (baseEntity, getObj, attributes) => {
const action = 'getGroups'
scimgateway.logDebug(baseEntity, `handling ${action} getObj=${getObj ? JSON.stringify(getObj) : ''} attributes=${attributes}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const groups = config.entity[baseEntity].groups
if (getObj.operator) { // convert to plugin supported syntax
switch (getObj.operator) {
case 'co':
getObj.operator = '$contains'
break
case 'ge':
getObj.operator = '$gte'
break
case 'le':
getObj.operator = '$lte'
break
case 'sw':
getObj.operator = '$regex'
getObj.value = new RegExp(`^${getObj.value}.*`)
break
case 'ew':
getObj.operator = '$regex'
getObj.value = new RegExp(`.*${getObj.value}$`)
break
default:
if (!validFilterOperators.includes(getObj.operator)) {
const err = new Error(`${action} error: filter operator '${getObj.operator}' is not valid, valid operators for this endpoint are: ${validFilterOperators}` + ',co,ge,le,sw,ew')
err.name = 'invalidFilter' // maps to scimType error handling
throw err
}
getObj.operator = '$' + getObj.operator
}
}
let groupsArr: Record<string, any>[] | undefined
// mandatory if-else logic - start
if (getObj.operator) { // note, loki using prefix '$'
if (getObj.operator === '$eq' && ['id', 'displayName', 'externalId'].includes(getObj.attribute)) {
// mandatory - unique filtering - single unique group to be returned - correspond to getGroup() in versions < 4.x.x
const queryObj: any = {}
if (getObj.attribute === 'id') queryObj[getObj.attribute] = getObj.value
else queryObj[getObj.attribute] = { $regex: [`^${getObj.value}$`, 'i'] } // case insensitive
groupsArr = groups.find(queryObj)
} else if (getObj.operator === '$eq' && getObj.attribute === 'members.value') {
// mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
// Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
const queryObj: any = {}
queryObj[getObj.attribute] = getObj.value
groupsArr = groups.chain().find(queryObj).data()
} else {
// optional - simpel filtering
const dt = Date.parse(getObj.value)
if (!isNaN(dt)) { // date string to timestamp
getObj.value = dt
}
const queryObj: any = {}
queryObj[getObj.attribute] = {}
queryObj[getObj.attribute][getObj.operator] = getObj.value
groupsArr = groups.chain().find(queryObj).data()
}
} else if (getObj.rawFilter) {
// optional - advanced filtering having and/or/not - use getObj.rawFilter
throw new Error(`${action} error: not supporting advanced filtering: ${getObj.rawFilter}`)
} else {
// mandatory - no filtering (!getObj.operator && !getObj.rawFilter) - all groups to be returned - correspond to exploreUsers() in versions < 4.x.x
groupsArr = groups.chain().data()
}
// mandatory if-else logic - end
if (!groupsArr) throw new Error(`${action} error: mandatory if-else logic not fully implemented`)
if (!getObj.startIndex) getObj.startIndex = 1
if (!getObj.count) getObj.count = 200
const ret: any = {
Resources: [],
totalResults: null, // total number of objects when using paging (ref. startIndex/count)
}
const arr = groupsArr.map((obj) => { return stripLoki(obj) }) // all attributes included
const delta = arr.slice(getObj.startIndex - 1, getObj.startIndex - 1 + getObj.count) // supporting paging "light"
Array.prototype.push.apply(ret.Resources, delta)
ret.totalResults = arr.length // set to maximum, will be corrected if needed by scimgateway
return ret
}
// =================================================
// createGroup
// =================================================
scimgateway.createGroup = async (baseEntity, groupObj, ctx) => {
const action = 'createGroup'
scimgateway.logDebug(baseEntity, `handling ${action} groupObj=${JSON.stringify(groupObj)}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const groups = config.entity[baseEntity].groups
if (groupObj.externalId) groupObj.id = groupObj.externalId // for loki-plugin (scim endpoint) id is mandatory and set to displayName
else groupObj.id = groupObj.displayName
if (groupObj.members) {
const noneExistingUsers: any = []
await Promise.all(groupObj.members.map(async (el: any) => {
if (el.value) {
const getObj = { attribute: 'id', operator: 'eq', value: el.value }
const usrs = await scimgateway.getUsers(baseEntity, getObj, ['id', 'displayName'], ctx) // check if user exist
if (!usrs || !usrs.Resources || usrs.Resources.length !== 1 || usrs.Resources[0].id !== el.value) {
noneExistingUsers.push(el.value)
} else if (usrs.Resources[0].displayName) {
el.display = usrs.Resources[0].displayName
}
}
}))
if (noneExistingUsers.length > 0) throw new Error(`following user(s) does not exist and can't be member of group: ${noneExistingUsers.join(', ')}`)
}
try {
await groups.insert(groupObj)
return null
} catch (err: any) {
const newErr = new Error(`${action} error: ${err.message}`)
if (err.message && err.message.startsWith('Duplicate key')) {
newErr.name += '#409' // customErrorCode
}
throw newErr
}
}
// =================================================
// deleteGroup
// =================================================
scimgateway.deleteGroup = async (baseEntity, id) => {
const action = 'deleteGroup'
scimgateway.logDebug(baseEntity, `handling ${action} id=${id}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const groups = config.entity[baseEntity].groups
const res = groups.find({ id: id })
if (res.length !== 1) throw new Error(`${action} error: failed for id=${id}`)
const groupObj = res[0]
await groups.remove(groupObj)
return null
}
// =================================================
// modifyGroup
// =================================================
scimgateway.modifyGroup = async (baseEntity, id, attrObj, ctx) => {
const action = 'modifyGroup'
scimgateway.logDebug(baseEntity, `handling ${action} id=${id} attrObj=${JSON.stringify(attrObj)}`)
if (!config.entity[baseEntity]) throw new Error(`unsupported baseEntity=${baseEntity}`)
const groups = config.entity[baseEntity].groups
const res = groups.find({ id: id })
if (res.length === 0) throw new Error(`${action} error: group id=${id} - group does not exist`)
if (res.length > 1) throw new Error(`${action} error: group id=${id} - group is not unique, more than one have been found`)
const groupObj = res[0]
if (!groupObj.members) groupObj.members = []
const usersNotExist: string[] = []
if (attrObj.members) {
if (!Array.isArray(attrObj.members)) {
throw new Error(`${action} error: ${JSON.stringify(attrObj)} - correct syntax is { "members": [...] }`)
}
for (const el of attrObj.members) {
if (el.operation && el.operation === 'delete') { // delete member from group
if (!el.value) groupObj.members = [] // members=[{"operation":"delete"}] => no value, delete all members
else {
groupObj.members = groupObj.members.filter((element: Record<string, any>) => element.value !== el.value)
}
} else { // Add member to group
if (el.value) {
const getObj = { attribute: 'id', operator: 'eq', value: el.value }
const usrs: any = await scimgateway.getUsers(baseEntity, getObj, ['id', 'displayName'], ctx) // check if user exist
if (usrs && usrs.Resources && usrs.Resources.length === 1 && usrs.Resources[0].id === el.value) {
const newMember = {
display: usrs.Resources[0].displayName || el.value,
value: el.value,
}
const exists = groupObj.members.some((e: Record<string, any>) => (e.value === el.value))
if (!exists) groupObj.members.push(newMember)
} else usersNotExist.push(el.value)
}
}
}
}
delete attrObj.members
for (const key in attrObj) { // displayName/externalId
groupObj[key] = attrObj[key]
}
await groups.update(groupObj)
if (usersNotExist.length > 0) throw new Error(`${action} error: failed for id=${groupObj.id} - includes none existing users: ${usersNotExist.toString()}`)
return null
}
// =================================================
// helpers
// =================================================
const stripLoki = (obj: Record<string, any>) => { // remove loki meta data and insert scim
const retObj = JSON.parse(JSON.stringify(obj)) // new object - don't modify loki source
if (retObj.meta) {
delete retObj.meta.lastModified // test users loaded
if (retObj.meta.created) retObj.meta.created = new Date(retObj.meta.created).toISOString()
if (retObj.meta.updated) {
retObj.meta.lastModified = new Date(retObj.meta.updated).toISOString()
delete retObj.meta.updated
} else retObj.meta.lastModified = retObj.meta.created
if (retObj.meta.revision !== undefined) {
retObj.meta.version = `W/"${retObj.meta.revision}"`
delete retObj.meta.revision
}
}
delete retObj.$loki
return retObj
}
//
// Cleanup on exit
//
process.on('SIGTERM', () => { // kill
for (const baseEntity in config.entity) {
if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
}
})
process.on('SIGINT', () => { // Ctrl+C
for (const baseEntity in config.entity) {
if (config.entity[baseEntity].db) config.entity[baseEntity].db.close()
}
})