import { randomUUID } from 'crypto'; import { Kysely, type CompiledQuery, type QueryResult } from 'kysely'; import { type Dependencies } from '../../iocContainer/index.js'; import { type MockedFn } from '../../test/mockHelpers/jestMocks.js'; import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; import { safePick } from '../../utils/misc.js'; import { makeFetchUserSubmissionStatistics } from './fetchUserSubmissionStatistics.js'; describe('fetchUserSubmissionStatistics', () => { let warehouseMock: MockedFn< (it: CompiledQuery) => Promise> >; let sut: ReturnType; beforeEach(() => { // This mutation is safe (while we're not running tests concurrently) as // it's local to the test suite. Consider using the `makeTestWithFixture` // helper instead to make a local copy of this state for each test. warehouseMock = jest.fn(async (_it) => Promise.resolve({ rows: [] })); // This mutation is safe (while we're not running tests concurrently) as // it's local to the test suite. Consider using the `makeTestWithFixture` // helper instead to make a local copy of this state for each test. const kysely = new Kysely({ dialect: makeMockWarehouseDialect(warehouseMock), }); const dialectMock: Dependencies['DataWarehouseDialect'] = { getKyselyInstance: () => kysely, destroy: jest.fn(async () => {}), }; sut = makeFetchUserSubmissionStatistics(dialectMock); }); test('should generate proper query given org + user ids only', async () => { await sut({ orgId: 'x', userItemIdentifiers: [{ id: '1', typeId: 'a' }] }); await sut({ orgId: 'x', userItemIdentifiers: [ { id: '1', typeId: 'a' }, { id: '3', typeId: 'b' }, ], }); expect(warehouseMock).toHaveBeenCalledTimes(2); const queriesRan = warehouseMock.mock.calls.map((it) => safePick(it[0], ['parameters', 'sql']), ); expect(queriesRan).toMatchInlineSnapshot(` [ { "parameters": [ "x", "1", "a", ], "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and ("USER_ID" = :2 and "USER_TYPE_ID" = :3) group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"", }, { "parameters": [ "x", "1", "a", "3", "b", ], "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5)) group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"", }, ] `); }); test('should batch queries of more than 16,000 unique user ids', async () => { const numUserIds = Math.floor(16_000 / Math.max(Math.random(), 0.05)); // some big int over 16,000 const largeUserIdList = Array.from({ length: numUserIds }, (_) => ({ id: randomUUID(), typeId: randomUUID(), })); await sut({ orgId: 'x', userItemIdentifiers: largeUserIdList }); expect(warehouseMock.mock.calls.length).toBeGreaterThan(1); }); test('should generate proper query given user/org ids + date filters', async () => { await sut({ orgId: 'x', userItemIdentifiers: [{ id: '1', typeId: 'a' }], startTime: new Date('2020-01-01T00:00Z'), }); await sut({ orgId: 'x', userItemIdentifiers: [ { id: '1', typeId: 'a' }, { id: '3', typeId: 'b' }, ], endTime: new Date('2020-01-01T00:00:00Z'), }); await sut({ orgId: 'x', userItemIdentifiers: [ { id: '1', typeId: 'a' }, { id: '3', typeId: 'b' }, ], endTime: new Date('2020-01-01T00:00:00Z'), startTime: new Date('2020-02-01T00:00:00Z'), }); expect(warehouseMock).toHaveBeenCalledTimes(3); const queriesRan = warehouseMock.mock.calls.map((it) => safePick(it[0], ['parameters', 'sql']), ); expect(queriesRan).toMatchInlineSnapshot(` [ { "parameters": [ "x", "1", "a", 2020-01-01T00:00:00.000Z, ], "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and ("USER_ID" = :2 and "USER_TYPE_ID" = :3) and "TS_START_INCLUSIVE" >= :4 group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"", }, { "parameters": [ "x", "1", "a", "3", "b", 2020-01-01T00:00:00.000Z, ], "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5)) and "TS_END_EXCLUSIVE" <= :6 group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"", }, { "parameters": [ "x", "1", "a", "3", "b", 2020-02-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, ], "sql": "select "USER_ID" as "userId", "USER_TYPE_ID" as "userTypeId", "ITEM_TYPE_ID" as "itemTypeId", sum("NUM_SUBMISSIONS") as "numSubmissions" from "USER_STATISTICS_SERVICE"."SUBMISSION_STATS" where "ORG_ID" = :1 and (("USER_ID" = :2 and "USER_TYPE_ID" = :3) or ("USER_ID" = :4 and "USER_TYPE_ID" = :5)) and "TS_START_INCLUSIVE" >= :6 and "TS_END_EXCLUSIVE" <= :7 group by "USER_ID", "USER_TYPE_ID", "ITEM_TYPE_ID"", }, ] `); }); });