ReviewOS

also looking at this

stacks/bun-query-builder

feat(types): export QueryBuilderOptions, the shape an app actually writes

#1063
Closed glennmichael123 wants to merge feat/query-builder-options into main
2 files +68 -6
packages/bun-query-builder/src/config.tsmodified+21-6
Changes to packages/bun-query-builder/src/config.ts
@@ -1,4 +1,4 @@
1import type { QueryBuilderConfig, SupportedDialect } from './types'
1import type { QueryBuilderConfig, QueryBuilderOptions, SupportedDialect } from './types'
22import process from 'node:process'
33import { loadConfig } from 'bunfig'
44
@@ -214,7 +214,7 @@ export async function getConfig(): Promise<QueryBuilderConfig> {
214214 defaultConfig,
215215 })
216216
217 const applicable: Partial<QueryBuilderConfig> = {}
217 const applicable: QueryBuilderOptions = {}
218218 for (const [key, value] of Object.entries(loaded ?? {})) {
219219 if (!configState.explicit.has(key))
220220 (applicable as Record<string, unknown>)[key] = value
@@ -267,7 +267,7 @@ const _warnedDialectConflicts = new Set<string>()
267267 * Shared by `setConfig()` (explicit, marks keys) and `getConfig()` (from a
268268 * file, does not) so the two paths can never drift on how nested objects merge.
269269 */
270function applyConfig(userConfig: Partial<QueryBuilderConfig>): void {
270function applyConfig(userConfig: QueryBuilderOptions): void {
271271 // NEVER reassign `config` here (i.e. `config = { ...defaultConfig }`).
272272 // Reassigning an `export let` triggers Bun's bundler to split the
273273 // binding: the write goes to one identifier and every reader (e.g.
@@ -290,15 +290,30 @@ function applyConfig(userConfig: Partial<QueryBuilderConfig>): void {
290290 if (userConfig.pagination) {
291291 config.pagination = { ...config.pagination, ...userConfig.pagination }
292292 }
293 // These two sections are optional on the resolved config, so merging only
294 // over the CURRENT value cannot guarantee the required members survive a
295 // partial override. Shallow `Partial` hid that by demanding a complete
296 // section from the caller; a real partial makes it visible. Seeding from
297 // `defaultConfig` is also the behaviour an app expects: setting
298 // `softDeletes: { enabled: true }` should inherit `column` and
299 // `defaultFilter`, not drop them.
293300 if (userConfig.softDeletes) {
294 config.softDeletes = { ...config.softDeletes, ...userConfig.softDeletes }
301 config.softDeletes = {
302 ...defaultConfig.softDeletes!,
303 ...config.softDeletes,
304 ...userConfig.softDeletes,
305 }
295306 }
296307 if (userConfig.vitess) {
297 config.vitess = { ...config.vitess, ...userConfig.vitess }
308 config.vitess = {
309 ...defaultConfig.vitess!,
310 ...config.vitess,
311 ...userConfig.vitess,
312 }
298313 }
299314}
300315
301export function setConfig(userConfig: Partial<QueryBuilderConfig>): void {
316export function setConfig(userConfig: QueryBuilderOptions): void {
302317 // Detect cross-instance dialect conflicts and warn once. The proper
303318 // fix is per-instance config (stacksjs/stacks#1862 #18); this guard
304319 // surfaces the symptom so callers can see the shared-state problem
packages/bun-query-builder/src/types.tsmodified+47-0
Changes to packages/bun-query-builder/src/types.ts
@@ -452,6 +452,53 @@ export interface QueryBuilderConfig {
452452 }
453453}
454454
455/**
456 * Every property optional, recursively, leaving arrays and functions intact.
457 *
458 * Plain `Partial` is only one level deep, which is not enough here: it would
459 * still demand a complete `TimestampConfig` from anyone who wanted to rename a
460 * single column.
461 */
462// `_args`, not `args`: this is a type-level signature, so the parameter name is
463// required syntax with nothing that could ever reference it. pickier's
464// no-unused-vars flags it all the same, and `^_` is the pattern it exempts.
465export type DeepPartial<T> = T extends (..._args: any[]) => any
466 ? T
467 : T extends readonly (infer U)[]
468 ? readonly U[]
469 : T extends object
470 ? { [K in keyof T]?: DeepPartial<T[K]> }
471 : T
472
473/**
474 * # `QueryBuilderOptions`
475 *
476 * **What an application writes.** Every field is optional, because every field
477 * has a default in {@link defaultConfig} — an app supplies only what it wants
478 * to differ.
479 *
480 * Distinct from {@link QueryBuilderConfig}, which is the RESOLVED shape this
481 * library reads after merging those defaults, and where a required field means
482 * "guaranteed present by the time anything reads it".
483 *
484 * Conflating the two made every field added to the resolved config a
485 * downstream compile error. 0.2.25 declared `migrationDir: string` without the
486 * optional marker, and the app that consumed it had to hard-code
487 * `'database/migrations'` — a value this package already defaults in three
488 * separate places — purely to satisfy the type. `snapshotDir` did the same
489 * thing one release earlier. See stacksjs/bun-query-builder#1062.
490 *
491 * `setConfig()` has always accepted a partial, so this names a contract the
492 * library already honoured rather than introducing a new one.
493 *
494 * ```ts
495 * export default {
496 * dialect: 'postgres',
497 * } satisfies QueryBuilderOptions
498 * ```
499 */
500export type QueryBuilderOptions = DeepPartial<QueryBuilderConfig>
501
455502export interface CliOption {
456503 verbose: boolean
457504}