Skip to content

Commit

Permalink
Add dynamic selects (#49)
Browse files Browse the repository at this point in the history
New select interface allows developers to chain the select fields:
```ts
const qb = new D1QB(env.DB)

const result = await qb.select('my-table')
                        .where('active = ?', true)
                        .where('department = ?', 'HR')
                        .orderBy('id')
                        .execute()
```
  • Loading branch information
G4brym authored Jul 30, 2024
1 parent a4725c2 commit 58e7323
Show file tree
Hide file tree
Showing 11 changed files with 725 additions and 348 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export default {
})
.execute()

// Or in a modular approach
const employeeList = await qb.select<Employee>('employees').where('active = ?', true).execute()

// You get IDE type hints on each employee data, like:
// employeeList.results[0].name

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "workers-qb",
"version": "1.3.1",
"version": "1.4.0",
"description": "Zero dependencies Query Builder for Cloudflare Workers",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down
68 changes: 55 additions & 13 deletions src/Builder.ts → src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from './interfaces'
import { ConflictTypes, FetchTypes, OrderTypes } from './enums'
import { Query, Raw } from './tools'
import { SelectBuilder } from './modularBuilder'

export class QueryBuilder<GenericResultWrapper> {
_debugger = false
Expand Down Expand Up @@ -64,6 +65,17 @@ export class QueryBuilder<GenericResultWrapper> {
}, `DROP TABLE ${params.ifExists ? 'IF EXISTS' : ''} ${params.tableName}`)
}

select<GenericResult = DefaultReturnObject>(tableName: string): SelectBuilder<GenericResultWrapper, GenericResult> {
return new SelectBuilder<GenericResultWrapper, GenericResult>(
{
tableName: tableName,
},
(params: SelectAll) => {
return this.fetchAll<GenericResult>(params)
}
)
}

fetchOne<GenericResult = DefaultReturnObject>(
params: SelectOne
): Query<OneResult<GenericResultWrapper, GenericResult>> {
Expand All @@ -73,7 +85,9 @@ export class QueryBuilder<GenericResultWrapper> {
},
this._select({ ...params, limit: 1 }),
typeof params.where === 'object' && !Array.isArray(params.where) && params.where?.params
? params.where?.params
? Array.isArray(params.where?.params)
? params.where?.params
: [params.where?.params]
: undefined,
FetchTypes.ONE
)
Expand All @@ -88,7 +102,9 @@ export class QueryBuilder<GenericResultWrapper> {
},
this._select(params),
typeof params.where === 'object' && !Array.isArray(params.where) && params.where?.params
? params.where?.params
? Array.isArray(params.where?.params)
? params.where?.params
: [params.where?.params]
: undefined,
FetchTypes.ALL
)
Expand Down Expand Up @@ -165,7 +181,11 @@ export class QueryBuilder<GenericResultWrapper> {
let args = this._parse_arguments(params.data)

if (typeof params.where === 'object' && !Array.isArray(params.where) && params.where?.params) {
args = (params.where?.params as Array<any>).concat(args)
if (Array.isArray(params.where?.params)) {
args = params.where?.params.concat(args)
} else {
args = [params.where?.params].concat(args)
}
}

return new Query(
Expand All @@ -189,7 +209,9 @@ export class QueryBuilder<GenericResultWrapper> {
},
this._delete(params),
typeof params.where === 'object' && !Array.isArray(params.where) && params.where?.params
? params.where?.params
? Array.isArray(params.where?.params)
? params.where?.params
: [params.where?.params]
: undefined,
FetchTypes.ALL
)
Expand Down Expand Up @@ -251,7 +273,11 @@ export class QueryBuilder<GenericResultWrapper> {
!Array.isArray(params.onConflict?.where) &&
params.onConflict?.where?.params
) {
index += (params.onConflict.where?.params).length
if (Array.isArray(params.onConflict.where?.params)) {
index += (params.onConflict.where?.params).length
} else {
index += 1
}
}

if (params.onConflict.data) {
Expand Down Expand Up @@ -287,7 +313,9 @@ export class QueryBuilder<GenericResultWrapper> {
_update(params: Update): string {
const whereParamsLength: number =
typeof params.where === 'object' && !Array.isArray(params.where) && params.where?.params
? Object.keys(params.where?.params as Array<any>).length
? Array.isArray(params.where?.params)
? Object.keys(params.where?.params).length
: 1
: 0

const set: Array<string> = []
Expand Down Expand Up @@ -384,26 +412,40 @@ export class QueryBuilder<GenericResultWrapper> {
return ` GROUP BY ${value.join(', ')}`
}

_having(value?: string): string {
_having(value?: string | Array<string>): string {
if (!value) return ''
if (typeof value === 'string') return ` HAVING ${value}`

return ` HAVING ${value}`
return ` HAVING ${value.join(' AND ')}`
}

_orderBy(value?: string | Array<string> | Record<string, string | OrderTypes>): string {
if (!value) return ''
if (typeof value === 'string') return ` ORDER BY ${value}`

const order: Array<Record<string, string> | string> = []
if (Array.isArray(value)) {
return ` ORDER BY ${value.join(', ')}`
for (const val of value) {
// @ts-ignore
order.push(val)
}
} else {
order.push(value)
}

const order: Array<string> = []
Object.entries(value).forEach(([key, item]) => {
order.push(`${key} ${item}`)
const result = order.map((obj) => {
if (typeof obj === 'object') {
const objs: Array<string> = []
Object.entries(obj).forEach(([key, item]) => {
objs.push(`${key} ${item}`)
})
return objs.join(', ')
} else {
return obj
}
})

return ` ORDER BY ${order.join(', ')}`
return ` ORDER BY ${result.join(', ')}`
}

_limit(value?: number): string {
Expand Down
2 changes: 1 addition & 1 deletion src/databases/d1.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QueryBuilder } from '../Builder'
import { QueryBuilder } from '../builder'
import { FetchTypes } from '../enums'
import { Query } from '../tools'
import { D1Result } from '../interfaces'
Expand Down
2 changes: 1 addition & 1 deletion src/databases/pg.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QueryBuilder } from '../Builder'
import { QueryBuilder } from '../builder'
import { FetchTypes } from '../enums'
import { Query } from '../tools'
import { PGResult } from '../interfaces'
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './Builder'
export * from './builder'
export * from './databases/d1'
export * from './databases/pg'
export * from './enums'
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type Where =
| {
conditions: string | Array<string>
// TODO: enable named parameters with DefaultObject
params?: Primitive[]
params?: Primitive | Primitive[]
}
| string
| Array<string>
Expand All @@ -29,7 +29,7 @@ export type SelectOne = {
where?: Where
join?: Join | Array<Join>
groupBy?: string | Array<string>
having?: string
having?: string | Array<string>
orderBy?: string | Array<string> | Record<string, string | OrderTypes>
offset?: number
}
Expand Down
126 changes: 126 additions & 0 deletions src/modularBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ArrayResult, DefaultReturnObject, Primitive, SelectAll } from './interfaces'
import { Query } from './tools'

export class SelectBuilder<GenericResultWrapper, GenericResult = DefaultReturnObject> {
_debugger = false
private _options: Partial<SelectAll> = {}
private _queryBuilder: (params: SelectAll) => Query

constructor(options: Partial<SelectAll>, queryBuilder: (params: SelectAll) => Query) {
this._options = options
this._queryBuilder = queryBuilder
}

setDebugger(state: boolean): void {
this._debugger = state
}

tableName(tableName: SelectAll['tableName']): SelectBuilder<GenericResultWrapper, GenericResult> {
return new SelectBuilder<GenericResultWrapper, GenericResult>(
{
...this._options,
tableName: tableName,
},
this._queryBuilder
)
}

fields(fields: SelectAll['fields']): SelectBuilder<GenericResultWrapper, GenericResult> {
return this._parseArray('fields', this._options.fields, fields)
}

where(
conditions: string | Array<string>,
params?: Primitive | Primitive[]
): SelectBuilder<GenericResultWrapper, GenericResult> {
if (!Array.isArray(conditions)) {
conditions = [conditions]
}
if (params === undefined) params = []
if (!Array.isArray(params)) {
params = [params]
}

if ((this._options.where as any)?.conditions) {
conditions = (this._options.where as any).conditions.concat(conditions)
}

if ((this._options.where as any)?.params) {
params = (this._options.where as any).params.concat(params)
}

return new SelectBuilder<GenericResultWrapper, GenericResult>(
{
...this._options,
where: {
conditions: conditions,
params: params,
},
},
this._queryBuilder
)
}

join(join: SelectAll['join']): SelectBuilder<GenericResultWrapper, GenericResult> {
return this._parseArray('join', this._options.join, join)
}

groupBy(groupBy: SelectAll['groupBy']): SelectBuilder<GenericResultWrapper, GenericResult> {
return this._parseArray('groupBy', this._options.groupBy, groupBy)
}

having(having: SelectAll['having']): SelectBuilder<GenericResultWrapper, GenericResult> {
return this._parseArray('having', this._options.having, having)
}

orderBy(orderBy: SelectAll['orderBy']): SelectBuilder<GenericResultWrapper, GenericResult> {
return this._parseArray('orderBy', this._options.orderBy, orderBy)
}

offset(offset: SelectAll['offset']): SelectBuilder<GenericResultWrapper, GenericResult> {
return new SelectBuilder<GenericResultWrapper, GenericResult>(
{
...this._options,
offset: offset,
},
this._queryBuilder
)
}

limit(limit: SelectAll['limit']): SelectBuilder<GenericResultWrapper, GenericResult> {
return new SelectBuilder<GenericResultWrapper, GenericResult>(
{
...this._options,
limit: limit,
},
this._queryBuilder
)
}

private _parseArray(fieldName: string, option: any, value: any): SelectBuilder<GenericResultWrapper, GenericResult> {
let val = []
if (!Array.isArray(value)) {
val.push(value)
}

if (option && Array.isArray(option)) {
val = [...option, ...val]
}

return new SelectBuilder<GenericResultWrapper, GenericResult>(
{
...this._options,
[fieldName]: val as Array<string>,
},
this._queryBuilder
)
}

getQuery(): Query<ArrayResult<GenericResultWrapper, GenericResult>> {
return this._queryBuilder(this._options as SelectAll)
}

async execute(): Promise<ArrayResult<GenericResultWrapper, GenericResult>> {
return this._queryBuilder(this._options as SelectAll).execute()
}
}
Loading

0 comments on commit 58e7323

Please sign in to comment.