Skip to content
Closed
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
98 changes: 98 additions & 0 deletions plugins/sql-macros/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from 'vitest'
import { SqlMacrosPlugin } from './index'
import type { DataSource } from '../../src/types'

const makeInternalDataSource = (columns: string[] = []): DataSource =>
({
source: 'internal',
rpc: {
executeQuery: vi
.fn()
.mockResolvedValue(
columns.map((column_name) => ({ column_name }))
),
},
}) as unknown as DataSource

const makeExternalDataSource = (): DataSource =>
({
source: 'external',
rpc: {
executeQuery: vi.fn(),
},
}) as unknown as DataSource

describe('SqlMacrosPlugin', () => {
it('returns the original query when no data source is available', async () => {
const plugin = new SqlMacrosPlugin({ preventSelectStar: true })

const result = await plugin.beforeQuery({
sql: 'SELECT * FROM users WHERE id = ?',
params: [1],
})

expect(result).toEqual({
sql: 'SELECT * FROM users WHERE id = ?',
params: [1],
})
})

it('does not rewrite queries for external data sources', async () => {
const dataSource = makeExternalDataSource()
const plugin = new SqlMacrosPlugin({ preventSelectStar: false })

const result = await plugin.beforeQuery({
sql: 'SELECT $_exclude(email) FROM users',
dataSource,
})

expect(result.sql).toBe('SELECT $_exclude(email) FROM users')
expect(dataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('rejects SELECT star for non-admin users when the guard is enabled', async () => {
const plugin = new SqlMacrosPlugin({ preventSelectStar: true })
plugin.config = { role: 'user' } as any

await expect(
plugin.beforeQuery({
sql: 'SELECT * FROM users',
dataSource: makeInternalDataSource(),
})
).rejects.toThrow(
'SELECT * is not allowed. Please specify explicit columns.'
)
})

it('allows SELECT star for admins', async () => {
const plugin = new SqlMacrosPlugin({ preventSelectStar: true })
plugin.config = { role: 'admin' } as any

const result = await plugin.beforeQuery({
sql: 'SELECT * FROM users',
dataSource: makeInternalDataSource(),
})

expect(result.sql).toBe('SELECT * FROM users')
})

it('expands $_exclude into explicit included columns for internal SQLite sources', async () => {
const dataSource = makeInternalDataSource(['id', 'name', 'email'])
const plugin = new SqlMacrosPlugin({ preventSelectStar: false })

const result = await plugin.beforeQuery({
sql: 'SELECT $_exclude(email) FROM users',
params: ['active'],
dataSource,
})

expect(dataSource.rpc.executeQuery).toHaveBeenCalledWith({
sql: expect.stringContaining("pragma_table_info('users')"),
})
expect(result.params).toEqual(['active'])
expect(result.sql).toContain('id')
expect(result.sql).toContain('name')
expect(result.sql).not.toContain('email')
expect(result.sql).not.toContain('$_exclude')
})
})
6 changes: 4 additions & 2 deletions plugins/sql-macros/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { DataSource, QueryResult } from '../../src/types'

const parser = new (require('node-sql-parser').Parser)()

const firstStatement = (ast: any) => (Array.isArray(ast) ? ast[0] : ast)

export class SqlMacrosPlugin extends StarbasePlugin {
config?: StarbaseDBConfiguration

Expand Down Expand Up @@ -59,7 +61,7 @@ export class SqlMacrosPlugin extends StarbasePlugin {

private checkSelectStar(sql: string, params?: unknown[]): string {
try {
const ast = parser.astify(sql)[0]
const ast = firstStatement(parser.astify(sql))

// Only check SELECT statements
if (ast.type === 'select') {
Expand Down Expand Up @@ -116,7 +118,7 @@ export class SqlMacrosPlugin extends StarbasePlugin {
'$_exclude',
'__exclude'
)
const normalizedQuery = parser.astify(preparedSql)[0]
const normalizedQuery = firstStatement(parser.astify(preparedSql))

// Only process SELECT statements
if (normalizedQuery.type !== 'select') {
Expand Down