Skip to content

Commit

Permalink
feat(*): support postgres
Browse files Browse the repository at this point in the history
feat(*): support postgres
  • Loading branch information
ArsenyYankovsky authored May 17, 2020
1 parent f4f4eab commit 9a0efb7
Show file tree
Hide file tree
Showing 16 changed files with 462 additions and 109 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,6 @@
"typescript": "^3.0.3"
},
"dependencies": {
"data-api-client": "ArsenyYankovsky/data-api-client#support-date"
"data-api-client": "ArsenyYankovsky/data-api-client#support-postgres"
}
}
3 changes: 3 additions & 0 deletions src/query-transformer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './mysql-query-transformer'
export * from './postgres-query-transformer'
export * from './query-transformer'
70 changes: 70 additions & 0 deletions src/query-transformer/mysql-query-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { QueryTransformer } from './query-transformer'

export class MysqlQueryTransformer extends QueryTransformer {
protected transformQuery(query: string, parameters: any[]): string {
const quoteCharacters = ["'", '"']
let newQueryString = ''
let currentQuote = null
let srcIndex = 0
let destIndex = 0

for (let i = 0; i < query.length; i += 1) {
const currentCharacter = query[i]
const currentCharacterEscaped = i !== 0 && query[i - 1] === '\\'

if (currentCharacter === '?' && !currentQuote) {
const parameter = parameters![srcIndex]

if (Array.isArray(parameter)) {
const additionalParameters = parameter.map((_, index) =>
`:param_${destIndex + index}`)

newQueryString += additionalParameters.join(', ')
destIndex += additionalParameters.length
} else {
newQueryString += `:param_${destIndex}`
destIndex += 1
}
srcIndex += 1
} else {
newQueryString += currentCharacter

if (quoteCharacters.includes(currentCharacter) && !currentCharacterEscaped) {
if (!currentQuote) {
currentQuote = currentCharacter
} else if (currentQuote === currentCharacter) {
currentQuote = null
}
}
}
}

return newQueryString
}

protected expandArrayParameters(parameters: any[]): any[] {
return parameters.reduce(
(expandedParameters, parameter) => {
if (Array.isArray(parameter)) {
expandedParameters.push(...parameter)
} else {
expandedParameters.push(parameter)
}
return expandedParameters
}, [])
}

protected transformParameters(parameters?: any[]) {
if (!parameters) {
return parameters
}

const expandedParameters = this.expandArrayParameters(parameters)

return [expandedParameters.reduce(
(params, parameter, index) => {
params[`param_${index}`] = parameter
return params
}, {})]
}
}
42 changes: 42 additions & 0 deletions src/query-transformer/postgres-query-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { QueryTransformer } from './query-transformer'

export class PostgresQueryTransformer extends QueryTransformer {
protected transformQuery(query: string) {
const quoteCharacters = ["'", '"']
let newQueryString = ''
let currentQuote = null

for (let i = 0; i < query.length; i += 1) {
const currentCharacter = query[i]
const currentCharacterEscaped = i !== 0 && query[i - 1] === '\\'

if (currentCharacter === '$' && !currentQuote) {
newQueryString += ':param_'
} else {
newQueryString += currentCharacter

if (quoteCharacters.includes(currentCharacter) && !currentCharacterEscaped) {
if (!currentQuote) {
currentQuote = currentCharacter
} else if (currentQuote === currentCharacter) {
currentQuote = null
}
}
}
}

return newQueryString
}

protected transformParameters(parameters?: any[]) {
if (!parameters) {
return parameters
}

return [parameters.reduce(
(params, parameter, index) => {
params[`param_${index + 1}`] = parameter
return params
}, {})]
}
}
20 changes: 20 additions & 0 deletions src/query-transformer/query-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface QueryTransformationResult {
queryString: string,
parameters: any[],
}

export abstract class QueryTransformer {
public transformQueryAndParameters(query: string, srcParameters: any[] = []) {
if (!srcParameters.length) {
return { queryString: query, parameters: [] }
}

const queryString = this.transformQuery(query, srcParameters)
const parameters = this.transformParameters(srcParameters)
return { queryString, parameters }
}

protected abstract transformQuery(query: string, srcParameters: any[]): string

protected abstract transformParameters(srcParameters?: any[]): any[] | undefined
}
69 changes: 0 additions & 69 deletions src/transform.utils.ts

This file was deleted.

41 changes: 38 additions & 3 deletions src/typeorm-aurora-data-api-driver.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// @ts-ignore
import createDataApiClient from 'data-api-client'
import { transformQueryAndParameters } from './transform.utils'
import { MysqlQueryTransformer, PostgresQueryTransformer, QueryTransformer } from './query-transformer'

export default class DataApiDriver {
class DataApiDriver {
private readonly client: any
private transactionId?: string

Expand All @@ -12,6 +12,7 @@ export default class DataApiDriver {
private readonly resourceArn: string,
private readonly database: string,
private readonly loggerFn: (query: string, parameters?: any[]) => void = () => undefined,
private readonly queryTransformer: QueryTransformer,
private readonly serviceConfigOptions?: any,
) {
this.region = region
Expand All @@ -27,10 +28,11 @@ export default class DataApiDriver {
database,
options: this.serviceConfigOptions,
})
this.queryTransformer = queryTransformer
}

public async query(query: string, parameters?: any[]): Promise<any> {
const transformedQueryData = transformQueryAndParameters(query, parameters)
const transformedQueryData = this.queryTransformer.transformQueryAndParameters(query, parameters)

this.loggerFn(transformedQueryData.queryString, transformedQueryData.parameters)

Expand Down Expand Up @@ -58,3 +60,36 @@ export default class DataApiDriver {
this.transactionId = undefined
}
}

const createMysqlDriver = (region: string, secretArn: string, resourceArn: string, database: string,
loggerFn: (query: string, parameters?: any[]) => void = () => undefined,
serviceConfigOptions?: any) => {

return new DataApiDriver(
region,
secretArn,
resourceArn,
database,
loggerFn,
new MysqlQueryTransformer(),
serviceConfigOptions,
)
}

export default createMysqlDriver

const createPostgresDriver = (region: string, secretArn: string, resourceArn: string, database: string,
loggerFn: (query: string, parameters?: any[]) => void = () => undefined,
serviceConfigOptions?: any) => {
return new DataApiDriver(
region,
secretArn,
resourceArn,
database,
loggerFn,
new PostgresQueryTransformer(),
serviceConfigOptions,
)
}

export const pg = createPostgresDriver
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ describe('aurora data api > simple queries', () => {
jest.setTimeout(240000)

it('should do a simple select', async () => {
await useCleanDatabase({ logger: 'simple-console' }, async (connection) => {
await useCleanDatabase('mysql', { logger: 'simple-console' }, async (connection) => {
const logSpy = jest.spyOn(global.console, 'log')

const result = await connection.query('select 1')
const result = await connection.query('select 1 as \"1\"')

expect(logSpy).toHaveBeenCalledWith('query: select 1')
expect(logSpy).toHaveBeenCalledWith('query: select 1 as \"1\"')
expect(logSpy).toBeCalledTimes(1)

expect(result[0][1]).toBe(1)
})
})

it('should create a table and be able to query it', async () => {
await useCleanDatabase({ entities: [Post, Category] }, async (connection) => {
await useCleanDatabase('mysql', { entities: [Post, Category] }, async (connection) => {
const postRepository = connection.getRepository(Post)

const post = new Post()
Expand All @@ -40,7 +40,7 @@ describe('aurora data api > simple queries', () => {
})

it('should be able to update a post', async () => {
await useCleanDatabase({ entities: [Post, Category] }, async (connection) => {
await useCleanDatabase('mysql', { entities: [Post, Category] }, async (connection) => {

const postRepository = connection.getRepository(Post)

Expand Down Expand Up @@ -72,7 +72,7 @@ describe('aurora data api > simple queries', () => {
})

it('should be able to handle dates and multiple inserts', async () => {
await useCleanDatabase({ entities: [Post, Category] }, async (connection) => {
await useCleanDatabase('mysql', { entities: [Post, Category] }, async (connection) => {
const postRepository = connection.getRepository(Post)

const post = new Post()
Expand Down Expand Up @@ -104,7 +104,7 @@ describe('aurora data api > simple queries', () => {
})

it('should be able to create and query a many-to-many relationship', async () => {
await useCleanDatabase({ entities: [Post, Category] }, async (connection) => {
await useCleanDatabase('mysql', { entities: [Post, Category] }, async (connection) => {
// Create categories
const categoryRepository = connection.getRepository(Category)

Expand Down Expand Up @@ -144,7 +144,7 @@ describe('aurora data api > simple queries', () => {
})

it('should be able to update a date field by primary key', async () => {
await useCleanDatabase({ entities: [Post, Category] }, async (connection) => {
await useCleanDatabase('mysql', { entities: [Post, Category] }, async (connection) => {
// Create a post and associate with created categories
const postRepository = connection.getRepository(Post)

Expand Down Expand Up @@ -174,7 +174,7 @@ describe('aurora data api > simple queries', () => {
})

it('should be able to correctly deal with bulk inserts', async () => {
await useCleanDatabase({ entities: [Category] }, async (connection) => {
await useCleanDatabase('mysql', { entities: [Category] }, async (connection) => {
const categoryNames = ['one', 'two', 'three', 'four']
const newCategories = categoryNames.map(name => ({ name }))

Expand Down
11 changes: 11 additions & 0 deletions test/functional/pg/basic/entity/Category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Column, Entity, Generated, PrimaryColumn } from 'typeorm'

@Entity('category', { schema: 'test1' })
export class Category {
@PrimaryColumn()
@Generated()
public id!: number

@Column()
public name!: string
}
28 changes: 28 additions & 0 deletions test/functional/pg/basic/entity/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Column, Entity, Generated, JoinTable, ManyToMany, PrimaryColumn } from 'typeorm'
import { Category } from './Category'

@Entity('post', { schema: 'test1' })
export class Post {
@PrimaryColumn()
@Generated()
public id!: number

@Column()
public title!: string

@Column()
public text!: string

@Column({ nullable: false })
public likesCount!: number

@Column({ nullable: false, type: 'timestamp', default: () => 'now()' })
public publishedAt!: Date

@Column({ nullable: true, type: 'timestamp' })
public updatedAt?: Date

@ManyToMany(type => Category)
@JoinTable()
public categories!: Category[]
}
Loading

0 comments on commit 9a0efb7

Please sign in to comment.