From 1e8ea64017430a2ce5ea197678bd78713ad77ecd Mon Sep 17 00:00:00 2001 From: Josh McFarlin <32019743+Josh-McFarlin@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:14:59 -0700 Subject: [PATCH] feat: Use uuid v7 in stub database adapter (#234) --- packages/entity/package.json | 3 +- .../src/utils/testing/StubDatabaseAdapter.ts | 4 +- .../__tests__/StubDatabaseAdapter-test.ts | 40 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/entity/package.json b/packages/entity/package.json index 1fdc0e986..b4e9c9d0a 100644 --- a/packages/entity/package.json +++ b/packages/entity/package.json @@ -31,6 +31,7 @@ "dataloader": "^2.0.0", "es6-error": "^4.1.1", "invariant": "^2.2.4", - "uuid": "^8.3.0" + "uuid": "^8.3.0", + "uuidv7": "^1.0.0" } } diff --git a/packages/entity/src/utils/testing/StubDatabaseAdapter.ts b/packages/entity/src/utils/testing/StubDatabaseAdapter.ts index 1fdec0bb3..9ad302eb9 100644 --- a/packages/entity/src/utils/testing/StubDatabaseAdapter.ts +++ b/packages/entity/src/utils/testing/StubDatabaseAdapter.ts @@ -1,5 +1,5 @@ import invariant from 'invariant'; -import { v4 as uuidv4 } from 'uuid'; +import { uuidv7 } from 'uuidv7'; import EntityConfiguration from '../../EntityConfiguration'; import EntityDatabaseAdapter, { @@ -163,7 +163,7 @@ export default class StubDatabaseAdapter extends EntityDatabaseAdapter { `No schema field found for ${String(this.entityConfiguration2.idField)}` ); if (idSchemaField instanceof StringField) { - return uuidv4(); + return uuidv7(); } else if (idSchemaField instanceof IntField) { return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); } else { diff --git a/packages/entity/src/utils/testing/__tests__/StubDatabaseAdapter-test.ts b/packages/entity/src/utils/testing/__tests__/StubDatabaseAdapter-test.ts index 7f64256b4..c06df3a07 100644 --- a/packages/entity/src/utils/testing/__tests__/StubDatabaseAdapter-test.ts +++ b/packages/entity/src/utils/testing/__tests__/StubDatabaseAdapter-test.ts @@ -300,6 +300,26 @@ describe(StubDatabaseAdapter, () => { databaseAdapter.getObjectCollectionForTable(testEntityConfiguration.tableName) ).toHaveLength(1); }); + + it('inserts a record with valid v7 id', async () => { + const expectedTime = new Date('2024-06-03T20:16:33.761Z'); + + jest.useFakeTimers({ + now: expectedTime, + }); + + const queryContext = instance(mock(EntityQueryContext)); + const databaseAdapter = new StubDatabaseAdapter( + testEntityConfiguration, + new Map() + ); + const result = await databaseAdapter.insertAsync(queryContext, { + stringField: 'hello', + }); + + const ts = getTimeFromUUIDv7(result.customIdField); + expect(ts).toEqual(expectedTime); + }); }); describe('updateAsync', () => { @@ -506,3 +526,23 @@ describe(StubDatabaseAdapter, () => { }); }); }); + +const UUIDV7_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Returns the Date object encoded in the first 48 bits of the given UUIDv7. + * @throws TypeError if the UUID is not version 7 + */ +function getTimeFromUUIDv7(uuid: string): Date { + if (!UUIDV7_REGEX.test(uuid)) { + throw new TypeError(`UUID must be version 7 to get its timestamp`); + } + + // The first 48 bits = 12 hex characters of the UUID encode the timestamp in big endian + const hexCharacters = uuid.replaceAll('-', '').split('', 12); + const milliseconds = hexCharacters.reduce( + (milliseconds, character) => milliseconds * 16 + parseInt(character, 16), + 0 + ); + return new Date(milliseconds); +}