@cleverbrush/knex-schema 3.1.0 → 4.1.0

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 CHANGED
@@ -214,6 +214,161 @@ const rows = await query(db, UserSchema)
214
214
 
215
215
  ---
216
216
 
217
+ ## Scopes
218
+
219
+ Define **reusable WHERE/ORDER/LIMIT conditions** on the schema. A default scope is applied
220
+ automatically unless bypassed with `.unscoped()`.
221
+
222
+ ```typescript
223
+ const PostSchema = object({
224
+ id: number(),
225
+ title: string(),
226
+ status: string(),
227
+ isActive: boolean().hasColumnName('is_active'),
228
+ })
229
+ .hasTableName('posts')
230
+ .scope('published', q => q.where(t => t.status, 'published'))
231
+ .scope('recent', q => q.orderBy(t => t.id, 'desc').limit(10))
232
+ .defaultScope( q => q.where(t => t.isActive, true));
233
+
234
+ // Apply named scopes
235
+ const posts = await query(db, PostSchema)
236
+ .scoped('published')
237
+ .scoped('recent');
238
+
239
+ // Bypass default scope (also skips soft-delete filter if present)
240
+ const all = await query(db, PostSchema).unscoped();
241
+ ```
242
+
243
+ `scoped()` is statically typed: TypeScript only allows registered scope names.
244
+
245
+ ---
246
+
247
+ ## Projections
248
+
249
+ Define **named column subsets** on the schema with `.projection(name, columns)`. At query time,
250
+ `.projected(name)` restricts the `SELECT` clause **and** narrows the TypeScript result type to
251
+ `Pick<Row, Keys>` — accessing columns outside the projection is a compile-time error.
252
+
253
+ ### String-tuple form
254
+
255
+ ```typescript
256
+ const PostSchema = object({
257
+ id: number().primaryKey(),
258
+ title: string(),
259
+ body: string(),
260
+ status: string(),
261
+ })
262
+ .hasTableName('posts')
263
+ .projection('summary', ['id', 'title'] as const)
264
+ .projection('withStatus', ['id', 'title', 'status'] as const);
265
+
266
+ const rows = await query(db, PostSchema)
267
+ .scoped('published')
268
+ .projected('summary');
269
+
270
+ // rows: Array<Pick<Post, 'id' | 'title'>>
271
+ // rows[0].body // ← TypeScript error: 'body' not in projection ✓
272
+ ```
273
+
274
+ ### Accessor form
275
+
276
+ ```typescript
277
+ .projection('withStatus', t => [t.id, t.title, t.status])
278
+ ```
279
+
280
+ The accessor receives the schema's property-descriptor tree; each element resolves to the
281
+ property name at runtime. This form is more refactor-safe but does not provide the compile-time
282
+ `Pick<>` narrowing that the tuple form offers.
283
+
284
+ ### Conflict rules
285
+
286
+ `.projected()` cannot be combined with `.select()`, `.distinct()`, or any aggregate
287
+ (`.count()`, `.min()`, etc.) on the same query. Attempting to do so throws at runtime.
288
+
289
+ ### Column-name mapping
290
+
291
+ `hasColumnName()` is respected: if `isActive` is mapped to `is_active`, the generated SQL
292
+ uses `is_active` automatically.
293
+
294
+ ---
295
+
296
+ ## ORM Extensions
297
+
298
+ In addition to query building, this package provides the schema-level primitives that
299
+ [`@cleverbrush/orm`](https://www.npmjs.com/package/@cleverbrush/orm) and
300
+ [`@cleverbrush/orm-cli`](https://www.npmjs.com/package/@cleverbrush/orm-cli) build on top of:
301
+
302
+ ### `defineEntity(schema)` — relations
303
+
304
+ Wrap a schema to declare typed `belongsTo` / `hasOne` / `hasMany` / `belongsToMany`
305
+ relations that downstream packages use for eager-loading joins and ORM navigation properties.
306
+
307
+ ```typescript
308
+ import { defineEntity, object, number, string } from '@cleverbrush/knex-schema';
309
+
310
+ const UserSchema = object({
311
+ id: number().primaryKey(),
312
+ email: string(),
313
+ }).hasTableName('users');
314
+
315
+ const PostSchema = object({
316
+ id: number().primaryKey(),
317
+ title: string(),
318
+ authorId: number().hasColumnName('author_id'),
319
+ author: UserSchema.optional(),
320
+ }).hasTableName('posts');
321
+
322
+ export const PostEntity = defineEntity(PostSchema)
323
+ .belongsTo(t => t.author, l => l.authorId, r => r.id);
324
+ ```
325
+
326
+ The returned `Entity` carries the relation map in its type, so downstream `query(db, entity)`
327
+ calls (and `@cleverbrush/orm`'s `DbSet.include()`) get full inference.
328
+
329
+ ### Polymorphism (STI / CTI)
330
+
331
+ Mark a schema as polymorphic to support single-table or class-table inheritance — variants
332
+ are discoverable via `getVariants()` / `getPolymorphicVariantSchemas()`:
333
+
334
+ ```typescript
335
+ import { POLYMORPHIC_TYPE_BRAND } from '@cleverbrush/knex-schema';
336
+ ```
337
+
338
+ See the `@cleverbrush/orm` docs for the full inheritance API (`.ofVariant()` etc.).
339
+
340
+ ### Migration generation (snapshot-based)
341
+
342
+ | Function | Purpose |
343
+ |---|---|
344
+ | `entitiesToSnapshot(entities)` | Materialise entity definitions into a JSON-serialisable schema snapshot |
345
+ | `loadSnapshot(path)` / `writeSnapshot(path, snap)` | Read/write the committed snapshot file |
346
+ | `generateMigrationsForContext(entities, prevSnapshot)` | Diff entities against the snapshot and emit a TS migration source plus the next snapshot |
347
+ | `generateMigration(snapshotA, snapshotB)` | Lower-level snapshot-vs-snapshot diff |
348
+ | `diffSchema(schema, dbState)` / `applyDiff(knex, diff, table)` | Live-database diff/apply (used by `cb-orm db push`) |
349
+ | `introspectDatabase(knex, table)` / `tableExistsInDb(knex, table)` | Database introspection helpers |
350
+ | `generateCreateTable(schema)` / `generateCreatePolymorphicTables(schema)` | Knex-statement builders for fresh `CREATE TABLE` |
351
+
352
+ Most users invoke these indirectly through the [`cb-orm`](https://www.npmjs.com/package/@cleverbrush/orm-cli)
353
+ CLI (`cb-orm migrate generate`, `cb-orm db push`).
354
+
355
+ ### Row-version optimistic concurrency
356
+
357
+ Mark a column as a row version with `.rowVersion()` to opt-in to optimistic concurrency
358
+ checks in `@cleverbrush/orm`'s change tracker:
359
+
360
+ ```typescript
361
+ const TodoSchema = object({
362
+ id: number().primaryKey(),
363
+ title: string(),
364
+ rowVersion: number().rowVersion(),
365
+ }).hasTableName('todos');
366
+ ```
367
+
368
+ `getRowVersionColumn(schema)` returns the marked column at runtime.
369
+
370
+ ---
371
+
217
372
  ## Column Reference Patterns
218
373
 
219
374
  Both styles are equivalent and resolve to the same SQL column:
@@ -246,7 +401,7 @@ Optionally pass a `baseQuery` (e.g. a scoped `knex('users').where('deleted_at',
246
401
  | Ordering | `.orderBy(col, dir?)`, `.orderByRaw(sql)` |
247
402
  | Grouping | `.groupBy(...cols)`, `.groupByRaw(sql)`, `.having(col, op, val)`, `.havingRaw(sql)` |
248
403
  | Pagination | `.limit(n)`, `.offset(n)` |
249
- | Selection | `.select(...cols)`, `.distinct(...cols)` |
404
+ | Selection | `.select(...cols)`, `.distinct(...cols)`, `.projected(name)` |
250
405
  | Aggregates | `.count(col?)`, `.countDistinct(col?)`, `.min(col)`, `.max(col)`, `.sum(col)`, `.avg(col)` |
251
406
  | Writes | `.insert(data)`, `.insertMany(data[])`, `.update(data)`, `.delete()` |
252
407
  | Execution | `.execute()`, `.first()`, `await builder` (thenable) |