From 3f6900f26b398c37f707ed921dd85fd2b3641135 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Thu, 31 Oct 2024 21:08:38 -0400 Subject: [PATCH] dev: add get() and post() to extension API --- mods/mods_available/example/main.js | 7 +++ mods/mods_available/example/package.json | 12 +++++ src/backend/src/Extension.js | 46 +++++++++++++++++ src/backend/src/ExtensionModule.js | 8 +++ src/backend/src/ExtensionService.js | 64 ++++++++++++++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 mods/mods_available/example/main.js create mode 100644 mods/mods_available/example/package.json create mode 100644 src/backend/src/ExtensionService.js diff --git a/mods/mods_available/example/main.js b/mods/mods_available/example/main.js new file mode 100644 index 00000000..f796977e --- /dev/null +++ b/mods/mods_available/example/main.js @@ -0,0 +1,7 @@ +extension.get('/example-mod-get', (req, res) => { + res.send('Hello World!'); +}); + +extension.on('install', ({ services }) => { + console.log('install was called'); +}) diff --git a/mods/mods_available/example/package.json b/mods/mods_available/example/package.json new file mode 100644 index 00000000..175f513a --- /dev/null +++ b/mods/mods_available/example/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-puter-extension", + "version": "1.0.0", + "description": "", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "AGPL-3.0-only" +} diff --git a/src/backend/src/Extension.js b/src/backend/src/Extension.js index 4f43ea15..b3dcbb16 100644 --- a/src/backend/src/Extension.js +++ b/src/backend/src/Extension.js @@ -1,6 +1,7 @@ const { AdvancedBase } = require("@heyputer/putility"); const EmitterFeature = require("@heyputer/putility/src/features/EmitterFeature"); const { Context } = require("./util/context"); +const { ExtensionService, ExtensionServiceState } = require("./ExtensionService"); class Extension extends AdvancedBase { static FEATURES = [ @@ -12,6 +13,51 @@ class Extension extends AdvancedBase { ] }), ]; + + constructor (...a) { + super(...a); + this.service = null; + } + + get (path, handler, options) { + // this extension will have a default service + this.ensure_service_(); + + // handler and options may be flipped + if ( typeof handler === 'object' ) { + [handler, options] = [options, handler]; + } + if ( ! options ) options = {}; + + this.service.register_route_handler_(path, handler, { + ...options, + methods: ['GET'], + }); + } + + post (path, handler, options) { + // this extension will have a default service + this.ensure_service_(); + + // handler and options may be flipped + if ( typeof handler === 'object' ) { + [handler, options] = [options, handler]; + } + if ( ! options ) options = {}; + + this.service.register_route_handler_(path, handler, { + ...options, + methods: ['POST'], + }); + } + + ensure_service_ () { + if ( this.service ) { + return; + } + + this.service = new ExtensionServiceState(); + } } module.exports = { diff --git a/src/backend/src/ExtensionModule.js b/src/backend/src/ExtensionModule.js index a4b3924b..8461a6b8 100644 --- a/src/backend/src/ExtensionModule.js +++ b/src/backend/src/ExtensionModule.js @@ -1,10 +1,18 @@ const { AdvancedBase } = require("@heyputer/putility"); +const uuid = require('uuid'); +const { ExtensionService } = require("./ExtensionService"); class ExtensionModule extends AdvancedBase { async install (context) { const services = context.get('services'); this.extension.emit('install', { context, services }) + + if ( this.extension.service ) { + services.registerService(uuid.v4(), ExtensionService, { + state: this.extension.service, + }); // uuid for now + } } } diff --git a/src/backend/src/ExtensionService.js b/src/backend/src/ExtensionService.js new file mode 100644 index 00000000..e6665f08 --- /dev/null +++ b/src/backend/src/ExtensionService.js @@ -0,0 +1,64 @@ +const { AdvancedBase } = require("@heyputer/putility"); +const BaseService = require("./services/BaseService"); +const { Endpoint } = require("./util/expressutil"); + +class ExtensionServiceState extends AdvancedBase { + constructor (...a) { + super(...a); + + this.endpoints_ = []; + } + register_route_handler_ (path, handler, options = {}) { + // handler and options may be flipped + if ( typeof handler === 'object' ) { + [handler, options] = [options, handler]; + } + + const mw = options.mw ?? []; + + // TODO: option for auth middleware is harcoded here, but eventually + // all exposed middlewares should be registered under the simpele names + // used in this options object (probably; still not 100% decided on that) + if ( options.auth ) { + const auth_conf = typeof options.auth === 'object' ? + options.auth : {}; + mw.push(configurable_auth(auth_conf)); + } + + const endpoint = Endpoint({ + methods: options.methods ?? ['GET'], + mw, + route: path, + handler: handler, + }); + + this.endpoints_.push(endpoint); + } +} + +/** + * A service that does absolutely nothing by default, but its behavior can be + * extended by adding route handlers and event listeners. This is used to + * provide a default service for extensions. + */ +class ExtensionService extends BaseService { + _construct () { + this.extension = null; + this.endpoints_ = []; + } + async _init (args) { + this.state = args.state; + } + + ['__on_install.routes'] (_, { app }) { + for ( const endpoint of this.state.endpoints_ ) { + endpoint.attach(app); + } + } + +} + +module.exports = { + ExtensionService, + ExtensionServiceState, +};