-
Notifications
You must be signed in to change notification settings - Fork 0
feat(node-sql): add SQL-based function template with example #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
+4
to
+17
|
||||||||||||||||||||||||||||
| 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(), | |
| })), | |
| })); |
| 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" | ||||||||
|
||||||||
| "description": "Example function using node-sql template for direct PostgreSQL access" | |
| "description": "Example function using node-sql template for direct PostgreSQL access", | |
| "port": 8085 |
| 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
AI
Apr 24, 2026
There was a problem hiding this comment.
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.
| 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
AI
Apr 24, 2026
There was a problem hiding this comment.
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
AI
Apr 24, 2026
There was a problem hiding this comment.
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.
| 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', |
There was a problem hiding this comment.
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 usepg(it only usescontext.withUserContext). The mock adds noise and can hide accidental dependencies; consider removing it and instead asserting thewithUserContextcallback callsclient.querywith the expected SQL.