All skills

Sails.js framework patterns for The Boring JavaScript Stack - actions, helpers, routes, policies, hooks, configuration, security, middleware, file uploads, deployment, and more. Use this skill when building, reviewing, or debugging any server-side code in a Sails.js application.

Use this Skill: https://skilld.dev/gh/sailscastshq/boring-stack/sails

This session only. Nothing lands on disk.

ruleshelpers.md

≈2.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Helpers

Helpers are reusable functions that follow the same machine specification as actions. They live in api/helpers/ and are automatically loaded as sails.helpers.*.

Basic Helper

// api/helpers/format-currency.js
module.exports = {
  friendlyName: 'Format currency',
  description: 'Format a cent amount as a human-readable currency string.',

  inputs: {
    amount: {
      type: 'number',
      required: true,
      description: 'Amount in cents.'
    },
    currency: {
      type: 'string',
      defaultsTo: 'USD'
    }
  },

  fn: async function ({ amount, currency }) {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency
    }).format(amount / 100)
  }
}

Calling Helpers

// Named arguments (recommended)
const formatted = await sails.helpers.formatCurrency.with({
  amount: 1999,
  currency: 'USD'
})

// Positional arguments (for single-input helpers)
const formatted = await sails.helpers.formatCurrency(1999)

Subdirectory Organization

Helpers in subdirectories are namespaced:

api/helpers/
├── format-currency.js        → sails.helpers.formatCurrency()
├── passwords/
│   ├── hash-password.js      → sails.helpers.passwords.hashPassword()
│   └── check-password.js     → sails.helpers.passwords.checkPassword()
├── billing/
│   ├── create-customer.js    → sails.helpers.billing.createCustomer()
│   └── handle-invoice-paid.js → sails.helpers.billing.handleInvoicePaid()
└── mail/
    ├── send-template.js      → sails.helpers.mail.sendTemplate()
    └── send.js               → sails.helpers.mail.send()

Generate helpers:

npx sails generate helper passwords/hash-password
npx sails generate helper billing/create-customer

Helpers with Multiple Exits

// api/helpers/passwords/check-password.js
module.exports = {
  friendlyName: 'Check password',
  description: 'Check if a password matches a hashed password.',

  inputs: {
    password: {
      type: 'string',
      required: true
    },
    hashedPassword: {
      type: 'string',
      required: true
    }
  },

  exits: {
    success: {
      description: 'Password matches.'
    },
    incorrect: {
      description: 'The provided password does not match.'
    }
  },

  fn: async function ({ password, hashedPassword }) {
    const bcrypt = require('bcrypt')
    const isMatch = await bcrypt.compare(password, hashedPassword)
    if (!isMatch) {
      throw 'incorrect'
    }
  }
}

Synchronous Helpers

For simple computations that don't need async:

// api/helpers/get-gravatar-url.js
module.exports = {
  sync: true,

  inputs: {
    email: { type: 'string', required: true }
  },

  fn: function ({ email }) {
    const crypto = require('crypto')
    const hash = crypto
      .createHash('md5')
      .update(email.toLowerCase().trim())
      .digest('hex')
    return `https://www.gravatar.com/avatar/${hash}?d=identicon`
  }
}

Synchronous helpers can be called without await:

const url = sails.helpers.getGravatarUrl('user@example.com')

Passing req to Helpers

When a helper needs request context, pass req as a ref input:

// api/helpers/get-current-user.js
module.exports = {
  inputs: {
    req: {
      type: 'ref',
      required: true
    }
  },

  fn: async function ({ req }) {
    if (!req.session.userId) return null
    return await User.findOne({ id: req.session.userId })
  }
}
// In an action:
const user = await sails.helpers.getCurrentUser(this.req)

Error Handling with Helpers

.intercept() -- Map Exits

Map a helper's exit to a different exit in the calling action:

// In an action:
await sails.helpers.passwords.checkPassword
  .with({ password, hashedPassword: user.password })
  .intercept('incorrect', () => ({
    badLoginRequest: {
      problems: [{ login: 'Wrong email/password combination.' }]
    }
  }))

.tolerate() -- Handle Exits Gracefully

Tolerate a specific exit and continue execution:

// Tolerate and return undefined
await sails.helpers.stripe
  .deleteCustomer(customerId)
  .tolerate('customerNotFound')

// Tolerate with a fallback value
const customer = await sails.helpers.stripe
  .getCustomer(customerId)
  .tolerate('notFound', () => null)

.intercept() with Error Transformation

await sails.helpers.billing.chargeCard
  .with({ amount, customerId })
  .intercept('cardDeclined', (err) => {
    return new Error(
      'Your card was declined. Please update your payment method.'
    )
  })
  .intercept('insufficientFunds', () => ({
    badRequest: {
      problems: [{ payment: 'Insufficient funds.' }]
    }
  }))

Common Helper Patterns

Password Hashing

// api/helpers/passwords/hash-password.js
module.exports = {
  inputs: {
    password: { type: 'string', required: true }
  },

  fn: async function ({ password }) {
    const bcrypt = require('bcrypt')
    const saltRounds = 10
    return await bcrypt.hash(password, saltRounds)
  }
}

Email Sending

// api/helpers/mail/send-template.js
module.exports = {
  inputs: {
    to: { type: 'string', required: true, isEmail: true },
    subject: { type: 'string', required: true },
    template: { type: 'string', required: true },
    templateData: { type: 'json', defaultsTo: {} }
  },

  exits: {
    success: { description: 'Email sent.' }
  },

  fn: async function ({ to, subject, template, templateData }) {
    // Use your email service (Mailgun, SES, etc.)
    await sails.helpers.mail.send.with({
      to,
      subject,
      htmlContent: await sails.renderView(`emails/${template}`, templateData)
    })
  }
}

File Upload Helper

// api/helpers/upload-one.js
module.exports = {
  inputs: {
    file: { type: 'ref', required: true },
    maxBytes: { type: 'number', defaultsTo: 10 * 1024 * 1024 }
  },

  fn: async function ({ file, maxBytes }) {
    const uploads = await sails.upload(file, { maxBytes })
    if (uploads.length === 0) {
      throw new Error('No file uploaded.')
    }
    return uploads[0]
  }
}

Generating Tokens

// api/helpers/generate-token.js
module.exports = {
  sync: true,

  inputs: {
    length: { type: 'number', defaultsTo: 32 }
  },

  fn: function ({ length }) {
    const crypto = require('crypto')
    return crypto.randomBytes(length).toString('hex')
  }
}

Advanced Helper Features

sideEffects Property

Mark a helper as cacheable when it has no side effects (pure computation):

module.exports = {
  sideEffects: 'cacheable',
  inputs: {
    prompt: { type: 'string', required: true }
  },
  fn: async function ({ prompt }) {
    // Result can be cached since this has no side effects
    return await sails.helpers.ai.prompt.with({ prompt })
  }
}

Strongly Typed Output

Declare the shape and type of the return value:

exits: {
  success: {
    outputType: {
      salesforceAccountId: 'string',
      salesforceContactId: 'string'
    }
  }
}
// Or for simple types:
exits: {
  success: {
    outputType: 'string',        // Returns a string
  }
}
// Or for any type:
exits: {
  success: {
    outputExample: '*',          // Returns anything
  }
}

Mutating Objects with ref + readOnly: false

Pass objects by reference for in-place mutation:

// api/helpers/redact-user.js
module.exports = {
  sync: true,
  inputs: {
    user: {
      type: 'ref',
      readOnly: false // Allow mutation of the passed object
    }
  },
  fn: function ({ user }) {
    for (let [attrName, attrDef] of Object.entries(User.attributes)) {
      if (attrDef.protect) {
        delete user[attrName]
      }
    }
  }
}

Environment Gating

Skip expensive integrations outside production:

fn: async function ({ emailAddress, firstName }) {
  if (sails.config.environment !== 'production') {
    sails.log.verbose('Skipping Salesforce integration...')
    return { salesforceAccountId: undefined, salesforceContactId: undefined }
  }

  require('assert')(sails.config.custom.salesforceIntegrationUsername)
  require('assert')(sails.config.custom.salesforceIntegrationPasskey)
  // ... actual Salesforce integration
}

Tolerating Specific Error Codes

Handle specific error codes from external services:

// Tolerate a duplicate record error from Salesforce
let newRecord = await sails.helpers.flow
  .build(async () => {
    return await connection.sobject('Contact').create({ Email: email })
  })
  .tolerate({ errorCode: 'DUPLICATES_DETECTED' }, (err) => {
    // Use the first duplicate found instead
    return {
      id: _.get(
        err,
        'duplicateResult.matchResults[0].matchRecords[0].record.Id'
      )
    }
  })

// Tolerate Waterline unique constraint violation
await NewsletterSubscription.create({ emailAddress }).tolerate('E_UNIQUE')

Helpers Calling Other Helpers

Helpers can call other helpers freely:

// api/helpers/billing/create-subscription.js
module.exports = {
  inputs: {
    userId: { type: 'string', required: true },
    plan: { type: 'string', required: true }
  },

  fn: async function ({ userId, plan }) {
    const user = await User.findOne({ id: userId })

    // Create Stripe customer if needed
    if (!user.stripeCustomerId) {
      const customer = await sails.helpers.billing.createCustomer.with({
        email: user.email,
        name: user.fullName
      })
      await User.updateOne({ id: userId }).set({
        stripeCustomerId: customer.id
      })
    }

    // Create the subscription
    const subscription =
      await sails.helpers.billing.createStripeSubscription.with({
        customerId: user.stripeCustomerId,
        priceId: sails.config.custom.stripePrices[plan]
      })

    // Send confirmation email
    await sails.helpers.mail.sendTemplate.with({
      to: user.email,
      subject: 'Subscription Confirmed',
      template: 'subscription-confirmed',
      templateData: { user, plan }
    })

    return subscription
  }
}

Source: SKILL.md on GitHub

1 alert17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill provides comprehensive documentation and coding patterns for the Sails.js framework as used in The Boring JavaScript Stack. It includes detailed guides on application anatomy, security best practices, and production deployment. No malicious patterns or security vulnerabilities were detected.

  • Socket17d

    2 alerts: gptSecurity

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    13/20 files flagged

Signed by skilld at bf19e10. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 8 months ago
Other metadata
metadata
{
  "author": "sailscastshq",
  "version": "2.1.0",
  "tags": "sails, sailsjs, backend, mvc, actions, helpers, routes, policies, hooks, middleware, deployment, boring-stack"
}

README badge

README badge for sailscastshq/boring-stack/sails