Skip to main content

Developer Onboarding Guide

This guide covers the core patterns you need to be productive on the Drumee backend quickly.


Two Repositories​

RepoPurpose
server-teamNode.js service layer — routing, ACL enforcement, business logic
schemasMariaDB schemas — tables, stored procedures, patches

The server never runs raw SQL for business logic. All data operations go through stored procedures defined in schemas/.


Adding a New Service Method​

There are exactly two files to touch. The router wires everything automatically.

Step 1 — Declare it in the ACL JSON (acl/{module}.json)​

{
"modules": {
"private": "service/private/{module}",
"public": "service/{module}"
},
"services": {
"my_method": {
"doc": "What this does",
"scope": "hub",
"permission": { "src": "write" },
"params": {
"node_id": { "type": "string", "required": true, "doc": "Target node" },
"page": { "type": "number", "required": false, "doc": "Page number" }
}
}
}
}

scope values: hub, domain, user
permission.src values (ascending): anonymous → read(2) → write(4) → delete → admin(6) → owner(7)

Step 2 — Implement it in the service class (service/{module}.js)​

const { Entity } = require('@drumee/server-core');
const { Attr } = require('@drumee/server-essentials');

class __mymodule extends Entity {
async my_method() {
const nid = this.input.need(Attr.node_id); // throws if missing
const page = this.input.get(Attr.page) || 1; // optional

const rows = await this.db.await_proc('sp_my_proc', nid, page);
this.output.data(rows);
}
}

module.exports = __mymodule;

That's it. The REST router reads acl/{module}.json at startup, enforces permission, then calls worker.my_method().


Service Class Internals​

Every service class extends Entity (@drumee/server-core). Available on this:

Input​

this.input.need(Attr.x)   // required — throws MISSING_PARAM if absent
this.input.get(Attr.x) // optional — returns undefined if absent
this.input.use(Attr.x) // optional — alias for get

Output​

this.output.data(obj)          // send JSON response
this.output.list(arr) // send array response
this.output.add_data({k: v}) // merge extra fields into response
this.output.html(str) // send HTML
this.output.status(code) // set HTTP status before data()

Exceptions​

this.exception.user('ERROR_CODE')    // → 400
this.exception.forbiden() // → 403
this.exception.not_found('MSG') // → 404
this.exception.server('MSG') // → 500

Calling any exception method ends execution immediately — do not call this.output.data() after.

Database Access​

HandleDatabaseUse for
this.dbCurrent hub's DBFile/node operations (mfs_* procs)
this.ypYellow Pages (central registry)User/org lookups, socket targets
// Stored procedure (returns rows)
const node = await this.db.await_proc('mfs_access_node', uid, nid);

// SQL function (returns scalar)
const id = await this.db.await_func('node_id_from_path', filepath);

// Fire-and-forget (no await needed)
this.yp.call_proc('entity_touch', uid, this.output.data);

Stored Procedure Naming​

{entity}_{verb} — e.g. mfs_create_node, mfs_list_by, contact_invitation_status, entity_sockets, disk_usage


Broadcasting to WebSocket Clients​

Three steps: get recipients → build payload → send via Redis.

const { RedisStore } = require('@drumee/server-essentials');
const { toArray } = require('@drumee/server-core');

async my_method() {
// ... do work, get `node` result ...

const recipients = toArray(
await this.yp.await_proc('entity_sockets', {
hub_id: node.hub_id,
exclude: [this.input.get(Attr.socket_id)], // omit sender
})
);

const payload = this.payload(node, { service: 'media.new' });
await RedisStore.sendData(payload, recipients);

this.output.data(node); // also respond to the caller
}

entity_sockets returns all connected sockets for a hub. Use user_sockets(uid) to target a specific user instead.


ACL Advanced: Pre-processors​

For operations that need multi-source permission checks (e.g. move requires delete on source AND write on destination):

"move": {
"permission": {
"src": "delete",
"dest": "write",
"preproc": { "checker": "pre_move" }
}
}

The router calls worker.pre_move() after granting, waits for the pre_move-done event signal, then calls worker.move(). This pattern lets you validate complex preconditions inside the class before main execution.


Database Schema Changes (schemas repo)​

All schema work lives in the schemas repository alongside this one.

Directory layout:

DirectoryDB type
yellow_page/Central registry (entities, users, hubs)
common/Shared procedures across all entities
drumate/User workspace tables and procedures
hub/Hub/org workspace tables and procedures

Rules:

  • One SQL routine per file (enforced by tooling).
  • Stored procedure names follow {entity}_{verb} convention.

Applying patches:

# Apply a single file
bin/patch-from-file path/to/routine.sql db_name

# Apply a manifest of changes between two commits
bin/make-manifest <hash1> <hash2>
bin/patch-from-manifest patches-dir

Request Lifecycle (Quick Reference)​

POST /-/svc/contact.invite
│
▼
router/rest/index.js
• Splits "contact.invite" → module="contact", method="invite"
• Loads acl/contact.json → checks permission.src = "owner"
• Requires service/private/contact.js (for authenticated users)
│
▼
new __contact({ session, permission })
• ACL emits GRANTED signal
• (optional) preproc.checker runs and signals done
│
▼
worker.invite()
• this.input.need(Attr.email) validates params
• this.db / this.yp call stored procedures
• this.output.data(result) sends response
• (optional) RedisStore.sendData() broadcasts to hub