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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SqlString from 'sqlstring'
import { cast, connect, format, hex, DatabaseError } from '../dist/index'
import { cast, connect, format, hex, DatabaseError, type Cast } from '../dist/index'
import { fetch, MockAgent, setGlobalDispatcher } from 'undici'
import packageJSON from '../package.json'

Expand Down Expand Up @@ -29,7 +29,7 @@ describe('config', () => {
result: { fields: [], rows: [] }
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toEqual(`Basic ${btoa('someuser:password')}`)
expect(opts.headers['User-Agent']).toEqual(`database-js/${packageJSON.version}`)
return mockResponse
Expand All @@ -46,7 +46,7 @@ describe('config', () => {
result: { fields: [], rows: [] }
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toEqual(`Basic ${btoa('someuser:password')}`)
expect(opts.headers['User-Agent']).toEqual(`database-js/${packageJSON.version}`)
return mockResponse
Expand All @@ -61,7 +61,6 @@ describe('config', () => {
const config = { url: 'mysql://someuser:password@example.com/db' }
const connection = connect(config)
expect(connection.config).toEqual({
fetch: expect.any(Function),
host: 'example.com',
username: 'someuser',
password: 'password',
Expand Down Expand Up @@ -170,7 +169,7 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toMatch(/Basic /)
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.session).toEqual(null)
Expand All @@ -182,7 +181,7 @@ describe('execute', () => {

expect(got).toEqual(want)

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toMatch(/Basic /)
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.session).toEqual(mockSession)
Expand Down Expand Up @@ -216,7 +215,7 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toMatch(/Basic /)
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.session).toEqual(null)
Expand All @@ -228,7 +227,7 @@ describe('execute', () => {

expect(got).toEqual(want)

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toMatch(/Basic /)
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.session).toEqual(mockSession)
Expand Down Expand Up @@ -262,7 +261,7 @@ describe('execute', () => {
insertId: '0'
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
expect(opts.headers['Authorization']).toMatch(/Basic /)
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.session).toEqual(null)
Expand Down Expand Up @@ -442,7 +441,7 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.query).toEqual(want.statement)
return mockResponse
Expand Down Expand Up @@ -476,7 +475,7 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.query).toEqual(want.statement)
return mockResponse
Expand Down Expand Up @@ -510,13 +509,13 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.query).toEqual(want.statement)
return mockResponse
})

const inflate = (field, value) => (field.type === 'INT64' ? BigInt(value) : value)
const inflate: Cast = (field, value) => (field.type === 'INT64' ? BigInt(value as string) : value)
const connection = connect({ ...config, cast: inflate })
const got = await connection.execute('select 1 from dual')

Expand Down Expand Up @@ -545,13 +544,13 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.query).toEqual(want.statement)
return mockResponse
})
const connInflate = (field, value) => (field.type === 'INT64' ? 'I am a biggish int' : value)
const inflate = (field, value) => (field.type === 'INT64' ? BigInt(value) : value)
const connInflate: Cast = (field, value) => (field.type === 'INT64' ? 'I am a biggish int' : value)
const inflate: Cast = (field, value) => (field.type === 'INT64' ? BigInt(value as string) : value)
const connection = connect({ ...config, cast: inflate })
const got = await connection.execute('select 1 from dual', {}, { cast: connInflate })

Expand Down Expand Up @@ -582,7 +581,7 @@ describe('execute', () => {
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => {
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
expect(bodyObj.query).toEqual(want.statement)
return mockResponse
Expand Down
46 changes: 26 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface ExecutedQuery<T = Row<'array'> | Row<'object'>> {
time: number
}

type Fetch = (input: string, init?: Req) => Promise<Res>

type Req = {
method: string
headers: Record<string, string>
Expand All @@ -52,14 +54,15 @@ type Res = {
}

export type Cast = typeof cast
type Format = typeof format

export interface Config {
url?: string
username?: string
password?: string
host?: string
fetch?: (input: string, init?: Req) => Promise<Res>
format?: (query: string, args: any) => string
fetch?: Fetch
format?: Format
cast?: Cast
}

Expand Down Expand Up @@ -102,7 +105,7 @@ interface QueryResult {

type ExecuteAs = 'array' | 'object'

type ExecuteArgs = object | any[] | null
type ExecuteArgs = Record<string, any> | any[] | null

export class Client {
public readonly config: Config
Expand Down Expand Up @@ -178,16 +181,14 @@ function buildURL(url: URL): string {

export class Connection {
public readonly config: Config
private fetch: Fetch
private session: QuerySession | null
private url: string

constructor(config: Config) {
this.config = config
this.fetch = config.fetch || fetch!
this.session = null
this.config = { ...config }

if (typeof fetch !== 'undefined') {
this.config.fetch ||= fetch
}

if (config.url) {
const url = new URL(config.url)
Expand Down Expand Up @@ -240,7 +241,10 @@ export class Connection {
const formatter = this.config.format || format
const sql = args ? formatter(query, args) : query

const saved = await postJSON<QueryExecuteResponse>(this.config, url, { query: sql, session: this.session })
const saved = await postJSON<QueryExecuteResponse>(this.config, this.fetch, url, {
query: sql,
session: this.session
})

const { result, session, error, timing } = saved
if (session) {
Expand Down Expand Up @@ -268,7 +272,7 @@ export class Connection {
const rows = result ? parse<T>(result, castFn, options.as || 'object') : []
const headers = fields.map((f) => f.name)

const typeByName = (acc, { name, type }) => ({ ...acc, [name]: type })
const typeByName = (acc: Types, { name, type }: Field) => ({ ...acc, [name]: type })
const types = fields.reduce<Types>(typeByName, {})
const timingSeconds = timing ?? 0

Expand All @@ -287,15 +291,14 @@ export class Connection {

private async createSession(): Promise<QuerySession> {
const url = new URL('/psdb.v1alpha1.Database/CreateSession', this.url)
const { session } = await postJSON<QueryExecuteResponse>(this.config, url)
const { session } = await postJSON<QueryExecuteResponse>(this.config, this.fetch, url)
this.session = session
return session
}
}

async function postJSON<T>(config: Config, url: string | URL, body = {}): Promise<T> {
async function postJSON<T>(config: Config, fetch: Fetch, url: string | URL, body = {}): Promise<T> {
const auth = btoa(`${config.username}:${config.password}`)
const { fetch } = config
const response = await fetch(url.toString(), {
method: 'POST',
body: JSON.stringify(body),
Expand Down Expand Up @@ -328,25 +331,28 @@ export function connect(config: Config): Connection {
return new Connection(config)
}

function parseArrayRow<T = Row<'array'>>(fields: Field[], rawRow: QueryResultRow, cast: Cast): T {
function parseArrayRow<T>(fields: Field[], rawRow: QueryResultRow, cast: Cast): T {
const row = decodeRow(rawRow)

return fields.map((field, ix) => {
return cast(field, row[ix])
}) as T
}

function parseObjectRow<T = Row<'object'>>(fields: Field[], rawRow: QueryResultRow, cast: Cast): T {
function parseObjectRow<T>(fields: Field[], rawRow: QueryResultRow, cast: Cast): T {
const row = decodeRow(rawRow)

return fields.reduce((acc, field, ix) => {
acc[field.name] = cast(field, row[ix])
return acc
}, {} as T)
return fields.reduce(
(acc, field, ix) => {
acc[field.name] = cast(field, row[ix])
return acc
},
{} as Record<string, ReturnType<Cast>>
) as T
}

function parse<T>(result: QueryResult, cast: Cast, returnAs: ExecuteAs): T[] {
const fields = result.fields
const fields = result.fields ?? []
const rows = result.rows ?? []
return rows.map((row) =>
returnAs === 'array' ? parseArrayRow<T>(fields, row, cast) : parseObjectRow<T>(fields, row, cast)
Expand Down
2 changes: 1 addition & 1 deletion src/sanitization.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type Stringable = { toString: () => string }
type Value = null | undefined | number | boolean | string | Array<Value> | Date | Stringable

export function format(query: string, values: Value[] | Record<string, Value>): string {
export function format(query: string, values: Record<string, any> | any[]): string {
return Array.isArray(values) ? replacePosition(query, values) : replaceNamed(query, values)
}

Expand Down
2 changes: 1 addition & 1 deletion src/text.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const decoder = new TextDecoder('utf-8')

export function decode(text: string | null): string {
export function decode(text: string | null | undefined): string {
return text ? decoder.decode(Uint8Array.from(bytes(text))) : ''
}

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"module": "es2020",
"target": "es2020",
"strict": false,
"strict": true,
"declaration": true,
"outDir": "dist",
"removeComments": true,
Expand Down