mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-03 16:10:31 +00:00
feat: add expiry support to DBKV, and eslint config
This commit is contained in:
+4
-1
@@ -43,4 +43,7 @@ jsconfig.json
|
||||
# node js
|
||||
# ======================================================================
|
||||
# the exact tree installed in the node_modules folder
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
.roo
|
||||
AGENTS.md
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
export default {
|
||||
meta: {
|
||||
type: 'layout',
|
||||
docs: {
|
||||
description: 'enforce spacing inside parentheses for control structures only',
|
||||
category: 'Stylistic Issues',
|
||||
},
|
||||
fixable: 'whitespace',
|
||||
schema: [],
|
||||
messages: {
|
||||
missingSpaceAfterOpen: 'Missing space after opening parenthesis in control structure.',
|
||||
missingSpaceBeforeClose: 'Missing space before closing parenthesis in control structure.',
|
||||
unexpectedSpaceAfterOpen: 'Unexpected space after opening parenthesis in function call.',
|
||||
unexpectedSpaceBeforeClose: 'Unexpected space before closing parenthesis in function call.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
function checkControlStructureSpacing(node) {
|
||||
// For control structures, we need to find the parentheses around the condition/test
|
||||
let conditionNode;
|
||||
|
||||
if ( node.type === 'IfStatement' || node.type === 'WhileStatement' || node.type === 'DoWhileStatement' ) {
|
||||
conditionNode = node.test;
|
||||
} else if ( node.type === 'ForStatement' || node.type === 'ForInStatement' || node.type === 'ForOfStatement' ) {
|
||||
// For loops, we want the parentheses around the entire for clause
|
||||
conditionNode = node;
|
||||
} else if ( node.type === 'SwitchStatement' ) {
|
||||
conditionNode = node.discriminant;
|
||||
} else if ( node.type === 'CatchClause' ) {
|
||||
conditionNode = node.param;
|
||||
}
|
||||
|
||||
if ( !conditionNode ) return;
|
||||
|
||||
// Find the opening paren - it should be right before the condition starts
|
||||
const openParen = sourceCode.getTokenBefore(conditionNode, token => token.value === '(');
|
||||
if ( !openParen || openParen.value !== '(' ) return;
|
||||
|
||||
// Find the closing paren - it should be right after the condition ends
|
||||
const closeParen = sourceCode.getTokenAfter(conditionNode, token => token.value === ')');
|
||||
if ( !closeParen || closeParen.value !== ')' ) return;
|
||||
|
||||
const afterOpen = sourceCode.getTokenAfter(openParen);
|
||||
const beforeClose = sourceCode.getTokenBefore(closeParen);
|
||||
|
||||
// Control structures should have spacing
|
||||
if ( afterOpen && openParen.range[1] === afterOpen.range[0] ) {
|
||||
context.report({
|
||||
node,
|
||||
loc: openParen.loc,
|
||||
messageId: 'missingSpaceAfterOpen',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(openParen, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ( beforeClose && beforeClose.range[1] === closeParen.range[0] ) {
|
||||
context.report({
|
||||
node,
|
||||
loc: closeParen.loc,
|
||||
messageId: 'missingSpaceBeforeClose',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closeParen, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkForLoopSpacing(node) {
|
||||
// For loops are special - we need to find the opening paren after the 'for' keyword
|
||||
// and the closing paren before the body
|
||||
const forKeyword = sourceCode.getFirstToken(node);
|
||||
if ( !forKeyword || forKeyword.value !== 'for' ) return;
|
||||
|
||||
const openParen = sourceCode.getTokenAfter(forKeyword, token => token.value === '(');
|
||||
if ( !openParen ) return;
|
||||
|
||||
// The closing paren should be right before the body
|
||||
const closeParen = sourceCode.getTokenBefore(node.body, token => token.value === ')');
|
||||
if ( !closeParen ) return;
|
||||
|
||||
const afterOpen = sourceCode.getTokenAfter(openParen);
|
||||
const beforeClose = sourceCode.getTokenBefore(closeParen);
|
||||
|
||||
if ( afterOpen && openParen.range[1] === afterOpen.range[0] ) {
|
||||
context.report({
|
||||
node,
|
||||
loc: openParen.loc,
|
||||
messageId: 'missingSpaceAfterOpen',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(openParen, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if ( beforeClose && beforeClose.range[1] === closeParen.range[0] ) {
|
||||
context.report({
|
||||
node,
|
||||
loc: closeParen.loc,
|
||||
messageId: 'missingSpaceBeforeClose',
|
||||
fix(fixer) {
|
||||
return fixer.insertTextBefore(closeParen, ' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkFunctionCallSpacing(node) {
|
||||
// Find the opening parenthesis for this function call
|
||||
const openParen = sourceCode.getFirstToken(node, token => token.value === '(');
|
||||
const closeParen = sourceCode.getLastToken(node, token => token.value === ')');
|
||||
|
||||
if ( !openParen || !closeParen ) return;
|
||||
|
||||
const afterOpen = sourceCode.getTokenAfter(openParen);
|
||||
const beforeClose = sourceCode.getTokenBefore(closeParen);
|
||||
|
||||
// Function calls should NOT have spacing
|
||||
if ( afterOpen && openParen.range[1] !== afterOpen.range[0] ) {
|
||||
const spaceAfter = sourceCode.getText().slice(openParen.range[1], afterOpen.range[0]);
|
||||
if ( /^\s+$/.test(spaceAfter) ) {
|
||||
context.report({
|
||||
node,
|
||||
loc: openParen.loc,
|
||||
messageId: 'unexpectedSpaceAfterOpen',
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([openParen.range[1], afterOpen.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ( beforeClose && beforeClose.range[1] !== closeParen.range[0] ) {
|
||||
const spaceBefore = sourceCode.getText().slice(beforeClose.range[1], closeParen.range[0]);
|
||||
if ( /^\s+$/.test(spaceBefore) ) {
|
||||
context.report({
|
||||
node,
|
||||
loc: closeParen.loc,
|
||||
messageId: 'unexpectedSpaceBeforeClose',
|
||||
fix(fixer) {
|
||||
return fixer.removeRange([beforeClose.range[1], closeParen.range[0]]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Control structures that should have spacing
|
||||
IfStatement(node) {
|
||||
checkControlStructureSpacing(node);
|
||||
},
|
||||
WhileStatement(node) {
|
||||
checkControlStructureSpacing(node);
|
||||
},
|
||||
DoWhileStatement(node) {
|
||||
checkControlStructureSpacing(node);
|
||||
},
|
||||
SwitchStatement(node) {
|
||||
checkControlStructureSpacing(node);
|
||||
},
|
||||
CatchClause(node) {
|
||||
if ( node.param ) {
|
||||
checkControlStructureSpacing(node);
|
||||
}
|
||||
},
|
||||
|
||||
// For loops need special handling
|
||||
ForStatement(node) {
|
||||
checkForLoopSpacing(node);
|
||||
},
|
||||
ForInStatement(node) {
|
||||
checkForLoopSpacing(node);
|
||||
},
|
||||
ForOfStatement(node) {
|
||||
checkForLoopSpacing(node);
|
||||
},
|
||||
|
||||
// Function calls that should NOT have spacing
|
||||
CallExpression(node) {
|
||||
checkFunctionCallSpacing(node);
|
||||
},
|
||||
NewExpression(node) {
|
||||
if ( node.arguments.length > 0 || sourceCode.getLastToken(node).value === ')' ) {
|
||||
checkFunctionCallSpacing(node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import js from '@eslint/js';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import controlStructureSpacing from './control-structure-spacing.js';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
js,
|
||||
'@stylistic': stylistic,
|
||||
custom: { rules: { 'control-structure-spacing': controlStructureSpacing } },
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/backend/**/*.{js,mjs,cjs}'],
|
||||
languageOptions: { globals: globals.node },
|
||||
rules: {
|
||||
'no-unused-vars': ['error', {
|
||||
'vars': 'all',
|
||||
'args': 'after-used',
|
||||
'caughtErrors': 'all',
|
||||
'ignoreRestSiblings': false,
|
||||
'ignoreUsingDeclarations': false,
|
||||
'reportUsedIgnorePattern': false,
|
||||
'argsIgnorePattern': '^_',
|
||||
'caughtErrorsIgnorePattern': '^_',
|
||||
'destructuredArrayIgnorePattern': '^_',
|
||||
|
||||
}],
|
||||
curly: ['error', 'multi-line'],
|
||||
'@stylistic/curly-newline': ['error', 'always'],
|
||||
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||
'@stylistic/indent': ['error', 4, {
|
||||
'CallExpression': 4,
|
||||
}],
|
||||
'@stylistic/indent-binary-ops': ['error', 4],
|
||||
'@stylistic/array-bracket-newline': ['error', 'consistent'],
|
||||
'@stylistic/semi': ['error', 'always'],
|
||||
'@stylistic/quotes': ['error', 'single'],
|
||||
'@stylistic/function-call-argument-newline': ['error', 'consistent'],
|
||||
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'@stylistic/space-before-function-paren': ['error', { 'anonymous': 'never', 'named': 'never', 'asyncArrow': 'always', 'catch': 'never' }],
|
||||
'@stylistic/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }],
|
||||
'@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }],
|
||||
'@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
|
||||
'@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }],
|
||||
'@stylistic/comma-dangle': ['error', 'always-multiline'],
|
||||
'@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }],
|
||||
'@stylistic/dot-location': ['error', 'property'],
|
||||
'@stylistic/space-infix-ops': ['error'],
|
||||
'no-undef': 'error',
|
||||
'custom/control-structure-spacing': 'error',
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
|
||||
},
|
||||
extends: ['js/recommended'],
|
||||
plugins: {
|
||||
js,
|
||||
'@stylistic': stylistic,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs}'],
|
||||
languageOptions: { globals: globals.browser },
|
||||
rules: {
|
||||
|
||||
'no-unused-vars': ['error', {
|
||||
'vars': 'all',
|
||||
'args': 'after-used',
|
||||
'caughtErrors': 'all',
|
||||
'ignoreRestSiblings': false,
|
||||
'ignoreUsingDeclarations': false,
|
||||
'reportUsedIgnorePattern': false,
|
||||
'argsIgnorePattern': '^_',
|
||||
'caughtErrorsIgnorePattern': '^_',
|
||||
'destructuredArrayIgnorePattern': '^_',
|
||||
}],
|
||||
'@stylistic/curly-newline': ['error', 'always'],
|
||||
'@stylistic/object-curly-spacing': ['error', 'always'],
|
||||
'@stylistic/indent': ['error', 4, {
|
||||
'CallExpression': { arguments: 4 },
|
||||
}],
|
||||
'@stylistic/indent-binary-ops': ['error', 4],
|
||||
'@stylistic/array-bracket-newline': ['error', 'consistent'],
|
||||
'@stylistic/semi': ['error', 'always'],
|
||||
'@stylistic/quotes': ['error', 'single'],
|
||||
'@stylistic/function-call-argument-newline': ['error', 'consistent'],
|
||||
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'@stylistic/space-before-function-paren': ['error', { 'anonymous': 'never', 'named': 'never', 'asyncArrow': 'always', 'catch': 'never' }],
|
||||
'@stylistic/key-spacing': ['error', { 'beforeColon': false, 'afterColon': true }],
|
||||
'@stylistic/keyword-spacing': ['error', { 'before': true, 'after': true }],
|
||||
'@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
|
||||
'@stylistic/comma-spacing': ['error', { 'before': false, 'after': true }],
|
||||
'@stylistic/comma-dangle': ['error', 'always-multiline'],
|
||||
'@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: true }],
|
||||
'@stylistic/dot-location': ['error', 'property'],
|
||||
'@stylistic/space-infix-ops': ['error'],
|
||||
'no-undef': 'error',
|
||||
curly: ['error', 'multi-line'],
|
||||
'custom/control-structure-spacing': 'error',
|
||||
'@stylistic/no-trailing-spaces': 'error',
|
||||
},
|
||||
extends: ['js/recommended'],
|
||||
plugins: {
|
||||
js,
|
||||
'@stylistic': stylistic,
|
||||
|
||||
},
|
||||
},
|
||||
]);
|
||||
Generated
+5968
-5813
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -11,11 +11,12 @@
|
||||
"lib": "lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.1.1",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"chalk": "^4.1.0",
|
||||
"clean-css": "^5.3.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^9.1.1",
|
||||
"eslint": "^9.35.0",
|
||||
"express": "^4.18.2",
|
||||
"globals": "^15.0.0",
|
||||
"html-entities": "^2.3.3",
|
||||
@@ -65,4 +66,4 @@
|
||||
"sharp-bmp": "^0.1.5",
|
||||
"sharp-ico": "^0.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,14 @@
|
||||
"import": "./dist/esm/exports.js"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"rollup": "^3.21.4",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"@rollup/plugin-commonjs": "^24.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@rollup/plugin-replace": "^5.0.2"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2025-present Puter Technologies Inc.
|
||||
*
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const BaseService = require("../../services/BaseService");
|
||||
const BaseService = require('../../services/BaseService');
|
||||
|
||||
/**
|
||||
* Service class that manages KVStore interface registrations.
|
||||
@@ -29,10 +29,10 @@ class KVStoreInterfaceService extends BaseService {
|
||||
* Service class for managing KVStore interface registrations.
|
||||
* Extends the base service to provide key-value store interface management.
|
||||
*/
|
||||
async ['__on_driver.register.interfaces'] () {
|
||||
async ['__on_driver.register.interfaces']() {
|
||||
const svc_registry = this.services.get('registry');
|
||||
const col_interfaces = svc_registry.get('interfaces');
|
||||
|
||||
|
||||
// Register the puter-kvstore interface
|
||||
col_interfaces.set('puter-kvstore', {
|
||||
description: 'A simple key-value store.',
|
||||
@@ -41,16 +41,15 @@ class KVStoreInterfaceService extends BaseService {
|
||||
description: 'Get a value by key.',
|
||||
parameters: {
|
||||
key: { type: 'json', required: true },
|
||||
app_uid: { type: 'string', optional: true },
|
||||
},
|
||||
result: { type: 'json' },
|
||||
},
|
||||
set: {
|
||||
description: 'Set a value by key.',
|
||||
parameters: {
|
||||
key: { type: 'string', required: true, },
|
||||
key: { type: 'string', required: true },
|
||||
value: { type: 'json' },
|
||||
app_uid: { type: 'string', optional: true },
|
||||
expireAt: { type: 'number' },
|
||||
},
|
||||
result: { type: 'void' },
|
||||
},
|
||||
@@ -58,7 +57,6 @@ class KVStoreInterfaceService extends BaseService {
|
||||
description: 'Delete a value by key.',
|
||||
parameters: {
|
||||
key: { type: 'string' },
|
||||
app_uid: { type: 'string', optional: true },
|
||||
},
|
||||
result: { type: 'void' },
|
||||
},
|
||||
@@ -68,7 +66,6 @@ class KVStoreInterfaceService extends BaseService {
|
||||
as: {
|
||||
type: 'string',
|
||||
},
|
||||
app_uid: { type: 'string', optional: true },
|
||||
},
|
||||
result: { type: 'array' },
|
||||
},
|
||||
@@ -80,26 +77,44 @@ class KVStoreInterfaceService extends BaseService {
|
||||
incr: {
|
||||
description: 'Increment a value by key.',
|
||||
parameters: {
|
||||
key: { type: 'string', required: true, },
|
||||
key: { type: 'string', required: true },
|
||||
amount: { type: 'number' },
|
||||
app_uid: { type: 'string', optional: true },
|
||||
|
||||
},
|
||||
result: { type: 'number' },
|
||||
},
|
||||
decr: {
|
||||
description: 'Decrement a value by key.',
|
||||
parameters: {
|
||||
key: { type: 'string', required: true, },
|
||||
key: { type: 'string', required: true },
|
||||
amount: { type: 'number' },
|
||||
app_uid: { type: 'string', optional: true },
|
||||
|
||||
},
|
||||
result: { type: 'number' },
|
||||
},
|
||||
}
|
||||
expireAt: {
|
||||
description: 'Set a key to expire at a given timestamp in sec.',
|
||||
parameters: {
|
||||
key: { type: 'string', required: true },
|
||||
timestamp: { type: 'number', required: true },
|
||||
|
||||
},
|
||||
result: { type: 'number' },
|
||||
},
|
||||
expire: {
|
||||
description: 'Set a key to expire in ttl many seconds.',
|
||||
parameters: {
|
||||
key: { type: 'string', required: true },
|
||||
ttl: { type: 'number', required: true },
|
||||
|
||||
},
|
||||
result: { type: 'number' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
KVStoreInterfaceService
|
||||
KVStoreInterfaceService,
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
"use strict"
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const auth = require('../../middleware/auth.js');
|
||||
@@ -28,21 +28,29 @@ const { DB_READ } = require('../../services/database/consts.js');
|
||||
// -----------------------------------------------------------------------//
|
||||
// POST /getItem
|
||||
// -----------------------------------------------------------------------//
|
||||
router.post('/getItem', auth, express.json(), async (req, res, next)=>{
|
||||
router.post('/getItem', auth, express.json(), async (req, res, next) => {
|
||||
// check subdomain
|
||||
if(require('../../helpers.js').subdomain(req) !== 'api')
|
||||
if ( require('../../helpers.js').subdomain(req) !== 'api' )
|
||||
{
|
||||
next();
|
||||
}
|
||||
|
||||
// check if user is verified
|
||||
if((config.strict_email_verification_required || req.user.requires_email_confirmation) && !req.user.email_confirmed)
|
||||
return res.status(400).send({code: 'account_is_not_verified', message: 'Account is not verified'});
|
||||
if ( (config.strict_email_verification_required || req.user.requires_email_confirmation) && !req.user.email_confirmed )
|
||||
{
|
||||
return res.status(400).send({ code: 'account_is_not_verified', message: 'Account is not verified' });
|
||||
}
|
||||
|
||||
// validation
|
||||
if(!req.body.key)
|
||||
if ( !req.body.key )
|
||||
{
|
||||
return res.status(400).send('`key` is required.');
|
||||
}
|
||||
// check size of key, if it's too big then it's an invalid key and we don't want to waste time on it
|
||||
else if(Buffer.byteLength(req.body.key, 'utf8') > config.kv_max_key_size)
|
||||
else if ( Buffer.byteLength(req.body.key, 'utf8') > config.kv_max_key_size )
|
||||
{
|
||||
return res.status(400).send('`key` is too long.');
|
||||
}
|
||||
|
||||
const actor = req.body.app
|
||||
? await Actor.create(AppUnderUserActorType, {
|
||||
@@ -69,7 +77,7 @@ router.post('/getItem', auth, express.json(), async (req, res, next)=>{
|
||||
throw new Error(driver_response.error?.message ?? 'Unknown error');
|
||||
}
|
||||
driver_result = driver_response.result;
|
||||
} catch (e) {
|
||||
} catch( e ) {
|
||||
return res.status(400).send('puter-kvstore driver error: ' + e.message);
|
||||
}
|
||||
|
||||
@@ -80,40 +88,40 @@ router.post('/getItem', auth, express.json(), async (req, res, next)=>{
|
||||
// modules
|
||||
const db = req.services.get('database').get(DB_READ, 'getItem-fallback');
|
||||
// get murmurhash module
|
||||
const murmurhash = require('murmurhash')
|
||||
const murmurhash = require('murmurhash');
|
||||
// hash key for faster search in DB
|
||||
const key_hash = murmurhash.v3(req.body.key);
|
||||
|
||||
let kv;
|
||||
// Get value from DB
|
||||
// If app is specified, then get value for that app
|
||||
if(req.body.app){
|
||||
kv = await db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND app=? AND kkey_hash=? LIMIT 1`,
|
||||
[
|
||||
req.user.id,
|
||||
req.body.app,
|
||||
key_hash,
|
||||
]
|
||||
)
|
||||
if ( req.body.app ){
|
||||
kv = await db.read('SELECT * FROM kv WHERE user_id=? AND app=? AND kkey_hash=? LIMIT 1',
|
||||
[
|
||||
req.user.id,
|
||||
req.body.app,
|
||||
key_hash,
|
||||
]);
|
||||
// If app is not specified, then get value for global (i.e. system) variables which is app='global'
|
||||
}else{
|
||||
kv = await db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global') AND kkey_hash=? LIMIT 1`,
|
||||
[
|
||||
req.user.id,
|
||||
key_hash,
|
||||
]
|
||||
)
|
||||
} else {
|
||||
kv = await db.read('SELECT * FROM kv WHERE user_id=? AND (app IS NULL OR app = \'global\') AND kkey_hash=? LIMIT 1',
|
||||
[
|
||||
req.user.id,
|
||||
key_hash,
|
||||
]);
|
||||
}
|
||||
|
||||
// send results to client
|
||||
if(kv[0])
|
||||
if ( kv[0] )
|
||||
{
|
||||
return res.send({
|
||||
key: kv[0].kkey,
|
||||
value: kv[0].value,
|
||||
});
|
||||
}
|
||||
else
|
||||
return res.send(null)
|
||||
})
|
||||
module.exports = router
|
||||
{
|
||||
return res.send(null);
|
||||
}
|
||||
});
|
||||
module.exports = router;
|
||||
@@ -1,39 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { get_app } = require("../helpers");
|
||||
const { Context } = require("../util/context");
|
||||
const BaseService = require("./BaseService");
|
||||
const { DB_READ } = require("./database/consts");
|
||||
const APIError = require('../api/APIError');
|
||||
const { Context } = require('../util/context');
|
||||
const BaseService = require('./BaseService');
|
||||
const { DB_READ } = require('./database/consts');
|
||||
|
||||
const GLOBAL_APP_KEY = 'global';
|
||||
class DBKVService extends BaseService {
|
||||
static MODULES = {
|
||||
murmurhash: require('murmurhash'),
|
||||
}
|
||||
};
|
||||
|
||||
_init () {
|
||||
_init() {
|
||||
this.db = this.services.get('database').get(DB_READ, 'kvstore');
|
||||
}
|
||||
|
||||
static IMPLEMENTS = {
|
||||
['puter-kvstore']: {
|
||||
async get ({ app_uid, key }) {
|
||||
async get({ key }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
// If the actor is an app then it gets its own KV store.
|
||||
@@ -45,23 +46,23 @@ class DBKVService extends BaseService {
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
if ( !user ) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const deleteExpired = async (rows) => {
|
||||
const query = `DELETE FROM kv WHERE user_id=? AND app=? AND kkey_hash IN (${rows.map(() => '?').join(',')})`;
|
||||
const params = [user.id, app?.uid ?? GLOBAL_APP_KEY, ...rows.map(r => r.kkey_hash)];
|
||||
return await this.db.write(query, params);
|
||||
};
|
||||
|
||||
if ( Array.isArray(key) ) {
|
||||
const keys = key;
|
||||
const key_hashes = keys.map(key => this.modules.murmurhash.v3(key));
|
||||
const rows = app ? await this.db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND app=? AND kkey_hash IN (?)`,
|
||||
[ user.id, app.uid, key_hashes ]
|
||||
) : await this.db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global') ` +
|
||||
`AND kkey_hash IN (${key_hashes.map(() => '?').join(',')})`,
|
||||
[ user.id, key_hashes ]
|
||||
);
|
||||
const rows = app ? await this.db.read('SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND app=? AND kkey_hash IN (?)',
|
||||
[user.id, app.uid, key_hashes]) : await this.db.read(`SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND (app IS NULL OR app = '${GLOBAL_APP_KEY}') ` +
|
||||
`AND kkey_hash IN (${key_hashes.map(() => '?').join(',')})`,
|
||||
[user.id, key_hashes]);
|
||||
|
||||
const kv = {};
|
||||
rows.forEach(row => {
|
||||
@@ -72,26 +73,48 @@ class DBKVService extends BaseService {
|
||||
kv[row.kkey] = row.value;
|
||||
});
|
||||
|
||||
const expiredKeys = [];
|
||||
const validRows = {};
|
||||
rows.forEach(row => {
|
||||
if ( row?.expireAt && row.expireAt < Date.now() ) {
|
||||
expiredKeys.push(row);
|
||||
validRows[row.kkey] = null;
|
||||
} else {
|
||||
validRows[row.kkey] = row.value ?? null;
|
||||
}
|
||||
});
|
||||
|
||||
// clean up expired keys asynchronously
|
||||
if ( expiredKeys.length ) {
|
||||
deleteExpired(expiredKeys);
|
||||
}
|
||||
|
||||
return keys.map(key => kv[key]);
|
||||
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
const kv = app ? await this.db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND app=? AND kkey_hash=? LIMIT 1`,
|
||||
[ user.id, app.uid, key_hash ]
|
||||
) : await this.db.read(
|
||||
`SELECT * FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global') AND kkey_hash=? LIMIT 1`,
|
||||
[ user.id, key_hash ]
|
||||
);
|
||||
|
||||
if ( kv[0] ) kv[0].value = this.db.case({
|
||||
mysql: () => kv[0].value,
|
||||
otherwise: () => JSON.parse(kv[0].value ?? 'null'),
|
||||
})();
|
||||
const kv = app ? await this.db.read('SELECT * FROM kv WHERE user_id=? AND app=? AND kkey_hash=? LIMIT 1',
|
||||
[user.id, app.uid, key_hash]) : await this.db.read(`SELECT * FROM kv WHERE user_id=? AND (app IS NULL OR app = '${GLOBAL_APP_KEY}') AND kkey_hash=? LIMIT 1`,
|
||||
[user.id, key_hash]);
|
||||
|
||||
if ( kv[0] ) {
|
||||
kv[0].value = this.db.case({
|
||||
mysql: () => kv[0].value,
|
||||
otherwise: () => JSON.parse(kv[0].value ?? 'null'),
|
||||
})();
|
||||
}
|
||||
|
||||
if ( kv[0]?.expireAt && kv[0].expireAt < (Date.now() / 1000) ) {
|
||||
// key has expired
|
||||
// clean up asynchronously
|
||||
deleteExpired([kv[0]]);
|
||||
return null;
|
||||
}
|
||||
|
||||
return kv[0]?.value ?? null;
|
||||
},
|
||||
async set ({ app_uid, key, value }) {
|
||||
async set({ key, value, expireAt }) {
|
||||
const actor = Context.get('actor');
|
||||
const config = this.global_config;
|
||||
|
||||
@@ -108,36 +131,32 @@ class DBKVService extends BaseService {
|
||||
if (
|
||||
value !== null &&
|
||||
Buffer.byteLength(JSON.stringify(value), 'utf8') >
|
||||
config.kv_max_value_size
|
||||
config.kv_max_value_size
|
||||
) {
|
||||
throw new Error(`value is too large. Max size is ${config.kv_max_value_size}.`);
|
||||
}
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
if ( !user ) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
try {
|
||||
await this.db.write(
|
||||
`INSERT INTO kv (user_id, app, kkey_hash, kkey, value)
|
||||
VALUES (?, ?, ?, ?, ?) ` +
|
||||
await this.db.write(`INSERT INTO kv (user_id, app, kkey_hash, kkey, value, expireAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?) ` +
|
||||
this.db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE value = ?',
|
||||
sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET value = excluded.value',
|
||||
}),
|
||||
[
|
||||
user.id, app?.uid ?? 'global', key_hash, key,
|
||||
JSON.stringify(value),
|
||||
...this.db.case({ mysql: [value], otherwise: [] }),
|
||||
]
|
||||
);
|
||||
} catch (e) {
|
||||
[
|
||||
user.id, app?.uid ?? GLOBAL_APP_KEY, key_hash, key,
|
||||
JSON.stringify(value), expireAt ?? null,
|
||||
...this.db.case({ mysql: [value], otherwise: [] }),
|
||||
]);
|
||||
} catch( e ) {
|
||||
// I discovered that my .sqlite file was corrupted and the update
|
||||
// above didn't work. The current database initialization does not
|
||||
// cause this issue so I'm adding this log as a safeguard.
|
||||
@@ -151,88 +170,148 @@ class DBKVService extends BaseService {
|
||||
|
||||
return true;
|
||||
},
|
||||
async del ({ app_uid, key }) {
|
||||
async del({ key }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
if ( !user ) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
await this.db.write(
|
||||
`DELETE FROM kv WHERE user_id=? AND app=? AND kkey_hash=?`,
|
||||
[ user.id, app?.uid ?? 'global', key_hash ]
|
||||
);
|
||||
await this.db.write('DELETE FROM kv WHERE user_id=? AND app=? AND kkey_hash=?',
|
||||
[user.id, app?.uid ?? GLOBAL_APP_KEY, key_hash]);
|
||||
|
||||
return true;
|
||||
},
|
||||
async list ({ app_uid, as }) {
|
||||
async list({ as }) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
if ( !user ) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
let rows = app ? await this.db.read('SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND app=?',
|
||||
[user.id, app.uid]) : await this.db.read(`SELECT kkey, value, expireAt FROM kv WHERE user_id=? AND (app IS NULL OR app = '${GLOBAL_APP_KEY}')`,
|
||||
[user.id]);
|
||||
|
||||
let rows = app ? await this.db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND app=?`,
|
||||
[ user.id, app.uid ]
|
||||
) : await this.db.read(
|
||||
`SELECT kkey, value FROM kv WHERE user_id=? AND (app IS NULL OR app = 'global')`,
|
||||
[ user.id ]
|
||||
);
|
||||
rows = rows.filter (row => {
|
||||
return !row?.expireAt || row?.expireAt > Date.now() / 1000;
|
||||
});
|
||||
|
||||
rows = rows.map(row => ({
|
||||
key: row.kkey,
|
||||
value: this.db.case({
|
||||
mysql: () => row.value,
|
||||
otherwise: () => JSON.parse(row.value ?? 'null')
|
||||
otherwise: () => JSON.parse(row.value ?? 'null'),
|
||||
})(),
|
||||
}));
|
||||
|
||||
as = as || 'entries';
|
||||
|
||||
if ( ! ['keys','values','entries'].includes(as) ) {
|
||||
if ( !['keys', 'values', 'entries'].includes(as) ) {
|
||||
throw APIError.create('field_invalid', null, {
|
||||
key: 'as',
|
||||
expected: '"keys", "values", or "entries"',
|
||||
});
|
||||
}
|
||||
|
||||
if ( as === 'keys' ) rows = rows.map(row => row.key);
|
||||
else if ( as === 'values' ) rows = rows.map(row => row.value);
|
||||
if ( as === 'keys' ) {
|
||||
rows = rows.map(row => row.key);
|
||||
}
|
||||
else if ( as === 'values' ) {
|
||||
rows = rows.map(row => row.value);
|
||||
}
|
||||
|
||||
return rows;
|
||||
},
|
||||
async flush ({ app_uid }) {
|
||||
async flush() {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
let app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( ! user ) throw new Error('User not found');
|
||||
|
||||
if ( ! app && app_uid ) {
|
||||
app = await get_app({ uid: app_uid });
|
||||
if ( !user ) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
await this.db.write(
|
||||
`DELETE FROM kv WHERE user_id=? AND app=?`,
|
||||
[ user.id, app?.uid ?? 'global' ]
|
||||
);
|
||||
await this.db.write('DELETE FROM kv WHERE user_id=? AND app=?',
|
||||
[user.id, app?.uid ?? GLOBAL_APP_KEY]);
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
async expireAt({ key, timestamp }) {
|
||||
if ( key === '' ) {
|
||||
throw APIError.create('field_empty', null, {
|
||||
key: 'key',
|
||||
});
|
||||
}
|
||||
|
||||
timestamp = Number(timestamp);
|
||||
|
||||
return await this._expireat(key, timestamp);
|
||||
},
|
||||
|
||||
async expire({ key, ttl }) {
|
||||
if ( key === '' ) {
|
||||
throw APIError.create('field_empty', null, {
|
||||
key: 'key',
|
||||
});
|
||||
}
|
||||
|
||||
ttl = Number(ttl);
|
||||
|
||||
// timestamp in seconds
|
||||
let timestamp = Math.floor(Date.now() / 1000) + ttl;
|
||||
|
||||
return await this._expireat(key, timestamp);
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
async _expireat(key, timestamp) {
|
||||
const actor = Context.get('actor');
|
||||
|
||||
const app = actor.type?.app ?? undefined;
|
||||
const user = actor.type?.user ?? undefined;
|
||||
if ( !user ) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const key_hash = this.modules.murmurhash.v3(key);
|
||||
|
||||
try {
|
||||
await this.db.write(`INSERT INTO kv (user_id, app, kkey_hash, kkey, value, expireAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?) ` +
|
||||
this.db.case({
|
||||
mysql: 'ON DUPLICATE KEY UPDATE expireAt = ?',
|
||||
sqlite: 'ON CONFLICT(user_id, app, kkey_hash) DO UPDATE SET expireAt = excluded.expireAt',
|
||||
}),
|
||||
[
|
||||
user.id,
|
||||
app?.uid ?? GLOBAL_APP_KEY,
|
||||
key_hash,
|
||||
key,
|
||||
null, // empty value
|
||||
timestamp,
|
||||
...this.db.case({ mysql: [timestamp], otherwise: [] }),
|
||||
]);
|
||||
} catch( e ) {
|
||||
// I discovered that my .sqlite file was corrupted and the update
|
||||
// above didn't work. The current database initialization does not
|
||||
// cause this issue so I'm adding this log as a safeguard.
|
||||
// - KernelDeimos / ED
|
||||
const svc_error = this.services.get('error-service');
|
||||
svc_error.report('kvstore:sqlite_error', {
|
||||
message: 'Broken database version - please contact maintainers',
|
||||
source: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const { es_import_promise } = require("../../fun/dev-console-ui-utils");
|
||||
const { surrounding_box } = require("../../fun/dev-console-ui-utils");
|
||||
const { Context } = require("../../util/context");
|
||||
const { CompositeError } = require("../../util/errorutil");
|
||||
const structutil = require("../../util/structutil");
|
||||
const { BaseDatabaseAccessService } = require("./BaseDatabaseAccessService");
|
||||
const { es_import_promise } = require('../../fun/dev-console-ui-utils');
|
||||
const { surrounding_box } = require('../../fun/dev-console-ui-utils');
|
||||
const { Context } = require('../../util/context');
|
||||
const { CompositeError } = require('../../util/errorutil');
|
||||
const structutil = require('../../util/structutil');
|
||||
const { BaseDatabaseAccessService } = require('./BaseDatabaseAccessService');
|
||||
|
||||
class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
static ENGINE_NAME = 'sqlite';
|
||||
@@ -33,14 +33,13 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
Database: require('better-sqlite3'),
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @description Method to handle database schema upgrades.
|
||||
* This method checks the current database version against the available migration scripts and performs any necessary upgrades.
|
||||
* @param {void}
|
||||
* @returns {void}
|
||||
*/
|
||||
async _init () {
|
||||
async _init() {
|
||||
const require = this.require;
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
@@ -48,7 +47,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
|
||||
const fs = require('fs');
|
||||
const path_ = require('path');
|
||||
const do_setup = ! fs.existsSync(this.config.path);
|
||||
const do_setup = !fs.existsSync(this.config.path);
|
||||
|
||||
this.db = new Database(this.config.path);
|
||||
|
||||
@@ -164,6 +163,9 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
[34, [
|
||||
'0038_custom-domains.sql',
|
||||
]],
|
||||
[35, [
|
||||
'0039_add-expireAt-to-kv-store.sql',
|
||||
]],
|
||||
];
|
||||
|
||||
// Database upgrade logic
|
||||
@@ -188,7 +190,6 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
: await this._read('PRAGMA user_version');
|
||||
this.log.info('database version: ' + user_version);
|
||||
|
||||
|
||||
for ( const [v_lt_or_eq, files] of available_migrations ) {
|
||||
if ( v_lt_or_eq + 1 >= TARGET_VERSION && TARGET_VERSION !== HIGHEST_VERSION ) {
|
||||
this.log.noticeme(`Early exit: target version set to ${TARGET_VERSION}`);
|
||||
@@ -204,41 +205,38 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
|
||||
this.log.noticeme(`${upgrade_files.length} .sql files to apply`);
|
||||
|
||||
const sql_files = upgrade_files.map(
|
||||
p => path_.join(__dirname, 'sqlite_setup', p)
|
||||
);
|
||||
const sql_files = upgrade_files.map(p => path_.join(__dirname, 'sqlite_setup', p));
|
||||
const fs = require('fs');
|
||||
for ( const filename of sql_files ) {
|
||||
const basename = path_.basename(filename);
|
||||
this.log.noticeme(`applying ${basename}`);
|
||||
const contents = fs.readFileSync(filename, 'utf8');
|
||||
switch ( path_.extname(filename) ) {
|
||||
case '.sql':
|
||||
const stmts = contents.split(/;\s*\n/);
|
||||
for ( let i=0 ; i < stmts.length ; i++ ) {
|
||||
if ( stmts[i].trim() === '' ) continue;
|
||||
const stmt = stmts[i] + ';';
|
||||
try {
|
||||
this.db.exec(stmt);
|
||||
} catch (e) {
|
||||
debugger;
|
||||
throw new CompositeError(`failed to apply: ${basename} at line ${i}`, e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '.js':
|
||||
case '.sql':
|
||||
{
|
||||
const stmts = contents.split(/;\s*\n/);
|
||||
for ( let i = 0; i < stmts.length; i++ ) {
|
||||
if ( stmts[i].trim() === '' ) continue;
|
||||
const stmt = stmts[i] + ';';
|
||||
try {
|
||||
await this.run_js_migration_({
|
||||
filename, contents,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new CompositeError(`failed to apply: ${basename}`, e);
|
||||
this.db.exec(stmt);
|
||||
} catch( e ) {
|
||||
throw new CompositeError(`failed to apply: ${basename} at line ${i}`, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`unrecognized migration type: ${filename}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '.js':
|
||||
try {
|
||||
await this.run_js_migration_({
|
||||
filename, contents,
|
||||
});
|
||||
} catch( e ) {
|
||||
throw new CompositeError(`failed to apply: ${basename}`, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`unrecognized migration type: ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,9 +255,9 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
// It does not include any parameters or return values since the method does not take any inputs and does not return any output.
|
||||
this.database_update_notice = () => {
|
||||
const lines = [
|
||||
`Database has been updated!`,
|
||||
'Database has been updated!',
|
||||
`Current version: ${TARGET_VERSION}`,
|
||||
`Type sqlite:dismiss to dismiss this message`,
|
||||
'Type sqlite:dismiss to dismiss this message',
|
||||
];
|
||||
surrounding_box('33;1', lines);
|
||||
return lines;
|
||||
@@ -274,7 +272,6 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
|
||||
const svc_serverHealth = this.services.get('server-health');
|
||||
|
||||
|
||||
/**
|
||||
* @description This method is used to register SQLite database-related commands with the dev-console service.
|
||||
* @param {object} commands - The dev-console service commands object.
|
||||
@@ -282,38 +279,34 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
svc_serverHealth.add_check('sqlite', async () => {
|
||||
const [{ user_version }] = await this.requireRead('PRAGMA user_version');
|
||||
if ( user_version !== TARGET_VERSION ) {
|
||||
throw new Error(
|
||||
`Database version mismatch: expected ${TARGET_VERSION}, ` +
|
||||
throw new Error(`Database version mismatch: expected ${TARGET_VERSION}, ` +
|
||||
`got ${user_version}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation for prepared statements for READ operations.
|
||||
*/
|
||||
async _read (query, params = []) {
|
||||
async _read(query, params = []) {
|
||||
query = this.sqlite_transform_query_(query);
|
||||
params = this.sqlite_transform_params_(params);
|
||||
return this.db.prepare(query).all(...params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation for prepared statements for READ operations.
|
||||
* This method may perform additional steps to obtain the data, which
|
||||
* is not applicable to the SQLite implementation.
|
||||
*/
|
||||
async _tryHardRead (query, params) {
|
||||
async _tryHardRead(query, params) {
|
||||
return await this._read(query, params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation for prepared statements for WRITE operations.
|
||||
*/
|
||||
async _write (query, params) {
|
||||
async _write(query, params) {
|
||||
query = this.sqlite_transform_query_(query);
|
||||
params = this.sqlite_transform_params_(params);
|
||||
|
||||
@@ -326,14 +319,13 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method initializes the SQLite database by checking if it exists, setting up the connection, and performing any necessary database upgrades based on the current version.
|
||||
*
|
||||
* @param {object} config - The configuration object for the database.
|
||||
* @returns {Promise} A promise that resolves when the database is initialized.
|
||||
*/
|
||||
async _batch_write (entries) {
|
||||
async _batch_write(entries) {
|
||||
/**
|
||||
* @description This method is used to execute SQL queries in batch mode.
|
||||
* It accepts an array of objects, where each object contains a SQL query as the `statement` property and an array of parameters as the `values` property.
|
||||
@@ -350,15 +342,14 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
sqlite_transform_query_ (query) {
|
||||
sqlite_transform_query_(query) {
|
||||
// replace `now()` with `datetime('now')`
|
||||
query = query.replace(/now\(\)/g, 'datetime(\'now\')');
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
sqlite_transform_params_ (params) {
|
||||
sqlite_transform_params_(params) {
|
||||
return params.map(p => {
|
||||
if ( typeof p === 'boolean' ) {
|
||||
return p ? 1 : 0;
|
||||
@@ -366,14 +357,13 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
return p;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description This method is responsible for performing database upgrades. It checks the current database version against the available versions and applies any necessary migrations.
|
||||
* @param {object} options - Optional parameters for the method.
|
||||
* @returns {Promise} A promise that resolves when the database upgrade is complete.
|
||||
*/
|
||||
async run_js_migration_ ({ filename, contents }) {
|
||||
async run_js_migration_({ filename: _filename, contents }) {
|
||||
/**
|
||||
* Method to run JavaScript migrations. This method is used to apply JavaScript code to the SQLite database during the upgrade process.
|
||||
*
|
||||
@@ -394,7 +384,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
await vm.runInContext(contents, context);
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
_register_commands(commands) {
|
||||
commands.registerCommands('sqlite', [
|
||||
{
|
||||
id: 'execfile',
|
||||
@@ -405,10 +395,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
const fs = require('fs');
|
||||
const contents = fs.readFileSync(filename, 'utf8');
|
||||
this.db.exec(contents);
|
||||
} catch (err) {
|
||||
} catch( err ) {
|
||||
log.error(err.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'read',
|
||||
@@ -418,25 +408,25 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
const [query] = args;
|
||||
const rows = this._read(query, []);
|
||||
log.log(rows);
|
||||
} catch (err) {
|
||||
} catch( err ) {
|
||||
log.error(err.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dismiss',
|
||||
description: 'dismiss the database update notice',
|
||||
handler: async (_, log) => {
|
||||
const svc_devConsole = this.services.get('dev-console');
|
||||
if ( ! svc_devConsole ) return;
|
||||
if ( ! this.database_update_notice ) return;
|
||||
if ( !svc_devConsole ) return;
|
||||
if ( !this.database_update_notice ) return;
|
||||
svc_devConsole.remove_widget(this.database_update_notice);
|
||||
const lines = this.database_update_notice();
|
||||
for ( const line of lines ) log.log(line);
|
||||
this.database_update_notice = null;
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `kv` ADD COLUMN `expireAt` TIMESTAMP DEFAULT NULL;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TeePromise } from '@heyputer/putility/src/libs/promise.js';
|
||||
import * as utils from '../lib/utils.js'
|
||||
import * as utils from '../lib/utils.js';
|
||||
|
||||
const gui_cache_keys = [
|
||||
'has_set_default_app_user_permissions',
|
||||
@@ -13,7 +13,6 @@ const gui_cache_keys = [
|
||||
'toolbar_auto_hide_enabled',
|
||||
'has_seen_welcome_window',
|
||||
];
|
||||
|
||||
class KV{
|
||||
MAX_KEY_SIZE = 1024;
|
||||
MAX_VALUE_SIZE = 400 * 1024;
|
||||
@@ -26,7 +25,7 @@ class KV{
|
||||
* @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
|
||||
* @param {string} appID - ID of the app to use.
|
||||
*/
|
||||
constructor (context) {
|
||||
constructor(context) {
|
||||
this.authToken = context.authToken;
|
||||
this.APIOrigin = context.APIOrigin;
|
||||
this.appID = context.appID;
|
||||
@@ -59,7 +58,7 @@ class KV{
|
||||
return;
|
||||
}
|
||||
const obj = {};
|
||||
for (let i = 0; i < gui_cache_keys.length; i++) {
|
||||
for ( let i = 0; i < gui_cache_keys.length; i++ ) {
|
||||
obj[gui_cache_keys[i]] = arr_values.result[i];
|
||||
}
|
||||
this.gui_cached.resolve(obj);
|
||||
@@ -76,50 +75,53 @@ class KV{
|
||||
* @memberof [KV]
|
||||
* @returns {void}
|
||||
*/
|
||||
setAuthToken (authToken) {
|
||||
setAuthToken(authToken) {
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API origin.
|
||||
*
|
||||
*
|
||||
* @param {string} APIOrigin - The new API origin.
|
||||
* @memberof [KV]
|
||||
* @returns {void}
|
||||
*/
|
||||
setAPIOrigin (APIOrigin) {
|
||||
setAPIOrigin(APIOrigin) {
|
||||
this.APIOrigin = APIOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves to 'true' on success, or rejects with an error on failure
|
||||
*
|
||||
*
|
||||
* `key` cannot be undefined or null.
|
||||
* `key` size cannot be larger than 1mb.
|
||||
* `value` size cannot be larger than 10mb.
|
||||
* `expireAt` is a timestamp in sec since epoch. If provided, the key will expire at the given time.
|
||||
*/
|
||||
set = utils.make_driver_method(['key', 'value'], 'puter-kvstore', undefined, 'set',{
|
||||
preprocess: (args)=>{
|
||||
set = utils.make_driver_method(['key', 'value', 'expireAt'], 'puter-kvstore', undefined, 'set', {
|
||||
preprocess: (args) => {
|
||||
console.log(args);
|
||||
|
||||
// key cannot be undefined or null
|
||||
if(args.key === undefined || args.key === null){
|
||||
throw { message: 'Key cannot be undefined', code: 'key_undefined'};
|
||||
if ( args.key === undefined || args.key === null ){
|
||||
throw { message: 'Key cannot be undefined', code: 'key_undefined' };
|
||||
}
|
||||
// key size cannot be larger than MAX_KEY_SIZE
|
||||
if(args.key.length > this.MAX_KEY_SIZE){
|
||||
throw {message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'};
|
||||
if ( args.key.length > this.MAX_KEY_SIZE ){
|
||||
throw { message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' };
|
||||
}
|
||||
// value size cannot be larger than MAX_VALUE_SIZE
|
||||
if(args.value && args.value.length > this.MAX_VALUE_SIZE){
|
||||
throw {message: 'Value size cannot be larger than ' + this.MAX_VALUE_SIZE, code: 'value_too_large'};
|
||||
if ( args.value && args.value.length > this.MAX_VALUE_SIZE ){
|
||||
throw { message: 'Value size cannot be larger than ' + this.MAX_VALUE_SIZE, code: 'value_too_large' };
|
||||
}
|
||||
return args;
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolves to the value if the key exists, or `undefined` if the key does not exist. Rejects with an error on failure.
|
||||
*/
|
||||
async get (...args) {
|
||||
async get(...args) {
|
||||
// Condition for gui boot cache
|
||||
if (
|
||||
typeof args[0] === 'string' &&
|
||||
@@ -136,116 +138,129 @@ class KV{
|
||||
}
|
||||
|
||||
get_ = utils.make_driver_method(['key'], 'puter-kvstore', undefined, 'get', {
|
||||
preprocess: (args)=>{
|
||||
preprocess: (args) => {
|
||||
// key size cannot be larger than MAX_KEY_SIZE
|
||||
if(args.key.length > this.MAX_KEY_SIZE){
|
||||
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
|
||||
if ( args.key.length > this.MAX_KEY_SIZE ){
|
||||
throw ({ message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' });
|
||||
}
|
||||
|
||||
return args;
|
||||
},
|
||||
transform: (res)=>{
|
||||
transform: (res) => {
|
||||
return res;
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
incr = async(...args) => {
|
||||
incr = async (...args) => {
|
||||
let options = {};
|
||||
|
||||
// arguments are required
|
||||
if(!args || args.length === 0){
|
||||
throw ({message: 'Arguments are required', code: 'arguments_required'});
|
||||
if ( !args || args.length === 0 ){
|
||||
throw ({ message: 'Arguments are required', code: 'arguments_required' });
|
||||
}
|
||||
|
||||
options.key = args[0];
|
||||
options.amount = args[1] ?? 1;
|
||||
|
||||
// key size cannot be larger than MAX_KEY_SIZE
|
||||
if(options.key.length > this.MAX_KEY_SIZE){
|
||||
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
|
||||
if ( options.key.length > this.MAX_KEY_SIZE ){
|
||||
throw ({ message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' });
|
||||
}
|
||||
|
||||
return utils.make_driver_method(['key'], 'puter-kvstore', undefined, 'incr').call(this, options);
|
||||
}
|
||||
};
|
||||
|
||||
decr = async(...args) => {
|
||||
decr = async (...args) => {
|
||||
let options = {};
|
||||
|
||||
// arguments are required
|
||||
if(!args || args.length === 0){
|
||||
throw ({message: 'Arguments are required', code: 'arguments_required'});
|
||||
if ( !args || args.length === 0 ){
|
||||
throw ({ message: 'Arguments are required', code: 'arguments_required' });
|
||||
}
|
||||
|
||||
options.key = args[0];
|
||||
options.amount = args[1] ?? 1;
|
||||
|
||||
// key size cannot be larger than MAX_KEY_SIZE
|
||||
if(options.key.length > this.MAX_KEY_SIZE){
|
||||
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
|
||||
if ( options.key.length > this.MAX_KEY_SIZE ){
|
||||
throw ({ message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' });
|
||||
}
|
||||
|
||||
return utils.make_driver_method(['key'], 'puter-kvstore', undefined, 'decr').call(this, options);
|
||||
}
|
||||
};
|
||||
|
||||
expire = async(...args) => {
|
||||
expire = async (...args) => {
|
||||
let options = {};
|
||||
options.key = args[0];
|
||||
options.seconds = args[1];
|
||||
|
||||
options.ttl = args[1];
|
||||
|
||||
// key size cannot be larger than MAX_KEY_SIZE
|
||||
if(options.key.length > this.MAX_KEY_SIZE){
|
||||
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
|
||||
if ( options.key.length > this.MAX_KEY_SIZE ){
|
||||
throw ({ message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' });
|
||||
}
|
||||
|
||||
return utils.make_driver_method(['key'], 'puter-kvstore', undefined, 'expire').call(this, options);
|
||||
}
|
||||
return utils.make_driver_method(['key', 'ttl'], 'puter-kvstore', undefined, 'expire').call(this, options);
|
||||
};
|
||||
|
||||
expireAt = async (...args) => {
|
||||
let options = {};
|
||||
options.key = args[0];
|
||||
options.timestamp = args[1];
|
||||
|
||||
// key size cannot be larger than MAX_KEY_SIZE
|
||||
if ( options.key.length > this.MAX_KEY_SIZE ){
|
||||
throw ({ message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' });
|
||||
}
|
||||
|
||||
return utils.make_driver_method(['key', 'timestamp'], 'puter-kvstore', undefined, 'expireAt').call(this, options);
|
||||
};
|
||||
|
||||
// resolves to 'true' on success, or rejects with an error on failure
|
||||
// will still resolve to 'true' if the key does not exist
|
||||
del = utils.make_driver_method(['key'], 'puter-kvstore', undefined, 'del', {
|
||||
preprocess: (args)=>{
|
||||
preprocess: (args) => {
|
||||
// key size cannot be larger than this.MAX_KEY_SIZE
|
||||
if(args.key.length > this.MAX_KEY_SIZE){
|
||||
throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
|
||||
if ( args.key.length > this.MAX_KEY_SIZE ){
|
||||
throw ({ message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large' });
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
list = async(...args) => {
|
||||
list = async (...args) => {
|
||||
let options = {};
|
||||
let pattern;
|
||||
let returnValues = false;
|
||||
|
||||
// list(true) or list(pattern, true) will return the key-value pairs
|
||||
if((args && args.length === 1 && args[0] === true) || (args && args.length === 2 && args[1] === true)){
|
||||
if ( (args && args.length === 1 && args[0] === true) || (args && args.length === 2 && args[1] === true) ){
|
||||
options = {};
|
||||
returnValues = true;
|
||||
}
|
||||
// return only the keys, default behavior
|
||||
else{
|
||||
options = { as: 'keys'};
|
||||
else {
|
||||
options = { as: 'keys' };
|
||||
}
|
||||
|
||||
// list(pattern)
|
||||
// list(pattern, true)
|
||||
if((args && args.length === 1 && typeof args[0] === 'string') || (args && args.length === 2 && typeof args[0] === 'string' && args[1] === true)){
|
||||
if ( (args && args.length === 1 && typeof args[0] === 'string') || (args && args.length === 2 && typeof args[0] === 'string' && args[1] === true) ){
|
||||
pattern = args[0];
|
||||
}
|
||||
|
||||
return utils.make_driver_method([], 'puter-kvstore', undefined, 'list', {
|
||||
transform: (res)=>{
|
||||
transform: (res) => {
|
||||
// glob pattern was provided
|
||||
if(pattern){
|
||||
// consider both the key and the value
|
||||
if(!returnValues) {
|
||||
let keys = res.filter((key)=>{
|
||||
if ( pattern ){
|
||||
// consider both the key and the value
|
||||
if ( !returnValues ) {
|
||||
let keys = res.filter((key) => {
|
||||
return globMatch(pattern, key);
|
||||
});
|
||||
return keys;
|
||||
}else{
|
||||
let keys = res.filter((key_value_pair)=>{
|
||||
} else {
|
||||
let keys = res.filter((key_value_pair) => {
|
||||
return globMatch(pattern, key_value_pair.key);
|
||||
});
|
||||
return keys;
|
||||
@@ -253,19 +268,18 @@ class KV{
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
},
|
||||
}).call(this, options);
|
||||
}
|
||||
};
|
||||
|
||||
// resolve to 'true' on success, or rejects with an error on failure
|
||||
// will still resolve to 'true' if there are no keys
|
||||
flush = utils.make_driver_method([], 'puter-kvstore', undefined, 'flush')
|
||||
flush = utils.make_driver_method([], 'puter-kvstore', undefined, 'flush');
|
||||
|
||||
// clear is an alias for flush
|
||||
clear = this.flush;
|
||||
}
|
||||
|
||||
|
||||
function globMatch(pattern, str) {
|
||||
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
@@ -280,6 +294,4 @@ function globMatch(pattern, str) {
|
||||
return re.test(str);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default KV
|
||||
export default KV;
|
||||
Reference in New Issue
Block a user