Skip to content

Commit

Permalink
Date field serialization (#16)
Browse files Browse the repository at this point in the history
* Date field serialization

Fixes #14

* Add links to JSON stringify and parse

* Prove that the original object is not modified
  • Loading branch information
huntharo committed Jul 13, 2023
1 parent beafd6c commit 8780bec
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 1 deletion.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,23 @@ app.listen(Number.parseInt(PORT, 10), () => {
});
```

## Supported Field Types

The following field types are fully supported by the `DynamoDBStore`:

- `string`
- `number`
- `boolean`
- `object`

The following field types are partially supported by the `DynamoDBStore`:

- `Date`
- Stored as a string in ISO 8601 format
- Will be returend as a string in ISO 8601 format
- Cannot be automatically converted back into a `Date` object since it is not known which fields were originally `Date` objects vs date strings
- Note: [connect-dynamodb](https://www.npmjs.com/package/connect-dynamodb) serializes `Date` objects to strings as well and also does not support automatic conversion back to `Date` objects since it serializes using [JSON.stringify()](https://github.com/ca98am79/connect-dynamodb/blob/87028bb10fa3c9d4b8adf4f6cdeea2c41c0e8f23/lib/connect-dynamodb.js#L203) and [JSON.parse()](https://github.com/ca98am79/connect-dynamodb/blob/87028bb10fa3c9d4b8adf4f6cdeea2c41c0e8f23/lib/connect-dynamodb.js#L185)

## API Documentation

After installing the package review the [API Documentation](https://pwrdrvr.github.io/dynamodb-session-store/classes/DynamoDBStore.html) for detailed on each configuration option.
Expand Down
57 changes: 57 additions & 0 deletions src/deep-replace-dates-with-strings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { deepReplaceDatesWithISOStrings } from './deep-replace-dates-with-strings';

describe('deepReplaceDatesWithISOStrings', () => {
it('should replace Date objects with their ISO string representation', () => {
const date1 = new Date();
const date2 = new Date();
const date3 = new Date();

const obj = {
name: 'John',
created: date1,
friends: [
{
name: 'Jane',
created: date2,
},
],
latestLog: {
time: date3,
message: 'Hello, world!',
},
};

const result = deepReplaceDatesWithISOStrings(obj);

expect(result.created).toBe(date1.toISOString());
expect(result.friends[0].created).toBe(date2.toISOString());
expect(result.latestLog.time).toBe(date3.toISOString());
expect(result).toEqual({
name: 'John',
created: date1.toISOString(),
friends: [
{
name: 'Jane',
created: date2.toISOString(),
},
],
latestLog: {
time: date3.toISOString(),
message: 'Hello, world!',
},
});
});

// Check that original object is unmodified
it('should not modify the original object', () => {
const date = new Date();
const obj = {
name: 'John',
created: date,
};

deepReplaceDatesWithISOStrings(obj);

expect(obj.created).toBe(date);
});
});
22 changes: 22 additions & 0 deletions src/deep-replace-dates-with-strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Deep clones the object and replaces all Date objects with their ISO string representation.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function deepReplaceDatesWithISOStrings(obj: any): any {
if (obj instanceof Date) {
return obj.toISOString();
} else if (Array.isArray(obj)) {
return obj.map(deepReplaceDatesWithISOStrings);
} else if (typeof obj === 'object' && obj !== null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = deepReplaceDatesWithISOStrings(obj[key]);
}
}
return result;
} else {
return obj;
}
}
158 changes: 158 additions & 0 deletions src/dynamodb-store.table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,163 @@ describe('dynamodb-store - table via jest-dynalite', () => {
},
);
});

it('can serialize Date objects to strings', (done) => {
const store = new DynamoDBStore({
dynamoDBClient: dynamoClient,
tableName,
});

const originalSessionObject = {
// Use a static date to ensure the same value is stored and retrieved
dateField: new Date('2021-07-01T01:02:03Z'),
};

store.set(
'129',
{
mySessionInfo: originalSessionObject,
// @ts-expect-error something
cookie: {
maxAge: 60 * 60 * 1000, // one hour in milliseconds
},
},
(err) => {
expect(err).toBeNull();

ddbDocClient
.send(new GetCommand({ TableName: tableName, Key: { id: 'session#129' } }))
.then(({ Item }) => {
expect(Item).toBeDefined();
expect(Item!.sess).toBeDefined();
expect(Item!.sess.mySessionInfo).toBeDefined();
expect(Item!.sess.mySessionInfo.dateField).toBeDefined();

// Check that the DB has a string
expect(Item!.sess.mySessionInfo.dateField).toBe('2021-07-01T01:02:03.000Z');

store.get('129', (err, session) => {
expect(err).toBeNull();
expect(session).toBeDefined();

// @ts-expect-error yes mySessionInfo exists
const typedSession = session as {
mySessionInfo: {
dateField: Date;
};
};

expect(typedSession!.mySessionInfo).toBeDefined();
// The date field is not going to be a date object
// since we do not have a schema to know which fields to
// convert to back into dates and which were strings
// to begin with
// expect(typedSession!.mySessionInfo.dateField).toBeInstanceOf(Date);
expect(typedSession!.mySessionInfo.dateField).toEqual('2021-07-01T01:02:03.000Z');

// Confirm that the original field is still a date object
expect(originalSessionObject.dateField).toBeInstanceOf(Date);

done();
});
})
.catch((err) => {
done(err);
});
},
);
});

it('can serialize / deserialize string / object / boolean / number values', (done) => {
const store = new DynamoDBStore({
dynamoDBClient: dynamoClient,
tableName,
});

store.set(
'129',
{
mySessionInfo: {
stringField: 'some string',
numberField: 123,
floatingNumberField: 123.456,
someBooleanField: true,
someOtherBooleanField: false,
someObjectField: {
nestedField: 'nested value',
},
someUndefinedField: undefined,
someNullField: null,
},
// @ts-expect-error something
cookie: {
maxAge: 60 * 60 * 1000, // one hour in milliseconds
},
},
(err) => {
expect(err).toBeNull();

ddbDocClient
.send(new GetCommand({ TableName: tableName, Key: { id: 'session#129' } }))
.then(({ Item }) => {
expect(Item).toBeDefined();
expect(Item!.sess).toBeDefined();
expect(Item!.sess.mySessionInfo).toBeDefined();

// Check that the DB record looks correct
expect(Item!.sess.mySessionInfo.stringField).toBe('some string');
expect(Item!.sess.mySessionInfo.numberField).toBe(123);
expect(Item!.sess.mySessionInfo.floatingNumberField).toBe(123.456);
expect(Item!.sess.mySessionInfo.someBooleanField).toBe(true);
expect(Item!.sess.mySessionInfo.someOtherBooleanField).toBe(false);
expect(Item!.sess.mySessionInfo.someObjectField).toBeDefined();
expect(Item!.sess.mySessionInfo.someObjectField.nestedField).toBe('nested value');
expect(Item!.sess.mySessionInfo.someUndefinedField).toBeUndefined();
expect(Item!.sess.mySessionInfo.someNullField).toBeNull();

store.get('129', (err, session) => {
expect(err).toBeNull();
expect(session).toBeDefined();

// @ts-expect-error yes mySessionInfo exists
const typedSession = session as {
mySessionInfo: {
stringField: string;
numberField: number;
floatingNumberField: number;
someBooleanField: boolean;
someOtherBooleanField: boolean;
someObjectField: {
nestedField: string;
};
someUndefinedField: undefined;
someNullField: null;
};
};

expect(typedSession!.mySessionInfo).toBeDefined();

// Check that the values are correct
expect(typedSession!.mySessionInfo.stringField).toBe('some string');
expect(typedSession!.mySessionInfo.numberField).toBe(123);
expect(typedSession!.mySessionInfo.floatingNumberField).toBe(123.456);
expect(typedSession!.mySessionInfo.someBooleanField).toBe(true);
expect(typedSession!.mySessionInfo.someOtherBooleanField).toBe(false);
expect(typedSession!.mySessionInfo.someObjectField).toBeDefined();
expect(typedSession!.mySessionInfo.someObjectField.nestedField).toBe(
'nested value',
);
expect(typedSession!.mySessionInfo.someUndefinedField).toBeUndefined();
expect(typedSession!.mySessionInfo.someNullField).toBeNull();

done();
});
})
.catch((err) => {
done(err);
});
},
);
});
});
});
3 changes: 2 additions & 1 deletion src/dynamodb-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
import Debug from 'debug';
import { promisify } from 'util';
import { deepReplaceDatesWithISOStrings } from './deep-replace-dates-with-strings';

const sleep = promisify(setTimeout);
const debug = Debug('@pwrdrvr/dynamodb-session-store');
Expand Down Expand Up @@ -464,7 +465,7 @@ export class DynamoDBStore extends session.Store {
// so we strip the fields that we don't want and make sure the `expires` field
// is turned into a string
sess: {
...session,
...deepReplaceDatesWithISOStrings(session),
...(session.cookie
? { cookie: { ...JSON.parse(JSON.stringify(session.cookie)) } }
: {}),
Expand Down

0 comments on commit 8780bec

Please sign in to comment.