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
88 changes: 88 additions & 0 deletions functions/sql-example/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const mockQuery = jest.fn();
const mockRelease = jest.fn();

jest.mock('pg', () => ({
Pool: jest.fn().mockImplementation(() => ({
connect: jest.fn().mockResolvedValue({
query: mockQuery,
release: mockRelease,
}),
})),
Client: jest.fn().mockImplementation(() => ({
connect: jest.fn(),
query: mockQuery,
end: jest.fn(),
})),
}));
Comment on lines +1 to +16
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This test mocks pg, but the handler under test doesn’t import or use pg (it only uses context.withUserContext). The mock adds noise and can hide accidental dependencies; consider removing it and instead asserting the withUserContext callback calls client.query with the expected SQL.

Copilot uses AI. Check for mistakes.

Comment on lines +4 to +17
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The jest.mock('pg', ...) block is unused in these tests: the handler doesn’t import pg, and the withUserContext/client are fully mocked via createMockContext(). Removing this mock would simplify the test and avoid implying that the handler is responsible for pool/client creation.

Suggested change
jest.mock('pg', () => ({
Pool: jest.fn().mockImplementation(() => ({
connect: jest.fn().mockResolvedValue({
query: mockQuery,
release: mockRelease,
}),
})),
Client: jest.fn().mockImplementation(() => ({
connect: jest.fn(),
query: mockQuery,
end: jest.fn(),
})),
}));

Copilot uses AI. Check for mistakes.
const createMockContext = () => {
const mockClient = {
query: mockQuery,
release: mockRelease,
};

return {
job: {
jobId: 'test-job-id',
workerId: 'test-worker',
databaseId: 'test-db',
},
pool: {
connect: jest.fn().mockResolvedValue(mockClient),
},
withUserContext: jest.fn(async (_actorId: string | undefined, fn: (client: typeof mockClient) => Promise<unknown>) => {
return fn(mockClient);
}),
log: {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
},
env: {},
};
};

const loadHandler = () => {
const mod = require('../handler');
return mod.default ?? mod;
};

describe('sql-example handler', () => {
beforeEach(() => {
jest.clearAllMocks();
mockQuery.mockReset();
});

it('should execute default query (SELECT version()) when no query provided', async () => {
const handler = loadHandler();
const context = createMockContext();
mockQuery.mockResolvedValueOnce({ rows: [{ version: 'PostgreSQL 15.0' }] });

const result = await handler({}, context);

expect(result.success).toBe(true);
expect(result.message).toBe('Query executed successfully');
expect(context.withUserContext).toHaveBeenCalledWith(undefined, expect.any(Function));
});

it('should execute custom query', async () => {
const handler = loadHandler();
const context = createMockContext();
mockQuery.mockResolvedValueOnce({ rows: [{ count: 5 }] });

const result = await handler({ query: 'SELECT count(*) FROM users' }, context);

expect(result.success).toBe(true);
expect(result.data).toEqual([{ count: 5 }]);
});

it('should pass actor_id to withUserContext', async () => {
const handler = loadHandler();
const context = createMockContext();
mockQuery.mockResolvedValueOnce({ rows: [] });

await handler({ actor_id: 'user-123' }, context);

expect(context.withUserContext).toHaveBeenCalledWith('user-123', expect.any(Function));
});
});
6 changes: 6 additions & 0 deletions functions/sql-example/handler.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "sql-example",
"version": "1.0.0",
"type": "node-sql",
"description": "Example function using node-sql template for direct PostgreSQL access"
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

handler.json omits an explicit port, but local dev wiring hardcodes 8085 for sql-example (skaffold profile and job-service function registry). To avoid port drift when new functions are added (generate.ts auto-assigns ports), set "port": 8085 here to make the assignment stable.

Suggested change
"description": "Example function using node-sql template for direct PostgreSQL access"
"description": "Example function using node-sql template for direct PostgreSQL access",
"port": 8085

Copilot uses AI. Check for mistakes.
}
34 changes: 34 additions & 0 deletions functions/sql-example/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { FunctionHandler } from './types';

Comment on lines +1 to +2
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

handler.ts imports ./types, but there is no functions/sql-example/types.ts in the PR. This makes the handler (and the unit test that requires it) fail to load. Either add a types.ts alongside the handler for tests, or switch to importing the handler/context types from a real package (as other functions do) so the functions/ source is self-contained.

Suggested change
import type { FunctionHandler } from './types';
type QueryResult = {
rows: unknown[];
};
type QueryClient = {
query: (query: string) => Promise<QueryResult>;
};
type Logger = {
info: (message: string, meta?: Record<string, unknown>) => void;
};
type FunctionContext = {
log: Logger;
withUserContext: <T>(
actorId: string | undefined,
callback: (client: QueryClient) => Promise<T>
) => Promise<T>;
};
type FunctionHandler<TParams, TResult> = (
params: TParams,
context: FunctionContext
) => Promise<TResult>;

Copilot uses AI. Check for mistakes.
type Params = {
query?: string;
actor_id?: string;
};

type Result = {
success: boolean;
message: string;
data?: unknown;
};

const handler: FunctionHandler<Params, Result> = async (params, context) => {
const { log, withUserContext } = context;
const { query = 'SELECT version()', actor_id } = params;

log.info('[sql-example] Executing query', { query, actor_id });

const result = await withUserContext(actor_id, async (client) => {
const res = await client.query(query);
Comment on lines +14 to +21
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This function executes arbitrary SQL passed in the job payload (client.query(query)). If job payloads can be influenced by users/tenants, this becomes a direct SQL injection / privilege escalation vector (including multi-statement ; queries). For an example function, consider hardcoding a safe query (or allowlisting known demo queries) and reject anything else.

Suggested change
const handler: FunctionHandler<Params, Result> = async (params, context) => {
const { log, withUserContext } = context;
const { query = 'SELECT version()', actor_id } = params;
log.info('[sql-example] Executing query', { query, actor_id });
const result = await withUserContext(actor_id, async (client) => {
const res = await client.query(query);
const SAFE_QUERIES: Record<string, string> = {
'SELECT version()': 'SELECT version()',
version: 'SELECT version()',
};
const handler: FunctionHandler<Params, Result> = async (params, context) => {
const { log, withUserContext } = context;
const { query: requestedQuery = 'SELECT version()', actor_id } = params;
const safeQuery = SAFE_QUERIES[requestedQuery];
if (!safeQuery) {
log.warn('[sql-example] Rejected unsupported query', {
requestedQuery,
actor_id,
});
return {
success: false,
message: 'Unsupported query. Only predefined demo queries are allowed.',
};
}
log.info('[sql-example] Executing allowlisted query', {
requestedQuery,
actor_id,
});
const result = await withUserContext(actor_id, async (client) => {
const res = await client.query(safeQuery);

Copilot uses AI. Check for mistakes.
return res.rows;
Comment on lines +16 to +22
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This example handler executes a raw SQL string from job payload (client.query(query)), which enables arbitrary SQL execution if a caller can enqueue/modify jobs. Even for an example function, this is a high-risk footgun once deployed via the job service registry. Prefer using a fixed demonstration query (e.g. SELECT version()) or a strict allowlist/parameterized queries only, and avoid accepting arbitrary SQL from untrusted input.

Copilot uses AI. Check for mistakes.
});

log.info('[sql-example] Query complete', { rowCount: result.length });

return {
success: true,
message: 'Query executed successfully',
Comment on lines +14 to +29
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This function executes an arbitrary SQL string from job payload (params.query). Since jobs can be user-influenced in many systems, this is effectively a remote SQL execution endpoint and a strong footgun for copy/paste. Suggestion: change the example to a fixed query (or an allowlisted set of queries) and demonstrate parameterized queries instead of accepting raw SQL.

Suggested change
const handler: FunctionHandler<Params, Result> = async (params, context) => {
const { log, withUserContext } = context;
const { query = 'SELECT version()', actor_id } = params;
log.info('[sql-example] Executing query', { query, actor_id });
const result = await withUserContext(actor_id, async (client) => {
const res = await client.query(query);
return res.rows;
});
log.info('[sql-example] Query complete', { rowCount: result.length });
return {
success: true,
message: 'Query executed successfully',
type AllowedQuery = {
key: 'version';
text: string;
values: unknown[];
};
const resolveAllowedQuery = (query?: string): AllowedQuery => {
const normalizedQuery = query?.trim();
if (normalizedQuery == null || normalizedQuery === '' || normalizedQuery === 'SELECT version()') {
return {
key: 'version',
text: 'SELECT version()',
values: [],
};
}
throw new Error('Unsupported query. This example only allows the version query.');
};
const handler: FunctionHandler<Params, Result> = async (params, context) => {
const { log, withUserContext } = context;
const { actor_id } = params;
const allowedQuery = resolveAllowedQuery(params.query);
log.info('[sql-example] Executing allowlisted query', {
queryKey: allowedQuery.key,
actor_id,
});
const result = await withUserContext(actor_id, async (client) => {
const res = await client.query(allowedQuery.text, allowedQuery.values);
return res.rows;
});
log.info('[sql-example] Query complete', {
queryKey: allowedQuery.key,
rowCount: result.length,
});
return {
success: true,
message: 'Allowlisted query executed successfully',

Copilot uses AI. Check for mistakes.
data: result,
};
};

export default handler;
Loading
Loading