diff --git a/extensions/example-db.js b/extensions/example-db.js new file mode 100644 index 0000000000..9e23be08d5 --- /dev/null +++ b/extensions/example-db.js @@ -0,0 +1,313 @@ +/* eslint-disable @stylistic/indent */ +const { db } = extension.import('data'); + +extension.on('init', async () => { + console.log('Initializing Example DB extension'); + + try { + // The "CREATE TABLE IF NOT EXIST" pattern is sometimes appropriate + // for extensions. + await db.write(` + CREATE TABLE IF NOT EXISTS example_extension_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + value TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `, []); + console.log('Example table created or already exists'); + + // Create some sample data once during initialization + const existingData = await db.read('SELECT COUNT(*) as count FROM example_extension_data'); + if ( existingData[0].count === 0 ) { + await db.write(` + INSERT INTO example_extension_data (name, value) VALUES + (?, ?), (?, ?), (?, ?) + `, [ + 'sample-1', 'This is sample data created during extension initialization', + 'sample-2', 'Database operations are working correctly', + 'sample-3', `Created at ${new Date().toISOString()}`, + ]); + console.log('Sample data created'); + } + // eslint-disable-next-line @stylistic/space-before-function-paren, custom/control-structure-spacing + } catch (error) { + console.error('Error creating example table:', error); + } +}); + +// The /example-db endpoint shows sample data. +extension.get('/example-db', { noauth: true }, async (req, res) => { + const su = extension.import('service:su'); + + await su.sudo(async () => { + try { + res.set('Content-Type', 'text/plain'); + + const exampleData = await db.read( + 'SELECT * FROM example_extension_data ORDER BY created_at DESC LIMIT 5', + ); + + let response = '=== Example DB Extension Demo ===\n\n'; + response += '=== Example Table Data (last 5 records) ===\n'; + exampleData.forEach(row => { + response += `ID: ${row.id}, Name: ${row.name}, Value: ${row.value}, Created: ${row.created_at}\n`; + }); + + res.send(response); + + // eslint-disable-next-line @stylistic/space-before-function-paren, custom/control-structure-spacing + } catch (error) { + console.error('Database operation error:', error); + res.status(500).send(`Database error: ${error.message}`); + } + }); +}); + +// The /example-db/cleanup endpoint erases sample data. +extension.get('/example-db/cleanup', async (req, res) => { + const su = extension.import('service:su'); + + await su.sudo(async () => { + try { + res.set('Content-Type', 'text/plain'); + + // Clean up old test data (older than 1 hour) - only sample data + const deleteResult = await db.write( + 'DELETE FROM example_extension_data WHERE name LIKE "sample-%" AND created_at < datetime("now", "-1 hour")', + [], + ); + + res.send(`Cleaned up ${deleteResult.anyRowsAffected ? 'some' : 'no'} old sample records`); + + // eslint-disable-next-line @stylistic/space-before-function-paren, custom/control-structure-spacing + } catch (error) { + console.error('Cleanup error:', error); + res.status(500).send(`Cleanup error: ${error.message}`); + } + }); +}); + +// The /example-db/search endpoint searches data based on the "q" query parameter. +// +// For example, try one of these: +// - GET /example-db/search?q=3 +// - GET /example-db/search?q=sam +extension.get('/example-db/search', { noauth: true }, async (req, res) => { + const su = extension.import('service:su'); + + await su.sudo(async () => { + try { + res.set('Content-Type', 'text/plain'); + + // Get search term from query parameter (safely parameterized) + const searchTerm = req.query.q ?? 'test'; + if ( typeof searchTerm !== 'string' ) { + res.status(400).send('Not like that - only strings please!'); + return; + } + + // Safe parameterized search - prevents SQL injection + const searchResults = await db.read( + 'SELECT * FROM example_extension_data WHERE name LIKE ? OR value LIKE ? ORDER BY created_at DESC LIMIT 10', + [`%${searchTerm}%`, `%${searchTerm}%`], + ); + + let response = `=== Search Results for "${searchTerm}" ===\n\n`; + if ( searchResults.length === 0 ) { + response += 'No results found.\n'; + } else { + searchResults.forEach(row => { + response += `ID: ${row.id}, Name: ${row.name}, Value: ${row.value}\n`; + }); + } + + res.send(response); + + // eslint-disable-next-line @stylistic/space-before-function-paren, custom/control-structure-spacing + } catch (error) { + console.error('Search error:', error); + res.status(500).send(`Search error: ${error.message}`); + } + }); +}); + +// /example-db/stats shows some stats that might be interesting +// +// This is only enabled in development environments to prevent abuse. +// +// eslint-disable-next-line no-undef +if ( global_config.env === 'dev' ) { + extension.get('/example-db/stats', { noauth: true }, async (req, res) => { + const su = extension.import('service:su'); + + await su.sudo(async () => { + try { + res.set('Content-Type', 'application/json'); + + const stats = { + apps: await db.read('SELECT COUNT(*) as count FROM apps'), + users: await db.read('SELECT COUNT(*) as count FROM user'), + sessions: await db.read('SELECT COUNT(*) as count FROM sessions'), + fsentries: await db.read('SELECT COUNT(*) as count FROM fsentries'), + notifications: await db.read('SELECT COUNT(*) as count FROM notification'), + example_records: await db.read('SELECT COUNT(*) as count FROM example_extension_data'), + }; + + const result = {}; + for ( const [key, value] of Object.entries(stats) ) { + result[key] = value[0].count; + } + + res.json(result); + + // eslint-disable-next-line @stylistic/space-before-function-paren, custom/control-structure-spacing + } catch (error) { + console.error('Stats error:', error); + res.status(500).json({ error: error.message }); + } + }); + }); + + // /example-db/add-data shows a simple HTML form for adding test data. + // + // The form itself is simply to aid in demonstration purposes rather than + // being an example for building a form, so it is terse, uncommented, and + // was generated by a robot. + extension.get('/example-db/add-data', { noauth: true }, async (req, res) => { + res.set('Content-Type', 'text/html'); + res.send(` + + + + Add Test Data - Example DB + + +

Add Test Data to Example DB

+
+
+
+ + + +
+
+ +

+ +
+ + + + + `); + }); + + // The POST handler for /example-db/add-data demonstrates adding some rows + extension.post('/example-db/add-data', { noauth: true }, async (req, res) => { + const su = extension.import('service:su'); + + await su.sudo(async () => { + try { + const names = req.body.name || []; + const values = req.body.value || []; + + if ( !Array.isArray(names) || !Array.isArray(values) || names.length !== values.length ) { + res.status(400).send('Invalid form data'); + return; + } + + if ( names.length === 0 ) { + res.status(400).send('No data to insert'); + return; + } + + // Build parameterized query for multiple inserts + const placeholders = names.map(() => '(?, ?)').join(', '); + const params = []; + for ( let i = 0; i < names.length; i++ ) { + params.push(names[i], values[i]); + } + + await db.write( + `INSERT INTO example_extension_data (name, value) VALUES ${placeholders}`, + params, + ); + + res.set('Content-Type', 'text/html'); + res.send(` + + + + Data Added - Example DB + + +

Success!

+

Added ${names.length} record(s) to the database.

+ Add More Data | + View Data + + + `); + + // eslint-disable-next-line @stylistic/space-before-function-paren, custom/control-structure-spacing + } catch (error) { + console.error('Add data error:', error); + res.status(500).send(`Error adding data: ${error.message}`); + } + }); + }); +} \ No newline at end of file diff --git a/extensions/example-kv.js b/extensions/example-kv.js index e1e7465342..a82fb8657d 100644 --- a/extensions/example-kv.js +++ b/extensions/example-kv.js @@ -28,3 +28,25 @@ extension.on('init', async () => { console.log('kv key should no longer have the value', kv.get('example-kv-key')); })(); }); + +// "kv" is always loaded by the time request handlers are active +extension.get('/example-kv', { noauth: true }, async (req, res) => { + // if ( ! req.actor ) { + // res.status(403).send('You need to be logged in to use kv!'); + // return; + // } + + // Puter has a convenient service called `su` that lets us change the user. + // We need to specify "sudo" (running as system user) because this is a + // request handler and we disabled authentication to make this example page + // a little easier to access. + // + // If we did not use "sudo" here, you could still `fetch` this URL from + // inside an authenticated Puter session, but it wouldn't work otherwise. + // + const su = extension.import('service:su'); + await su.sudo(async () => { + res.set('Content-Type', 'text/plain'); // don't treat output as HTML + res.send(`kv value is: ${await kv.get('example-kv-key')}`); + }); +});