Skip to content

Commit 1e07c5e

Browse files
committed
Merge branch 'release/0.0.1'
2 parents 5512a40 + 5138de8 commit 1e07c5e

File tree

11 files changed

+1118
-0
lines changed

11 files changed

+1118
-0
lines changed

.github/workflows/prettier.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Prettier
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
prettier:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- name: Checkout code
11+
uses: actions/checkout@v3
12+
13+
- name: Setup Node.js
14+
uses: actions/setup-node@v3
15+
with:
16+
node-version: 18
17+
18+
- name: Run npm ci
19+
run: npm ci
20+
21+
- name: Run Prettier
22+
run: npx prettier --write .

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npx lint-staged

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 The Sailscasts Company
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Sails Stash
2+
3+
With Sails Stash, you can easily implement efficient caching for your Sails applications, enhancing performance and scalability without the need for complex setup.
4+
5+
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.
6+
7+
## Features
8+
9+
- Seamless integration with Sails projects
10+
- Efficient caching using Redis
11+
- Improved performance and scalability
12+
- Simple setup and usage
13+
14+
## Installation
15+
16+
You can install Sails Stash via npm:
17+
18+
```sh
19+
npm i sails-stash
20+
```
21+
22+
## Using Redis as store
23+
24+
To use Redis as a cache store, install the `sails-redis` adapter
25+
26+
```sh
27+
npm i sails-redis
28+
```
29+
30+
### Setup the datastore
31+
32+
```js
33+
// config/datastores.js
34+
...
35+
cache: {
36+
adapter: 'sails-redis'
37+
url: '<REDIS_URL>'
38+
}
39+
```
40+
41+
## Usage
42+
43+
You can now cache values in your Sails actions.
44+
45+
```js
46+
await sails.cache.fetch(
47+
'posts',
48+
async function () {
49+
return await Post.find()
50+
},
51+
6000,
52+
)
53+
```
54+
55+
Check out the [documentation](https://docs.sailscasts.com/stash) for more ways to setup and use Sails Stash.
56+
57+
## Contributing
58+
59+
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).
60+
61+
## Sponsors
62+
63+
If you'd like to become a sponsor, check out [DominusKelvin](https://github.com/sponsors/DominusKelvin) sponsor page and tiers.

index.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const RedisStore = require('./lib/stores/redis-store')
2+
3+
module.exports = function defineSailsCacheHook(sails) {
4+
return {
5+
defaults: {
6+
stash: {
7+
store: process.env.CACHE_STORE || 'redis',
8+
stores: {
9+
redis: {
10+
store: 'redis',
11+
datastore: 'cache',
12+
},
13+
memcached: {
14+
store: 'memcached',
15+
datastore: 'cache',
16+
},
17+
},
18+
},
19+
},
20+
initialize: async function () {
21+
function getCacheStore(store) {
22+
switch (sails.config.stash.stores[store].store) {
23+
case 'redis':
24+
return new RedisStore(sails)
25+
default:
26+
throw new Error('Invalid cache store provided')
27+
}
28+
}
29+
30+
let cacheStore = getCacheStore(
31+
sails.config.stash.stores[sails.config.stash.store].store,
32+
)
33+
34+
sails.cache = {
35+
get: cacheStore.get.bind(cacheStore),
36+
set: cacheStore.set.bind(cacheStore),
37+
has: cacheStore.has.bind(cacheStore),
38+
delete: cacheStore.delete.bind(cacheStore),
39+
fetch: cacheStore.fetch.bind(cacheStore),
40+
add: cacheStore.add.bind(cacheStore),
41+
pull: cacheStore.pull.bind(cacheStore),
42+
forever: cacheStore.forever.bind(cacheStore),
43+
destroy: cacheStore.destroy.bind(cacheStore),
44+
store: function (store) {
45+
return getCacheStore(store)
46+
},
47+
}
48+
},
49+
}
50+
}

lib/stores/cache-store.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
function CacheStore(sails) {
2+
if (new.target === CacheStore) {
3+
throw new Error(
4+
'CacheStore is an abstract class and cannot be instantiated directly',
5+
)
6+
}
7+
8+
this.sails = sails
9+
this.datastore = sails.config.stash.stores[sails.config.stash.store].datastore
10+
this.store = null
11+
}
12+
/**
13+
* Retrieves the cache store instance.
14+
* @returns {Promise<any>} A promise that resolves to the cache store instance.
15+
*/
16+
CacheStore.prototype.getStore = async function () {
17+
if (!this.store) {
18+
this.store = await this.sails.getDatastore(this.datastore)
19+
}
20+
return this.store
21+
}
22+
23+
module.exports = CacheStore

lib/stores/redis-store.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
const util = require('util')
2+
const CacheStore = require('./cache-store')
3+
4+
function RedisStore(sails) {
5+
CacheStore.call(this, sails)
6+
}
7+
8+
RedisStore.prototype = Object.create(CacheStore.prototype)
9+
RedisStore.prototype.constructor = RedisStore
10+
RedisStore.prototype.getStore = CacheStore.prototype.getStore
11+
12+
/**
13+
* Retrieves the value associated with the specified key from the cache store.
14+
* @param {string} key - The key to retrieve the value for.
15+
* @param {any | Function} [defaultValueOrCallback] - Optional. Default value or callback function.
16+
* @returns {Promise<any>} Promise resolving to the value associated with the key or default value.
17+
*/
18+
RedisStore.prototype.get = async function (key, defaultValueOrCallback) {
19+
const store = await this.getStore()
20+
const value = await store.leaseConnection(async function (db) {
21+
const cacheHit = await util.promisify(db.get).bind(db)(key)
22+
23+
if (cacheHit === null) {
24+
if (typeof defaultValueOrCallback === 'function') {
25+
const defaultValue = await defaultValueOrCallback()
26+
return defaultValue
27+
} else {
28+
return defaultValueOrCallback
29+
}
30+
} else {
31+
return JSON.parse(cacheHit)
32+
}
33+
})
34+
return value
35+
}
36+
/**
37+
* Sets the value associated with the specified key in the cache store.
38+
* @param {string} key - The key for the value.
39+
* @param {any} value - The value to store.
40+
* @param {number} [ttlInSeconds] - Optional. Time to live for the key-value pair in seconds.
41+
* @returns {Promise<void>} Promise indicating completion of the set operation.
42+
*/
43+
RedisStore.prototype.set = async function (key, value, ttlInSeconds) {
44+
const store = await this.getStore()
45+
await store.leaseConnection(async function (db) {
46+
if (ttlInSeconds) {
47+
await util.promisify(db.setex).bind(db)(
48+
key,
49+
ttlInSeconds,
50+
JSON.stringify(value),
51+
)
52+
} else {
53+
await util.promisify(db.set).bind(db)(key, JSON.stringify(value))
54+
}
55+
})
56+
}
57+
58+
/**
59+
* Checks if the cache store contains the specified key.
60+
* @param {string} key - The key to check.
61+
* @returns {Promise<boolean>} Promise resolving to true if the key exists, false otherwise.
62+
*/
63+
RedisStore.prototype.has = async function (key) {
64+
const cacheHit = await this.get(key)
65+
if (cacheHit) return true
66+
return false
67+
}
68+
69+
/**
70+
* Deletes the key-value pair associated with the specified key from the cache store.
71+
* @param {string | string[]} key - The key to delete.
72+
* @returns {Promise<void>} Promise indicating completion of the delete operation.
73+
*/
74+
RedisStore.prototype.delete = async function (key) {
75+
const store = await this.getStore()
76+
const deletedCount = await store.leaseConnection(async function (db) {
77+
return await util.promisify(db.del).bind(db)(key)
78+
})
79+
return deletedCount
80+
}
81+
/**
82+
* Retrieves the value associated with the specified key from the cache store.
83+
* If the key exists in the cache, returns the corresponding value.
84+
* 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.
85+
* @param {string} key - The key to retrieve the value for.
86+
* @param {any | Function} [defaultValueOrCallback] - Optional. Default value or callback function to compute the default value.
87+
* @param {number} [ttlInSeconds] - Optional. Time to live for the key-value pair in seconds.
88+
* @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.
89+
*/
90+
RedisStore.prototype.fetch = async function (
91+
key,
92+
defaultValueOrCallback,
93+
ttlInSeconds,
94+
) {
95+
const cacheHit = await this.get(key)
96+
if (!cacheHit) {
97+
let defaultValue
98+
if (typeof defaultValueOrCallback === 'function') {
99+
defaultValue = await defaultValueOrCallback()
100+
} else {
101+
defaultValue = defaultValueOrCallback
102+
}
103+
await this.set(key, defaultValue, ttlInSeconds)
104+
return defaultValue
105+
} else {
106+
return cacheHit
107+
}
108+
}
109+
/**
110+
* Adds a key-value pair to the cache store if the key does not already exist.
111+
* If the key already exists, the value is not updated.
112+
* @param {string} key - The key for the value.
113+
* @param {any} value - The value to store.
114+
* @param {number} [ttlInSeconds] - Optional. Time to live for the key-value pair in seconds.
115+
* @returns {Promise<boolean>} A promise that resolves to true if the key was added successfully, false if the key already exists.
116+
*/
117+
RedisStore.prototype.add = async function (key, value, ttlInSeconds) {
118+
const cacheHit = await this.get(key)
119+
if (!cacheHit) {
120+
await this.set(key, value, ttlInSeconds)
121+
return true
122+
} else {
123+
return false
124+
}
125+
}
126+
/**
127+
* Retrieves and removes the value associated with the specified key from the cache store.
128+
* If the key exists in the cache, returns the corresponding value and removes the key-value pair.
129+
* If the key does not exist, returns null.
130+
* @param {string} key - The key to retrieve and remove the value for.
131+
* @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.
132+
*/
133+
RedisStore.prototype.pull = async function (key) {
134+
const value = await this.get(key)
135+
await this.delete(key)
136+
return value
137+
}
138+
/**
139+
* Stores the specified key-value pair in the cache store indefinitely.
140+
* The key-value pair will not have an expiry time and will remain in the cache until explicitly removed.
141+
* @param {string} key - The key for the value.
142+
* @param {any} value - The value to store.
143+
* @returns {Promise<void>} A promise indicating completion of the operation.
144+
*/
145+
RedisStore.prototype.forever = async function (key, value) {
146+
await this.set(key, value)
147+
}
148+
149+
/**
150+
* Destroys the cache store, removing all stored key-value pairs.
151+
* @returns {Promise<void>} A promise indicating completion of the operation.
152+
*/
153+
RedisStore.prototype.destroy = async function () {
154+
const store = await this.getStore()
155+
await store.leaseConnection(async function (db) {
156+
return await util.promisify(db.flushall).bind(db)('ASYNC')
157+
})
158+
}
159+
160+
module.exports = RedisStore

0 commit comments

Comments
 (0)