Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions src/audit-logs/audit-logs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import fetch from 'jest-fetch-mock';
import { UnauthorizedException } from '../common/exceptions';
import { BadRequestException } from '../common/exceptions/bad-request.exception';
import { ListResponse } from '../common/interfaces';
import { mockWorkOsResponse } from '../common/utils/workos-mock-response';
import { WorkOS } from '../workos';
import {
AuditLogExport,
AuditLogExportOptions,
AuditLogExportResponse,
AuditLogSchema,
AuditLogSchemaResponse,
CreateAuditLogEventOptions,
CreateAuditLogSchemaOptions,
CreateAuditLogSchemaResponse,
Expand Down Expand Up @@ -844,4 +846,230 @@ describe('AuditLogs', () => {
});
});
});

describe('listSchemas', () => {
describe('when the api responds with a 200', () => {
it('returns a paginated list of schemas', async () => {
const workosSpy = jest.spyOn(WorkOS.prototype, 'get');

const time = new Date().toISOString();

const schemaResponse: AuditLogSchemaResponse = {
object: 'audit_log_schema',
version: 1,
targets: [
{
type: 'user',
metadata: {
type: 'object',
properties: {
user_id: { type: 'string' },
},
},
},
],
actor: {
metadata: {
type: 'object',
properties: {
actor_id: { type: 'string' },
},
},
},
metadata: {
type: 'object',
properties: {
foo: { type: 'number' },
},
},
created_at: time,
};

const listResponse: ListResponse<AuditLogSchemaResponse> = {
object: 'list',
data: [schemaResponse],
list_metadata: {
before: undefined,
after: undefined,
},
};

workosSpy.mockResolvedValueOnce(mockWorkOsResponse(200, listResponse));

const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');

const result = await workos.auditLogs.listSchemas('user.logged_in');

expect(result.data).toHaveLength(1);
// Metadata is deserialized to simplified format (same as createSchema)
expect(result.data[0]).toEqual({
object: 'audit_log_schema',
version: 1,
targets: [
{
type: 'user',
metadata: { user_id: 'string' },
},
],
actor: {
metadata: { actor_id: 'string' },
},
metadata: { foo: 'number' },
createdAt: time,
});

expect(workosSpy).toHaveBeenCalledWith(
'/audit_logs/actions/user.logged_in/schemas',
{ query: { order: 'desc' } },
);
});
});

describe('with pagination options', () => {
it('passes pagination parameters to the API', async () => {
const workosSpy = jest.spyOn(WorkOS.prototype, 'get');

const listResponse: ListResponse<AuditLogSchemaResponse> = {
object: 'list',
data: [],
list_metadata: {
before: undefined,
after: undefined,
},
};

workosSpy.mockResolvedValueOnce(mockWorkOsResponse(200, listResponse));

const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');

await workos.auditLogs.listSchemas('user.logged_in', {
limit: 10,
after: 'cursor_123',
order: 'asc',
});

expect(workosSpy).toHaveBeenCalledWith(
'/audit_logs/actions/user.logged_in/schemas',
{ query: { limit: 10, after: 'cursor_123', order: 'asc' } },
);
});
});

describe('when the api responds with a 401', () => {
it('throws an UnauthorizedException', async () => {
const workosSpy = jest.spyOn(WorkOS.prototype, 'get');

workosSpy.mockImplementationOnce(() => {
throw new UnauthorizedException('a-request-id');
});

const workos = new WorkOS('invalid apikey');

await expect(
workos.auditLogs.listSchemas('user.logged_in'),
).rejects.toThrow(UnauthorizedException);
});
});

describe('with schema without optional fields', () => {
it('returns schema with undefined actor and metadata', async () => {
const workosSpy = jest.spyOn(WorkOS.prototype, 'get');

const time = new Date().toISOString();

const schemaResponse: AuditLogSchemaResponse = {
object: 'audit_log_schema',
version: 1,
targets: [
{
type: 'document',
},
],
created_at: time,
};

const listResponse: ListResponse<AuditLogSchemaResponse> = {
object: 'list',
data: [schemaResponse],
list_metadata: {
before: undefined,
after: undefined,
},
};

workosSpy.mockResolvedValueOnce(mockWorkOsResponse(200, listResponse));

const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');

const result = await workos.auditLogs.listSchemas('document.created');

expect(result.data).toHaveLength(1);
expect(result.data[0]).toEqual({
object: 'audit_log_schema',
version: 1,
targets: [
{
type: 'document',
metadata: undefined,
},
],
actor: undefined,
metadata: undefined,
createdAt: time,
});
});
});

describe('with multiple schemas', () => {
it('returns all schemas in the response', async () => {
const workosSpy = jest.spyOn(WorkOS.prototype, 'get');

const time1 = new Date().toISOString();
const time2 = new Date(Date.now() - 1000).toISOString();

const schemaResponse1: AuditLogSchemaResponse = {
object: 'audit_log_schema',
version: 2,
targets: [{ type: 'user' }],
created_at: time1,
};

const schemaResponse2: AuditLogSchemaResponse = {
object: 'audit_log_schema',
version: 1,
targets: [{ type: 'user' }],
metadata: {
type: 'object',
properties: {
ip_address: { type: 'string' },
},
},
created_at: time2,
};

const listResponse: ListResponse<AuditLogSchemaResponse> = {
object: 'list',
data: [schemaResponse1, schemaResponse2],
list_metadata: {
before: 'cursor_before',
after: 'cursor_after',
},
};

workosSpy.mockResolvedValueOnce(mockWorkOsResponse(200, listResponse));

const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');

const result = await workos.auditLogs.listSchemas('user.logged_in');

expect(result.data).toHaveLength(2);
expect(result.data[0].version).toBe(2);
expect(result.data[1].version).toBe(1);
// Metadata is deserialized to simplified format
expect(result.data[1].metadata).toEqual({ ip_address: 'string' });
expect(result.listMetadata.before).toBe('cursor_before');
expect(result.listMetadata.after).toBe('cursor_after');
});
});
});
});
36 changes: 33 additions & 3 deletions src/audit-logs/audit-logs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { PaginationOptions } from '../common/interfaces';
import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';
import { AutoPaginatable } from '../common/utils/pagination';
import { WorkOS } from '../workos';
import {
CreateAuditLogEventOptions,
Expand All @@ -10,16 +13,19 @@ import {
} from './interfaces/audit-log-export.interface';
import {
AuditLogSchema,
CreateAuditLogSchemaOptions,
CreateAuditLogSchemaRequestOptions,
AuditLogSchemaResponse,
} from './interfaces/audit-log-schema.interface';
import {
CreateAuditLogSchemaResponse,
CreateAuditLogSchemaRequestOptions,
CreateAuditLogSchemaOptions,
} from './interfaces/create-audit-log-schema-options.interface';
import {
deserializeAuditLogExport,
deserializeAuditLogSchema,
serializeAuditLogExportOptions,
serializeCreateAuditLogEventOptions,
serializeCreateAuditLogSchemaOptions,
deserializeAuditLogSchema,
} from './serializers';

export class AuditLogs {
Expand Down Expand Up @@ -77,4 +83,28 @@ export class AuditLogs {

return deserializeAuditLogSchema(data);
}

async listSchemas(
action: string,
options?: PaginationOptions,
): Promise<AutoPaginatable<AuditLogSchema, PaginationOptions>> {
const endpoint = `/audit_logs/actions/${action}/schemas`;
Comment on lines +87 to +91
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] listSchemas should accept optional options like other list methods

listSchemas(options: ListSchemasOptions) forces callers to pass an object, even ListSchemasOptions is mostly pagination + required action. Most other list-style SDK methods take options?: ... and allow a simpler call-site / future optional expansion. Consider listSchemas(action: string, options?: PaginationOptions) or listSchemas(options: ListSchemasOptions) but make options optional with a runtime check.

(If this is intentional API design, please ignore—just flagging because it differs from the rest of the SDK pattern.)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/audit-logs/audit-logs.ts
Line: 88:92

Comment:
[P2] `listSchemas` should accept optional options like other list methods

`listSchemas(options: ListSchemasOptions)` forces callers to pass an object, even `ListSchemasOptions` is mostly pagination + required `action`. Most other list-style SDK methods take `options?: ...` and allow a simpler call-site / future optional expansion. Consider `listSchemas(action: string, options?: PaginationOptions)` or `listSchemas(options: ListSchemasOptions)` but make `options` optional with a runtime check.

(If this is intentional API design, please ignore—just flagging because it differs from the rest of the SDK pattern.)

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swaroopAkkineniWorkos This isn't a bad suggestion, since it might be a bit easier for devs to use listSchemas(action) if they're using the auto-pagination.


return new AutoPaginatable(
await fetchAndDeserialize<AuditLogSchemaResponse, AuditLogSchema>(
this.workos,
endpoint,
deserializeAuditLogSchema,
options,
),
(params: PaginationOptions) =>
fetchAndDeserialize<AuditLogSchemaResponse, AuditLogSchema>(
this.workos,
endpoint,
deserializeAuditLogSchema,
params,
),
options,
);
}
}
46 changes: 46 additions & 0 deletions src/audit-logs/interfaces/audit-log-schema.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export type AuditLogSchemaMetadata =
| Record<string, { type: 'string' | 'boolean' | 'number' }>
| undefined;

export interface AuditLogActorSchema {
metadata: Record<string, string | boolean | number>;
}

export interface AuditLogTargetSchema {
type: string;
metadata?: Record<string, string | boolean | number>;
}

export interface AuditLogSchema {
object: 'audit_log_schema';
version: number;
targets: AuditLogTargetSchema[];
actor: AuditLogActorSchema | undefined;
metadata: Record<string, string | boolean | number> | undefined;
createdAt: string;
}

interface SerializedAuditLogTargetSchema {
type: string;
metadata?: {
type: 'object';
properties: AuditLogSchemaMetadata;
};
}

export interface AuditLogSchemaResponse {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is exported publicly, this would be a breaking change. Perhaps we can alias CreateAuditLogSchemaResponse to AuditLogSchemaResponse and mark CreateAuditLogSchemaResponse as deprecated?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattgd updated. Added the alias and also leaving all the exports in CreateAuditLogSchemaResponse as is and just exporting the types from the common, new audit log interface i made.

object: 'audit_log_schema';
version: number;
targets: SerializedAuditLogTargetSchema[];
actor?: {
metadata: {
type: 'object';
properties: AuditLogSchemaMetadata;
};
};
metadata?: {
type: 'object';
properties: AuditLogSchemaMetadata;
};
created_at: string;
}
Loading