Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
94939a6
chore: Scaffold node redis store package.
kinyoklion Jun 5, 2023
f9c2bcd
Add note on store organization.
kinyoklion Jun 5, 2023
a6a3918
Better text.
kinyoklion Jun 5, 2023
e57b38e
new
kinyoklion Jun 5, 2023
c3263d8
Update disable cache example.
kinyoklion Jun 5, 2023
73ac3bd
Add LICENSE.
kinyoklion Jun 5, 2023
e721f0c
chore: Scaffold node redis store package. (#136)
kinyoklion Jun 5, 2023
df18981
feat: Implement redis persistent store.
kinyoklion Jun 5, 2023
e444fbe
First draft of redis implementation.
kinyoklion Jun 5, 2023
01bdc2c
Start adding tests.
kinyoklion Jun 6, 2023
317f3e8
Work on tests
kinyoklion Jun 6, 2023
b958550
Recursive deps for build order.
kinyoklion Jun 6, 2023
bf07269
Fix update queue.
kinyoklion Jun 6, 2023
0743f4f
Add remainder of feature store tests.
kinyoklion Jun 6, 2023
a44e72b
Add redis core tests.
kinyoklion Jun 6, 2023
25eb6f9
Ignore redis files.
kinyoklion Jun 6, 2023
33146ec
Lint
kinyoklion Jun 6, 2023
708ed53
Add architecture diagram.
kinyoklion Jun 6, 2023
69e3868
Add initial big segments store.
kinyoklion Jun 7, 2023
b8b6f1d
Add big segment store to diagram.
kinyoklion Jun 7, 2023
3a1f9d2
Add description for BigSegmentStore.
kinyoklion Jun 7, 2023
77b5974
Add big segments tests. Extend store tests to use multiple prefixes.
kinyoklion Jun 7, 2023
f6d0ab6
Linting
kinyoklion Jun 7, 2023
1ab1ee9
Update CI configuration.
kinyoklion Jun 7, 2023
0aca35b
merge feature branch
kinyoklion Jun 7, 2023
fa2aa4b
Enable workflow for branch for testing.
kinyoklion Jun 7, 2023
a5a3129
Add sudo commands.
kinyoklion Jun 7, 2023
1903d66
Add dev dependency changes to CI build.
kinyoklion Jun 7, 2023
58f2415
Remove running on branch.
kinyoklion Jun 7, 2023
307c171
Correct typo in readme.
kinyoklion Jun 7, 2023
f12d89f
feat: Add support for DynamoDB persistent store.
kinyoklion Jun 7, 2023
97d47c1
Work on implementation.
kinyoklion Jun 8, 2023
6a7be01
Add logger line.
kinyoklion Jun 8, 2023
bef964d
All store tests passing.
kinyoklion Jun 9, 2023
c15e8a4
Big segment store and tests.
kinyoklion Jun 9, 2023
a8c7949
Lint fixes.
kinyoklion Jun 9, 2023
66755ef
Add factories.
kinyoklion Jun 9, 2023
1eb7fd2
Add size calculation test. Add exports.
kinyoklion Jun 9, 2023
aac9142
Fix name of big segement store factory. Update index.
kinyoklion Jun 9, 2023
d5f989a
Merge branch 'rlamb/implement-redis-store' of github.com:launchdarkly…
kinyoklion Jun 9, 2023
252f6df
Merge branch 'rlamb/implement-redis-store' into rlamb/implement-dynam…
kinyoklion Jun 9, 2023
7c4e533
Test CI.
kinyoklion Jun 9, 2023
26ca99b
Lint
kinyoklion Jun 9, 2023
da48379
Add table name
kinyoklion Jun 9, 2023
7d4d02b
Change job name.
kinyoklion Jun 9, 2023
628a1ef
Change job name
kinyoklion Jun 9, 2023
faa9899
Merge branch 'rlamb/implement-redis-store' into rlamb/implement-dynam…
kinyoklion Jun 9, 2023
bb96c56
Setup table correctly for redis core tests.
kinyoklion Jun 9, 2023
f1b405d
Add support for big segments contract tests.
kinyoklion Jun 12, 2023
d9d5cca
lint
kinyoklion Jun 12, 2023
5c319ce
Rename exports for greater backward compatibility.
kinyoklion Jun 13, 2023
9f5c0b6
Merge branch 'rlamb/implement-redis-store' into rlamb/implement-dynam…
kinyoklion Jun 13, 2023
61af43c
Rename default exports for greater backward compatibility.
kinyoklion Jun 13, 2023
b5c347f
Add stores to manual publish.
kinyoklion Jun 13, 2023
1e7b67e
Merge branch 'main' into rlamb/implement-dynamodb-store
kinyoklion Jun 13, 2023
006673b
Update version numbers
kinyoklion Jun 13, 2023
68506c9
Merge branch 'main' into rlamb/implement-redis-store
kinyoklion Jun 13, 2023
3445518
Update node server dep.
kinyoklion Jun 13, 2023
b9d8782
Catch on quit.
kinyoklion Jun 13, 2023
57edf27
Fix big segment included/excluded keys.
kinyoklion Jun 13, 2023
958e5b5
Merge branch 'rlamb/implement-redis-store' into rlamb/implement-dynam…
kinyoklion Jun 13, 2023
127b604
Add doc script.
kinyoklion Jun 13, 2023
03315e6
Merge branch 'rlamb/implement-redis-store' into rlamb/implement-dynam…
kinyoklion Jun 13, 2023
7c56025
Add doc script.
kinyoklion Jun 13, 2023
a0a39a6
Make dynamodb options optional.
kinyoklion Jun 13, 2023
a472f98
Merge branch 'main' into rlamb/feat/add-redis-persistent-store
kinyoklion Jun 13, 2023
e955907
Merge branch 'rlamb/feat/add-redis-persistent-store' into rlamb/imple…
kinyoklion Jun 13, 2023
4e53827
Merge branch 'rlamb/implement-redis-store' into rlamb/implement-dynam…
kinyoklion Jun 13, 2023
8713d56
remove change from Reasons.ts
kinyoklion Jun 13, 2023
1b8cf42
Update node server version.
kinyoklion Jun 13, 2023
030e9f5
Update packages/store/node-server-sdk-dynamodb/src/DynamoDBClientStat…
kinyoklion Jun 15, 2023
afff4f3
Merge branch 'main' into rlamb/implement-dynamodb-store
kinyoklion Jun 15, 2023
e328b60
Merge branch 'rlamb/implement-dynamodb-store' of github.com:launchdar…
kinyoklion Jun 15, 2023
958f8ce
Fix merge
kinyoklion Jun 15, 2023
03e19f5
Add readme line.
kinyoklion Jun 15, 2023
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: 2 additions & 0 deletions .github/workflows/manual-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ on:
- packages/sdk/vercel
- packages/sdk/akamai-base
- packages/sdk/akamai-edgekv
- packages/store/node-server-sdk-redis
- packages/store/node-server-sdk-dynamodb
prerelease:
description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.'
type: boolean
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/node-dynamodb.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: store/node-server-sdk-dynamodb

on:
push:
branches: [main, rlamb/implement-dynamodb-store]
paths-ignore:
- '**.md' #Do not need to run CI for markdown changes.
pull_request:
branches: [main]
paths-ignore:
- '**.md'

jobs:
build-test-node-dynamo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: 'https://registry.npmjs.org'
- run: |
sudo docker run -d -p 8000:8000 amazon/dynamodb-local
- id: shared
name: Shared CI Steps
uses: ./actions/ci
with:
workspace_name: '@launchdarkly/node-server-sdk-dynamodb'
workspace_path: packages/store/node-server-sdk-dynamodb
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This includes shared libraries, used by SDKs and other tools, as well as SDKs.
| Store Packages | npm | issues | tests |
| ------------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------- | ------------------------------------------------------- |
| [@launchdarkly/node-server-sdk-redis](packages/store/node-server-sdk-redis/README.md) | [![NPM][node-redis-npm-badge]][node-redis-npm-link] | [Node Redis][node-redis-issues] | [![Actions Status][node-redis-ci-badge]][node-redis-ci] |
| [@launchdarkly/node-server-sdk-dynamodb](packages/store/node-server-sdk-dynamodb/README.md) | [![NPM][node-dynamodb-npm-badge]][node-dynamodb-npm-link] | [Node DynamoDB][node-dynamodb-issues] | [![Actions Status][node-dynamodb-ci-badge]][node-dynamodb-ci] |

## Organization

Expand Down Expand Up @@ -131,3 +132,9 @@ We encourage pull requests and other contributions from the community. Check out
[node-redis-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/node-server-sdk-redis.svg?style=flat-square
[node-redis-npm-link]: https://www.npmjs.com/package/@launchdarkly/node-server-sdk-redis
[node-redis-issues]: https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+store%2Fnode-server-sdk-redis%22+
[//]: # 'store/node-server-sdk-dynamodb'
[node-dynamodb-ci-badge]: https://github.com/launchdarkly/js-core/actions/workflows/node-dynamodb.yml/badge.svg
[node-dynamodb-ci]: https://github.com/launchdarkly/js-core/actions/workflows/node-dynamodb.yml
[node-dynamodb-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/node-server-sdk-dynamodb.svg?style=flat-square
[node-dynamodb-npm-link]: https://www.npmjs.com/package/@launchdarkly/node-server-sdk-dynamodb
[node-dynamodb-issues]: https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+store%2Fnode-server-sdk-dynamodb%22+
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"packages/sdk/akamai-base/example",
"packages/sdk/akamai-edgekv",
"packages/sdk/akamai-edgekv/example",
"packages/store/node-server-sdk-redis"
"packages/store/node-server-sdk-redis",
"packages/store/node-server-sdk-dynamodb"
],
"private": true,
"scripts": {
Expand Down
Empty file.
13 changes: 13 additions & 0 deletions packages/store/node-server-sdk-dynamodb/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2023 Catamorphic, Co.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
110 changes: 110 additions & 0 deletions packages/store/node-server-sdk-dynamodb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# LaunchDarkly Server-Side SDK for Node.js

[![NPM][node-dynamodb-npm-badge]][node-dynamodb-npm-link]
[![Actions Status][node-dynamodb-ci-badge]][node-dynamodb-ci]
[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/js-core/packages/store/node-server-sdk-dynamodb/docs/)

This library provides a DynamoDB-backed persistence mechanism (feature store) for the [LaunchDarkly Node.js SDK](https://github.com/launchdarkly/js-core/packages/sdk/server-node), replacing the default in-memory feature store. It uses the AWS SDK for Node.js.
The minimum version of the LaunchDarkly Server-Side SDK for Node for use with this library is 8.0.0.

This SDK is a beta version and should not be considered ready for production use while this message is visible.

## LaunchDarkly overview

[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!

[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly)

## Supported Node versions

This package is compatible with Node.js versions 14 and above.

## Getting started

Refer to [Using DynamoDB as a persistent feature store](https://docs.launchdarkly.com/sdk/features/storing-data/dynamodb#nodejs-server-side).

## Quick setup

1. In DynamoDB, create a table which has the following schema: a partition key called "namespace" and a sort key called "key", both with a string type. The LaunchDarkly library does not create the table automatically, because it has no way of knowing what additional properties (such as permissions and throughput) you would want it to have.

2. Install this package with `npm` or `yarn`:

`npm install launchdarkly-node-server-sdk-dynamodb --save`

3. If your application does not already have its own dependency on the `@aws-sdk/client-dynamodb` package, and if it will _not_ be running in AWS Lambda, add `@aws-sdk/client-dynamodb` as well:

`npm install @aws-sdk/client-dynamodb --save`

The `launchdarkly-node-server-sdk-dynamodb` package does not provide `@aws-sdk/client-dynamodb` as a transitive dependency, because it is provided automatically by the Lambda runtime and this would unnecessarily increase the size of applications deployed in Lambda. Therefore, if you are not using Lambda you need to provide `@aws-sdk/client-dynamodb` separately.

4. Import the package:

```typescript
const { DynamoDBFeatureStoreFactory } = require('launchdarkly-node-server-sdk-dynamodb');
```

5. When configuring your SDK client, add the DynamoDB feature store:

```typescript
const store = DynamoDBFeatureStoreFactory('YOUR TABLE NAME');
const config = { featureStore: store };
const client = LaunchDarkly.init('YOUR SDK KEY', config);
```

By default, the DynamoDB client will try to get your AWS credentials and region name from environment variables and/or local configuration files, as described in the AWS SDK documentation. You can also specify any valid [DynamoDB client options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#constructor-property) like this:

```typescript
const dynamoDBOptions = { accessKeyId: 'YOUR KEY', secretAccessKey: 'YOUR SECRET' };
const store = DynamoDBFeatureStoreFactory('YOUR TABLE NAME', { clientOptions: dynamoDBOptions });
```

Alternatively, if you already have a fully configured DynamoDB client object, you can tell LaunchDarkly to use that:

```typescript
const store = DynamoDBFeatureStoreFactory('YOUR TABLE NAME', { dynamoDBClient: myDynamoDBClientInstance });
```

6. If you are running a [LaunchDarkly Relay Proxy](https://github.com/launchdarkly/ld-relay) instance, or any other process that will pre-populate the DynamoDB table with feature flags from LaunchDarkly, you can use [daemon mode](https://github.com/launchdarkly/ld-relay#daemon-mode), so that the SDK retrieves flag data only from DynamoDB and does not communicate directly with LaunchDarkly. This is controlled by the SDK's `useLdd` option:

```typescript
const config = { featureStore: store, useLdd: true };
const client = LaunchDarkly.init('YOUR SDK KEY', config);
```

7. If the same DynamoDB table is being shared by SDK clients for different LaunchDarkly environments, set the `prefix` option to a different short string for each one to keep the keys from colliding:

```typescript
const store = DynamoDBFeatureStoreFactory('YOUR TABLE NAME', { prefix: 'env1' });
```

## Caching behavior

To reduce traffic to DynamoDB, there is an optional in-memory cache that retains the last known data for a configurable amount of time. This is on by default; to turn it off (and guarantee that the latest feature flag data will always be retrieved from DynamoDB for every flag evaluation), configure the store as follows:

```typescript
const factory = DynamoDBFeatureStoreFactory({ cacheTTL: 0 });
```

## Contributing

We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.

## About LaunchDarkly

- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
- Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
- Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
- Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
- Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
- Explore LaunchDarkly
- [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information
- [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides
- [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation
- [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates

[node-dynamodb-ci-badge]: https://github.com/launchdarkly/js-core/actions/workflows/node-dynamodb.yml/badge.svg
[node-dynamodb-ci]: https://github.com/launchdarkly/js-core/actions/workflows/node-dynamodb.yml

[node-dynamodb-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/node-server-sdk-dynamodb.svg?style=flat-square
[node-dynamodb-npm-link]: https://www.npmjs.com/package/@launchdarkly/node-server-sdk-dynamodb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { DynamoDBClient, PutItemCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb';
import { interfaces } from '@launchdarkly/node-server-sdk';
import DynamoDBBigSegmentStore, {
KEY_METADATA,
KEY_USER_DATA,
ATTR_EXCLUDED,
ATTR_INCLUDED,
ATTR_SYNC_ON,
} from '../src/DynamoDBBigSegmentStore';
import clearPrefix from './clearPrefix';
import setupTable from './setupTable';
import LDDynamoDBOptions from '../src/LDDynamoDBOptions';
import { numberValue, stringValue } from '../src/Value';

const FAKE_HASH = 'userhash';

const DEFAULT_TABLE_NAME = 'test-table-big-segments';

const DEFAULT_CLIENT_OPTIONS: LDDynamoDBOptions = {
clientOptions: {
endpoint: 'http://localhost:8000',
region: 'us-west-2',
credentials: { accessKeyId: 'fake', secretAccessKey: 'fake' },
},
};

async function setMetadata(
prefix: string | undefined,
metadata: interfaces.BigSegmentStoreMetadata
): Promise<void> {
const client = new DynamoDBClient(DEFAULT_CLIENT_OPTIONS.clientOptions!);
const key = prefix ? `${prefix}:${KEY_METADATA}` : KEY_METADATA;
await client.send(
new PutItemCommand({
TableName: DEFAULT_TABLE_NAME,
Item: {
namespace: stringValue(key),
key: stringValue(key),
[ATTR_SYNC_ON]: numberValue(metadata.lastUpToDate!),
},
})
);
client.destroy();
}

async function setSegments(
prefix: string | undefined,
userHashKey: string,
included: string[],
excluded: string[]
): Promise<void> {
const client = new DynamoDBClient(DEFAULT_CLIENT_OPTIONS.clientOptions!);
const key = prefix ? `${prefix}:${KEY_USER_DATA}` : KEY_USER_DATA;

async function addToSet(attrName: string, values: string[]) {
await client.send(
new UpdateItemCommand({
TableName: DEFAULT_TABLE_NAME,
Key: {
namespace: stringValue(key),
key: stringValue(userHashKey),
},
UpdateExpression: `ADD ${attrName} :value`,
ExpressionAttributeValues: {
':value': { SS: values },
},
})
);
}

if (included && included.length) {
await addToSet(ATTR_INCLUDED, included);
}

if (excluded && excluded.length) {
await addToSet(ATTR_EXCLUDED, excluded);
}

client.destroy();
}

describe.each([undefined, 'app1'])('given a redis big segment store', (prefixParam) => {
let store: DynamoDBBigSegmentStore;

beforeEach(async () => {
await setupTable(DEFAULT_TABLE_NAME, DEFAULT_CLIENT_OPTIONS.clientOptions!);
await clearPrefix(DEFAULT_TABLE_NAME, prefixParam);
// Use param directly to test undefined.
store = new DynamoDBBigSegmentStore(DEFAULT_TABLE_NAME, {
...DEFAULT_CLIENT_OPTIONS,
prefix: prefixParam,
});
});

afterEach(async () => {
store.close();
});

it('can get populated meta data', async () => {
const expected = { lastUpToDate: 1234567890 };
await setMetadata(prefixParam, expected);
const meta = await store.getMetadata();
expect(meta).toEqual(expected);
});

it('can get metadata when not populated', async () => {
const meta = await store.getMetadata();
expect(meta?.lastUpToDate).toBeUndefined();
});

it('can get user membership for a user which has no membership', async () => {
const membership = await store.getUserMembership(FAKE_HASH);
expect(membership).toBeUndefined();
});

it('can get membership for a user that is only included', async () => {
await setSegments(prefixParam, FAKE_HASH, ['key1', 'key2'], []);

const membership = await store.getUserMembership(FAKE_HASH);
expect(membership).toEqual({ key1: true, key2: true });
});

it('can get membership for a user that is only excluded', async () => {
await setSegments(prefixParam, FAKE_HASH, [], ['key1', 'key2']);

const membership = await store.getUserMembership(FAKE_HASH);
expect(membership).toEqual({ key1: false, key2: false });
});

it('can get membership for a user that is included and excluded', async () => {
await setSegments(prefixParam, FAKE_HASH, ['key1', 'key2'], ['key2', 'key3']);

const membership = await store.getUserMembership(FAKE_HASH);
expect(membership).toEqual({ key1: true, key2: true, key3: false }); // include of key2 overrides exclude
});
});
Loading