@cleverbrush/knex-schema 0.0.0-beta-20260413145651
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -0
- package/dist/SchemaQueryBuilder.d.ts +567 -0
- package/dist/columns.d.ts +27 -0
- package/dist/extension.d.ts +286 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mappers.d.ts +61 -0
- package/dist/types.d.ts +92 -0
- package/dist/validate.d.ts +8 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# @cleverbrush/knex-schema
|
|
2
|
+
|
|
3
|
+
Type-safe, schema-driven query builder for [Knex](https://knexjs.org/). Use `@cleverbrush/schema` object builders to describe your PostgreSQL tables — column name mapping, eager loading, and full CRUD are handled automatically with complete TypeScript inference.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cleverbrush/knex-schema
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependency:** `knex >= 3.1.0`
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import knex from 'knex';
|
|
17
|
+
import { query, object, string, number, date } from '@cleverbrush/knex-schema';
|
|
18
|
+
|
|
19
|
+
// 1. Describe your table with a schema
|
|
20
|
+
const UserSchema = object({
|
|
21
|
+
id: number(),
|
|
22
|
+
firstName: string().hasColumnName('first_name'),
|
|
23
|
+
lastName: string().hasColumnName('last_name'),
|
|
24
|
+
age: number().optional(),
|
|
25
|
+
createdAt: date().hasColumnName('created_at'),
|
|
26
|
+
}).hasTableName('users');
|
|
27
|
+
|
|
28
|
+
// 2. Create a Knex instance
|
|
29
|
+
const db = knex({ client: 'pg', connection: process.env.DB_URL });
|
|
30
|
+
|
|
31
|
+
// 3. Query — fully typed, column names resolved automatically
|
|
32
|
+
const adults = await query(db, UserSchema)
|
|
33
|
+
.where(t => t.age, '>', 18)
|
|
34
|
+
.orderBy(t => t.lastName);
|
|
35
|
+
// → typed as Array<{ id: number; firstName: string; lastName: string; age?: number; createdAt: Date }>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Schema Definition
|
|
39
|
+
|
|
40
|
+
Import schema builders from `@cleverbrush/knex-schema` (re-exported with the database extensions applied) instead of from `@cleverbrush/schema`:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { object, string, number, date, boolean, any } from '@cleverbrush/knex-schema';
|
|
44
|
+
// NOT from '@cleverbrush/schema' — those builders lack hasColumnName / hasTableName
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `.hasColumnName(sqlCol)`
|
|
48
|
+
|
|
49
|
+
Set the SQL column name for a property when it differs from the property key:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const OrderSchema = object({
|
|
53
|
+
id: number(),
|
|
54
|
+
customerId: number().hasColumnName('customer_id'), // snake_case column
|
|
55
|
+
totalAmount: number().hasColumnName('total_amount'),
|
|
56
|
+
createdAt: date().hasColumnName('created_at'),
|
|
57
|
+
}).hasTableName('orders');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `.hasTableName(table)`
|
|
61
|
+
|
|
62
|
+
Set the SQL table name on an object schema. Required before calling `query()`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## CRUD Operations
|
|
67
|
+
|
|
68
|
+
### Fetch All Rows
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const users = await query(db, UserSchema);
|
|
72
|
+
// or explicitly:
|
|
73
|
+
const users = await query(db, UserSchema).execute();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Fetch First Row
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const user = await query(db, UserSchema)
|
|
80
|
+
.where(t => t.id, 1)
|
|
81
|
+
.first();
|
|
82
|
+
// → UserType | undefined
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Insert
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Single row — returns the inserted record (including database-generated fields)
|
|
89
|
+
const newUser = await query(db, UserSchema).insert({
|
|
90
|
+
firstName: 'Alice',
|
|
91
|
+
lastName: 'Smith',
|
|
92
|
+
age: 30,
|
|
93
|
+
createdAt: new Date(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Multiple rows
|
|
97
|
+
const newUsers = await query(db, UserSchema).insertMany([
|
|
98
|
+
{ firstName: 'Bob', lastName: 'Jones', createdAt: new Date() },
|
|
99
|
+
{ firstName: 'Carol', lastName: 'White', createdAt: new Date() },
|
|
100
|
+
]);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Update
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Updates rows matching the WHERE clause, returns updated records
|
|
107
|
+
const updated = await query(db, UserSchema)
|
|
108
|
+
.where(t => t.id, userId)
|
|
109
|
+
.update({ firstName: 'Alicia' });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Delete
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Returns the number of deleted rows
|
|
116
|
+
const count = await query(db, UserSchema)
|
|
117
|
+
.where(t => t.id, userId)
|
|
118
|
+
.delete();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Filtering
|
|
124
|
+
|
|
125
|
+
Column references accept either a **property accessor** (`t => t.firstName`) or a **string property key** (`'firstName'`) — both are resolved to the correct SQL column name:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
query(db, UserSchema)
|
|
129
|
+
.where(t => t.firstName, 'like', 'A%')
|
|
130
|
+
.andWhere(t => t.age, '>', 18)
|
|
131
|
+
.orWhere({ lastName: 'Smith' }) // record syntax — keys mapped to columns
|
|
132
|
+
.whereIn(t => t.id, [1, 2, 3])
|
|
133
|
+
.whereNotNull(t => t.createdAt)
|
|
134
|
+
.whereBetween(t => t.age, [20, 40])
|
|
135
|
+
.whereILike(t => t.lastName, 'sm%') // case-insensitive (PostgreSQL)
|
|
136
|
+
.whereRaw('extract(year from created_at) = ?', [2025]);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Available WHERE methods: `where`, `andWhere`, `orWhere`, `whereNot`, `whereIn`, `whereNotIn`, `orWhereIn`, `orWhereNotIn`, `whereNull`, `whereNotNull`, `orWhereNull`, `orWhereNotNull`, `whereBetween`, `whereNotBetween`, `whereLike`, `whereILike`, `whereRaw`, `whereExists`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Ordering, Pagination, Grouping
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
query(db, UserSchema)
|
|
147
|
+
.orderBy(t => t.lastName)
|
|
148
|
+
.orderBy(t => t.firstName, 'desc')
|
|
149
|
+
.limit(20)
|
|
150
|
+
.offset(40)
|
|
151
|
+
.groupBy(t => t.age)
|
|
152
|
+
.having(t => t.age, '>', 18)
|
|
153
|
+
.select(t => t.firstName, t => t.age)
|
|
154
|
+
.distinct(t => t.age);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Eager Loading (No N+1)
|
|
160
|
+
|
|
161
|
+
Related rows are loaded in a **single query** using PostgreSQL CTEs and `jsonb_agg`.
|
|
162
|
+
|
|
163
|
+
### `joinOne` — one-to-one / many-to-one
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const PostSchema = object({
|
|
167
|
+
id: number(),
|
|
168
|
+
title: string(),
|
|
169
|
+
authorId: number().hasColumnName('author_id'),
|
|
170
|
+
}).hasTableName('posts');
|
|
171
|
+
|
|
172
|
+
const posts = await query(db, PostSchema)
|
|
173
|
+
.joinOne({
|
|
174
|
+
foreignSchema: UserSchema,
|
|
175
|
+
localColumn: t => t.authorId,
|
|
176
|
+
foreignColumn: t => t.id,
|
|
177
|
+
as: 'author',
|
|
178
|
+
});
|
|
179
|
+
// posts[0].author.firstName — typed as string ✓
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `joinMany` — one-to-many
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const users = await query(db, UserSchema)
|
|
186
|
+
.joinMany({
|
|
187
|
+
foreignSchema: PostSchema,
|
|
188
|
+
localColumn: t => t.id,
|
|
189
|
+
foreignColumn: t => t.authorId,
|
|
190
|
+
as: 'posts',
|
|
191
|
+
limit: 5,
|
|
192
|
+
orderBy: { column: t => t.id, direction: 'desc' },
|
|
193
|
+
});
|
|
194
|
+
// users[0].posts — typed as Array<{ id: number; title: string; authorId: number }> ✓
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The `joinMany` spec supports:
|
|
198
|
+
- `limit` / `offset` — per-parent pagination using `row_number()` window functions
|
|
199
|
+
- `orderBy` — `{ column, direction }` for the sub-collection
|
|
200
|
+
- `foreignQuery` — pre-filtered `Knex.QueryBuilder` (e.g. for soft-delete scopes)
|
|
201
|
+
- `required` (`joinOne` only) — `true` = inner join, `false` = left join (nullable result)
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Escape Hatch
|
|
206
|
+
|
|
207
|
+
When you need a Knex feature not exposed by this API, use `.apply()`:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const rows = await query(db, UserSchema)
|
|
211
|
+
.apply(qb => qb.forUpdate().noWait())
|
|
212
|
+
.where(t => t.id, id);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Column Reference Patterns
|
|
218
|
+
|
|
219
|
+
Both styles are equivalent and resolve to the same SQL column:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// 1. Property accessor (recommended — refactor-safe, IDE auto-complete)
|
|
223
|
+
.where(t => t.firstName, 'Alice')
|
|
224
|
+
|
|
225
|
+
// 2. String property key
|
|
226
|
+
.where('firstName', 'Alice')
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The schema's `hasColumnName()` metadata is used to map `firstName` → `first_name` in both cases.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## API Reference
|
|
234
|
+
|
|
235
|
+
### `query(knex, schema, baseQuery?)`
|
|
236
|
+
|
|
237
|
+
Creates a `SchemaQueryBuilder`. `schema` must have `.hasTableName()` set.
|
|
238
|
+
Optionally pass a `baseQuery` (e.g. a scoped `knex('users').where('deleted_at', null)`) as the starting point.
|
|
239
|
+
|
|
240
|
+
### `SchemaQueryBuilder<TLocalSchema, TResult>`
|
|
241
|
+
|
|
242
|
+
| Category | Methods |
|
|
243
|
+
|---|---|
|
|
244
|
+
| Eager loading | `.joinOne(spec)`, `.joinMany(spec)` |
|
|
245
|
+
| Filtering | `.where()`, `.andWhere()`, `.orWhere()`, `.whereNot()`, `.whereIn()`, `.whereNotIn()`, `.orWhereIn()`, `.orWhereNotIn()`, `.whereNull()`, `.whereNotNull()`, `.orWhereNull()`, `.orWhereNotNull()`, `.whereBetween()`, `.whereNotBetween()`, `.whereLike()`, `.whereILike()`, `.whereRaw()`, `.whereExists()` |
|
|
246
|
+
| Ordering | `.orderBy(col, dir?)`, `.orderByRaw(sql)` |
|
|
247
|
+
| Grouping | `.groupBy(...cols)`, `.groupByRaw(sql)`, `.having(col, op, val)`, `.havingRaw(sql)` |
|
|
248
|
+
| Pagination | `.limit(n)`, `.offset(n)` |
|
|
249
|
+
| Selection | `.select(...cols)`, `.distinct(...cols)` |
|
|
250
|
+
| Aggregates | `.count(col?)`, `.countDistinct(col?)`, `.min(col)`, `.max(col)`, `.sum(col)`, `.avg(col)` |
|
|
251
|
+
| Writes | `.insert(data)`, `.insertMany(data[])`, `.update(data)`, `.delete()` |
|
|
252
|
+
| Execution | `.execute()`, `.first()`, `await builder` (thenable) |
|
|
253
|
+
| Debugging | `.toQuery()`, `.toString()` |
|
|
254
|
+
| Escape hatch | `.apply(fn)` |
|