CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-sails

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

Pending
Overview
Eval results
Files

actions.mddocs/

Actions System

Sails.js uses a flexible action system for handling HTTP requests and business logic. Actions can be traditional controller methods or standalone machine-compatible functions. The actions system provides registration, management, and discovery capabilities with support for input validation, output formatting, and middleware integration.

Action Management

registerAction()

Register an action with Sails for route binding and execution:

sails.registerAction(action: Function|Dictionary, identity: String, force?: Boolean): void

Parameters:

  • action (Function|Dictionary) - The action function or machine definition
  • identity (String) - Unique action identifier (e.g., 'user/find', 'account/login')
  • force (Boolean, optional) - Overwrite existing action if it exists (default: false)

Throws:

  • E_CONFLICT - Action already exists and force is not true
  • E_INVALID - Action definition is invalid

Examples:

// Register a simple function action
sails.registerAction((req, res) => {
  return res.json({ message: 'Hello from action!' });
}, 'hello/world');

// Register a machine-compatible action
sails.registerAction({
  friendlyName: 'Create user',
  description: 'Create a new user account',
  
  inputs: {
    email: {
      type: 'string',
      required: true,
      isEmail: true
    },
    password: {
      type: 'string', 
      required: true,
      minLength: 6
    },
    fullName: {
      type: 'string',
      required: true
    }
  },
  
  exits: {
    success: {
      description: 'User created successfully'
    },
    emailAlreadyInUse: {
      statusCode: 409,
      description: 'Email address already in use'
    }
  },
  
  fn: async function(inputs, exits) {
    try {
      // Check if email exists
      const existingUser = await User.findOne({ email: inputs.email });
      if (existingUser) {
        return exits.emailAlreadyInUse();
      }
      
      // Create new user
      const newUser = await User.create({
        email: inputs.email,
        password: await sails.helpers.passwords.hashPassword(inputs.password),
        fullName: inputs.fullName
      }).fetch();
      
      return exits.success({
        user: newUser,
        message: 'User created successfully'
      });
      
    } catch (err) {
      return exits.serverError(err);
    }
  }
}, 'user/create');

// Force overwrite existing action
sails.registerAction(newImplementation, 'user/create', true);

getActions()

Retrieve all registered actions:

sails.getActions(): Dictionary

Returns: Dictionary - Shallow clone of the internal actions registry

Example:

const actions = sails.getActions();

console.log('Registered actions:');
Object.keys(actions).forEach(identity => {
  console.log(`- ${identity}`);
});

// Access specific action
const userCreateAction = actions['user/create'];
if (userCreateAction) {
  console.log('User create action found');
}

reloadActions()

Reload all registered actions from their source files:

sails.reloadActions(cb?: Function): void

Parameters:

  • cb (Function, optional) - Node-style callback (err) => {}

Example:

// Reload all actions (useful during development)
sails.reloadActions((err) => {
  if (err) {
    console.error('Failed to reload actions:', err);
  } else {
    console.log('Actions reloaded successfully');
  }
});

// Promise-style using util.promisify
const { promisify } = require('util');
const reloadActionsAsync = promisify(sails.reloadActions.bind(sails));

try {
  await reloadActionsAsync();
  console.log('Actions reloaded');
} catch (err) {
  console.error('Reload failed:', err);
}

Action Middleware Management

registerActionMiddleware()

Register middleware specifically for actions:

sails.registerActionMiddleware(actionMiddleware: Function|Dictionary, identity: String, force?: Boolean): void

Parameters:

  • actionMiddleware (Function|Dictionary) - Middleware function or definition
  • identity (String) - Unique middleware identifier
  • force (Boolean, optional) - Overwrite existing middleware

Example:

// Register authentication middleware
sails.registerActionMiddleware(
  (req, res, proceed) => {
    if (!req.session.userId) {
      return res.status(401).json({ error: 'Authentication required' });
    }
    return proceed();
  }, 
  'auth/require-login'
);

// Register validation middleware
sails.registerActionMiddleware({
  friendlyName: 'Validate API key',
  
  fn: function(req, res, proceed) {
    const apiKey = req.headers['x-api-key'];
    if (!apiKey || !isValidApiKey(apiKey)) {
      return res.status(403).json({ error: 'Invalid API key' });
    }
    return proceed();
  }
}, 'auth/validate-api-key');

Action Formats

Function Actions

Simple function-based actions for basic request handling:

// Basic function action
function simpleAction(req, res) {
  const { id } = req.params;
  return res.json({ 
    message: 'Simple action response',
    id: id 
  });
}

sails.registerAction(simpleAction, 'simple/action');

// Async function action
async function asyncAction(req, res) {
  try {
    const data = await fetchDataFromDatabase();
    return res.json(data);
  } catch (err) {
    return res.serverError(err);
  }
}

sails.registerAction(asyncAction, 'async/action');

Machine Actions

Machine-compatible actions with input validation and structured exits:

const machineAction = {
  friendlyName: 'Find users',
  description: 'Find users with optional filtering',
  
  inputs: {
    limit: {
      type: 'number',
      defaultsTo: 10,
      min: 1,
      max: 100
    },
    skip: {
      type: 'number', 
      defaultsTo: 0,
      min: 0
    },
    search: {
      type: 'string',
      description: 'Search term for user names or emails'
    }
  },
  
  exits: {
    success: {
      description: 'Users found successfully',
      outputType: {
        users: ['ref'],
        totalCount: 'number'
      }
    }
  },
  
  fn: async function(inputs, exits) {
    let criteria = {};
    
    if (inputs.search) {
      criteria.or = [
        { fullName: { contains: inputs.search } },
        { email: { contains: inputs.search } }
      ];
    }
    
    const users = await User.find(criteria)
      .limit(inputs.limit) 
      .skip(inputs.skip);
      
    const totalCount = await User.count(criteria);
    
    return exits.success({
      users: users,
      totalCount: totalCount
    });
  }
};

sails.registerAction(machineAction, 'user/find');

Route Discovery and URL Generation

getRouteFor()

Find the first explicit route that matches a given action target:

sails.getRouteFor(routeQuery: String|Dictionary): {url: String, method: String}

Parameters:

  • routeQuery (String|Dictionary) - Target action or route query

Returns: Object with url and method properties

Throws:

  • E_NOT_FOUND - No matching route found
  • E_USAGE - Invalid route query

Examples:

// Find route by controller method
try {
  const route = sails.getRouteFor('UserController.find');
  console.log(`Route: ${route.method.toUpperCase()} ${route.url}`);
  // Output: Route: GET /api/users
} catch (err) {
  if (err.code === 'E_NOT_FOUND') {
    console.log('Route not found');
  }
}

// Find route by action identity
const loginRoute = sails.getRouteFor('entrance/login');
console.log(loginRoute); // { url: '/login', method: 'get' }

// Find route by object query
const userShowRoute = sails.getRouteFor({
  target: 'UserController.findOne'
});
console.log(userShowRoute); // { url: '/api/users/:id', method: 'get' }

getUrlFor()

Get URL pattern for a route target:

sails.getUrlFor(routeQuery: String|Dictionary): String

Parameters:

  • routeQuery (String|Dictionary) - Target action or route query

Returns: String - URL pattern for the route

Examples:

// Get URL pattern for action
const userCreateUrl = sails.getUrlFor('user/create');
console.log(userCreateUrl); // '/api/users'

// Get URL with parameters
const userShowUrl = sails.getUrlFor('UserController.findOne');  
console.log(userShowUrl); // '/api/users/:id'

// Build actual URL with parameters
const actualUrl = userShowUrl.replace(':id', '123');
console.log(actualUrl); // '/api/users/123'

Request Simulation

request()

Create virtual requests for testing actions without HTTP server:

sails.request(opts: {url: String, method?: String, params?: Dictionary, headers?: Dictionary}, cb?: Function): MockClientResponse
sails.request(address: String, params?: Dictionary, cb?: Function): MockClientResponse

Parameters:

  • opts (Dictionary) - Request configuration object
    • url (String) - Target URL/route
    • method (String, optional) - HTTP method (default: 'GET')
    • params (Dictionary, optional) - Request parameters/body
    • headers (Dictionary, optional) - Request headers
  • address (String) - Simple URL/route address
  • params (Dictionary, optional) - Request parameters
  • cb (Function, optional) - Response callback

Returns: MockClientResponse - Stream-like response object

Examples:

// Basic virtual request
sails.request('/api/users', (err, res, body) => {
  if (err) throw err;
  console.log('Response status:', res.statusCode);
  console.log('Response body:', body);
});

// Request with parameters
sails.request({
  url: '/api/users',
  method: 'POST',
  params: {
    email: 'test@example.com',
    fullName: 'Test User',
    password: 'password123'
  }
}, (err, res, body) => {
  console.log('User created:', body);
});

// Request with headers
sails.request({
  url: '/api/protected',
  headers: {
    'Authorization': 'Bearer token123',
    'Content-Type': 'application/json'
  }
}, (err, res, body) => {
  console.log('Protected resource:', body);
});

// Promise-style virtual request
const response = await new Promise((resolve, reject) => {
  sails.request('/api/users/123', (err, res, body) => {
    if (err) return reject(err);
    resolve({ res, body });
  });
});

Action Location and Discovery

Actions are typically located in:

Controllers Directory

api/controllers/
├── UserController.js
├── AuthController.js
└── AdminController.js

Actions Directory

api/actions/
├── user/
│   ├── create.js
│   ├── find.js
│   └── update.js
├── auth/
│   ├── login.js
│   └── logout.js
└── admin/
    └── dashboard.js

Action File Examples

Controller-based action (api/controllers/UserController.js):

module.exports = {
  find: async function(req, res) {
    const users = await User.find();
    return res.json(users);
  },
  
  create: async function(req, res) {
    const newUser = await User.create(req.allParams()).fetch();
    return res.json(newUser);
  }
};

Standalone action (api/actions/user/create.js):

module.exports = {
  friendlyName: 'Create user',
  description: 'Create a new user account',
  
  inputs: {
    email: { type: 'string', required: true },
    fullName: { type: 'string', required: true }
  },
  
  fn: async function(inputs) {
    const user = await User.create(inputs).fetch();
    return { user };
  }
};

Action Execution Context

Actions receive different parameters based on their format:

Function Actions

function actionHandler(req, res) {
  // req - HTTP request object
  // res - HTTP response object
  // Direct access to Sails globals (User, sails, etc.)
}

Machine Actions

{
  fn: async function(inputs, exits) {
    // inputs - Validated input parameters
    // exits - Exit functions (success, error, etc.)
    // Access to this.req and this.res for HTTP context
  }
}

Error Handling

Actions support comprehensive error handling:

// Function action error handling
sails.registerAction(async (req, res) => {
  try {
    const result = await riskyOperation();
    return res.json(result);
  } catch (err) {
    if (err.code === 'E_NOT_FOUND') {
      return res.notFound();
    }
    return res.serverError(err);
  }
}, 'risky/operation');

// Machine action error handling
sails.registerAction({
  fn: async function(inputs, exits) {
    try {
      const result = await performOperation(inputs);
      return exits.success(result);
    } catch (err) {
      if (err.name === 'ValidationError') {
        return exits.invalid(err.details);
      }
      throw err; // Will be caught by framework
    }
  },
  
  exits: {
    invalid: {
      statusCode: 400,
      description: 'Invalid input provided'
    }
  }
}, 'validated/operation');

Development and Testing

Hot Reloading

Actions can be reloaded during development:

// Watch for file changes and reload actions
if (sails.config.environment === 'development') {
  const chokidar = require('chokidar');
  
  chokidar.watch('api/actions/**/*.js').on('change', () => {
    sails.reloadActions((err) => {
      if (!err) console.log('Actions reloaded');
    });
  });
}

Testing Actions

// Test action directly
describe('User Actions', () => {
  it('should create user', (done) => {
    sails.request({
      url: '/api/users',
      method: 'POST',
      params: {
        email: 'test@example.com',
        fullName: 'Test User'
      }
    }, (err, res, body) => {
      expect(res.statusCode).to.equal(201);
      expect(body.user).to.exist;
      done();
    });
  });
});

The Sails actions system provides a flexible foundation for building scalable web applications with comprehensive input validation, error handling, and testing capabilities.

Install with Tessl CLI

npx tessl i tessl/npm-sails

docs

actions.md

application-lifecycle.md

cli.md

configuration.md

events.md

hooks.md

index.md

routing.md

tile.json