feat: add expiry support to DBKV, and eslint config

This commit is contained in:
Daniel Salazar
2025-09-09 18:45:44 -07:00
parent 3f746b94de
commit f8ea790824
13 changed files with 6659 additions and 6090 deletions
+4 -1
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
{
"singleQuote": true,
"semi": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
+194
View File
@@ -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);
}
},
};
},
};
+111
View File
@@ -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,
},
},
]);
+5968 -5813
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -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"
}
}
}
+8 -1
View File
@@ -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,
};
+38 -30
View File
@@ -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;
+164 -85
View File
@@ -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;
+79 -67
View File
@@ -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;