Files
twenty/packages/twenty-server/test/integration/graphql/suites/event-logs/event-logs.integration-spec.ts
T
dependabot[bot] bfa50f566e Bump @clickhouse/client from 1.11.0 to 1.18.1 (#18410)
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 />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@clickhouse/client&package-manager=npm_and_yarn&previous-version=1.11.0&new-version=1.18.1)](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>
2026-03-05 10:11:32 +01:00

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();
});
});
});