parent
243a0c159c
commit
bd65cdfcc2
@ -1,2 +1,25 @@
|
||||
# signpost
|
||||
# Signpost
|
||||
|
||||
Simple fast secure Webhook handling Service. Add Flows that handle Actions-Classes
|
||||
written in Javascript. Each Flow is secured by an Token. Optional you can add
|
||||
an JsonSchema for the Flow to validated incoming Data.
|
||||
|
||||
## install
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run migrate --workspace=server
|
||||
npm run start --workspace=server
|
||||
```
|
||||
|
||||
## add flow
|
||||
|
||||
```
|
||||
npm run addFlow
|
||||
```
|
||||
|
||||
### action
|
||||
|
||||
A Class for that have only the async method run. Each Object of class
|
||||
has the current flow, the requested body and optional a JsonSchema. The Class
|
||||
must be placed.
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
import { client, xml } from '@xmpp/client'
|
||||
import debug from '@xmpp/debug'
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
|
||||
import Action from './../../packages/server/actions/action.js'
|
||||
import logger from './../../packages/server/helper/logger.js'
|
||||
|
||||
/**
|
||||
* using a xmpp-server to send messages in a group
|
||||
*
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
|
||||
class Xmpp extends Action {
|
||||
|
||||
/**
|
||||
* run
|
||||
*
|
||||
*/
|
||||
async run() {
|
||||
|
||||
// cleaning data
|
||||
this._sanitize()
|
||||
|
||||
const xmpp = client({
|
||||
service: process.env.XMPP_SERVICE,
|
||||
domain: process.env.XMPP_DOMAIN,
|
||||
username: process.env.XMPP_USERNAME,
|
||||
password: process.env.XMPP_PASSWORD
|
||||
})
|
||||
|
||||
if (process.env.APP_DEBUG) {
|
||||
debug(xmpp, true)
|
||||
}
|
||||
|
||||
// handle if client has errors
|
||||
xmpp.on('error', (error) => {
|
||||
logger(this.flow.uuid).error('xmpp / error ' + error)
|
||||
})
|
||||
|
||||
// handle if client goes online
|
||||
xmpp.once('online', async (address) => {
|
||||
|
||||
// join group
|
||||
await xmpp.send(xml('presence', { to: process.env.XMPP_ROOM + '/' + process.env.XMPP_USERNAME }, xml('x', { xmlns: 'http://jabber.org/protocol/muc' })))
|
||||
|
||||
// sends a message to the room
|
||||
const message = xml(
|
||||
'message', {
|
||||
type: 'groupchat',
|
||||
to: process.env.XMPP_ROOM
|
||||
},
|
||||
xml('body', {}, this.data.message)
|
||||
)
|
||||
|
||||
await xmpp.send(message)
|
||||
logger(this.flow.uuid).info('xmpp / message send')
|
||||
|
||||
await xmpp.disconnect()
|
||||
})
|
||||
|
||||
await xmpp.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* clearing data from any malicous code
|
||||
*
|
||||
*/
|
||||
_sanitize() {
|
||||
this.data.message = DOMPurify.sanitize(this.data.message)
|
||||
}
|
||||
}
|
||||
|
||||
export default Xmpp
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "signpost",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import fastify from 'fastify'
|
||||
import getOrCreateSqlite from './db/sqlite.js'
|
||||
|
||||
// get config
|
||||
import config from './_config.js'
|
||||
config()
|
||||
|
||||
// create server
|
||||
const server = fastify()
|
||||
|
||||
// add rate limit
|
||||
import rateLimit from '@fastify/rate-limit'
|
||||
|
||||
server.register(rateLimit, {
|
||||
max: process.env.APP_RATE_LIMIT_MAX,
|
||||
timeWindow: process.env.APP_RATE_LIMIT_TIMEWINDOW
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
server.decorate('db', () => {
|
||||
return getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||
})
|
||||
|
||||
/**
|
||||
* routing
|
||||
*
|
||||
*/
|
||||
|
||||
import flowHttp from './http/api/flow.js'
|
||||
|
||||
server
|
||||
.register(flowHttp, {
|
||||
'prefix': '/api/flow/v1'
|
||||
})
|
||||
|
||||
export default server
|
||||
@ -0,0 +1,28 @@
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
/**
|
||||
* getting emv amd merge with basic values
|
||||
*
|
||||
*
|
||||
*/
|
||||
function config() {
|
||||
|
||||
// getting dir
|
||||
const server_dir = path.resolve()
|
||||
const base_dir = path.join(server_dir, '/../..')
|
||||
|
||||
dotenv.config({ path: base_dir + '/.env' })
|
||||
|
||||
const config = {
|
||||
APP_BASE_DIR: base_dir,
|
||||
APP_SERVER_DIR: server_dir,
|
||||
APP_SQLITE_URI_MAIN: path.join(base_dir, '/storage/main.db'),
|
||||
APP_HASH_TYPE: 'sha512'
|
||||
}
|
||||
|
||||
// merge
|
||||
dotenv.populate(process.env, config)
|
||||
}
|
||||
|
||||
export default config
|
||||
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Action
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
class Action {
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
constructor(flow, data) {
|
||||
this.flow = flow
|
||||
this.data = data
|
||||
|
||||
if (!this.run()) {
|
||||
throw new Error('run method needed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Action
|
||||
@ -0,0 +1,58 @@
|
||||
import { input, password } from '@inquirer/prompts'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import path from 'path'
|
||||
import chalk from 'chalk'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import runMigrationSqlite from './../db/migration.js'
|
||||
import getOrCreateSqlite from './../db/sqlite.js'
|
||||
|
||||
import TokenHelper from './../helper/token.js'
|
||||
|
||||
// getting flow
|
||||
import FlowStore from './../store/flow.js'
|
||||
|
||||
// get config
|
||||
import config from './../_config.js'
|
||||
config()
|
||||
|
||||
// getting db and flowStore
|
||||
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||
const flowStore = new FlowStore(mainDB)
|
||||
|
||||
const token = crypto.randomBytes(64).toString('base64')
|
||||
const flow = {
|
||||
'hash' : TokenHelper.create(token)
|
||||
}
|
||||
|
||||
flow.uuid = uuidv4()
|
||||
|
||||
// adding action
|
||||
flow.action = await input({
|
||||
message: 'action:',
|
||||
async validate(value) {
|
||||
if (!value) {
|
||||
return 'Required!'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// adding schema
|
||||
flow.schema = await input({
|
||||
message: 'schema:'
|
||||
})
|
||||
|
||||
// adding description
|
||||
flow.description = await input({
|
||||
message: 'description:'
|
||||
})
|
||||
|
||||
const id = flowStore.create(flow, token)
|
||||
|
||||
if (id) {
|
||||
console.log(chalk.green('done'))
|
||||
console.log(chalk.blue('uuid: ' + flow.uuid))
|
||||
console.log(chalk.blue('token: ' + token))
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import runMigrationSqlite from './../db/migration.js'
|
||||
import getOrCreateSqlite from './../db/sqlite.js'
|
||||
import path from 'path'
|
||||
|
||||
// get config
|
||||
import config from './../_config.js'
|
||||
config()
|
||||
|
||||
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||
runMigrationSqlite(path.join(path.resolve(), './../server/migration'), mainDB)
|
||||
@ -0,0 +1,24 @@
|
||||
import { input, password } from '@inquirer/prompts'
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getOrCreateSqlite } from '@nano/sqlite'
|
||||
import path from 'path'
|
||||
|
||||
import FlowStore from './../store/flow.js'
|
||||
|
||||
// getting db and flowStore
|
||||
const mainDB = getOrCreateSqlite({ 'uri': process.env.APP_SQLITE_URI_MAIN, 'create': true, 'readwrite': true })
|
||||
const flowStore = new FlowStore(mainDB)
|
||||
|
||||
// adding action
|
||||
const uuid = await input({
|
||||
message: 'uuid:',
|
||||
async validate(value) {
|
||||
if (!value) {
|
||||
return 'Required!'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
flowStore.deleteByUuid(uuid)
|
||||
@ -0,0 +1,41 @@
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* runMigrationSqlite
|
||||
*
|
||||
* @param {string} path
|
||||
* @param {object} db
|
||||
*
|
||||
*/
|
||||
async function runMigrationSqlite(filePath, db) {
|
||||
|
||||
const SQL_EXTENSION = '.sql'
|
||||
|
||||
// getting files
|
||||
const files = await readdir(filePath)
|
||||
|
||||
for (const fileName of files) {
|
||||
|
||||
// skip if extension not sql
|
||||
if (path.extname(fileName).toLowerCase() !== SQL_EXTENSION) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sql = await readFile(filePath + '/' + fileName, 'utf8')
|
||||
|
||||
try {
|
||||
if (!sql.trim()) {
|
||||
throw new Error(fileName + ' is empty!')
|
||||
}
|
||||
|
||||
db.prepare(sql.trim()).run()
|
||||
|
||||
console.log('migrated ' + path.basename(fileName, SQL_EXTENSION))
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default runMigrationSqlite
|
||||
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* db
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3'
|
||||
|
||||
function getOrCreateSqlite(options = {}) {
|
||||
|
||||
let db
|
||||
|
||||
options = Object.assign({
|
||||
uri: process.env.NANO_SQLITE_PATH,
|
||||
wal: true
|
||||
}, options)
|
||||
|
||||
const uri = options.uri
|
||||
delete options.uri
|
||||
|
||||
const wal = options.wal
|
||||
delete options.wal
|
||||
|
||||
//
|
||||
if (Object.keys(options).length === 0) {
|
||||
db = new Database(uri)
|
||||
} else {
|
||||
db = new Database(uri, options)
|
||||
}
|
||||
|
||||
if (wal) {
|
||||
db.pragma('journal_mode = WAL')
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
export default getOrCreateSqlite
|
||||
@ -0,0 +1,53 @@
|
||||
import { resolveActionClass, resolveSchema } from './../helper/resolver.js'
|
||||
import FlowStore from './../store/flow.js'
|
||||
import logger from './../helper/logger.js'
|
||||
|
||||
/**
|
||||
*
|
||||
* handle parser
|
||||
*
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
|
||||
async function flowHandler(request, response) {
|
||||
|
||||
// get single flow
|
||||
const flowStore = new FlowStore(this.db())
|
||||
const flow = await flowStore.findOneByUuid(request.params.uuid)
|
||||
|
||||
if (!flow) {
|
||||
return response
|
||||
.code(404)
|
||||
.send()
|
||||
}
|
||||
|
||||
// add to locals
|
||||
response.locals = {
|
||||
'flow' : flow
|
||||
}
|
||||
|
||||
// getting action class
|
||||
try {
|
||||
response.locals.action = await import(resolveActionClass(flow.action))
|
||||
} catch(error) {
|
||||
logger(flow.uuid).error('flowHandler / resolve class / ' + error)
|
||||
return response
|
||||
.code(500)
|
||||
.send()
|
||||
}
|
||||
|
||||
// check for schema
|
||||
if (flow.schema) {
|
||||
try {
|
||||
response.locals.schema = await import(resolveSchema(flow.schema))
|
||||
} catch(error) {
|
||||
logger(flow.uuid).error('flowHandler / resolve schema / ' + error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default flowHandler
|
||||
@ -0,0 +1,44 @@
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
import TokenHelper from './../helper/token.js'
|
||||
import logger from './../helper/logger.js'
|
||||
|
||||
/**
|
||||
* handle token
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
|
||||
async function tokenHandler(request, response) {
|
||||
|
||||
if (!request.headers.authorization) {
|
||||
return response
|
||||
.code(403)
|
||||
.send()
|
||||
}
|
||||
|
||||
let token = DOMPurify.sanitize(request.headers.authorization)
|
||||
token = token.match(/^Bearer ([A-Za-z0-9._~+/-]+=*)$/)
|
||||
|
||||
// check if token exists
|
||||
if (!token[1]) {
|
||||
logger(response.locals.flow.uuid).error('token not found in header')
|
||||
|
||||
return response
|
||||
.code(403)
|
||||
.send()
|
||||
}
|
||||
|
||||
// check if token is same as for the flow
|
||||
if (!TokenHelper.equal(token[1], response.locals.flow.hash)) {
|
||||
logger(response.locals.flow.uuid).error('token not equal with hash from flow')
|
||||
|
||||
return response
|
||||
.code(403)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
|
||||
export default tokenHandler
|
||||
@ -0,0 +1,35 @@
|
||||
import pino from 'pino'
|
||||
import pretty from 'pino-pretty'
|
||||
import dayjs from 'dayjs'
|
||||
import { mkdirSync } from "node:fs"
|
||||
|
||||
/**
|
||||
* Wrapper for logger
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
function logger(uuid) {
|
||||
|
||||
let destination = process.env.APP_BASE_DIR + '/storage/logs/'
|
||||
|
||||
// if name is set, adding name as directory
|
||||
if (uuid) {
|
||||
destination += uuid + '/'
|
||||
mkdirSync(destination, { recursive: true })
|
||||
}
|
||||
|
||||
destination += dayjs().format('DD-MM-YYYY') + '.log'
|
||||
|
||||
return pino({
|
||||
timestamp: () => {
|
||||
return `, "time":"${new Date(Date.now()).toISOString()}"`
|
||||
},
|
||||
},
|
||||
pino.destination(destination)
|
||||
)
|
||||
}
|
||||
|
||||
export default logger
|
||||
@ -0,0 +1,54 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* resolve action class
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
function resolveActionClass(className) {
|
||||
|
||||
let classPath = path.join(process.env.APP_BASE_DIR, 'resources/actions/' + className + '.js')
|
||||
let result = undefined
|
||||
|
||||
if (fs.existsSync(classPath)) {
|
||||
result = classPath
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Action Class ' + className + ' / ' + classPath + ' not found!')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* resolve schema
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
function resolveSchema(schemaName) {
|
||||
|
||||
let schemaPath = path.join(process.env.APP_BASE_DIR, 'resources/schemas/' + schemaName + '.json')
|
||||
let result = undefined
|
||||
|
||||
if (fs.existsSync(schemaPath)) {
|
||||
result = schemaPath
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Schema ' + schemaName + ' / ' + schemaPath + ' not found!')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export {
|
||||
resolveActionClass,
|
||||
resolveSchema
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import { createHmac, randomBytes } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* TokenHelper
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
const TokenHelper = {
|
||||
|
||||
/**
|
||||
* create hash from token
|
||||
*
|
||||
* @param string token
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
create(token) {
|
||||
const hmac = createHmac(process.env.APP_HASH_TYPE, process.env.APP_SALT)
|
||||
const buffer = new Buffer(token)
|
||||
|
||||
return hmac.update(buffer).digest('base64')
|
||||
},
|
||||
|
||||
/**
|
||||
* check if token and hash are equal
|
||||
*
|
||||
* @param string token
|
||||
* @param string hash
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
*/
|
||||
equal(token, hash) {
|
||||
return TokenHelper.create(token) === hash
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenHelper
|
||||
@ -0,0 +1,48 @@
|
||||
import tokenHandler from './../../handler/token.js'
|
||||
import flowHandler from './../../handler/flow.js'
|
||||
import logger from './../../helper/logger.js'
|
||||
|
||||
/**
|
||||
* handle webhook
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
|
||||
export default async function(fastify, options) {
|
||||
|
||||
fastify.addHook('preHandler', flowHandler)
|
||||
fastify.addHook('preHandler', tokenHandler)
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} request
|
||||
* @param {object} response
|
||||
*
|
||||
*/
|
||||
fastify.post('/:uuid(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$)', async function (request, response) {
|
||||
|
||||
if (response.locals.schema && !request.validateInput(request.body, response.locals.schema)) {
|
||||
return response
|
||||
.code(400)
|
||||
.send()
|
||||
}
|
||||
|
||||
// running actions
|
||||
const action = new response.locals.action.default(response.locals.flow, request.body)
|
||||
|
||||
response
|
||||
.code(204)
|
||||
.send()
|
||||
|
||||
// try run from action
|
||||
try {
|
||||
await action.run()
|
||||
} catch(error) {
|
||||
logger(response.locals.flow.uuid).error('webhook / run / ' + error)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import server from './_bootstrap.js'
|
||||
|
||||
// let it rain
|
||||
const start = async () => {
|
||||
try {
|
||||
|
||||
await server.listen({
|
||||
port: process.env.APP_PORT
|
||||
})
|
||||
|
||||
console.log('Server: start')
|
||||
console.log('Server: running on port ' + process.env.APP_PORT)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
start()
|
||||
@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS flows (
|
||||
id INTEGER PRIMARY KEY,
|
||||
uuid TEXT NOT NULL UNIQUE,
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
action TEXT NOT NULL,
|
||||
schema TEXT,
|
||||
date_created_at TEXT
|
||||
)
|
||||
@ -0,0 +1,28 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "server",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@inquirer/prompts": "^8.3.0",
|
||||
"@xmpp/client": "^0.14.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"chalk": "^5.6.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.3.1",
|
||||
"fastify": "^5.7.4",
|
||||
"isomorphic-dompurify": "^3.0.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@xmpp/debug": "^0.14.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"migrate": "node cli/migrate.js",
|
||||
"addFlow": "node cli/addFlow.js"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Store from './store.js'
|
||||
|
||||
/**
|
||||
* Store for Flow
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
class FlowStore extends Store {
|
||||
|
||||
constructor(db) {
|
||||
super(db, 'flows')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
create(data, hash) {
|
||||
data.date_created_at = dayjs().toISOString()
|
||||
return super.create(data)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
findOneByUuid(uuid) {
|
||||
return this._db.prepare('SELECT * FROM ' + this._tableName + ' WHERE uuid = ?')
|
||||
.get(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
export default FlowStore
|
||||
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* store - abstract class
|
||||
*
|
||||
* @author Björn Hase <me@herr-hase.wtf>
|
||||
* @license hhttps://www.gnu.org/licenses/gpl-3.0.en.html GPL-3
|
||||
* @link https://git.node001.net/HerrHase/signpost.git
|
||||
*
|
||||
*/
|
||||
|
||||
class Store {
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {tableName}
|
||||
*
|
||||
*/
|
||||
constructor(db, tableName) {
|
||||
this._db = db
|
||||
this._tableName = tableName
|
||||
}
|
||||
|
||||
/**
|
||||
* create row
|
||||
*
|
||||
*/
|
||||
findOneById(id) {
|
||||
return this.db
|
||||
.prepare('SELECT * FROM ' + this._tableName + ' WHERE id = ?')
|
||||
.run(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* create row
|
||||
*
|
||||
*/
|
||||
remove(id) {
|
||||
this.db
|
||||
.prepare('DELETE FROM ' + this._tableName + ' WHERE id = ?')
|
||||
.run(id)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
update(id, data) {
|
||||
return this._db.prepare('UPDATE ' + this._tableName + ' SET ' + this._prepareUpdateBinding(data) + ' WHERE id = ?')
|
||||
.run(id, data)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
create(data) {
|
||||
return this._db.prepare('INSERT INTO ' + this._tableName + ' (' + Object.keys(data).join(', ') + ') VALUES (' + this._prepareInsertBinding(data) + ')')
|
||||
.run(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* run through object and add key to string for
|
||||
* binding parameters for insert
|
||||
*
|
||||
* @param {object} data
|
||||
* @return {string}
|
||||
*/
|
||||
_prepareInsertBinding(data) {
|
||||
let result = ''
|
||||
let length = Object.keys(data).length
|
||||
let index = 0
|
||||
|
||||
for (const key in data) {
|
||||
result += '@' + key
|
||||
|
||||
if (index++ < (length - 1)) {
|
||||
result += ', '
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* run through object and add key to string for
|
||||
* binding parameters for update
|
||||
*
|
||||
* @param {object} data
|
||||
* @return {string}
|
||||
*/
|
||||
_prepareUpdateBinding(data) {
|
||||
let result = ''
|
||||
let length = Object.keys(data).length
|
||||
let index = 0
|
||||
|
||||
for (const key in data) {
|
||||
result += key + ' = @' + key
|
||||
|
||||
if (index++ < (length - 1)) {
|
||||
result += ', '
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export default Store
|
||||
@ -0,0 +1,14 @@
|
||||
import {describe, it} from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
|
||||
import config from './../_config.js'
|
||||
config()
|
||||
|
||||
import { resolveActionClass } from './../helper/resolver.js'
|
||||
|
||||
describe('helper / resolver', () => {
|
||||
it('should get a action class', () => {
|
||||
const action = resolveActionClass('test')
|
||||
assert.match(action, /test.js/)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in new issue