Skip to content

Commit 0ccd481

Browse files
feat(cache): add sequelize support
add sequelize support GH-2552
1 parent cedd708 commit 0ccd481

8 files changed

Lines changed: 17417 additions & 17099 deletions

File tree

package-lock.json

Lines changed: 16919 additions & 17092 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
"packages/cli/",
7676
"packages/cache/",
7777
"packages/feature-toggle/",
78-
"packages/observability/",
7978
"packages/file-utils/",
8079
"packages/custom-sf-changelog/",
8180
"services/*",

packages/cache/README.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,49 @@ export class TestWithMixinRepository extends CacheMixin(
5151
}
5252
```
5353

54+
### Using with Sequelize Repositories
55+
56+
For repositories based on `SequelizeCrudRepository` from `@loopback/sequelize`, use the `SequelizeCacheMixin`:
57+
58+
```ts
59+
import {SequelizeCacheMixin} from '@sourceloop/cache/sequelize';
60+
import {SequelizeCrudRepository, SequelizeDataSource} from '@loopback/sequelize';
61+
62+
export class UserRepository extends SequelizeCacheMixin(
63+
SequelizeCrudRepository<User, number, {}>,
64+
{
65+
ttl: 1800, // Time to live in seconds (optional)
66+
invalidationTags: ['users'], // Tags for cache invalidation on writes
67+
cachedItemTags: ['user-item'], // Tags for individual cache entries
68+
disableCachedFetch: false, // Disable caching if needed (optional)
69+
},
70+
) {
71+
cacheIdentifier = 'userRepo'; // Required: unique cache namespace
72+
73+
constructor(
74+
@inject('datasources.postgres') dataSource: SequelizeDataSource,
75+
) {
76+
super(User, dataSource);
77+
}
78+
}
79+
```
80+
81+
**Note:** Import from `@sourceloop/cache/sequelize` for Sequelize-specific caching.
82+
83+
**Difference from CacheMixin:**
84+
- `CacheMixin` works with `DefaultCrudRepository` (Juggler-based)
85+
- `SequelizeCacheMixin` works with `SequelizeCrudRepository` (Sequelize-based)
86+
- Use the appropriate mixin based on your repository type
87+
88+
**Configuration Options:**
89+
90+
| Option | Type | Default | Description |
91+
|--------|------|---------|-------------|
92+
| `ttl` | `number` | `86400` (1 day) | Cache expiration time in seconds |
93+
| `invalidationTags` | `string[]` | `[]` | Tags to invalidate on create/update/delete |
94+
| `cachedItemTags` | `string[]` | `[]` | Tags to apply to cached items |
95+
| `disableCachedFetch` | `boolean` | `false` | Bypass cache for all reads |
96+
5497
### In a controller or service
5598

5699
To add caching to a service or controller, just implement the `ICachedService` interface, adding a binding for the `ICacheService` and the applying the relevant decorators to the methods you want cached -
@@ -98,3 +141,189 @@ export class TestController implements ICachedService {
98141
}
99142
/// ...
100143
```
144+
145+
## Advanced Usage
146+
147+
### Cache Strategies
148+
149+
The component supports two cache strategies:
150+
151+
#### 1. InMemoryStoreStrategy (Default)
152+
153+
Uses an in-memory Map. Suitable for development and single-instance deployments.
154+
155+
```ts
156+
import {InMemoryStoreStrategy} from '@sourceloop/cache';
157+
158+
this.bind(CacheComponentBindings.CacheConfig).to({
159+
ttl: 3600,
160+
strategy: InMemoryStoreStrategy,
161+
});
162+
```
163+
164+
#### 2. RedisStoreStrategy
165+
166+
Uses Redis for distributed caching. Recommended for production.
167+
168+
```ts
169+
import {RedisStoreStrategy} from '@sourceloop/cache';
170+
import {juggler} from '@loopback/repository';
171+
172+
const redisDs = new juggler.DataSource({
173+
name: 'redisCacheStore',
174+
connector: 'kv-redis',
175+
host: process.env.REDIS_HOST,
176+
port: Number(process.env.REDIS_PORT),
177+
password: process.env.REDIS_PASSWORD,
178+
});
179+
180+
this.dataSource(redisDs);
181+
182+
this.bind(CacheComponentBindings.CacheConfig).to({
183+
ttl: 3600,
184+
strategy: RedisStoreStrategy,
185+
datasourceName: 'redisCacheStore',
186+
});
187+
```
188+
189+
### Cache Invalidation
190+
191+
Cache can be invalidated in two ways:
192+
193+
#### 1. Automatic Invalidation (Mixins)
194+
195+
Write operations (`create`, `update`, `delete`) automatically invalidate cache entries tagged with `invalidationTags`.
196+
197+
#### 2. Manual Invalidation (Decorators)
198+
199+
Use `@cacheInvalidator()` decorator on methods that modify data:
200+
201+
```ts
202+
@cacheInvalidator(['users', 'notifications'])
203+
async updateUserStatus(userId: number, status: string) {
204+
// This will invalidate all cache entries tagged with 'users' or 'notifications'
205+
return this.userRepository.updateById(userId, {status});
206+
}
207+
```
208+
209+
### Cache Customization
210+
211+
#### Per-Method Options
212+
213+
Override cache behavior for specific method calls:
214+
215+
```ts
216+
// Force fresh data from database
217+
const users = await this.userRepository.find(undefined, {
218+
forceUpdate: true,
219+
});
220+
221+
// Add custom tags
222+
const user = await this.userRepository.findById(1, undefined, {
223+
tags: ['admin-user', 'high-priority'],
224+
});
225+
```
226+
227+
#### Tenant-Aware Caching
228+
229+
Cache keys automatically include tenant ID when `AUTH_USER_KEY` is bound:
230+
231+
```ts
232+
import {AUTH_USER_KEY} from '@sourceloop/cache';
233+
234+
this.bind(AUTH_USER_KEY).to({
235+
tenantId: 'tenant-123',
236+
username: 'admin',
237+
});
238+
```
239+
240+
## API Reference
241+
242+
### Decorators
243+
244+
#### `@cachedItem()`
245+
Cache the return value of a method.
246+
247+
```ts
248+
@cachedItem()
249+
async getUser(id: number): Promise<User> {
250+
return this.userRepository.findById(id);
251+
}
252+
```
253+
254+
#### `@cacheInvalidator(tags: string[])`
255+
Invalidate cache entries matching the given tags.
256+
257+
```ts
258+
@cacheInvalidator(['users'])
259+
async updateUser(id: number, data: Partial<User>): Promise<User> {
260+
return this.userRepository.updateById(id, data);
261+
}
262+
```
263+
264+
### Interfaces
265+
266+
#### `ICacheMixinOptions`
267+
268+
Configuration options for cache mixins.
269+
270+
```ts
271+
interface ICacheMixinOptions {
272+
ttl?: number; // Time to live in seconds
273+
invalidationTags?: string[]; // Tags for write operations
274+
cachedItemTags?: string[]; // Tags for read operations
275+
disableCachedFetch?: boolean; // Disable caching
276+
}
277+
```
278+
279+
#### `ICachedMethodOptions`
280+
281+
Options for individual method calls.
282+
283+
```ts
284+
interface ICachedMethodOptions {
285+
forceUpdate?: boolean; // Force fresh data from source
286+
tags?: string[]; // Additional cache tags
287+
}
288+
```
289+
290+
## Migration from CacheMixin to SequelizeCacheMixin
291+
292+
If you're migrating from Juggler to Sequelize repositories:
293+
294+
1. **Update import:**
295+
```ts
296+
// Before
297+
import {CacheMixin} from '@sourceloop/cache';
298+
299+
// After
300+
import {SequelizeCacheMixin} from '@sourceloop/cache/sequelize';
301+
```
302+
303+
2. **Update repository base:**
304+
```ts
305+
// Before
306+
export class UserRepo extends CacheMixin(
307+
DefaultCrudRepository<User, number, {}>,
308+
options,
309+
) { ... }
310+
311+
// After
312+
export class UserRepo extends SequelizeCacheMixin(
313+
SequelizeCrudRepository<User, number, {}>,
314+
options,
315+
) { ... }
316+
```
317+
318+
3. **Update constructor:**
319+
```ts
320+
// Before
321+
constructor(@inject('datasources.memorydb') dataSource: juggler.DataSource) { ... }
322+
323+
// After
324+
constructor(@inject('datasources.postgres') dataSource: SequelizeDataSource) { ... }
325+
```
326+
327+
## License
328+
329+
MIT

packages/cache/package.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@
1010
],
1111
"main": "dist/index.js",
1212
"types": "dist/index.d.ts",
13+
"exports": {
14+
".": "./dist/index.js",
15+
"./sequelize": {
16+
"types": "./dist/mixins/sequelize/index.d.ts",
17+
"default": "./dist/mixins/sequelize/index.js"
18+
}
19+
},
20+
"typesVersions": {
21+
"*": {
22+
"sequelize": [
23+
"./dist/mixins/sequelize/index.d.ts"
24+
]
25+
}
26+
},
1327
"engines": {
1428
"node": ">=20"
1529
},
@@ -45,7 +59,13 @@
4559
"!*/__tests__"
4660
],
4761
"peerDependencies": {
48-
"@loopback/core": "^7.0.3"
62+
"@loopback/core": "^7.0.3",
63+
"@loopback/sequelize": "^0.8.0"
64+
},
65+
"peerDependenciesMeta": {
66+
"@loopback/sequelize": {
67+
"optional": true
68+
}
4969
},
5070
"dependencies": {
5171
"@loopback/core": "^7.0.3",

packages/cache/src/mixins/cache.mixin.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,8 @@ function addTagsToOptions(
211211
if (!options) {
212212
options = {};
213213
}
214-
if (!options.tags && tags?.length) {
215-
options.tags = [];
216-
}
217214
if (tags?.length) {
218-
options.tags = options.tags?.concat(tags);
215+
options.tags = (options.tags ?? []).concat(tags);
219216
}
220217
return options;
221218
}

0 commit comments

Comments
 (0)