Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pipeline {
axes {
axis {
name 'NODE_VERSION'
values '12', '14', '16'
values '20', '22', '24'
}
}

Expand Down
23 changes: 23 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict'

const {defineConfig} = require('eslint/config')
const logdna = require('eslint-config-logdna')

module.exports = defineConfig([
{
'extends': [logdna]
, 'ignores': ['skills/**']
, 'languageOptions': {
ecmaVersion: 2022
, sourceType: 'script'
, globals: {
fetch: 'readonly'
}
}
, 'rules': {
'sensible/check-require': [2, 'always', {
root: __dirname
}]
}
}
])
2 changes: 1 addition & 1 deletion lib/actions/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const assert = require('assert')
const {typeOf} = require('@logdna/stdlib')

const NAME = 'SetupChain.map'
const ARRAY_ERR = `${NAME} first param should be an array. Supports dynamic lookups`
const ARRAY_ERR = `${NAME} first param should be an array. Supports dynamic lookups`
const FN_ERR = `${NAME} second param should be a function`

module.exports = async function map(collection, fn) {
Expand Down
20 changes: 4 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,11 @@
"bugs": {
"url": "https://github.com/logdna/setup-chain-node/issues"
},
"eslintConfig": {
"root": true,
"ignorePatterns": [
"node_modules/",
"coverage/"
],
"extends": [
"logdna"
],
"parserOptions": {
"ecmaVersion": 2022
},
"plugins": []
},
"homepage": "https://github.com/logdna/setup-chain-node",
"devDependencies": {
"casual": "^1.6.2",
"eslint": "^8.35.0",
"eslint-config-logdna": "^6.1.0",
"eslint": "^10.4.1",
"eslint-config-logdna": "^8.0.1",
"luxon": "^3.2.1",
"moment": "^2.29.1",
"semantic-release": "^17.4.4",
Expand Down Expand Up @@ -98,6 +84,8 @@
"--exclude=coverage/",
"--exclude=scripts/",
"--exclude=examples/",
"--exclude=skills/",
"--exclude=eslint.config.js/",
"--all"
]
},
Expand Down
114 changes: 114 additions & 0 deletions skills/setup-chain/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
name: setup-chain
description: create new actions, functions or workflows for logdna/setup-chain
license: MIT
metadata:
author: Mezmo Inc,.
---

# Setup-chain Agent

Adds new actions and functions to logdna setup-chain

## Prerequisites

1. Confirm `node` executable is installed on the host
2. Make sure there is a package.json in the current working directory
3. package.json should have `@logdna/setup-chain` in dependencies or devDependencies

If any of the above are not met, explain what is missing. Do not proceed with custom action creation.

## Creating a Custom Action

Follow these steps to create and wire up a custom action:

1. **Define the action function** in an actions object
- **Requirement**: Actions MUST be `async` functions or return a `Promise`
```javascript
const actions = {
myAction: async (opts) => {
return opts.value || 'default'
}
}
```

2. **Create or extend SetupChain class**
```javascript
class MyChain extends SetupChain {
constructor(state) {
super(state, actions)
}
}
```

3. **Use the action** in your chain
```javascript
await new MyChain().myAction({value: 'test'}, 'result').execute()
// state: {result: 'test'}
```

4. **Validate** your implementation works as expected

## Custom Signatures (Advanced)

Only manually push `this.tasks` when action signature deviates from `(opts, label)`:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (opts, label) is the true default signature, perhaps the signature in number 1 should be (opts, _label) or (opts, label)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for number 1 - the label isn't passed to the actual action function, Thats only for defining the custom hook on the chain instance

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async myAction(arg1, arg2) {

}

vs

class MyChain extends Chain {
  myAction(arg1, ar2, label) {
    this.tasks.push(['myAction', label, arg1, arg2])
  }
}


```javascript
class MyChain extends SetupChain {
constructor(state) {
super(state, yourActions)
}

customAction(arg1, arg2, label) {
this.tasks.push(['customAction', label, arg1, arg2])
return this
}
}
```

See `create-action.md` for detailed patterns and examples.

## Usage & Workflow

SetupChain implements chain-of-responsibility pattern. Actions are available as top level functions on a chain instance.
Chain actions, execute, and results are stored in state.

```javascript
const chain = new MyChain()

const state = await chain
.set('user', 'alice')
.map('#user', n => n * 2, 'doubled')
.execute()

// state: {user: 'alice', doubled: [2, 4, 6]}
```

Best practices:
- Use labels explicitly to avoid state key collisions
- Chain actions for readability: `.action1().action2().execute()`
- Reuse state across instances for persistence: `new SetupChain(state2)`
- Group Long action chains by use case with comments

```javascript
const chain = new MyChain()

// test scenerio 1
chain
.account({}, 'account_one')
.user({account: '#account_one'}, 'user_one')

// test scenerio 1
chain
.account({}, 'account_two')
.user({account: '#account_two'}, 'user_two')

const state = await chain.execute()
```

## Additional Resources

- [Detailed action patterns](./references/create-action.md)
- [Adding helper functions](./references/create-function.md)
- [Quick reference](./references/QUICKREF.md)
- [Code examples](./references/examples/)
88 changes: 88 additions & 0 deletions skills/setup-chain/references/create-action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Creating Custom Actions


## Steps to Create and Wire a Custom Action

1. **Define your action function**
- **Requirement**: Actions MUST be `async` functions or return a `Promise`
- Accept single `opts` object
- Return the result directly
```javascript
const actions = {
hello: async (opts) => opts.name || 'World'
}
```

2. **Create SetupChain class**
- Pass actions object to constructor
```javascript
class MyChain extends SetupChain {
constructor(state) {
super(state, actions)
}
}
```

3. **Call action with label**
```javascript
await new MyChain().hello({name: 'Alice'}, 'result').execute()
```
- Label is key in state for this action's result

4. **Test your implementation**

## Custom Signature (Use Only When Needed)

1. Define action function manually
2. Override with custom method signature
3. Manually push task with correct format
4. Return `this` for chaining

```javascript
const actions = {
printNames: async (opts) => [opts.first, opts.last].join(', ')
}

class MyChain extends SetupChain {
constructor(state) {
super(state, actions)
}

printNames(first, last, label) {
this.tasks.push(['printNames', label, first, last])
return this
}
}
```

## Pattern: State-Dependent with Defaults

1. Define defaults with template placeholders
2. Use `this.lookup()` to merge defaults and opts
3. Use `#this` to reference action's result context
Comment thread
esatterwhite marked this conversation as resolved.
4. Use `assert` module to validate values after lookup resolution

```javascript
const actions = {
person: async (opts) => {
const defaults = {
first: 'bobby'
, last: 'fischer'
, full: '!template:"{{#this.first}} {{#this.last}}"'
}
const result = this.lookup({...defaults, ...opts})

// Validate required fields exist
assert.ok(result.first, 'First name is required')
assert.ok(result.last, 'Last name is required')

// Validate types
assert.equal(typeof result.first, 'string', 'First name must be a string')
assert.equal(typeof result.last, 'string', 'Last name must be a string')

return result
}
}
```

5. **Validate** after executing with various inputs
71 changes: 71 additions & 0 deletions skills/setup-chain/references/create-function.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Adding Helper Functions

## Overview

Helper functions are synchronous methods defined on a chain class that extend the capabilities of the `lookup` system. They are called when the `lookup` function encounters a value starting with `!`.

## Creating a Helper Function

1. **Define the method on your chain class**
- **Prefix**: Method name MUST start with `$` (e.g., `$slugify`)
- **Naming**: Keep names short, single-word, and lowercase (`$<verb>`)
- **Synchronous**: Functions MUST be synchronous; they cannot be `async` by design. Use [Actions](./create-action.md) for async operations.
- **Arguments**: They can accept any number of arguments.

```javascript
class MyChain extends SetupChain {
$slugify(text) {
return text.toLowerCase().replace(/\s+/g, '-');
}
}
```

2. **Use the function via `lookup`**
- Call the function by using the `!` prefix in lookup strings or objects.
- The `$` prefix is removed when calling via lookup (e.g., `$slugify` is called as `!slugify`).

## Usage Patterns

### Basic Call
Pass arguments directly after the function name, separated by commas.
```javascript
chain.lookup('!slugify:"Hello World"')
// returns: "hello-world"
```

### Nested & Complex Lookups
Functions can accept other lookup values (state properties or other functions) as arguments.

- **Using State Properties**:
```javascript
await chain.set('title', 'My Page').execute();
chain.lookup('!slugify:#title')
// returns: "my-page"
```

- **Array of Lookups**:
```javascript
await chain.set(('one', 'ONE').set('two', 'TWO').execute()
chain.lookup(['!lower:#one', '!lower:#two'])
Comment thread
esatterwhite marked this conversation as resolved.
// returns ['one', 'two']
```

- **Deeply Nested Functions**:
```javascript
var chain = new MyChain({bar: 'Hello World})
chain.lookup({one: '!lower(!slugify(#bar))' })
// returns {one: 'hello-world'}
```

- **Inside Action Options**:
You can pass function calls as values in action options; they will be resolved by the action's `this.lookup()` call.
```javascript
await chain.set({title: 'Hello World'}).myAction({ slug: '!slugify(#title)' }).execute()
// returns {slug: 'hello-world'}
```

## Summary Checklist
- [ ] Method starts with `$`
- [ ] Method is synchronous
- [ ] Name is short, lowercase, single-word
- [ ] Called using `!` in `lookup`
5 changes: 5 additions & 0 deletions skills/setup-chain/references/examples/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Basic set() usage
const SetupChain = require('@logdna/setup-chain')
const chain = new SetupChain()
await chain.set('hello', 'world').set('goodbye', 'world').execute()
// state: {hello: 'world', goodbye: 'world'}
26 changes: 26 additions & 0 deletions skills/setup-chain/references/examples/custom-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Custom actions with defaults
const SetupChain = require('@logdna/setup-chain')

const defaults = {
first: 'bobby'
, last: 'fischer'
, full: '!template:"{{#this.first}} {{#this.last}}"'
}

const actions = {
person: async function person(opts) {
return this.lookup({...defaults, ...opts})
}
}

const chain = new SetupChain(null, actions)
const state = await chain
.person({}, 'bobby') // uses defaults
.person({first: 'fred'}, 'fred')
.person({last: 'williams'}, 'williams')
.execute()
// state: {
// bobby: {first: 'bobby', last: 'fischer', full: 'bobby fischer'},
// fred: {first: 'fred', last: 'fischer', full: 'fred fischer'},
// williams: {first: 'bobby', last: 'williams', full: 'bobby williams'}
// }
Loading