--- name: standalone-sage-app-deploy description: "Deploy standalone Sage apps. Monkey-patches and RBAC setup." version: 1.0.0 category: devops --- # Standalone Sage-Style App Deployment Deploying an ahserver+bricks+rbac app without the full Sage framework. ## Monkey-Patch Boilerplate (app/appname.py) ```python import os, sys from contextlib import asynccontextmanager # 1. Patch DictObject — ServerEnv instances get get_module_dbname from appPublic.dictObject import DictObject _orig_getattr = DictObject.__getattr__ def _patched_getattr(self, key): if key == 'get_module_dbname': return lambda m: 'pccs' return _orig_getattr(self, key) DictObject.__getattr__ = _patched_getattr # 2. Patch get_sor_context — reuse global DBPools import sqlor.dbpools as _dbp from appPublic.jsonConfig import getConfig as _getConfig from sqlor.dbpools import DBPools _workdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _config = _getConfig(_workdir, NS={'workdir': _workdir}) _global_db = DBPools(_config.databases) @asynccontextmanager async def _patched_sor_ctx(env, modulename, errorback=None): dbname = env.get_module_dbname(modulename) async with _global_db.sqlorContext(dbname) as sor: yield sor _dbp.get_sor_context = _patched_sor_ctx # 3. Register with set_serverenv from ahserver.serverenv import set_serverenv set_serverenv('get_module_dbname', lambda m: 'pccs') # 4. Fix globalEnv cached reference from ahserver.webapp import webapp from ahserver.globalEnv import GlobalEnv GlobalEnv().get_sor_context = _patched_sor_ctx def init(): from appbase.init import load_appbase from rbac.init import load_rbac load_appbase() load_rbac() import rbac.userperm as _up _up.get_sor_context = _patched_sor_ctx if __name__ == '__main__': webapp(init) ``` **Why each patch:** | Patch | Without it | |-------|-----------| | DictObject.__getattr__ | `get_module_dbname` is None | | set_serverenv | `get_serverenv` uses dict.get, not __getattr__ | | get_sor_context | Creates empty `DBPools()` → `name=None` | | globalEnv | Caches original before our patch | | userperm | `from X import Y` captures at import time | ## config.json ```json { "password_key": "...", "databases": { "pccs": { "driver": "mysql", "minsize": 2, "maxsize": 10, "kwargs": { "host": "127.0.0.1", "port": 3306, "user": "test", "password": "", "db": "pccs", "charset": "utf8mb4", "autocommit": true } } }, "website": { "host": "0.0.0.0", "port": 9180, "paths": [["$[workdir]$/wwwroot", "/"]], "processors": [ [".tmpl", "tmpl"], [".ui", "bui"], [".dspy", "dspy"], [".wss", "ws"] ], "indexes": ["index.ui", "index.html"], "statics": [ ["/bricks/", "$[workdir]$/wwwroot/bricks/"], ["/imgs/", "$[workdir]$/wwwroot/imgs/"] ] } } ``` Password must be AES-encrypted: `aes_encode_b64(password_key, plaintext)`. **Do NOT add `session` config** — breaks .ui routing. ## RBAC Setup ```python await sor.C('permission', {'id': getID(), 'path': '/path'}) await sor.C('rolepermission', {'id': getID(), 'roleid': 'any', 'permid': pid}) ``` Roles needed: `anonymous`, `any`, `logined` (orgtypeid='*'). Restart after. ## Common Pitfalls - **`name=None`** — missing patch on get_sor_context or set_serverenv - **AES padding** — password must be encrypted, not plaintext - **`minsize` in kwargs** — pool params at DB level, not kwargs - **`.dspy` subdir 404** — custom endpoints in own module's wwwroot - **`.html` 401** — register `any` permission for static paths - **mysql-client** removes MariaDB; use mariadb-client - **npm missing** — `curl -fsSL https://deb.nodesource.com/setup_20.x | bash`