Skip to content

Commit 81ec912

Browse files
committed
chore: wip
1 parent 76e5196 commit 81ec912

File tree

2 files changed

+158
-8
lines changed

2 files changed

+158
-8
lines changed

src/client.ts

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { SchemaMeta } from './meta'
2-
import type { DatabaseSchema } from './schema'
2+
import type { DatabaseSchema, InferTableName, ModelRecord } from './schema'
33
import { sql as bunSql } from 'bun'
44
import { config } from './config'
55

@@ -1083,6 +1083,7 @@ export interface BaseSelectQueryBuilder<
10831083
*/
10841084
toSQL: () => string
10851085
execute: () => Promise<SelectedRow<DB, TTable, TSelected>[]>
1086+
executeTakeFirst: () => Promise<SelectedRow<DB, TTable, TSelected> | undefined>
10861087
// Laravel-style retrieval helpers
10871088
/**
10881089
* # `get`
@@ -1248,6 +1249,7 @@ export interface UpdateQueryBuilder<DB extends DatabaseSchema<any>, TTable exten
12481249
* ```
12491250
*/
12501251
execute: () => Promise<number>
1252+
executeTakeFirst?: () => Promise<{ numUpdatedRows?: number }>
12511253
}
12521254

12531255
export interface DeleteQueryBuilder<DB extends DatabaseSchema<any>, TTable extends keyof DB & string> {
@@ -1295,6 +1297,7 @@ export interface DeleteQueryBuilder<DB extends DatabaseSchema<any>, TTable exten
12951297
* ```
12961298
*/
12971299
execute: () => Promise<number>
1300+
executeTakeFirst?: () => Promise<{ numDeletedRows?: number }>
12981301
}
12991302

13001303
export interface QueryBuilder<DB extends DatabaseSchema<any>> {
@@ -1557,6 +1560,81 @@ export interface QueryBuilder<DB extends DatabaseSchema<any>> {
15571560
insertGetId: <TTable extends keyof DB & string>(table: TTable, values: Partial<DB[TTable]['columns']>, idColumn?: keyof DB[TTable]['columns'] & string) => Promise<any>
15581561
updateOrInsert: <TTable extends keyof DB & string>(table: TTable, match: Partial<DB[TTable]['columns']>, values: Partial<DB[TTable]['columns']>) => Promise<boolean>
15591562
upsert: <TTable extends keyof DB & string>(table: TTable, rows: Partial<DB[TTable]['columns']>[], conflictColumns: (keyof DB[TTable]['columns'] & string)[], mergeColumns?: (keyof DB[TTable]['columns'] & string)[]) => Promise<any>
1563+
1564+
/**
1565+
* # `create(table, values)`
1566+
*
1567+
* Inserts a row and returns the created record.
1568+
*/
1569+
create: <TTable extends keyof DB & string>(
1570+
table: TTable,
1571+
values: Partial<DB[TTable]['columns']>,
1572+
) => Promise<DB[TTable]['columns']>
1573+
1574+
/**
1575+
* # `createMany(table, rows)`
1576+
*
1577+
* Inserts multiple rows. Returns void.
1578+
*/
1579+
createMany: <TTable extends keyof DB & string>(
1580+
table: TTable,
1581+
rows: Partial<DB[TTable]['columns']>[],
1582+
) => Promise<void>
1583+
1584+
/**
1585+
* # `firstOrCreate(table, match, [defaults])`
1586+
*
1587+
* Returns the first matching row, or creates one with defaults merged and returns it.
1588+
*/
1589+
firstOrCreate: <TTable extends keyof DB & string>(
1590+
table: TTable,
1591+
match: Partial<DB[TTable]['columns']>,
1592+
defaults?: Partial<DB[TTable]['columns']>,
1593+
) => Promise<DB[TTable]['columns']>
1594+
1595+
/**
1596+
* # `updateOrCreate(table, match, values)`
1597+
*
1598+
* Updates the first matching row with values or creates a new one if none exists, then returns it.
1599+
*/
1600+
updateOrCreate: <TTable extends keyof DB & string>(
1601+
table: TTable,
1602+
match: Partial<DB[TTable]['columns']>,
1603+
values: Partial<DB[TTable]['columns']>,
1604+
) => Promise<DB[TTable]['columns']>
1605+
1606+
/**
1607+
* # `save(table, values)`
1608+
* If values contain the primary key and a row exists, updates it; otherwise creates a new row. Returns the row.
1609+
*/
1610+
save: <TTable extends keyof DB & string>(
1611+
table: TTable,
1612+
values: Partial<DB[TTable]['columns']>,
1613+
) => Promise<DB[TTable]['columns']>
1614+
1615+
/**
1616+
* # `remove(table, id)`
1617+
* Deletes by primary key and returns adapter's first result object.
1618+
*/
1619+
remove: <TTable extends keyof DB & string>(
1620+
table: TTable,
1621+
id: DB[TTable]['columns'][DB[TTable]['primaryKey'] & keyof DB[TTable]['columns']] | any,
1622+
) => Promise<any>
1623+
1624+
/**
1625+
* # `find(table, id)`
1626+
* Fetch by primary key. Returns the row or undefined.
1627+
*/
1628+
find: <TTable extends keyof DB & string>(
1629+
table: TTable,
1630+
id: DB[TTable]['columns'][DB[TTable]['primaryKey'] & keyof DB[TTable]['columns']] | any,
1631+
) => Promise<DB[TTable]['columns'] | undefined>
1632+
1633+
/**
1634+
* # `rawQuery(sql)`
1635+
* Execute a raw SQL string (single statement) with no parameters.
1636+
*/
1637+
rawQuery: (query: string) => Promise<any>
15601638
}
15611639

15621640
// Typed INSERT builder to expose a structured SQL literal in hovers
@@ -2686,6 +2764,59 @@ export function createQueryBuilder<DB extends DatabaseSchema<any>>(state?: Parti
26862764
const built = bunSql`INSERT INTO ${bunSql(String(table))} ${bunSql(rows as any)} ON CONFLICT (${bunSql(targetCols as any)}) DO UPDATE SET ${bunSql(setCols.reduce((acc, c) => ({ ...acc, [c]: (bunSql as any)(`EXCLUDED.${c}`) }), {}))}`
26872765
return (built as any).execute()
26882766
},
2767+
async save(table, values) {
2768+
const pk = meta?.primaryKeys[String(table)] ?? 'id'
2769+
const id = (values as any)[pk]
2770+
if (id != null) {
2771+
await (this as any).updateTable(table).set(values as any).where({ [pk]: id } as any).execute()
2772+
const row = await (this as any).selectFrom(table).find(id)
2773+
if (!row)
2774+
throw new Error('save() failed to retrieve updated row')
2775+
return row
2776+
}
2777+
return await (this as any).create(table, values)
2778+
},
2779+
async remove(table, id) {
2780+
return await (this as any).deleteFrom(table).where({ id } as any).execute()
2781+
},
2782+
async find(table, id) {
2783+
return await (this as any).selectFrom(table).find(id)
2784+
},
2785+
async rawQuery(query: string) {
2786+
return await (bunSql as any).unsafe(query)
2787+
},
2788+
async create(table, values) {
2789+
const pk = meta?.primaryKeys[String(table)] ?? 'id'
2790+
const id = await (this as any).insertGetId(table, values, pk)
2791+
const row = await (this as any).selectFrom(table).find(id)
2792+
if (!row)
2793+
throw new Error('create() failed to retrieve inserted row')
2794+
return row
2795+
},
2796+
async createMany(table, rows) {
2797+
await (this as any).insertInto(table).values(rows).execute()
2798+
},
2799+
async firstOrCreate(table, match, defaults) {
2800+
const existing = await (this as any).selectFrom(table).where(match as any).first()
2801+
if (existing)
2802+
return existing
2803+
return await (this as any).create(table, { ...(match as any), ...(defaults as any) })
2804+
},
2805+
async updateOrCreate(table, match, values) {
2806+
const existing = await (this as any).selectFrom(table).where(match as any).first()
2807+
if (existing) {
2808+
await (this as any).updateTable(table).set(values as any).where(match as any).execute()
2809+
const pk = meta?.primaryKeys[String(table)] ?? 'id'
2810+
const id = (existing as any)[pk]
2811+
const refreshed = id != null
2812+
? await (this as any).selectFrom(table).find(id)
2813+
: await (this as any).selectFrom(table).where(match as any).first()
2814+
if (!refreshed)
2815+
throw new Error('updateOrCreate() failed to retrieve updated row')
2816+
return refreshed
2817+
}
2818+
return await (this as any).create(table, { ...(match as any), ...(values as any) })
2819+
},
26892820
async count(table, column) {
26902821
const col = column ? bunSql(String(column)) : bunSql`*`
26912822
const q = bunSql`SELECT COUNT(${col}) as c FROM ${bunSql(String(table))}`

test.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Dummy typed showcase for bun-query-builder
33
// This file is not meant to run database operations. It demonstrates types.
44

5-
import type { QueryBuilder, SelectQueryBuilder } from './src'
5+
import type { SelectQueryBuilder } from './src'
66
// Import example models
77
import Comment from './examples/models/Comment'
88
import Post from './examples/models/Post'
@@ -52,8 +52,7 @@ const usersQ = db
5252
.limit(10)
5353
// .toSQL()
5454

55-
type UsersSelected = SelectedOf<typeof usersQ>
56-
const usersSelectedExample: UsersSelected | undefined = undefined
55+
const usersQHover = usersQ.rows
5756
const usersRowsPromise = usersQ.execute()
5857

5958
// Join across typed tables
@@ -83,13 +82,11 @@ const sql = db.insertInto('comments').values(newComment).toSQL()
8382

8483
// Returning examples to hover precise row shapes
8584
const insertUserQ = db.insertInto('users').values(newUser).returning('id', 'email')
86-
type InsertUserRow = SelectedOf<typeof insertUserQ>
87-
const insertUserRowExample: InsertUserRow | undefined = undefined
85+
const insertUserHover = insertUserQ.rows
8886
const insertedUsersPromise = insertUserQ.execute()
8987

9088
const updateUserQ = db.updateTable('users').set({ role: 'member' }).where({ id: 1 }).returning('id', 'created_at')
91-
type UpdateUserRow = SelectedOf<typeof updateUserQ>
92-
const updateUserRowExample: UpdateUserRow | undefined = undefined
89+
const updateUserHover = updateUserQ.rows
9390
const updatedUsersPromise = updateUserQ.execute()
9491

9592
// No explicit types needed: hover these locals to see fully inferred row shapes
@@ -130,3 +127,25 @@ const usersRecentHover = usersRecentQ.rows
130127
// db.select('users', 'does_not_exist')
131128
// db.updateTable('users').set({ nope: 123 })
132129
// db.insertInto('posts').values({ not_a_column: true })
130+
131+
// CRUD helper examples
132+
async function typedCrudHelpers() {
133+
const created = await db.create('users', { email: '[email protected]', name: 'Bob', role: 'member' })
134+
const fetched = await db.find('users', 1)
135+
const upserted = await db.save('users', { id: 1, role: 'admin' })
136+
await db.createMany('users', [{ email: '[email protected]', name: 'C1', role: 'guest' }])
137+
const firstOrCreated = await db.firstOrCreate('users', { email: '[email protected]' }, { name: 'Alice', role: 'member' })
138+
const updatedOrCreated = await db.updateOrCreate('users', { email: '[email protected]' }, { name: 'D', role: 'guest' })
139+
const deleted = await db.remove('users', 123)
140+
const rawRes = await db.rawQuery('SELECT 1')
141+
const totalUsers = await db.count('users', 'id')
142+
void created
143+
void fetched
144+
void upserted
145+
void firstOrCreated
146+
void updatedOrCreated
147+
void deleted
148+
void rawRes
149+
void totalUsers
150+
}
151+
void typedCrudHelpers

0 commit comments

Comments
 (0)