All skills

Waterline ORM query language, model definitions, associations, and data access patterns for Sails.js. Use this skill when writing, reviewing, or debugging Waterline queries, model attributes, associations, lifecycle callbacks, validations, or any database interaction in a Sails.js application.

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

This session only. Nothing lands on disk.

ruleslifecycle-callbacks.md

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

Lifecycle Callbacks

Lifecycle callbacks are functions defined on a model that run automatically at certain points during query execution.

Available Callbacks

Callback Runs When
beforeValidate Before validation on create/update
afterValidate After validation passes
beforeCreate Before a new record is inserted
afterCreate After a new record is inserted
beforeUpdate Before a record is modified
afterUpdate After a record is modified
beforeDestroy Before a record is deleted
afterDestroy After a record is deleted
beforeFind Before a find query executes
afterFind After find results are returned
beforeFindOne Before a findOne query executes
afterFindOne After findOne result is returned

Callback Signature

All callbacks receive (valuesToSet, proceed) for create/update, or (criteria, proceed) for find/destroy. You must call proceed() when done:

beforeCreate: async function (valuesToSet, proceed) {
  // Do work here
  return proceed()
}

To abort with an error:

beforeCreate: async function (valuesToSet, proceed) {
  const existing = await Environment.findOne({ project: valuesToSet.project, slug: valuesToSet.slug })
  if (existing) {
    return proceed(new Error('Environment already exists in this project'))
  }
  return proceed()
}

beforeCreate -- Password Hashing + Computed Fields

The most common callback pattern. Hash passwords, generate IDs, compute derived values:

// api/models/User.js
module.exports = {
  attributes: {
    /* ... */
  },

  beforeCreate: async function (valuesToSet, proceed) {
    // Generate initials from full name
    valuesToSet.initials = sails.helpers.getUserInitials(valuesToSet.fullName)

    // Hash password
    if (valuesToSet.password) {
      valuesToSet.password = await sails.helpers.passwords.hashPassword(
        valuesToSet.password
      )
    }

    return proceed()
  }
}

beforeCreate -- Generate Public IDs

// api/models/Invoice.js
module.exports = {
  attributes: {
    /* ... */
  },

  beforeCreate: async function (valuesToSet, proceed) {
    valuesToSet.publicId = sails.helpers.generateId()
    return proceed()
  }
}

beforeCreate -- Slug Generation

// api/models/Project.js
module.exports = {
  attributes: {
    /* ... */
  },

  beforeCreate: async function (values, proceed) {
    if (!values.slug && values.name) {
      values.slug = values.name
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '')
    }
    return proceed()
  }
}

beforeCreate -- Price Computation

// api/models/Course.js
module.exports = {
  attributes: {
    /* ... */
  },

  beforeCreate: async function (valuesToSet, proceed) {
    valuesToSet.price = sails.helpers.convertAmountToCent(valuesToSet.price)
    valuesToSet.discount = sails.helpers.convertAmountToCent(
      valuesToSet.discount
    )
    valuesToSet.amount = valuesToSet.price - valuesToSet.discount
    valuesToSet.slug = sails.helpers.getSlug(valuesToSet.title)
    return proceed()
  }
}

beforeCreate -- Expiration Timestamps

// api/models/Onboarding.js
beforeCreate: async function (valuesToSet, proceed) {
  valuesToSet.publicId = sails.helpers.generateId()
  valuesToSet.expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000  // 30 days
  return proceed()
}

beforeCreate -- Conditional Timestamps

// api/models/Membership.js
beforeCreate: async function (valuesToSet, proceed) {
  if (valuesToSet.role === 'owner' || valuesToSet.status === 'active') {
    valuesToSet.joinedAt = Date.now()
  }
  if (valuesToSet.status === 'invited' && !valuesToSet.invitedAt) {
    valuesToSet.invitedAt = Date.now()
  }
  return proceed()
}

beforeCreate -- Generate Invite Token

// api/models/Team.js
beforeCreate: async function (valuesToSet, proceed) {
  if (!valuesToSet.inviteToken) {
    valuesToSet.inviteToken = await sails.helpers.strings.random('url-friendly')
  }
  return proceed()
}

beforeUpdate -- Re-hash Password on Change

// api/models/User.js
beforeUpdate: async function (valuesToSet, proceed) {
  if (valuesToSet.password) {
    valuesToSet.password = await sails.helpers.passwords.hashPassword(valuesToSet.password)
  }
  return proceed()
}

beforeUpdate -- Recompute Slug

// api/models/Voyage.js
beforeUpdate: async function (valuesToSet, proceed) {
  if (valuesToSet.title) {
    valuesToSet.slug = sails.helpers.getSlug(valuesToSet.title)
  }
  return proceed()
}

customToJSON -- Custom Serialization

Not a lifecycle callback per se, but a model method that controls how records are serialized to JSON. Use it to strip sensitive fields:

// api/models/User.js
module.exports = {
  attributes: {
    /* ... */
  },

  customToJSON: function () {
    return Object.keys(this).reduce((result, key) => {
      if (
        ![
          'password',
          'passwordResetTokenExpiresAt',
          'emailProofToken',
          'emailProofTokenExpiresAt',
          'googleIdToken',
          'googleAccessToken'
        ].includes(key)
      ) {
        result[key] = this[key]
      }
      return result
    }, {})
  }
}

Custom Model Methods

Define reusable methods directly on the model:

// api/models/App.js
module.exports = {
  attributes: {
    /* ... */
  },

  generateContainerName: async function (environmentId) {
    const env = await Environment.findOne({ id: environmentId }).populate(
      'project'
    )
    if (!env) return null
    return `slipway-${env.project.slug}-${env.slug}`
  }
}

// api/models/Deployment.js
module.exports = {
  attributes: {
    /* ... */
  },

  appendBuildLog: async function (deploymentId, log) {
    const deployment = await Deployment.findOne({ id: deploymentId })
    if (!deployment) return
    const currentLogs = deployment.buildLogs || ''
    await Deployment.updateOne({ id: deploymentId }).set({
      buildLogs: currentLogs + log
    })
  },

  getDuration: function (deployment) {
    if (!deployment.startedAt) return null
    const end = deployment.finishedAt || Date.now()
    return Math.round((end - deployment.startedAt) / 1000)
  }
}

// Usage:
const containerName = await App.generateContainerName(envId)
await Deployment.appendBuildLog(deploymentId, 'Building...\n')
const seconds = Deployment.getDuration(deployment)

Accessing Model Attributes Metadata

You can read attribute definitions programmatically:

// In a helper -- strip protected attributes
for (let [attrName, attrDef] of Object.entries(User.attributes)) {
  if (attrDef.protect) {
    delete user[attrName]
  }
}

Source: SKILL.md on GitHub

1 warning17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides comprehensive documentation and best practice rules for writing Waterline ORM queries and model definitions in Sails.js applications. It contains only Markdown files and does not include any executable scripts or malicious instructions.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    10/13 files flagged

Signed by skilld at 383ecf8. 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": "1.0.0",
  "tags": "waterline, sails, orm, database, query, models"
}

README badge

README badge for sailscastshq/boring-stack/waterline