Skip to content

Commit 8780bec

Browse files
authored
Date field serialization (#16)
* Date field serialization Fixes #14 * Add links to JSON stringify and parse * Prove that the original object is not modified
1 parent beafd6c commit 8780bec

File tree

5 files changed

+256
-1
lines changed

5 files changed

+256
-1
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ app.listen(Number.parseInt(PORT, 10), () => {
8888
});
8989
```
9090

91+
## Supported Field Types
92+
93+
The following field types are fully supported by the `DynamoDBStore`:
94+
95+
- `string`
96+
- `number`
97+
- `boolean`
98+
- `object`
99+
100+
The following field types are partially supported by the `DynamoDBStore`:
101+
102+
- `Date`
103+
- Stored as a string in ISO 8601 format
104+
- Will be returend as a string in ISO 8601 format
105+
- Cannot be automatically converted back into a `Date` object since it is not known which fields were originally `Date` objects vs date strings
106+
- 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)
107+
91108
## API Documentation
92109

93110
After installing the package review the [API Documentation](https://pwrdrvr.github.io/dynamodb-session-store/classes/DynamoDBStore.html) for detailed on each configuration option.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { deepReplaceDatesWithISOStrings } from './deep-replace-dates-with-strings';
2+
3+
describe('deepReplaceDatesWithISOStrings', () => {
4+
it('should replace Date objects with their ISO string representation', () => {
5+
const date1 = new Date();
6+
const date2 = new Date();
7+
const date3 = new Date();
8+
9+
const obj = {
10+
name: 'John',
11+
created: date1,
12+
friends: [
13+
{
14+
name: 'Jane',
15+
created: date2,
16+
},
17+
],
18+
latestLog: {
19+
time: date3,
20+
message: 'Hello, world!',
21+
},
22+
};
23+
24+
const result = deepReplaceDatesWithISOStrings(obj);
25+
26+
expect(result.created).toBe(date1.toISOString());
27+
expect(result.friends[0].created).toBe(date2.toISOString());
28+
expect(result.latestLog.time).toBe(date3.toISOString());
29+
expect(result).toEqual({
30+
name: 'John',
31+
created: date1.toISOString(),
32+
friends: [
33+
{
34+
name: 'Jane',
35+
created: date2.toISOString(),
36+
},
37+
],
38+
latestLog: {
39+
time: date3.toISOString(),
40+
message: 'Hello, world!',
41+
},
42+
});
43+
});
44+
45+
// Check that original object is unmodified
46+
it('should not modify the original object', () => {
47+
const date = new Date();
48+
const obj = {
49+
name: 'John',
50+
created: date,
51+
};
52+
53+
deepReplaceDatesWithISOStrings(obj);
54+
55+
expect(obj.created).toBe(date);
56+
});
57+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Deep clones the object and replaces all Date objects with their ISO string representation.
3+
*/
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
export function deepReplaceDatesWithISOStrings(obj: any): any {
6+
if (obj instanceof Date) {
7+
return obj.toISOString();
8+
} else if (Array.isArray(obj)) {
9+
return obj.map(deepReplaceDatesWithISOStrings);
10+
} else if (typeof obj === 'object' && obj !== null) {
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12+
const result: any = {};
13+
for (const key in obj) {
14+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
15+
result[key] = deepReplaceDatesWithISOStrings(obj[key]);
16+
}
17+
}
18+
return result;
19+
} else {
20+
return obj;
21+
}
22+
}

src/dynamodb-store.table.spec.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,163 @@ describe('dynamodb-store - table via jest-dynalite', () => {
203203
},
204204
);
205205
});
206+
207+
it('can serialize Date objects to strings', (done) => {
208+
const store = new DynamoDBStore({
209+
dynamoDBClient: dynamoClient,
210+
tableName,
211+
});
212+
213+
const originalSessionObject = {
214+
// Use a static date to ensure the same value is stored and retrieved
215+
dateField: new Date('2021-07-01T01:02:03Z'),
216+
};
217+
218+
store.set(
219+
'129',
220+
{
221+
mySessionInfo: originalSessionObject,
222+
// @ts-expect-error something
223+
cookie: {
224+
maxAge: 60 * 60 * 1000, // one hour in milliseconds
225+
},
226+
},
227+
(err) => {
228+
expect(err).toBeNull();
229+
230+
ddbDocClient
231+
.send(new GetCommand({ TableName: tableName, Key: { id: 'session#129' } }))
232+
.then(({ Item }) => {
233+
expect(Item).toBeDefined();
234+
expect(Item!.sess).toBeDefined();
235+
expect(Item!.sess.mySessionInfo).toBeDefined();
236+
expect(Item!.sess.mySessionInfo.dateField).toBeDefined();
237+
238+
// Check that the DB has a string
239+
expect(Item!.sess.mySessionInfo.dateField).toBe('2021-07-01T01:02:03.000Z');
240+
241+
store.get('129', (err, session) => {
242+
expect(err).toBeNull();
243+
expect(session).toBeDefined();
244+
245+
// @ts-expect-error yes mySessionInfo exists
246+
const typedSession = session as {
247+
mySessionInfo: {
248+
dateField: Date;
249+
};
250+
};
251+
252+
expect(typedSession!.mySessionInfo).toBeDefined();
253+
// The date field is not going to be a date object
254+
// since we do not have a schema to know which fields to
255+
// convert to back into dates and which were strings
256+
// to begin with
257+
// expect(typedSession!.mySessionInfo.dateField).toBeInstanceOf(Date);
258+
expect(typedSession!.mySessionInfo.dateField).toEqual('2021-07-01T01:02:03.000Z');
259+
260+
// Confirm that the original field is still a date object
261+
expect(originalSessionObject.dateField).toBeInstanceOf(Date);
262+
263+
done();
264+
});
265+
})
266+
.catch((err) => {
267+
done(err);
268+
});
269+
},
270+
);
271+
});
272+
273+
it('can serialize / deserialize string / object / boolean / number values', (done) => {
274+
const store = new DynamoDBStore({
275+
dynamoDBClient: dynamoClient,
276+
tableName,
277+
});
278+
279+
store.set(
280+
'129',
281+
{
282+
mySessionInfo: {
283+
stringField: 'some string',
284+
numberField: 123,
285+
floatingNumberField: 123.456,
286+
someBooleanField: true,
287+
someOtherBooleanField: false,
288+
someObjectField: {
289+
nestedField: 'nested value',
290+
},
291+
someUndefinedField: undefined,
292+
someNullField: null,
293+
},
294+
// @ts-expect-error something
295+
cookie: {
296+
maxAge: 60 * 60 * 1000, // one hour in milliseconds
297+
},
298+
},
299+
(err) => {
300+
expect(err).toBeNull();
301+
302+
ddbDocClient
303+
.send(new GetCommand({ TableName: tableName, Key: { id: 'session#129' } }))
304+
.then(({ Item }) => {
305+
expect(Item).toBeDefined();
306+
expect(Item!.sess).toBeDefined();
307+
expect(Item!.sess.mySessionInfo).toBeDefined();
308+
309+
// Check that the DB record looks correct
310+
expect(Item!.sess.mySessionInfo.stringField).toBe('some string');
311+
expect(Item!.sess.mySessionInfo.numberField).toBe(123);
312+
expect(Item!.sess.mySessionInfo.floatingNumberField).toBe(123.456);
313+
expect(Item!.sess.mySessionInfo.someBooleanField).toBe(true);
314+
expect(Item!.sess.mySessionInfo.someOtherBooleanField).toBe(false);
315+
expect(Item!.sess.mySessionInfo.someObjectField).toBeDefined();
316+
expect(Item!.sess.mySessionInfo.someObjectField.nestedField).toBe('nested value');
317+
expect(Item!.sess.mySessionInfo.someUndefinedField).toBeUndefined();
318+
expect(Item!.sess.mySessionInfo.someNullField).toBeNull();
319+
320+
store.get('129', (err, session) => {
321+
expect(err).toBeNull();
322+
expect(session).toBeDefined();
323+
324+
// @ts-expect-error yes mySessionInfo exists
325+
const typedSession = session as {
326+
mySessionInfo: {
327+
stringField: string;
328+
numberField: number;
329+
floatingNumberField: number;
330+
someBooleanField: boolean;
331+
someOtherBooleanField: boolean;
332+
someObjectField: {
333+
nestedField: string;
334+
};
335+
someUndefinedField: undefined;
336+
someNullField: null;
337+
};
338+
};
339+
340+
expect(typedSession!.mySessionInfo).toBeDefined();
341+
342+
// Check that the values are correct
343+
expect(typedSession!.mySessionInfo.stringField).toBe('some string');
344+
expect(typedSession!.mySessionInfo.numberField).toBe(123);
345+
expect(typedSession!.mySessionInfo.floatingNumberField).toBe(123.456);
346+
expect(typedSession!.mySessionInfo.someBooleanField).toBe(true);
347+
expect(typedSession!.mySessionInfo.someOtherBooleanField).toBe(false);
348+
expect(typedSession!.mySessionInfo.someObjectField).toBeDefined();
349+
expect(typedSession!.mySessionInfo.someObjectField.nestedField).toBe(
350+
'nested value',
351+
);
352+
expect(typedSession!.mySessionInfo.someUndefinedField).toBeUndefined();
353+
expect(typedSession!.mySessionInfo.someNullField).toBeNull();
354+
355+
done();
356+
});
357+
})
358+
.catch((err) => {
359+
done(err);
360+
});
361+
},
362+
);
363+
});
206364
});
207365
});

src/dynamodb-store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
1010
import Debug from 'debug';
1111
import { promisify } from 'util';
12+
import { deepReplaceDatesWithISOStrings } from './deep-replace-dates-with-strings';
1213

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

0 commit comments

Comments
 (0)