bfa50f566e
Bumps [@clickhouse/client](https://github.com/ClickHouse/clickhouse-js) from 1.11.0 to 1.18.1. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/ClickHouse/clickhouse-js/releases"><code>@clickhouse/client</code>'s releases</a>.</em></p> <blockquote> <h2>1.18.1</h2> <h2>Improvements</h2> <ul> <li>Setting <code>log.level</code> default value to <code>ClickHouseLogLevel.WARN</code> instead of <code>ClickHouseLogLevel.OFF</code> to provide better visibility into potential issues without overwhelming users with too much information by default.</li> </ul> <pre lang="ts"><code>const client = createClient({ // ... log: { level: ClickHouseLogLevel.WARN, // default is now ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF }, }) </code></pre> <ul> <li>Logging is now lazy, which means that the log messages will only be constructed if the log level is appropriate for the message. This can improve performance in cases where constructing the log message is expensive, and the log level is set to ignore such messages. See <code>ClickHouseLogLevel</code> enum for the complete list of log levels. (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/520">#520</a>)</li> </ul> <pre lang="ts"><code>const client = createClient({ // ... log: { level: ClickHouseLogLevel.TRACE, // to log everything available down to the network level events }, }) </code></pre> <ul> <li>Enhanced the logging of the HTTP request / socket lifecycle with additional trace messages and context such as Connection ID (UUID) and Request ID and Socket ID that embed the connection ID for ease of tracing the logs of a particular request across the connection lifecycle. To enable such logs, set the <code>log.level</code> config option to <code>ClickHouseLogLevel.TRACE</code>. (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li> </ul> <pre lang="console"><code>[2026-02-25T09:19:13.511Z][TRACE][@clickhouse/client][Connection] Insert: received 'close' event, 'free' listener removed Arguments: { operation: 'Insert', connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c', query_id: '9dfda627-39a2-41a6-9fc9-8f8716574826', request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:3', socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2', event: 'close' } [2026-02-25T09:19:13.502Z][TRACE][@clickhouse/client][Connection] Query: reusing socket Arguments: { operation: 'Query', connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c', query_id: 'ad0127e8-b1c7-4ed6-9681-c0162f7a0ea9', request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:4', socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2', usage_count: 1 } </code></pre> <ul> <li>A step towards structured logging: the client now passes rich context to the logger <code>args</code> parameter (e.g. <code>connection_id</code>, <code>query_id</code>, <code>request_id</code>, <code>socket_id</code>). (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/ClickHouse/clickhouse-js/blob/main/CHANGELOG.md"><code>@clickhouse/client</code>'s changelog</a>.</em></p> <blockquote> <h1>1.18.1</h1> <h2>Improvements</h2> <ul> <li>Setting <code>log.level</code> default value to <code>ClickHouseLogLevel.WARN</code> instead of <code>ClickHouseLogLevel.OFF</code> to provide better visibility into potential issues without overwhelming users with too much information by default.</li> </ul> <pre lang="ts"><code>const client = createClient({ // ... log: { level: ClickHouseLogLevel.WARN, // default is now ClickHouseLogLevel.WARN instead of ClickHouseLogLevel.OFF }, }) </code></pre> <ul> <li>Logging is now lazy, which means that the log messages will only be constructed if the log level is appropriate for the message. This can improve performance in cases where constructing the log message is expensive, and the log level is set to ignore such messages. See <code>ClickHouseLogLevel</code> enum for the complete list of log levels. (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/520">#520</a>)</li> </ul> <pre lang="ts"><code>const client = createClient({ // ... log: { level: ClickHouseLogLevel.TRACE, // to log everything available down to the network level events }, }) </code></pre> <ul> <li>Enhanced the logging of the HTTP request / socket lifecycle with additional trace messages and context such as Connection ID (UUID) and Request ID and Socket ID that embed the connection ID for ease of tracing the logs of a particular request across the connection lifecycle. To enable such logs, set the <code>log.level</code> config option to <code>ClickHouseLogLevel.TRACE</code>. (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li> </ul> <pre lang="console"><code>[2026-02-25T09:19:13.511Z][TRACE][@clickhouse/client][Connection] Insert: received 'close' event, 'free' listener removed Arguments: { operation: 'Insert', connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c', query_id: '9dfda627-39a2-41a6-9fc9-8f8716574826', request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:3', socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2', event: 'close' } [2026-02-25T09:19:13.502Z][TRACE][@clickhouse/client][Connection] Query: reusing socket Arguments: { operation: 'Query', connection_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c', query_id: 'ad0127e8-b1c7-4ed6-9681-c0162f7a0ea9', request_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:4', socket_id: 'da3c9796-5dc5-46ef-83b0-ed1f4422094c:2', usage_count: 1 } </code></pre> <ul> <li>A step towards structured logging: the client now passes rich context to the logger <code>args</code> parameter (e.g. <code>connection_id</code>, <code>query_id</code>, <code>request_id</code>, <code>socket_id</code>). (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/cbdd7bf20904626956e0ff7808d17015813400c1"><code>cbdd7bf</code></a> Release 1.18.1 (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/590">#590</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/c9f61ebb3a2ec6201f87417e30c4fc4271451ae8"><code>c9f61eb</code></a> Beta 1.18.0 (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/588">#588</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/d0f67b71ef896d47fc3d8d0942612ced22aa79dc"><code>d0f67b7</code></a> Split public and internal <code>drainStream</code> and cover with tests (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/578">#578</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/535e9b726e328ce8468c159f935c27910731f4bb"><code>535e9b7</code></a> Remove <code>unsafeLogUnredactedQueries</code> for now (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/580">#580</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/44e73c73019a3956c1fac67ce0f4f170b3a4f19a"><code>44e73c7</code></a> Default log level to <code>WARN</code> (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/581">#581</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/5146fbc13e5c23d08e2cc5773bea0adf58d83a5c"><code>5146fbc</code></a> Focus AI on security and API stability (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/579">#579</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/b7b1d8d7ffe9b6786c9e883379d3edc5a5ed5c58"><code>b7b1d8d</code></a> Trivial E2E test against <code>beta</code> (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/577">#577</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/761e29ebb5d1bd7a107d2535b385b6565b7120f5"><code>761e29e</code></a> Structured logs, part 1 (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/576">#576</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/fd23dd7fc9e91ff810a7bb45984a24682ba25482"><code>fd23dd7</code></a> Provide more context in logs for connection and request handling (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/567">#567</a>)</li> <li><a href="https://github.com/ClickHouse/clickhouse-js/commit/a7866e72e356244cae9d20d9fef38a6aafe68ba8"><code>a7866e7</code></a> Adjusting CI DevX (<a href="https://redirect.github.com/ClickHouse/clickhouse-js/issues/574">#574</a>)</li> <li>Additional commits viewable in <a href="https://github.com/ClickHouse/clickhouse-js/compare/1.11.0...1.18.1">compare view</a></li> </ul> </details> <details> <summary>Maintainer changes</summary> <p>This version was pushed to npm by <a href="https://www.npmjs.com/~4b819b88c84b">4b819b88c84b</a>, a new releaser for <code>@clickhouse/client</code> since your current version.</p> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
538 lines
16 KiB
TypeScript
538 lines
16 KiB
TypeScript
import process from 'process';
|
|
|
|
import {
|
|
type ClickHouseClient,
|
|
ClickHouseLogLevel,
|
|
createClient,
|
|
} from '@clickhouse/client';
|
|
import request from 'supertest';
|
|
|
|
const client = request(`http://localhost:${APP_PORT}`);
|
|
|
|
describe('Event Logs (integration)', () => {
|
|
let clickHouseClient: ClickHouseClient;
|
|
const testWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
|
const testUserWorkspaceId = '20202020-3957-45c9-be39-337dc4d9100a';
|
|
|
|
beforeAll(async () => {
|
|
jest.useRealTimers();
|
|
|
|
clickHouseClient = createClient({
|
|
url: process.env.CLICKHOUSE_URL,
|
|
clickhouse_settings: {
|
|
allow_experimental_json_type: 1,
|
|
},
|
|
log: { level: ClickHouseLogLevel.OFF },
|
|
});
|
|
|
|
await seedTestData();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await cleanupTestData();
|
|
|
|
if (clickHouseClient) {
|
|
await clickHouseClient.close();
|
|
}
|
|
});
|
|
|
|
const seedTestData = async () => {
|
|
const now = new Date();
|
|
|
|
const pageviewRecords = Array.from({ length: 25 }, (_, i) => ({
|
|
workspaceId: testWorkspaceId,
|
|
userId: testUserWorkspaceId,
|
|
name: i % 2 === 0 ? 'settings/profile' : 'objects/companies',
|
|
timestamp: new Date(now.getTime() - i * 60000)
|
|
.toISOString()
|
|
.replace('T', ' ')
|
|
.replace('Z', ''),
|
|
properties: { path: `/settings/${i}` },
|
|
}));
|
|
|
|
const workspaceEventRecords = Array.from({ length: 15 }, (_, i) => ({
|
|
workspaceId: testWorkspaceId,
|
|
userId: testUserWorkspaceId,
|
|
event:
|
|
i % 3 === 0
|
|
? 'user.login'
|
|
: i % 3 === 1
|
|
? 'user.logout'
|
|
: 'settings.updated',
|
|
timestamp: new Date(now.getTime() - i * 120000)
|
|
.toISOString()
|
|
.replace('T', ' ')
|
|
.replace('Z', ''),
|
|
properties: { action: `action_${i}` },
|
|
}));
|
|
|
|
const objectEventRecords = Array.from({ length: 20 }, (_, i) => ({
|
|
workspaceId: testWorkspaceId,
|
|
userId: testUserWorkspaceId,
|
|
event: i % 2 === 0 ? 'company.created' : 'company.updated',
|
|
timestamp: new Date(now.getTime() - i * 90000)
|
|
.toISOString()
|
|
.replace('T', ' ')
|
|
.replace('Z', ''),
|
|
properties: { field: `field_${i}` },
|
|
recordId: `record-${i}`,
|
|
objectMetadataId: i % 2 === 0 ? 'object-meta-1' : 'object-meta-2',
|
|
isCustom: i % 4 === 0,
|
|
}));
|
|
|
|
await clickHouseClient.insert({
|
|
table: 'pageview',
|
|
values: pageviewRecords,
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
await clickHouseClient.insert({
|
|
table: 'workspaceEvent',
|
|
values: workspaceEventRecords,
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
await clickHouseClient.insert({
|
|
table: 'objectEvent',
|
|
values: objectEventRecords,
|
|
format: 'JSONEachRow',
|
|
});
|
|
|
|
// Wait for ClickHouse async inserts to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
};
|
|
|
|
const cleanupTestData = async () => {
|
|
try {
|
|
await clickHouseClient.command({
|
|
query: `ALTER TABLE pageview DELETE WHERE workspaceId = '${testWorkspaceId}'`,
|
|
});
|
|
await clickHouseClient.command({
|
|
query: `ALTER TABLE workspaceEvent DELETE WHERE workspaceId = '${testWorkspaceId}'`,
|
|
});
|
|
await clickHouseClient.command({
|
|
query: `ALTER TABLE objectEvent DELETE WHERE workspaceId = '${testWorkspaceId}'`,
|
|
});
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
};
|
|
|
|
const makeEventLogsQuery = (
|
|
input: Record<string, unknown>,
|
|
token = APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
) => {
|
|
return client
|
|
.post('/metadata')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({
|
|
query: `
|
|
query EventLogs($input: EventLogQueryInput!) {
|
|
eventLogs(input: $input) {
|
|
records {
|
|
event
|
|
timestamp
|
|
userId
|
|
properties
|
|
recordId
|
|
objectMetadataId
|
|
isCustom
|
|
}
|
|
totalCount
|
|
pageInfo {
|
|
endCursor
|
|
hasNextPage
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
variables: { input },
|
|
});
|
|
};
|
|
|
|
describe('querying different tables', () => {
|
|
it('should query PAGEVIEW table', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 10,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.eventLogs).toBeDefined();
|
|
expect(response.body.data.eventLogs.records.length).toBeGreaterThan(0);
|
|
expect(response.body.data.eventLogs.totalCount).toBeGreaterThanOrEqual(
|
|
response.body.data.eventLogs.records.length,
|
|
);
|
|
});
|
|
|
|
it('should query WORKSPACE_EVENT table', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'WORKSPACE_EVENT',
|
|
first: 10,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.eventLogs).toBeDefined();
|
|
expect(response.body.data.eventLogs.records.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should query OBJECT_EVENT table', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'OBJECT_EVENT',
|
|
first: 10,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeUndefined();
|
|
expect(response.body.data.eventLogs).toBeDefined();
|
|
expect(response.body.data.eventLogs.records.length).toBeGreaterThan(0);
|
|
|
|
const record = response.body.data.eventLogs.records[0];
|
|
|
|
expect(record.recordId).toBeDefined();
|
|
expect(record.objectMetadataId).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('pagination', () => {
|
|
it('should return hasNextPage=true when more records exist', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 5,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.eventLogs.records.length).toBe(5);
|
|
expect(response.body.data.eventLogs.pageInfo.hasNextPage).toBe(true);
|
|
expect(response.body.data.eventLogs.pageInfo.endCursor).toBeDefined();
|
|
});
|
|
|
|
it('should fetch next page using cursor', async () => {
|
|
const firstPage = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 5,
|
|
});
|
|
|
|
expect(firstPage.body.data.eventLogs.pageInfo.hasNextPage).toBe(true);
|
|
const cursor = firstPage.body.data.eventLogs.pageInfo.endCursor;
|
|
|
|
const secondPage = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 5,
|
|
after: cursor,
|
|
});
|
|
|
|
expect(secondPage.status).toBe(200);
|
|
expect(secondPage.body.data.eventLogs.records.length).toBeGreaterThan(0);
|
|
|
|
const firstPageTimestamps = firstPage.body.data.eventLogs.records.map(
|
|
(r: { timestamp: string }) => r.timestamp,
|
|
);
|
|
const secondPageTimestamps = secondPage.body.data.eventLogs.records.map(
|
|
(r: { timestamp: string }) => r.timestamp,
|
|
);
|
|
|
|
const lastFirstPage = new Date(
|
|
firstPageTimestamps[firstPageTimestamps.length - 1],
|
|
);
|
|
const firstSecondPage = new Date(secondPageTimestamps[0]);
|
|
|
|
expect(lastFirstPage.getTime()).toBeGreaterThanOrEqual(
|
|
firstSecondPage.getTime(),
|
|
);
|
|
});
|
|
|
|
it('should return correct totalCount regardless of page size', async () => {
|
|
const smallPage = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 3,
|
|
});
|
|
|
|
const largePage = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 100,
|
|
});
|
|
|
|
expect(smallPage.body.data.eventLogs.totalCount).toBe(
|
|
largePage.body.data.eventLogs.totalCount,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('filters', () => {
|
|
describe('eventType filter', () => {
|
|
it('should filter by event type (partial match)', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 50,
|
|
filters: {
|
|
eventType: 'settings',
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.eventLogs.records.length).toBeGreaterThan(0);
|
|
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { event: string }) => {
|
|
expect(record.event.toLowerCase()).toContain('settings');
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should filter by event type case-insensitively', async () => {
|
|
const lowerCase = await makeEventLogsQuery({
|
|
table: 'WORKSPACE_EVENT',
|
|
first: 50,
|
|
filters: { eventType: 'login' },
|
|
});
|
|
|
|
const upperCase = await makeEventLogsQuery({
|
|
table: 'WORKSPACE_EVENT',
|
|
first: 50,
|
|
filters: { eventType: 'LOGIN' },
|
|
});
|
|
|
|
expect(lowerCase.body.data.eventLogs.totalCount).toBe(
|
|
upperCase.body.data.eventLogs.totalCount,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('dateRange filter', () => {
|
|
it('should filter by start date', async () => {
|
|
const now = new Date();
|
|
const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000);
|
|
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 50,
|
|
filters: {
|
|
dateRange: {
|
|
start: tenMinutesAgo.toISOString(),
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { timestamp: string }) => {
|
|
expect(new Date(record.timestamp).getTime()).toBeGreaterThanOrEqual(
|
|
tenMinutesAgo.getTime(),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should filter by end date', async () => {
|
|
const now = new Date();
|
|
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
|
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 50,
|
|
filters: {
|
|
dateRange: {
|
|
end: fiveMinutesAgo.toISOString(),
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { timestamp: string }) => {
|
|
expect(new Date(record.timestamp).getTime()).toBeLessThanOrEqual(
|
|
fiveMinutesAgo.getTime(),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should filter by date range (start and end)', async () => {
|
|
const now = new Date();
|
|
const twentyMinutesAgo = new Date(now.getTime() - 20 * 60 * 1000);
|
|
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
|
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 50,
|
|
filters: {
|
|
dateRange: {
|
|
start: twentyMinutesAgo.toISOString(),
|
|
end: fiveMinutesAgo.toISOString(),
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { timestamp: string }) => {
|
|
const timestamp = new Date(record.timestamp).getTime();
|
|
|
|
expect(timestamp).toBeGreaterThanOrEqual(
|
|
twentyMinutesAgo.getTime(),
|
|
);
|
|
expect(timestamp).toBeLessThanOrEqual(fiveMinutesAgo.getTime());
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('object event specific filters', () => {
|
|
it('should filter by recordId', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'OBJECT_EVENT',
|
|
first: 50,
|
|
filters: {
|
|
recordId: 'record-0',
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
if (response.body.data.eventLogs.records.length > 0) {
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { recordId: string }) => {
|
|
expect(record.recordId).toBe('record-0');
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
it('should filter by objectMetadataId', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'OBJECT_EVENT',
|
|
first: 50,
|
|
filters: {
|
|
objectMetadataId: 'object-meta-1',
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
if (response.body.data.eventLogs.records.length > 0) {
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { objectMetadataId: string }) => {
|
|
expect(record.objectMetadataId).toBe('object-meta-1');
|
|
},
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('combined filters', () => {
|
|
it('should apply multiple filters together', async () => {
|
|
const now = new Date();
|
|
const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000);
|
|
|
|
const response = await makeEventLogsQuery({
|
|
table: 'WORKSPACE_EVENT',
|
|
first: 50,
|
|
filters: {
|
|
eventType: 'login',
|
|
dateRange: {
|
|
start: thirtyMinutesAgo.toISOString(),
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
response.body.data.eventLogs.records.forEach(
|
|
(record: { event: string; timestamp: string }) => {
|
|
expect(record.event.toLowerCase()).toContain('login');
|
|
expect(new Date(record.timestamp).getTime()).toBeGreaterThanOrEqual(
|
|
thirtyMinutesAgo.getTime(),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('record structure', () => {
|
|
it('should return properly structured pageview records', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'PAGEVIEW',
|
|
first: 1,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
const record = response.body.data.eventLogs.records[0];
|
|
|
|
expect(record).toHaveProperty('event');
|
|
expect(record).toHaveProperty('timestamp');
|
|
expect(record).toHaveProperty('userId');
|
|
expect(record).toHaveProperty('properties');
|
|
expect(typeof record.event).toBe('string');
|
|
expect(typeof record.timestamp).toBe('string');
|
|
});
|
|
|
|
it('should return properly structured object event records', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'OBJECT_EVENT',
|
|
first: 1,
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
const record = response.body.data.eventLogs.records[0];
|
|
|
|
expect(record).toHaveProperty('event');
|
|
expect(record).toHaveProperty('timestamp');
|
|
expect(record).toHaveProperty('recordId');
|
|
expect(record).toHaveProperty('objectMetadataId');
|
|
expect(record).toHaveProperty('isCustom');
|
|
});
|
|
});
|
|
|
|
describe('permissions', () => {
|
|
it('should deny access to member role', async () => {
|
|
const response = await makeEventLogsQuery(
|
|
{ table: 'PAGEVIEW', first: 10 },
|
|
APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.errors[0].extensions.code).toBe('FORBIDDEN');
|
|
});
|
|
|
|
it('should deny access without authentication', async () => {
|
|
const response = await client.post('/metadata').send({
|
|
query: `
|
|
query EventLogs($input: EventLogQueryInput!) {
|
|
eventLogs(input: $input) {
|
|
records { event }
|
|
totalCount
|
|
pageInfo { hasNextPage }
|
|
}
|
|
}
|
|
`,
|
|
variables: { input: { table: 'PAGEVIEW', first: 10 } },
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.errors[0].extensions.code).toBe('UNAUTHENTICATED');
|
|
});
|
|
});
|
|
|
|
describe('error handling', () => {
|
|
it('should reject invalid table name', async () => {
|
|
const response = await makeEventLogsQuery({
|
|
table: 'INVALID_TABLE' as 'PAGEVIEW',
|
|
first: 10,
|
|
});
|
|
|
|
// Invalid enum values are rejected by GraphQL validation before reaching the resolver
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors).toBeDefined();
|
|
});
|
|
});
|
|
});
|