Skip to content

Commit

Permalink
Merge branch 'release/0.0.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
DominusKelvin committed Feb 9, 2024
2 parents 5512a40 + 5138de8 commit 1e07c5e
Show file tree
Hide file tree
Showing 11 changed files with 1,118 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Prettier

on: [push, pull_request]

jobs:
prettier:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18

- name: Run npm ci
run: npm ci

- name: Run Prettier
run: npx prettier --write .
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npx lint-staged
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 The Sailscasts Company

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Sails Stash

With Sails Stash, you can easily implement efficient caching for your Sails applications, enhancing performance and scalability without the need for complex setup.

Sails Stash integrates seamlessly with your Sails project, providing a straightforward way to cache data using Redis. By leveraging Redis as a caching layer, you can optimize the retrieval of frequently accessed data, reducing database load and improving overall application performance.

## Features

- Seamless integration with Sails projects
- Efficient caching using Redis
- Improved performance and scalability
- Simple setup and usage

## Installation

You can install Sails Stash via npm:

```sh
npm i sails-stash
```

## Using Redis as store

To use Redis as a cache store, install the `sails-redis` adapter

```sh
npm i sails-redis
```

### Setup the datastore

```js
// config/datastores.js
...
cache: {
adapter: 'sails-redis'
url: '<REDIS_URL>'
}
```

## Usage

You can now cache values in your Sails actions.

```js
await sails.cache.fetch(
'posts',
async function () {
return await Post.find()
},
6000,
)
```

Check out the [documentation](https://docs.sailscasts.com/stash) for more ways to setup and use Sails Stash.

## Contributing

If you're interested in contributing to Sails Content, please read our [contributing guide](https://github.com/sailscastshq/sails-stash/blob/develop/.github/CONTRIBUTING.md).

## Sponsors

If you'd like to become a sponsor, check out [DominusKelvin](https://github.com/sponsors/DominusKelvin) sponsor page and tiers.
50 changes: 50 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const RedisStore = require('./lib/stores/redis-store')

module.exports = function defineSailsCacheHook(sails) {
return {
defaults: {
stash: {
store: process.env.CACHE_STORE || 'redis',
stores: {
redis: {
store: 'redis',
datastore: 'cache',
},
memcached: {
store: 'memcached',
datastore: 'cache',
},
},
},
},
initialize: async function () {
function getCacheStore(store) {
switch (sails.config.stash.stores[store].store) {
case 'redis':
return new RedisStore(sails)
default:
throw new Error('Invalid cache store provided')
}
}

let cacheStore = getCacheStore(
sails.config.stash.stores[sails.config.stash.store].store,
)

sails.cache = {
get: cacheStore.get.bind(cacheStore),
set: cacheStore.set.bind(cacheStore),
has: cacheStore.has.bind(cacheStore),
delete: cacheStore.delete.bind(cacheStore),
fetch: cacheStore.fetch.bind(cacheStore),
add: cacheStore.add.bind(cacheStore),
pull: cacheStore.pull.bind(cacheStore),
forever: cacheStore.forever.bind(cacheStore),
destroy: cacheStore.destroy.bind(cacheStore),
store: function (store) {
return getCacheStore(store)
},
}
},
}
}
23 changes: 23 additions & 0 deletions lib/stores/cache-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function CacheStore(sails) {
if (new.target === CacheStore) {
throw new Error(
'CacheStore is an abstract class and cannot be instantiated directly',
)
}

this.sails = sails
this.datastore = sails.config.stash.stores[sails.config.stash.store].datastore
this.store = null
}
/**
* Retrieves the cache store instance.
* @returns {Promise<any>} A promise that resolves to the cache store instance.
*/
CacheStore.prototype.getStore = async function () {
if (!this.store) {
this.store = await this.sails.getDatastore(this.datastore)
}
return this.store
}

module.exports = CacheStore
160 changes: 160 additions & 0 deletions lib/stores/redis-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
const util = require('util')
const CacheStore = require('./cache-store')

function RedisStore(sails) {
CacheStore.call(this, sails)
}

RedisStore.prototype = Object.create(CacheStore.prototype)
RedisStore.prototype.constructor = RedisStore
RedisStore.prototype.getStore = CacheStore.prototype.getStore

/**
* Retrieves the value associated with the specified key from the cache store.
* @param {string} key - The key to retrieve the value for.
* @param {any | Function} [defaultValueOrCallback] - Optional. Default value or callback function.
* @returns {Promise<any>} Promise resolving to the value associated with the key or default value.
*/
RedisStore.prototype.get = async function (key, defaultValueOrCallback) {
const store = await this.getStore()
const value = await store.leaseConnection(async function (db) {
const cacheHit = await util.promisify(db.get).bind(db)(key)

if (cacheHit === null) {
if (typeof defaultValueOrCallback === 'function') {
const defaultValue = await defaultValueOrCallback()
return defaultValue
} else {
return defaultValueOrCallback
}
} else {
return JSON.parse(cacheHit)
}
})
return value
}
/**
* Sets the value associated with the specified key in the cache store.
* @param {string} key - The key for the value.
* @param {any} value - The value to store.
* @param {number} [ttlInSeconds] - Optional. Time to live for the key-value pair in seconds.
* @returns {Promise<void>} Promise indicating completion of the set operation.
*/
RedisStore.prototype.set = async function (key, value, ttlInSeconds) {
const store = await this.getStore()
await store.leaseConnection(async function (db) {
if (ttlInSeconds) {
await util.promisify(db.setex).bind(db)(
key,
ttlInSeconds,
JSON.stringify(value),
)
} else {
await util.promisify(db.set).bind(db)(key, JSON.stringify(value))
}
})
}

/**
* Checks if the cache store contains the specified key.
* @param {string} key - The key to check.
* @returns {Promise<boolean>} Promise resolving to true if the key exists, false otherwise.
*/
RedisStore.prototype.has = async function (key) {
const cacheHit = await this.get(key)
if (cacheHit) return true
return false
}

/**
* Deletes the key-value pair associated with the specified key from the cache store.
* @param {string | string[]} key - The key to delete.
* @returns {Promise<void>} Promise indicating completion of the delete operation.
*/
RedisStore.prototype.delete = async function (key) {
const store = await this.getStore()
const deletedCount = await store.leaseConnection(async function (db) {
return await util.promisify(db.del).bind(db)(key)
})
return deletedCount
}
/**
* Retrieves the value associated with the specified key from the cache store.
* If the key exists in the cache, returns the corresponding value.
* If the key does not exist in the cache, the provided default value or the result of the callback function will be stored in the cache and returned.
* @param {string} key - The key to retrieve the value for.
* @param {any | Function} [defaultValueOrCallback] - Optional. Default value or callback function to compute the default value.
* @param {number} [ttlInSeconds] - Optional. Time to live for the key-value pair in seconds.
* @returns {Promise<any>} A promise that resolves to the value associated with the key, or the default value if the key does not exist in the cache.
*/
RedisStore.prototype.fetch = async function (
key,
defaultValueOrCallback,
ttlInSeconds,
) {
const cacheHit = await this.get(key)
if (!cacheHit) {
let defaultValue
if (typeof defaultValueOrCallback === 'function') {
defaultValue = await defaultValueOrCallback()
} else {
defaultValue = defaultValueOrCallback
}
await this.set(key, defaultValue, ttlInSeconds)
return defaultValue
} else {
return cacheHit
}
}
/**
* Adds a key-value pair to the cache store if the key does not already exist.
* If the key already exists, the value is not updated.
* @param {string} key - The key for the value.
* @param {any} value - The value to store.
* @param {number} [ttlInSeconds] - Optional. Time to live for the key-value pair in seconds.
* @returns {Promise<boolean>} A promise that resolves to true if the key was added successfully, false if the key already exists.
*/
RedisStore.prototype.add = async function (key, value, ttlInSeconds) {
const cacheHit = await this.get(key)
if (!cacheHit) {
await this.set(key, value, ttlInSeconds)
return true
} else {
return false
}
}
/**
* Retrieves and removes the value associated with the specified key from the cache store.
* If the key exists in the cache, returns the corresponding value and removes the key-value pair.
* If the key does not exist, returns null.
* @param {string} key - The key to retrieve and remove the value for.
* @returns {Promise<any>} A promise that resolves to the value associated with the key, or null if the key does not exist in the cache.
*/
RedisStore.prototype.pull = async function (key) {
const value = await this.get(key)
await this.delete(key)
return value
}
/**
* Stores the specified key-value pair in the cache store indefinitely.
* The key-value pair will not have an expiry time and will remain in the cache until explicitly removed.
* @param {string} key - The key for the value.
* @param {any} value - The value to store.
* @returns {Promise<void>} A promise indicating completion of the operation.
*/
RedisStore.prototype.forever = async function (key, value) {
await this.set(key, value)
}

/**
* Destroys the cache store, removing all stored key-value pairs.
* @returns {Promise<void>} A promise indicating completion of the operation.
*/
RedisStore.prototype.destroy = async function () {
const store = await this.getStore()
await store.leaseConnection(async function (db) {
return await util.promisify(db.flushall).bind(db)('ASYNC')
})
}

module.exports = RedisStore
Loading

0 comments on commit 1e07c5e

Please sign in to comment.