Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds caching for services #6082

Merged
merged 104 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from 98 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
4ad89d5
cache() and cacheLatest() working
cannikin Jul 27, 2022
3eca534
Move clients to standalone files
cannikin Jul 28, 2022
6645579
Move to closure style to properly export required functions
cannikin Jul 28, 2022
e5d5e71
Comments
cannikin Jul 28, 2022
86c7336
Adds RedisClient
cannikin Jul 28, 2022
88d7f2e
Simplify logic to remove separate init() function
cannikin Jul 28, 2022
b2544b6
Refactor for more generic client usage, no more init()
cannikin Jul 28, 2022
74dbf6f
Adds redis package
cannikin Jul 28, 2022
89f1d9a
Moves cache clients to devDependencies
cannikin Jul 28, 2022
54a0f2e
Merge branch 'main' into rc-service-caching
cannikin Jul 29, 2022
38fba8c
Simplify memcached options on init
cannikin Jul 29, 2022
2b1cc0a
Use logger for messages
cannikin Jul 30, 2022
ce39cff
Server connection string must be included
cannikin Jul 30, 2022
e6bafa5
Merge branch 'main' into rc-service-caching
cannikin Aug 24, 2022
d3aead0
Adds docs on service caching
cannikin Aug 24, 2022
49b6460
Add timeout for cache calls
cannikin Aug 24, 2022
4b1c871
Adds setup command for cache
cannikin Aug 24, 2022
b181182
Updates templates with new timeout option
cannikin Aug 25, 2022
34adb9b
Updates docs for new createCache() client
cannikin Aug 25, 2022
5d4e947
Comment
cannikin Aug 25, 2022
cdbb4a6
Move errors to separate file
cannikin Aug 25, 2022
2240e32
Allow renaming of id/updatedAt fields, catch error if model has no id…
cannikin Aug 25, 2022
111c844
Allow adding a global prefix to cache key
cannikin Aug 25, 2022
9a455f1
Adds docs for global prefix, options, array key syntax
cannikin Aug 26, 2022
5b1d6af
Move formatting of cache key to a standalone function, allow cache ke…
cannikin Aug 26, 2022
45a2809
cacheLatest -> cacheFindMany, exports some additional functions, upda…
cannikin Aug 26, 2022
4179f17
Start of tests
cannikin Aug 26, 2022
3b5f34c
Adds InMemoryClient for cache testing
cannikin Aug 29, 2022
bdee22a
Create base cache client class to extend from, rename clients for con…
cannikin Aug 29, 2022
56d3614
Adds cache tests
cannikin Aug 29, 2022
33979ce
Doc updates
cannikin Aug 30, 2022
c21a3d2
Merge branch 'main' into rc-service-caching
cannikin Aug 30, 2022
487b94c
--rebuild-fixture
cannikin Aug 30, 2022
2f7e023
Update templates for cacheFindMany
cannikin Aug 30, 2022
9d40b61
yarn constraints --fix
cannikin Sep 1, 2022
cfc3801
Updates lock file with constraints fix
cannikin Sep 1, 2022
f0d6c0d
Refactor to use TS abstract class
cannikin Sep 1, 2022
8fd5863
Types in template
cannikin Sep 1, 2022
64d3377
Fixes `setup cache` CLI command
cannikin Sep 1, 2022
45cbdd9
Export client defaults as named exports
cannikin Sep 1, 2022
5522b87
Merge branch 'main' into rc-service-caching
cannikin Sep 1, 2022
597eeeb
InMemoryCache is a default export now
cannikin Sep 2, 2022
0bb3013
Fix link
cannikin Sep 3, 2022
16862a8
Doc updates
cannikin Sep 4, 2022
785e645
More doc updates
cannikin Sep 7, 2022
3d1ce5c
Adds docs for `setup cache` command
cannikin Sep 12, 2022
c67ac2e
Adds some complex types to inputs and results
cannikin Sep 14, 2022
394597b
Adds test for no records found
cannikin Sep 14, 2022
e97831f
Adds spys to check on client calls
cannikin Sep 14, 2022
73187a9
Fix some type issues
dac09 Sep 15, 2022
0fa2048
Remove specific types for cache and cacheFindMany result
dac09 Sep 15, 2022
42556ce
Handle redis disconnect
cannikin Sep 15, 2022
a4a516b
Pass logger to RedisClient
cannikin Sep 15, 2022
cd52c72
Use logger instead of console in Memcached config
cannikin Sep 15, 2022
671f7b9
Merge branch 'main' into rc-service-caching
cannikin Sep 15, 2022
37f179f
Adds reconnect() function
cannikin Sep 15, 2022
316cbec
Attempt reconnect on timeout error
cannikin Sep 15, 2022
77f151c
Adds test for reconnect()
cannikin Sep 15, 2022
ad46dfb
Remove commented mock code
cannikin Sep 28, 2022
6b12f52
Update docs/docs/cli-commands.md
cannikin Oct 11, 2022
8eed195
Update packages/api/src/cache/clients/BaseClient.ts
cannikin Oct 11, 2022
8432279
Update packages/cli/src/commands/setup/cache/cache.js
cannikin Oct 11, 2022
63a33a0
Update docs/docs/services.md
cannikin Oct 12, 2022
d17c669
Moves addPackagesTask to shared location
cannikin Oct 12, 2022
31ad0f1
Add memjs/redis package based on client choice during setup
cannikin Oct 12, 2022
c477ca7
Fix type issue in BaseClient
dac09 Oct 13, 2022
b42f074
Refactor to use async imports
dac09 Oct 13, 2022
9fbd589
fix: reconnect -> disconnect tests
dac09 Oct 13, 2022
b15bd13
Adds testing helpers for contents of InMemoryCache
cannikin Oct 13, 2022
85850be
Updates cache templates to include testing check, moves host URL to E…
cannikin Oct 13, 2022
9db1f7c
Move cache server connection string to .env
cannikin Oct 13, 2022
8975841
Move adding env var code to shared helper
cannikin Oct 13, 2022
3637bc1
Export client for testing
cannikin Oct 13, 2022
2f9bc6b
Merge branch 'main' of github.com:redwoodjs/redwood into rc-service-c…
dac09 Oct 26, 2022
1866d1e
Fix merge conflicts
dac09 Oct 26, 2022
187bdb6
Use addEnvVarTask from cli helpers
dac09 Oct 26, 2022
1fdcf9c
Use listr2 instead
dac09 Oct 26, 2022
0dc5e25
Merge branch 'main' into rc-service-caching
dac09 Oct 27, 2022
df49081
WIP(testing): Add custom matcher to check cached values
dac09 Oct 27, 2022
ec91bf3
Merge branch 'rc-service-caching' of github.com:redwoodjs/redwood int…
dac09 Oct 27, 2022
525d7e7
Add contents and clear to InMemoryCache
dac09 Oct 31, 2022
bcb712d
Merge branch 'main' of github.com:redwoodjs/redwood into rc-service-c…
dac09 Oct 31, 2022
804d68d
Unused imports in deploy helpers
dac09 Oct 31, 2022
a8f46ae
Fix bugs in the cli helper
dac09 Oct 31, 2022
3517a08
Add partialMatch helper
dac09 Oct 31, 2022
9be4d2a
Updates `toHaveCached()` to accept an optional argument
cannikin Nov 1, 2022
92c94b9
Update custom matcher, comments
dac09 Nov 2, 2022
fa990ac
Support multiple values in partialMatch array |
dac09 Nov 2, 2022
85846f7
Provide options to toHaveCached()
cannikin Nov 2, 2022
be443c7
fix: Check string values after serializing
dac09 Nov 3, 2022
55d3402
docs: Update testing docs
dac09 Nov 4, 2022
054e97e
docs: small update
dac09 Nov 4, 2022
f7edb1d
Merge branch 'main' of github.com:redwoodjs/redwood into rc-service-c…
dac09 Nov 7, 2022
5b33cd6
fix: Remove matcher options
dac09 Nov 7, 2022
63dadc7
Update docs/docs/testing.md
dac09 Nov 7, 2022
94ecbc2
fix(cli): Fix Listr import in cache setup cli
dac09 Nov 7, 2022
ad55b55
Update docs/docs/services.md
cannikin Nov 7, 2022
f5a9386
Update docs/docs/services.md
cannikin Nov 7, 2022
86adeab
Apply suggestions from code review
dac09 Nov 8, 2022
a6e62d0
Apply suggestions from code review
dac09 Nov 8, 2022
ddb5ae6
Update docs/docs/services.md
cannikin Nov 8, 2022
b069817
Code example changes
cannikin Nov 8, 2022
7e05ff1
Merge branch 'main' into rc-service-caching
cannikin Nov 8, 2022
61a58d9
Merge branch 'main' into rc-service-caching
cannikin Nov 10, 2022
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
15 changes: 15 additions & 0 deletions docs/docs/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,7 @@ yarn redwood setup <category>
| Commands | Description |
| ------------------ | ------------------------------------------------------------------------------------------ |
| `auth` | Set up auth configuration for a provider |
| `cache` | Set up cache configuration for memcached or redis |
| `custom-web-index` | Set up an `index.js` file, so you can customize how Redwood web is mounted in your browser |
| `deploy` | Set up a deployment configuration for a provider |
| `generator` | Copy default Redwood generator templates locally for customization |
Expand Down Expand Up @@ -1727,6 +1728,20 @@ yarn redwood setup graphiql <provider>
| `--expiry, -e` | Token expiry in minutes. Default is 60 |
| `--view, -v` | Print out generated headers to console |


### setup cache

This command creates a setup file in `api/src/lib/cache.{ts|js}` for connecting to a Memcached or Redis server and allows caching in services. See the [**Caching** section of the Services docs](/docs/services#caching) for usage.

```
yarn redwood setup cache <client>
```

| Arguments & Options | Description |
| :------------------ | :----------------------- |
| `client` | Name of the client to configure, `memcached` or `redis` |
| `--force, -f` | Overwrite existing files |

### setup custom-web-index

Redwood automatically mounts your `<App />` to the DOM, but if you want to customize how that happens, you can use this setup command to generate an `index.js` file in `web/src`.
Expand Down
303 changes: 303 additions & 0 deletions docs/docs/services.md

Large diffs are not rendered by default.

219 changes: 219 additions & 0 deletions docs/docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,225 @@ Luckily, RedwoodJS has several api testing utilities to make [testing functions
## Testing GraphQL Directives

Please refer to the [Directives documentation](./directives.md) for details on how to write Redwood [Validator](./directives.md#writing-validator-tests) or [Transformer](./directives.md#writing-transformer-tests) Directives tests.


## Testing Caching
If you're using Redwood's [caching](services#caching) - we provide a handful of utilities, and patterns, to help you test this too!
dac09 marked this conversation as resolved.
Show resolved Hide resolved

Let's say you have a service where you cache the result of products, and individual products
dac09 marked this conversation as resolved.
Show resolved Hide resolved

```ts
export const listProducts: QueryResolvers['listProducts'] = () => {
// highlight-next-line
return cacheFindMany('products-list', db.product, {
expires: 3600,
})
}

export const product: QueryResolvers['product'] = async ({ id }) => {
// highlight-next-line
return cache(
`cached-product-${id}-`,
dac09 marked this conversation as resolved.
Show resolved Hide resolved
() =>
db.product.findUnique({
where: { id },
}),
{ expires: 3600 }
)
}
```

With this code, we'll be caching an array of products (from the find many), and individual products that get queried too.


:::tip
It's important to note that when you write scenario or unit tests, it will use the `InMemoryClient`.

The InMemoryClient has a few extra features to help with testing.

1. Allows you to call `cacheClient.clear()` so each of your tests have a fresh cache state
2. Allows you to get all its contents (without cache-keys) with the `cacheClient.contents` getter
:::


There's a few different things you may want to test, but let's start with the basics.

In your test let's import your cache client and clear on after each test:
dac09 marked this conversation as resolved.
Show resolved Hide resolved


```ts
import type { InMemoryClient } from '@redwoodjs/api/cache'
import { client } from 'src/lib/cache'

// For TypeScript users
const testCacheClient = client as InMemoryClient

describe('products', () => {
// highlight-start
afterEach(() => {
testCacheClient.clear()
})
// highlight-end
//....
})
```

### The `toHaveCached` matcher
We have a custom Jest matcher included in Redwood to make things a little easier. To use it simply add an import to the top of your test file
dac09 marked this conversation as resolved.
Show resolved Hide resolved

```ts
// highlight-next-line
import '@redwoodjs/testing/cache'
// ^^ make `.toHaveCached` available
```

The `toHaveCached` matcher can take three forms:

`expect(testCacheClient)`
1. `.toHaveCached(expectedData)` - check for an exact match of the data, regardless of the key
2. `.toHaveCached('expected-key', expectedData)` - check that the data is cached in the key you supply
3. `.toHaveCached(/key-regex.*/, expectedData)` - check that data is cached in a key that matches the regex supplied


Let's see these in action now:

```ts

scenario('returns a single product', async (scenario: StandardScenario) => {
await product({ id: scenario.product.three.id })

// Pattern 1: Only check that the data is present in the cache
expect(testCacheClient).toHaveCached(scenario.product.three)

// Pattern 2: Check that data is cached, at a specific key
expect(testCacheClient).toHaveCached(
`cached-product-${scenario.product.three.id}`,
scenario.product.three
)

// Pattern 3: Check that data is cached, in a key matching the regex
expect(testCacheClient).toHaveCached(
/cached-.*/,
scenario.product.three
)
dac09 marked this conversation as resolved.
Show resolved Hide resolved
```


:::info Serialized Objects in Cache
Remember that the cache only ever contains serialized objects. So if you passed an object like this:
```js
{
id: 5,
published: new Date('12/10/1995')
}

```

The published key will be serialized and stored as a string. To make testing easier for you, we serialize the object you are passing when you use the `toHaveCached` matcher, before we compare it against the value in the cache
dac09 marked this conversation as resolved.
Show resolved Hide resolved
:::

### Partial Matching
It can be a little tedious to check that every key in the object you are looking for matches. This is especially true if you have autogenerated values such as `updatedAt` and `cuid` Ids.
dac09 marked this conversation as resolved.
Show resolved Hide resolved

To help with this, we've provided a helper for partial matching!

```ts
// highlight-next-line
import { partialMatch } from '@redwoodjs/testing/cache'


scenario('returns all products', async (scenario: StandardScenario) => {
await products()

// Partial match using the toHaveCached, if you supply a key
expect(testCacheClient).toHaveCached(
/cached-products.*/,
// highlight-next-line
partialMatch([{ name: 'LS50', brand: 'KEF' }])
)

// Or you can use the .contents getter
expect(testCacheClient.contents).toContainEqual(
// check that an array contains an object matching
// highlight-next-line
partialMatch([{ name: 'LS50', brand: 'KEF' }])
)
}

scenario('finds a single product', () = {
await product({id: 5})

// You can also check for a partial match of an object
expect(testCacheClient).toHaveCached(
/cached-.*/,
// highlight-start
partialMatch({
name: 'LS50',
brand: 'KEF'
})
)
// highlight-end
})

dac09 marked this conversation as resolved.
Show resolved Hide resolved
```

Partial match is just syntactic sugar - underneath it just uses Jest's `expect.objectContaining` and `expect.arrayContaining`.
cannikin marked this conversation as resolved.
Show resolved Hide resolved
dac09 marked this conversation as resolved.
Show resolved Hide resolved

The `partialMatch` helper takes two forms of arguments:

- If you supply an object, you are expecting a partial match of that object
- If you supply an array of objects, you are expecting an array containing a partial match of each of the objects


:::tip
Note that you cannot use `partialMatch` with toHaveCached without supplying a key!

```ts
// 🛑 Will never pass!
expect(testCacheClient).toHaveCached(partialMatch({name: 'LS50'}))
```

For partial matches, you either have to supply a key to `toHaveCached` or use the `cacheClient.contents` helper.
:::


### Strict Matching
cannikin marked this conversation as resolved.
Show resolved Hide resolved

If you'd like more strict checking i.e. you do not want helpers to automatically serialize/deserialize your _expected_ value, you can use the `.contents` getter in test cache client. Note that the `.contents` helper will still de-serialize the values in your cache (to make it easier to compare), just not the expected value.
dac09 marked this conversation as resolved.
Show resolved Hide resolved

For example:

```ts

const expectedValue = {
// Note that this is a date 👇
publishDate: new Date('12/10/1988'),
title: 'A book from the eighties',
id: 1988
}

// ✅ will pass, because we will serialize the publishedDate for you
expect(testCacheClient).toHaveCached(expectedValue)


// 🛑 won't pass, because publishDate in cache is a string, but you supplied a Date object
expect(testCacheClient.contents).toContainEqual(expectedValue)

// ✅ will pass, because you serialized the date
expect(testCacheClient.contents).toContainEqual({
...expectedValue,
publishDate: expectedValue.publishDate.toISOString()
})

// And if you wanted to view the raw contents of the cache
console.log(testCacheClient.storage)
```

This is mainly helpful when you are testing for a very specific value, or have edgecases in how the serialization/deserialization works in the cache.




## Wrapping Up

So that's the world of testing according to Redwood. Did we miss anything? Can we make it even more awesome? Stop by [the community](https://community.redwoodjs.com) and ask questions, or if you've thought of a way to make this doc even better then [open a PR](https://github.com/redwoodjs/redwoodjs.com/pulls).
Expand Down
2 changes: 2 additions & 0 deletions packages/api/cache/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-env es6, commonjs */
module.exports = require('../dist/cache/index')
4 changes: 4 additions & 0 deletions packages/api/cache/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "./index.js",
"types": "../dist/cache/index.d.ts"
}
4 changes: 4 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"files": [
"dist",
"cache",
"logger",
"webhooks"
],
Expand Down Expand Up @@ -55,11 +56,14 @@
"@types/crypto-js": "4.1.1",
"@types/jsonwebtoken": "8.5.9",
"@types/md5": "2.3.2",
"@types/memjs": "1",
"@types/pascalcase": "1.0.1",
"@types/split2": "3.2.1",
"@types/uuid": "8.3.4",
"aws-lambda": "1.0.7",
"jest": "29.2.2",
"memjs": "1.3.0",
"redis": "4.2.0",
"split2": "4.1.0",
"typescript": "4.7.4"
},
Expand Down
30 changes: 30 additions & 0 deletions packages/api/src/cache/__tests__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import InMemoryClient from '../clients/InMemoryClient'
import { createCache } from '../index'

describe('cache', () => {
it('adds a missing key to the cache', async () => {
const client = new InMemoryClient()
const { cache } = createCache(client)

const result = await cache('test', () => {
return { foo: 'bar' }
})

expect(result).toEqual({ foo: 'bar' })
expect(client.storage.test.value).toEqual(JSON.stringify({ foo: 'bar' }))
})

it('finds an existing key in the cache', async () => {
const client = new InMemoryClient({
test: { expires: 1977175194415, value: '{"foo":"bar"}' },
})
const { cache } = createCache(client)

const result = await cache('test', () => {
return { bar: 'baz' }
})

// returns existing cached value, not the one that was just set
expect(result).toEqual({ foo: 'bar' })
})
})
Loading