这是提交给《The Pinata Challenge》的参赛作品。
我所创建的: 项目一个具有弹性的 web 应用程序,用于在 MySQL 或 PostgreSQL 数据库上执行查询,提供编写查询的界面,并给出它们所引发的详细错误反馈。
有一个开关可以在查询发送到数据库执行之前开启或关闭连接池的功能。
这个 webapp 提供了一个小部件,用于接收 Pinata 账户的 gateway
和 JWT
信息,以便将其用于将查询和响应上传到 IPFS,并在 Pinata 账户仪表板的文件部分展示这些上传。
以下是应用程序的一些UI截图。该应用程序是为本地运行而构建的,尚未配置以在公共服务器上运行。
__
在对话框中粘贴数据库URL
__
显示从数据库查询到的几个细节
网页界面在等待数据库查询结果时的显示
查询成功执行 - 点击“显示结果”按钮会在下面显示查询结果
点击“显示结果”按钮后显示的结果
查询出错,如图所示。
显示因查询格式不正确引起的错误
__
为Pinata小部件提供用于上传查询和结果到IPFS的详细信息
成功上传到Pinata后的JSON显示
通过CID检索上传时服务器返回的错误消息
代码基础采用 Node.js 和 Vercel 无服务器环境混合编写的方式——在这两种环境中都能成功运行。
ogbotemi-2000 / sqlUI_pinataSQL用户界面Pinata (ogbotemi-2000/sqlUI_pinata) — 一个由ogbotemi-2000创建的SQL用户界面项目。
一个功能完整的SQL RDBMS(MySQL或PostgreSQL)UI网页应用,接受并存储数据库连接字符串,并以稳定的方式执行查询,这些查询由用户在一个简单的IDE中编辑。错误会直观地显示给用户,供用户纠正或学习。可以在运行时通过该应用配置数据库,支持连接池功能,并支持通过Pinata将SQL查询和响应上传到IPFS中。
neon-starter-kit.mp4
HTTP
Node.js 服务器,可应用于 api/
端点MySQL
和/或 PostgreSQL
数据库之间动态切换,不会出现混淆连接池
查看 GitHub 上的
await pinata.upload.json({})
因为一些关于缺少 File
的不明错误而崩溃pinata.upload.file()
的文档使用了 new File
的方法,尽管Node.js中没有这个构造函数以下是文档中未提及的代码,经过多次尝试和错误后才使用。
pinata.upload.file(new Blob([string_data], type)) // 将字符串数据作为Blob上传到pinata
进入全屏 退出全屏
api/pinata.js
文件中定义了一个 API 端点,用于处理向 Pinata 上传数据和从 Pinata 检索数据,还会将提供的 JWT
和 Gateway
写入 sql_ui_config.json
文件中。
let { PinataSDK } = require('pinata'), both = require('../js/both'), fs = require('fs'), path = require('path'), filePath = path.join(require('../rootDir')(__dirname), './sql_ui_config.json'), config = fs.existsSync(filePath) && require(filePath), pinata; function upload(file, name, buffer, date) { date = new Date, buffer = Buffer.from(file, 'utf-8'), /** 类似于浏览器中使用的文件对象 */ // file = { buffer, name, type: 'text/plain', size: buffer.length, lastModified: +date, lastModifiedDate: date.toISOString() }, file = new Blob([file], { type: 'text/plain' }); return pinata.upload.file(file) } module.exports = async function(request, response) { let { data } = request.body || request.query; /** 硬编码的字符串,用于客户端和服务器上的分割 */ data = data.slice(data.indexOf('=') + 1).split('<_8c23#_>'), pinata = new PinataSDK({ pinataGateway: data[2], pinataJwt: data[3] }); /** 将提供的数据写入文件中 */ config && (config.PINATA_GATEWAY = data[2], config.PINATA_JWT = data[3]), config || { PINATA_GATEWAY: data[2], PINATA_JWT: data[3] }, fs.writeFile(filePath, both.format(JSON.stringify(config)), _ => _) // 测试认证(注释掉了这条) if (!data[4]) { let res // 如果CID没有被发送 upload(data[0], data[1]) .then(json => { console.log('::JSON::', res = json) }) .catch(err => { console.log('::ERROR::', res = err) }) .finally(_ => response.json(res)) } else { let res; pinata.gateways.get(data[4]) .then(file => console.log('::RETRIEVED::', res = file)) .catch(error => console.log('::RETRIEVED::ERRORED::', res = error)) .finally(_ => response.json(res)) } }
全屏 全屏退出
server.js
文件是这样编写的,就像用 Vercel 接收和响应 API 端点一样。
let fs = require('fs'); /** 将可用的 ENV 变量写入 process.env */ fs.readFile('.env.development.local', (err, data)=>{ if(err) { /*console.error(err); */return; } data.toString().replace(/\#[^\n]+\n/, '').split('\n').filter(e=>e) .forEach(el=>{ let { 0:key, 1:value } = el.split('='); process.env[key] = value.replace(/"/g, ''); // console.log(process.env[key]) }) }); let http = require('http'), path = require('path'), config = fs.existsSync('./config.json')&&require('./config.json')||{PORT: process.env.PORT||3000}, mime = require('mime-types'), jobs = { GET:function(req, res, parts, fxn) { /** 响应 GET 请求的中间件在这里被调用 */ fxn = fs.existsSync(fxn='.'+parts.url+'.js')&&require(fxn) if(parts.query) req.query = parts.params, fxn&&fxn(req, res); return !!fxn; }, POST:function(req, res, parts, fxn, bool) { fxn = fs.existsSync(fxn='.'+parts.url+'.js')&&require(fxn), req.on('data', function(data) { /** 创建 req.body 和 res.json,因为调用的模块需要它们被定义 */ req.body = /\{|\}/.test(data=data.toString()) ? { data } : (parts = urlParts('?'+data)).params, fxn&&fxn(req, res) }); if(!fxn) res.end(); /** 由于 POST 请求不需要获取 HTML 资源,这里返回 true 而不是 !!fxn */ return !!fxn||bool; } }, cache = {}; /** 用于存储从文件中读取的数据字符串 */ http.createServer((req, res, url, parts, data, verb)=>{ ({ url } = parts = urlParts(req.url)), /** 预期发送给客户端的数据,这种做法避免了在 jobs 中使用 res.write 和 res.send */ res.json=obj=>res.end(JSON.stringify(obj)), // 为 vercel 函数 data = jobs[verb=req.method](https://dev.to/req, res, parts), url = url === '/' ? 'index.html' : url, /** 以下代码可以移到一个 job 中,但出于优先级考虑,它被保留在这里 */ data || new Promise((resolve, rej, cached)=>{ if (data) { resolve(/*动态数据,退出*/); return; } /*(cached=cache[req.url])?resolve(cached):*/fs.readFile(path.join('./', url), (err, buf)=>{ if(err) rej(err); else resolve(cache[req.url]=buf) }) }).then(cached=>{ res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Content-type': mime.lookup(url) || 'application/octet-stream' }), /** 返回动态数据或已读取的静态文件 */ // console.log("::PROMISE", [url]), res.end(cached) }).catch((err, str)=>{ str='::ERROR:: '+err, // console.error(str='::ERROR:: '+err, [url]) res.end(str) }) }).listen(config.PORT, _=>{ console.log(`Server listening on PORT ${config.PORT}`) }) function urlParts(url, params, query, is_html) { params = {}, query='', url.replace(/\?[^]*/, e=>((query=e.replace('?', '')).split('&').forEach(e=>params[(e=e.split('='))[0]]=decodeURIComponent(e[1])), '')), query &&= '?'+query, is_html = !/\.[^]+$/.test(is_html = (url = url.replace(query, '')).split('/').pop())||/\.html$/.test(is_html); return { params, query: decodeURIComponent(query), url, is_html } }
点击全屏进入,点击退出全屏
路由 api/accountData.js
定义的 API 端点处理与配置、响应和将提供的数据持久化到 Web 应用的 sql_ui_config.json
相关的所有内容。
let data = {}, fs = require('fs'), path = require('path'), file = path.join(require('../rootDir')(__dirname), './sql_ui_config.json'), both = require('../js/both'), dB, stringErr = (err, cause, fix)=>[`"${err.message}"<center>----------</center>${cause},代码为:${err.code},严重程度:${err.severity||'<N/A>'},对于sql状态:\`${err.sqlState||'<N/A>'}\`,位置:\`${err.position||'<N/A>'}\`,操作为:\`${err.routine||'<N/A>'}\``, fix], isMySQL; module.exports = function(request, response) { let { pooled, query, setup } = request.body||request.query, /** 为了适应通过此服务器或Vercel无服务器进行的GET或POST请求 */ config=fs.existsSync(file)&&require(file), stored = (config||{ }).CONNECTION_STRING; data.result='', data.errors = []; // console.log('::SETUP::', [setup, config, __dirname]); if(setup||stored) { config&&(config.CONNECTION_STRING = setup||stored||''), config||={ CONNECTION_STRING: setup||stored }, // console.log('::SETUP::', setup, config, file), fs.writeFileSync(file, both.format(JSON.stringify(config))); if(!query) dB = null; /** 缺少查询且存在设置,意味着应用程序需要重新配置,使用新的连接字符串,需重新连接数据库驱动 */ isMySQL = /^mysql/.test(setup||stored), data.configured = setup!=stored ? setup||stored /** 第一次发送存储的数据库字符串,以与客户端同步 */ : /* 为了安全,用一个真理值而不是存储的数据库字符串 */ 1, /** 设置 dB = null 会破坏下面的闭包,|= 为了在错误或重新配置后更新connectionString:setup */ setup&&(dB ||= require('../db')({isMySQL, connectionString:setup})) /** 避免从头开始加载模块和调用导出的函数,除非设置为null或销毁在无服务器函数中*/ .then(operate) .catch(err=>{ data.errors[0] = stringErr(err, '原因:提供的配置连接字符串包含非实体', '重新配置应用程序,并确保提供的数据库URL解析为实际数据库'), dB = null, response.json(data) }); if(stored&&!setup) { /** 如果可用,提供pinata配置以发送数据,作为客户端填充它们的UI小技巧 */ let res = { configured: setup||stored }; ['JWT', 'GATEWAY'].forEach((env, value)=>{ (value = config['PINATA_'+env])&&(res[env.toLowerCase()] = value) }), response.json(res) } } else return response.json({configured:0}); /* 实际应用自定义查询到数据库的部分 */ function operate(db) { db.pooled = pooled; if(query = query.replace(/\++/g, '\t')) db.query(query).then(arr=>{ data.result = isMySQL ? arr[0] : arr }).catch(err=>data.errors[0] = stringErr(err, `查询:\`${query.split(/\s/).shift()}\``, '请编写正确的语法查询,并仅指定数据库中存在的字段或表,或支持的操作')) .finally(_=>response.json(data)); else { let count = 0;/** 用于循环外部,因为异步性质使得索引i不可靠 */ /** 添加条件以避免从不存在的函数或字段读取错误 */ ['version'].concat(isMySQL ? [] : ['inet_server_addr', 'inet_server_port']).forEach((query, i, a)=>{ db.query(`select ${query}()`).then(arr=>{ data[query.replace('inet_', '')] = (arr=arr.flat())[0][query]||arr[0][query+'()'] }) .catch(err=>data.errors[0] = ['::数据库连接:: '+(/*data.version=*/err.message), '连接到互联网并删除数据库连接环境变量中的拼写错误']) .finally(_=>{ if(!a[++count]) data.version = "版本:" + data.version, response.json(data); }) }) } } }
进入全屏 退出全屏
谢谢阅读。