Browse Source

dev: add get() and post() to extension API

KernelDeimos 6 months ago
parent
commit
3f6900f26b

+ 7 - 0
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');
+})

+ 12 - 0
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"
+}

+ 46 - 0
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 = {

+ 8 - 0
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
+        }
     }
 }
 

+ 64 - 0
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,
+};