@forinda/kickjs-drizzle 1.2.8 → 1.2.10

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/dist/index.d.ts CHANGED
@@ -104,6 +104,112 @@ interface DrizzleQueryConfig {
104
104
  /** Columns to search across when a search string is provided */
105
105
  searchColumns?: string[];
106
106
  }
107
+ /**
108
+ * Type-safe Drizzle query configuration using Column objects.
109
+ *
110
+ * Use this instead of `DrizzleQueryConfig` for type-safe column references
111
+ * that are validated at compile time. Column objects carry `dataType` metadata
112
+ * enabling automatic type coercion of filter values.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { users } from './schema'
117
+ * import type { DrizzleColumnQueryConfig } from '@forinda/kickjs-drizzle'
118
+ *
119
+ * const config: DrizzleColumnQueryConfig = {
120
+ * columns: {
121
+ * status: users.status,
122
+ * isActive: users.isActive,
123
+ * createdAt: users.createdAt,
124
+ * },
125
+ * sortable: {
126
+ * name: users.name,
127
+ * createdAt: users.createdAt,
128
+ * },
129
+ * searchColumns: [users.firstName, users.lastName, users.email],
130
+ * baseCondition: eq(users.tenantId, tenantId),
131
+ * }
132
+ * ```
133
+ */
134
+ interface DrizzleColumnQueryConfig {
135
+ /**
136
+ * Map of filterable field names to Drizzle Column objects.
137
+ * Keys are the query parameter names, values are the actual schema columns.
138
+ * The column's `dataType` is used for automatic type coercion.
139
+ */
140
+ columns: Record<string, any>;
141
+ /**
142
+ * Map of sortable field names to Drizzle Column objects.
143
+ * If not provided, falls back to `columns` for sort lookups.
144
+ */
145
+ sortable?: Record<string, any>;
146
+ /**
147
+ * Column objects to search across when a search string is provided.
148
+ * Each entry should be a Drizzle Column (not a string).
149
+ */
150
+ searchColumns?: any[];
151
+ /**
152
+ * A pre-built SQL condition that is always prepended to the WHERE clause.
153
+ * Use for scoping queries by tenant, workspace, or other invariants.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * baseCondition: and(eq(tasks.tenantId, tid), eq(tasks.workspaceId, wid))
158
+ * ```
159
+ */
160
+ baseCondition?: any;
161
+ }
162
+ /**
163
+ * Configuration type for defining query param schemas with Drizzle Column objects.
164
+ *
165
+ * Used in constants files to define which columns are filterable, sortable, and searchable.
166
+ * This type is consumed by both `DrizzleQueryAdapter.buildFromColumns()` and `@ApiQueryParams()`.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * import type { DrizzleQueryParamsConfig } from '@forinda/kickjs-drizzle'
171
+ * import { tasks } from '@/db/schema'
172
+ *
173
+ * export const TASK_QUERY_CONFIG: DrizzleQueryParamsConfig = {
174
+ * columns: {
175
+ * status: tasks.status,
176
+ * priority: tasks.priority,
177
+ * },
178
+ * sortable: {
179
+ * title: tasks.title,
180
+ * createdAt: tasks.createdAt,
181
+ * },
182
+ * searchColumns: [tasks.title, tasks.key],
183
+ * }
184
+ * ```
185
+ */
186
+ interface DrizzleQueryParamsConfig {
187
+ /** Filterable columns: keys are query param names, values are Drizzle Column objects */
188
+ columns: Record<string, any>;
189
+ /** Sortable columns: keys are query param names, values are Drizzle Column objects */
190
+ sortable?: Record<string, any>;
191
+ /** Columns for text search */
192
+ searchColumns?: any[];
193
+ /** Optional base condition for scoping (tenant, workspace, etc.) */
194
+ baseCondition?: any;
195
+ }
196
+ /**
197
+ * Convert a DrizzleQueryParamsConfig into a string-based QueryFieldConfig.
198
+ * Useful for passing to `@ApiQueryParams()` or other APIs that expect string arrays.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * import { toQueryFieldConfig } from '@forinda/kickjs-drizzle'
203
+ *
204
+ * const fieldConfig = toQueryFieldConfig(TASK_QUERY_CONFIG)
205
+ * // → { filterable: ['status', 'priority'], sortable: ['title', 'createdAt'], searchable: [] }
206
+ * ```
207
+ */
208
+ declare function toQueryFieldConfig(config: DrizzleQueryParamsConfig): {
209
+ filterable: string[];
210
+ sortable: string[];
211
+ searchable: string[];
212
+ };
107
213
  /**
108
214
  * Result shape compatible with Drizzle's query builder.
109
215
  * Use with `db.select().from(table).where(result.where).orderBy(...result.orderBy).limit(result.limit).offset(result.offset)`
@@ -124,10 +230,10 @@ interface DrizzleQueryResult {
124
230
  *
125
231
  * @example
126
232
  * ```ts
127
- * import { eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc } from 'drizzle-orm'
233
+ * import { eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc } from 'drizzle-orm'
128
234
  *
129
235
  * const adapter = new DrizzleQueryAdapter({
130
- * eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc,
236
+ * eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc,
131
237
  * })
132
238
  * ```
133
239
  */
@@ -140,6 +246,7 @@ interface DrizzleOps {
140
246
  lte: (column: any, value: any) => any;
141
247
  ilike: (column: any, value: string) => any;
142
248
  inArray: (column: any, values: any[]) => any;
249
+ between?: (column: any, min: any, max: any) => any;
143
250
  and: (...conditions: any[]) => any;
144
251
  or: (...conditions: any[]) => any;
145
252
  asc: (column: any) => any;
@@ -148,20 +255,24 @@ interface DrizzleOps {
148
255
  /**
149
256
  * Translates a ParsedQuery into Drizzle-compatible query parts.
150
257
  *
258
+ * Supports two modes:
259
+ * 1. **String-based** (legacy): `build(parsed, { table, searchColumns })` — looks up columns by string name
260
+ * 2. **Column-based** (recommended): `buildFromColumns(parsed, config)` — uses actual Column objects for type safety
261
+ *
151
262
  * @example
152
263
  * ```ts
153
- * import { eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc } from 'drizzle-orm'
154
- * import { users } from './schema'
264
+ * // String-based (legacy)
265
+ * const query = adapter.build(parsed, { table: users, searchColumns: ['name', 'email'] })
155
266
  *
156
- * const adapter = new DrizzleQueryAdapter({
157
- * eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc,
267
+ * // Column-based (recommended)
268
+ * const query = adapter.buildFromColumns(parsed, {
269
+ * columns: { status: users.status, isActive: users.isActive },
270
+ * searchColumns: [users.name, users.email],
271
+ * baseCondition: eq(users.tenantId, tid),
158
272
  * })
159
- * const parsed = ctx.qs({ filters: ['name', 'email'], sort: ['name'] })
160
- * const query = adapter.build(parsed, { table: users, searchColumns: ['name', 'email'] })
161
273
  *
162
274
  * const results = await db
163
- * .select()
164
- * .from(users)
275
+ * .select().from(users)
165
276
  * .where(query.where)
166
277
  * .orderBy(...query.orderBy)
167
278
  * .limit(query.limit)
@@ -172,13 +283,52 @@ declare class DrizzleQueryAdapter implements QueryBuilderAdapter<DrizzleQueryRes
172
283
  private ops;
173
284
  readonly name = "DrizzleQueryAdapter";
174
285
  constructor(ops: DrizzleOps);
286
+ /**
287
+ * Build query from string-based config (legacy API).
288
+ * Prefer `buildFromColumns()` for type safety.
289
+ */
175
290
  build(parsed: ParsedQuery, config?: DrizzleQueryConfig): DrizzleQueryResult;
176
- /** Map a single FilterItem to a Drizzle condition */
291
+ /**
292
+ * Build query using Column objects for type-safe filtering, sorting, and search.
293
+ *
294
+ * Features over `build()`:
295
+ * - Column references validated at compile time
296
+ * - Automatic type coercion based on `column.dataType` (boolean, number, date)
297
+ * - `baseCondition` support for tenant/workspace scoping
298
+ * - Native `between` operator support
299
+ * - Separate `sortable` map so filterable and sortable columns can differ
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * const query = adapter.buildFromColumns(parsed, {
304
+ * columns: { status: tasks.status, priority: tasks.priority },
305
+ * sortable: { title: tasks.title, createdAt: tasks.createdAt },
306
+ * searchColumns: [tasks.title, tasks.key],
307
+ * baseCondition: eq(tasks.workspaceId, wid),
308
+ * })
309
+ * ```
310
+ */
311
+ buildFromColumns(parsed: ParsedQuery, config: DrizzleColumnQueryConfig): DrizzleQueryResult;
312
+ /** Map a single FilterItem to a Drizzle condition using string-based table lookup */
177
313
  private buildFilter;
314
+ /**
315
+ * Map a FilterItem to a Drizzle condition using a Column object.
316
+ * Coerces values based on `column.dataType` for type-safe filtering.
317
+ */
318
+ private buildColumnFilter;
178
319
  /** Build Drizzle orderBy from SortItem[] */
179
320
  private buildSort;
180
321
  /** Attempt to coerce a string value to a number or boolean if appropriate */
181
322
  private coerce;
323
+ /**
324
+ * Coerce a string value based on the column's dataType.
325
+ *
326
+ * - `'boolean'` → `true`/`false`
327
+ * - `'number'` / `'bigint'` → `Number(value)`
328
+ * - `'date'` → `new Date(value)` (ISO 8601 strings)
329
+ * - Everything else → original string
330
+ */
331
+ private coerceByDataType;
182
332
  }
183
333
 
184
- export { DRIZZLE_DB, DrizzleAdapter, type DrizzleAdapterOptions, type DrizzleOps, DrizzleQueryAdapter, type DrizzleQueryConfig, type DrizzleQueryResult };
334
+ export { DRIZZLE_DB, DrizzleAdapter, type DrizzleAdapterOptions, type DrizzleColumnQueryConfig, type DrizzleOps, DrizzleQueryAdapter, type DrizzleQueryConfig, type DrizzleQueryParamsConfig, type DrizzleQueryResult, toQueryFieldConfig };
package/dist/index.js CHANGED
@@ -40,6 +40,14 @@ var DrizzleAdapter = class {
40
40
  };
41
41
 
42
42
  // src/query-adapter.ts
43
+ function toQueryFieldConfig(config) {
44
+ return {
45
+ filterable: Object.keys(config.columns),
46
+ sortable: config.sortable ? Object.keys(config.sortable) : [],
47
+ searchable: config.searchColumns ? config.searchColumns.map((col) => col.name ?? "").filter(Boolean) : []
48
+ };
49
+ }
50
+ __name(toQueryFieldConfig, "toQueryFieldConfig");
43
51
  var DrizzleQueryAdapter = class {
44
52
  static {
45
53
  __name(this, "DrizzleQueryAdapter");
@@ -49,6 +57,10 @@ var DrizzleQueryAdapter = class {
49
57
  constructor(ops) {
50
58
  this.ops = ops;
51
59
  }
60
+ /**
61
+ * Build query from string-based config (legacy API).
62
+ * Prefer `buildFromColumns()` for type safety.
63
+ */
52
64
  build(parsed, config = {}) {
53
65
  const result = {
54
66
  orderBy: [],
@@ -74,7 +86,58 @@ var DrizzleQueryAdapter = class {
74
86
  result.orderBy = this.buildSort(config.table, parsed.sort);
75
87
  return result;
76
88
  }
77
- /** Map a single FilterItem to a Drizzle condition */
89
+ /**
90
+ * Build query using Column objects for type-safe filtering, sorting, and search.
91
+ *
92
+ * Features over `build()`:
93
+ * - Column references validated at compile time
94
+ * - Automatic type coercion based on `column.dataType` (boolean, number, date)
95
+ * - `baseCondition` support for tenant/workspace scoping
96
+ * - Native `between` operator support
97
+ * - Separate `sortable` map so filterable and sortable columns can differ
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const query = adapter.buildFromColumns(parsed, {
102
+ * columns: { status: tasks.status, priority: tasks.priority },
103
+ * sortable: { title: tasks.title, createdAt: tasks.createdAt },
104
+ * searchColumns: [tasks.title, tasks.key],
105
+ * baseCondition: eq(tasks.workspaceId, wid),
106
+ * })
107
+ * ```
108
+ */
109
+ buildFromColumns(parsed, config) {
110
+ const result = {
111
+ orderBy: [],
112
+ limit: parsed.pagination.limit,
113
+ offset: parsed.pagination.offset
114
+ };
115
+ const conditions = [];
116
+ if (config.baseCondition) {
117
+ conditions.push(config.baseCondition);
118
+ }
119
+ for (const filter of parsed.filters) {
120
+ const column = config.columns[filter.field];
121
+ if (!column) continue;
122
+ const condition = this.buildColumnFilter(column, filter);
123
+ if (condition) conditions.push(condition);
124
+ }
125
+ if (parsed.search && config.searchColumns && config.searchColumns.length > 0) {
126
+ const searchConditions = config.searchColumns.map((col) => this.ops.ilike(col, `%${parsed.search}%`));
127
+ if (searchConditions.length > 0) {
128
+ conditions.push(this.ops.or(...searchConditions));
129
+ }
130
+ }
131
+ if (conditions.length === 1) {
132
+ result.where = conditions[0];
133
+ } else if (conditions.length > 1) {
134
+ result.where = this.ops.and(...conditions);
135
+ }
136
+ const sortMap = config.sortable ?? config.columns;
137
+ result.orderBy = parsed.sort.filter((item) => sortMap[item.field]).map((item) => item.direction === "desc" ? this.ops.desc(sortMap[item.field]) : this.ops.asc(sortMap[item.field]));
138
+ return result;
139
+ }
140
+ /** Map a single FilterItem to a Drizzle condition using string-based table lookup */
78
141
  buildFilter(table, filter) {
79
142
  const column = table[filter.field];
80
143
  if (!column) return null;
@@ -110,6 +173,48 @@ var DrizzleQueryAdapter = class {
110
173
  return this.ops.eq(column, value);
111
174
  }
112
175
  }
176
+ /**
177
+ * Map a FilterItem to a Drizzle condition using a Column object.
178
+ * Coerces values based on `column.dataType` for type-safe filtering.
179
+ */
180
+ buildColumnFilter(column, filter) {
181
+ const value = this.coerceByDataType(filter.value, column.dataType);
182
+ switch (filter.operator) {
183
+ case "eq":
184
+ return this.ops.eq(column, value);
185
+ case "neq":
186
+ return this.ops.ne(column, value);
187
+ case "gt":
188
+ return this.ops.gt(column, value);
189
+ case "gte":
190
+ return this.ops.gte(column, value);
191
+ case "lt":
192
+ return this.ops.lt(column, value);
193
+ case "lte":
194
+ return this.ops.lte(column, value);
195
+ case "contains":
196
+ return this.ops.ilike(column, `%${filter.value}%`);
197
+ case "starts":
198
+ return this.ops.ilike(column, `${filter.value}%`);
199
+ case "ends":
200
+ return this.ops.ilike(column, `%${filter.value}`);
201
+ case "in": {
202
+ const values = filter.value.split(",").map((v) => this.coerceByDataType(v.trim(), column.dataType));
203
+ return this.ops.inArray(column, values);
204
+ }
205
+ case "between": {
206
+ const [minStr, maxStr] = filter.value.split(",").map((v) => v.trim());
207
+ const min = this.coerceByDataType(minStr, column.dataType);
208
+ const max = this.coerceByDataType(maxStr, column.dataType);
209
+ if (this.ops.between) {
210
+ return this.ops.between(column, min, max);
211
+ }
212
+ return this.ops.and(this.ops.gte(column, min), this.ops.lte(column, max));
213
+ }
214
+ default:
215
+ return this.ops.eq(column, value);
216
+ }
217
+ }
113
218
  /** Build Drizzle orderBy from SortItem[] */
114
219
  buildSort(table, sort) {
115
220
  return sort.filter((item) => table[item.field]).map((item) => item.direction === "desc" ? this.ops.desc(table[item.field]) : this.ops.asc(table[item.field]));
@@ -122,10 +227,39 @@ var DrizzleQueryAdapter = class {
122
227
  if (!Number.isNaN(num) && value.trim() !== "") return num;
123
228
  return value;
124
229
  }
230
+ /**
231
+ * Coerce a string value based on the column's dataType.
232
+ *
233
+ * - `'boolean'` → `true`/`false`
234
+ * - `'number'` / `'bigint'` → `Number(value)`
235
+ * - `'date'` → `new Date(value)` (ISO 8601 strings)
236
+ * - Everything else → original string
237
+ */
238
+ coerceByDataType(value, dataType) {
239
+ if (!dataType) return this.coerce(value);
240
+ switch (dataType) {
241
+ case "boolean":
242
+ return value === "true" || value === "1";
243
+ case "number":
244
+ case "bigint": {
245
+ const num = Number(value);
246
+ return Number.isNaN(num) ? value : num;
247
+ }
248
+ case "date":
249
+ case "localDate":
250
+ case "localDateTime": {
251
+ const date = new Date(value);
252
+ return Number.isNaN(date.getTime()) ? value : date;
253
+ }
254
+ default:
255
+ return value;
256
+ }
257
+ }
125
258
  };
126
259
  export {
127
260
  DRIZZLE_DB,
128
261
  DrizzleAdapter,
129
- DrizzleQueryAdapter
262
+ DrizzleQueryAdapter,
263
+ toQueryFieldConfig
130
264
  };
131
265
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/drizzle.adapter.ts","../src/types.ts","../src/query-adapter.ts"],"sourcesContent":["import { Logger, type AppAdapter, type Container, Scope } from '@forinda/kickjs-core'\nimport { DRIZZLE_DB, type DrizzleAdapterOptions } from './types'\n\nconst log = Logger.for('DrizzleAdapter')\n\n/**\n * Drizzle ORM adapter — registers a Drizzle database instance in the DI\n * container and manages its lifecycle.\n *\n * Works with any Drizzle driver: `drizzle-orm/postgres-js`, `drizzle-orm/node-postgres`,\n * `drizzle-orm/mysql2`, `drizzle-orm/better-sqlite3`, `drizzle-orm/libsql`, etc.\n *\n * The adapter is generic — the db type is inferred from what you pass in,\n * so services can inject the fully-typed database instance.\n *\n * @example\n * ```ts\n * import { drizzle } from 'drizzle-orm/better-sqlite3'\n * import * as schema from './schema'\n * import { DrizzleAdapter } from '@forinda/kickjs-drizzle'\n *\n * const db = drizzle({ client: sqlite, schema })\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new DrizzleAdapter({ db, onShutdown: () => sqlite.close() }),\n * ],\n * })\n * ```\n *\n * Inject the typed db instance in services:\n * ```ts\n * import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'\n * import * as schema from './schema'\n *\n * @Service()\n * class UserService {\n * constructor(@Inject(DRIZZLE_DB) private db: BetterSQLite3Database<typeof schema>) {}\n * }\n * ```\n */\nexport class DrizzleAdapter<TDb = unknown> implements AppAdapter {\n name = 'DrizzleAdapter'\n private db: TDb\n private onShutdown?: () => void | Promise<void>\n\n constructor(private options: DrizzleAdapterOptions<TDb>) {\n this.db = options.db\n this.onShutdown = options.onShutdown\n }\n\n /** Register the Drizzle db instance in the DI container */\n beforeStart(_app: unknown, container: Container): void {\n if (this.options.logging) {\n log.info('Query logging enabled')\n }\n\n container.registerFactory(DRIZZLE_DB, () => this.db, Scope.SINGLETON)\n\n log.info('Drizzle database registered in DI container')\n }\n\n /** Close the underlying connection on shutdown */\n async shutdown(): Promise<void> {\n if (this.onShutdown) {\n await this.onShutdown()\n log.info('Drizzle connection closed')\n }\n }\n}\n","import type { MaybePromise } from '@forinda/kickjs-core'\n\n/** DI token for resolving the Drizzle database instance from the container */\nexport const DRIZZLE_DB = Symbol('DrizzleDB')\n\nexport interface DrizzleAdapterOptions<TDb = unknown> {\n /**\n * Drizzle database instance — the return value of `drizzle()`.\n * Preserves the full type so services can inject it type-safely.\n *\n * @example\n * ```ts\n * import { drizzle } from 'drizzle-orm/better-sqlite3'\n * import * as schema from './schema'\n *\n * const db = drizzle({ client: sqlite, schema })\n * // db is BetterSQLite3Database<typeof schema>\n *\n * new DrizzleAdapter({ db })\n * // TDb is inferred as BetterSQLite3Database<typeof schema>\n * ```\n */\n db: TDb\n\n /** Enable query logging (default: false) */\n logging?: boolean\n\n /**\n * Optional shutdown function to close the underlying connection pool.\n * Drizzle doesn't expose a universal disconnect — this lets you pass your\n * driver's cleanup (e.g., `pool.end()` for postgres, `client.close()` for libsql).\n *\n * @example\n * ```ts\n * const pool = new Pool({ connectionString: '...' })\n * const db = drizzle(pool)\n *\n * new DrizzleAdapter({\n * db,\n * onShutdown: () => pool.end(),\n * })\n * ```\n */\n onShutdown?: () => MaybePromise<any>\n}\n","import type { QueryBuilderAdapter, ParsedQuery, FilterItem, SortItem } from '@forinda/kickjs-http'\n\n/**\n * Configuration for the Drizzle query builder adapter.\n *\n * Unlike Prisma which uses its own query builder API, Drizzle uses SQL-like\n * operators (`eq`, `gt`, `like`, etc.) from `drizzle-orm`. This adapter\n * produces a config object that can be spread into Drizzle's `select().from().where()`.\n */\nexport interface DrizzleQueryConfig {\n /** The Drizzle table schema object (e.g., `users` from your schema) */\n table: Record<string, any>\n /** Columns to search across when a search string is provided */\n searchColumns?: string[]\n}\n\n/**\n * Result shape compatible with Drizzle's query builder.\n * Use with `db.select().from(table).where(result.where).orderBy(...result.orderBy).limit(result.limit).offset(result.offset)`\n */\nexport interface DrizzleQueryResult {\n /** SQL condition — pass to `.where()` */\n where?: any\n /** Array of order expressions — spread into `.orderBy()` */\n orderBy: any[]\n /** Row limit — pass to `.limit()` */\n limit: number\n /** Row offset — pass to `.offset()` */\n offset: number\n}\n\n/**\n * Drizzle operator functions required by the query adapter.\n * Pass these from your `drizzle-orm` import to avoid version coupling.\n *\n * @example\n * ```ts\n * import { eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc } from 'drizzle-orm'\n *\n * const adapter = new DrizzleQueryAdapter({\n * eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc,\n * })\n * ```\n */\nexport interface DrizzleOps {\n eq: (column: any, value: any) => any\n ne: (column: any, value: any) => any\n gt: (column: any, value: any) => any\n gte: (column: any, value: any) => any\n lt: (column: any, value: any) => any\n lte: (column: any, value: any) => any\n ilike: (column: any, value: string) => any\n inArray: (column: any, values: any[]) => any\n and: (...conditions: any[]) => any\n or: (...conditions: any[]) => any\n asc: (column: any) => any\n desc: (column: any) => any\n}\n\n/**\n * Translates a ParsedQuery into Drizzle-compatible query parts.\n *\n * @example\n * ```ts\n * import { eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc } from 'drizzle-orm'\n * import { users } from './schema'\n *\n * const adapter = new DrizzleQueryAdapter({\n * eq, ne, gt, gte, lt, lte, ilike, inArray, and, or, asc, desc,\n * })\n * const parsed = ctx.qs({ filters: ['name', 'email'], sort: ['name'] })\n * const query = adapter.build(parsed, { table: users, searchColumns: ['name', 'email'] })\n *\n * const results = await db\n * .select()\n * .from(users)\n * .where(query.where)\n * .orderBy(...query.orderBy)\n * .limit(query.limit)\n * .offset(query.offset)\n * ```\n */\nexport class DrizzleQueryAdapter implements QueryBuilderAdapter<\n DrizzleQueryResult,\n DrizzleQueryConfig\n> {\n readonly name = 'DrizzleQueryAdapter'\n\n constructor(private ops: DrizzleOps) {}\n\n build(\n parsed: ParsedQuery,\n config: DrizzleQueryConfig = {} as DrizzleQueryConfig,\n ): DrizzleQueryResult {\n const result: DrizzleQueryResult = {\n orderBy: [],\n limit: parsed.pagination.limit,\n offset: parsed.pagination.offset,\n }\n\n // Build where conditions\n const conditions: any[] = []\n\n // Filters\n for (const filter of parsed.filters) {\n const condition = this.buildFilter(config.table, filter)\n if (condition) conditions.push(condition)\n }\n\n // Search\n if (parsed.search && config.searchColumns && config.searchColumns.length > 0) {\n const searchConditions = config.searchColumns\n .filter((col) => config.table[col])\n .map((col) => this.ops.ilike(config.table[col], `%${parsed.search}%`))\n\n if (searchConditions.length > 0) {\n conditions.push(this.ops.or(...searchConditions))\n }\n }\n\n // Combine conditions\n if (conditions.length === 1) {\n result.where = conditions[0]\n } else if (conditions.length > 1) {\n result.where = this.ops.and(...conditions)\n }\n\n // Sort\n result.orderBy = this.buildSort(config.table, parsed.sort)\n\n return result\n }\n\n /** Map a single FilterItem to a Drizzle condition */\n private buildFilter(table: Record<string, any>, filter: FilterItem): any {\n const column = table[filter.field]\n if (!column) return null\n\n const value = this.coerce(filter.value)\n\n switch (filter.operator) {\n case 'eq':\n return this.ops.eq(column, value)\n case 'neq':\n return this.ops.ne(column, value)\n case 'gt':\n return this.ops.gt(column, value)\n case 'gte':\n return this.ops.gte(column, value)\n case 'lt':\n return this.ops.lt(column, value)\n case 'lte':\n return this.ops.lte(column, value)\n case 'contains':\n return this.ops.ilike(column, `%${filter.value}%`)\n case 'starts':\n return this.ops.ilike(column, `${filter.value}%`)\n case 'ends':\n return this.ops.ilike(column, `%${filter.value}`)\n case 'in': {\n const values = filter.value.split(',').map((v) => this.coerce(v.trim()))\n return this.ops.inArray(column, values)\n }\n case 'between': {\n const [min, max] = filter.value.split(',').map((v) => this.coerce(v.trim()))\n return this.ops.and(this.ops.gte(column, min), this.ops.lte(column, max))\n }\n default:\n return this.ops.eq(column, value)\n }\n }\n\n /** Build Drizzle orderBy from SortItem[] */\n private buildSort(table: Record<string, any>, sort: SortItem[]): any[] {\n return sort\n .filter((item) => table[item.field])\n .map((item) =>\n item.direction === 'desc'\n ? this.ops.desc(table[item.field])\n : this.ops.asc(table[item.field]),\n )\n }\n\n /** Attempt to coerce a string value to a number or boolean if appropriate */\n private coerce(value: string): string | number | boolean {\n if (value === 'true') return true\n if (value === 'false') return false\n const num = Number(value)\n if (!Number.isNaN(num) && value.trim() !== '') return num\n return value\n }\n}\n"],"mappings":"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","names":["Logger","Scope","DRIZZLE_DB","Symbol","log","Logger","for","DrizzleAdapter","name","db","onShutdown","options","beforeStart","_app","container","logging","info","registerFactory","DRIZZLE_DB","Scope","SINGLETON","shutdown","DrizzleQueryAdapter","name","ops","build","parsed","config","result","orderBy","limit","pagination","offset","conditions","filter","filters","condition","buildFilter","table","push","search","searchColumns","length","searchConditions","col","map","ilike","or","where","and","buildSort","sort","column","field","value","coerce","operator","eq","ne","gt","gte","lt","lte","values","split","v","trim","inArray","min","max","item","direction","desc","asc","num","Number","isNaN"]}
1
+ {"version":3,"sources":["../src/drizzle.adapter.ts","../src/types.ts","../src/query-adapter.ts"],"sourcesContent":["import { Logger, type AppAdapter, type Container, Scope } from '@forinda/kickjs-core'\nimport { DRIZZLE_DB, type DrizzleAdapterOptions } from './types'\n\nconst log = Logger.for('DrizzleAdapter')\n\n/**\n * Drizzle ORM adapter — registers a Drizzle database instance in the DI\n * container and manages its lifecycle.\n *\n * Works with any Drizzle driver: `drizzle-orm/postgres-js`, `drizzle-orm/node-postgres`,\n * `drizzle-orm/mysql2`, `drizzle-orm/better-sqlite3`, `drizzle-orm/libsql`, etc.\n *\n * The adapter is generic — the db type is inferred from what you pass in,\n * so services can inject the fully-typed database instance.\n *\n * @example\n * ```ts\n * import { drizzle } from 'drizzle-orm/better-sqlite3'\n * import * as schema from './schema'\n * import { DrizzleAdapter } from '@forinda/kickjs-drizzle'\n *\n * const db = drizzle({ client: sqlite, schema })\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new DrizzleAdapter({ db, onShutdown: () => sqlite.close() }),\n * ],\n * })\n * ```\n *\n * Inject the typed db instance in services:\n * ```ts\n * import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'\n * import * as schema from './schema'\n *\n * @Service()\n * class UserService {\n * constructor(@Inject(DRIZZLE_DB) private db: BetterSQLite3Database<typeof schema>) {}\n * }\n * ```\n */\nexport class DrizzleAdapter<TDb = unknown> implements AppAdapter {\n name = 'DrizzleAdapter'\n private db: TDb\n private onShutdown?: () => void | Promise<void>\n\n constructor(private options: DrizzleAdapterOptions<TDb>) {\n this.db = options.db\n this.onShutdown = options.onShutdown\n }\n\n /** Register the Drizzle db instance in the DI container */\n beforeStart(_app: unknown, container: Container): void {\n if (this.options.logging) {\n log.info('Query logging enabled')\n }\n\n container.registerFactory(DRIZZLE_DB, () => this.db, Scope.SINGLETON)\n\n log.info('Drizzle database registered in DI container')\n }\n\n /** Close the underlying connection on shutdown */\n async shutdown(): Promise<void> {\n if (this.onShutdown) {\n await this.onShutdown()\n log.info('Drizzle connection closed')\n }\n }\n}\n","import type { MaybePromise } from '@forinda/kickjs-core'\n\n/** DI token for resolving the Drizzle database instance from the container */\nexport const DRIZZLE_DB = Symbol('DrizzleDB')\n\nexport interface DrizzleAdapterOptions<TDb = unknown> {\n /**\n * Drizzle database instance — the return value of `drizzle()`.\n * Preserves the full type so services can inject it type-safely.\n *\n * @example\n * ```ts\n * import { drizzle } from 'drizzle-orm/better-sqlite3'\n * import * as schema from './schema'\n *\n * const db = drizzle({ client: sqlite, schema })\n * // db is BetterSQLite3Database<typeof schema>\n *\n * new DrizzleAdapter({ db })\n * // TDb is inferred as BetterSQLite3Database<typeof schema>\n * ```\n */\n db: TDb\n\n /** Enable query logging (default: false) */\n logging?: boolean\n\n /**\n * Optional shutdown function to close the underlying connection pool.\n * Drizzle doesn't expose a universal disconnect — this lets you pass your\n * driver's cleanup (e.g., `pool.end()` for postgres, `client.close()` for libsql).\n *\n * @example\n * ```ts\n * const pool = new Pool({ connectionString: '...' })\n * const db = drizzle(pool)\n *\n * new DrizzleAdapter({\n * db,\n * onShutdown: () => pool.end(),\n * })\n * ```\n */\n onShutdown?: () => MaybePromise<any>\n}\n","import type { QueryBuilderAdapter, ParsedQuery, FilterItem, SortItem } from '@forinda/kickjs-http'\n\n/**\n * Configuration for the Drizzle query builder adapter.\n *\n * Unlike Prisma which uses its own query builder API, Drizzle uses SQL-like\n * operators (`eq`, `gt`, `like`, etc.) from `drizzle-orm`. This adapter\n * produces a config object that can be spread into Drizzle's `select().from().where()`.\n */\nexport interface DrizzleQueryConfig {\n /** The Drizzle table schema object (e.g., `users` from your schema) */\n table: Record<string, any>\n /** Columns to search across when a search string is provided */\n searchColumns?: string[]\n}\n\n/**\n * Type-safe Drizzle query configuration using Column objects.\n *\n * Use this instead of `DrizzleQueryConfig` for type-safe column references\n * that are validated at compile time. Column objects carry `dataType` metadata\n * enabling automatic type coercion of filter values.\n *\n * @example\n * ```ts\n * import { users } from './schema'\n * import type { DrizzleColumnQueryConfig } from '@forinda/kickjs-drizzle'\n *\n * const config: DrizzleColumnQueryConfig = {\n * columns: {\n * status: users.status,\n * isActive: users.isActive,\n * createdAt: users.createdAt,\n * },\n * sortable: {\n * name: users.name,\n * createdAt: users.createdAt,\n * },\n * searchColumns: [users.firstName, users.lastName, users.email],\n * baseCondition: eq(users.tenantId, tenantId),\n * }\n * ```\n */\nexport interface DrizzleColumnQueryConfig {\n /**\n * Map of filterable field names to Drizzle Column objects.\n * Keys are the query parameter names, values are the actual schema columns.\n * The column's `dataType` is used for automatic type coercion.\n */\n columns: Record<string, any>\n\n /**\n * Map of sortable field names to Drizzle Column objects.\n * If not provided, falls back to `columns` for sort lookups.\n */\n sortable?: Record<string, any>\n\n /**\n * Column objects to search across when a search string is provided.\n * Each entry should be a Drizzle Column (not a string).\n */\n searchColumns?: any[]\n\n /**\n * A pre-built SQL condition that is always prepended to the WHERE clause.\n * Use for scoping queries by tenant, workspace, or other invariants.\n *\n * @example\n * ```ts\n * baseCondition: and(eq(tasks.tenantId, tid), eq(tasks.workspaceId, wid))\n * ```\n */\n baseCondition?: any\n}\n\n/**\n * Configuration type for defining query param schemas with Drizzle Column objects.\n *\n * Used in constants files to define which columns are filterable, sortable, and searchable.\n * This type is consumed by both `DrizzleQueryAdapter.buildFromColumns()` and `@ApiQueryParams()`.\n *\n * @example\n * ```ts\n * import type { DrizzleQueryParamsConfig } from '@forinda/kickjs-drizzle'\n * import { tasks } from '@/db/schema'\n *\n * export const TASK_QUERY_CONFIG: DrizzleQueryParamsConfig = {\n * columns: {\n * status: tasks.status,\n * priority: tasks.priority,\n * },\n * sortable: {\n * title: tasks.title,\n * createdAt: tasks.createdAt,\n * },\n * searchColumns: [tasks.title, tasks.key],\n * }\n * ```\n */\nexport interface DrizzleQueryParamsConfig {\n /** Filterable columns: keys are query param names, values are Drizzle Column objects */\n columns: Record<string, any>\n /** Sortable columns: keys are query param names, values are Drizzle Column objects */\n sortable?: Record<string, any>\n /** Columns for text search */\n searchColumns?: any[]\n /** Optional base condition for scoping (tenant, workspace, etc.) */\n baseCondition?: any\n}\n\n/**\n * Convert a DrizzleQueryParamsConfig into a string-based QueryFieldConfig.\n * Useful for passing to `@ApiQueryParams()` or other APIs that expect string arrays.\n *\n * @example\n * ```ts\n * import { toQueryFieldConfig } from '@forinda/kickjs-drizzle'\n *\n * const fieldConfig = toQueryFieldConfig(TASK_QUERY_CONFIG)\n * // → { filterable: ['status', 'priority'], sortable: ['title', 'createdAt'], searchable: [] }\n * ```\n */\nexport function toQueryFieldConfig(config: DrizzleQueryParamsConfig): {\n filterable: string[]\n sortable: string[]\n searchable: string[]\n} {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : [],\n searchable: config.searchColumns\n ? config.searchColumns.map((col) => col.name ?? '').filter(Boolean)\n : [],\n }\n}\n\n/**\n * Result shape compatible with Drizzle's query builder.\n * Use with `db.select().from(table).where(result.where).orderBy(...result.orderBy).limit(result.limit).offset(result.offset)`\n */\nexport interface DrizzleQueryResult {\n /** SQL condition — pass to `.where()` */\n where?: any\n /** Array of order expressions — spread into `.orderBy()` */\n orderBy: any[]\n /** Row limit — pass to `.limit()` */\n limit: number\n /** Row offset — pass to `.offset()` */\n offset: number\n}\n\n/**\n * Drizzle operator functions required by the query adapter.\n * Pass these from your `drizzle-orm` import to avoid version coupling.\n *\n * @example\n * ```ts\n * import { eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc } from 'drizzle-orm'\n *\n * const adapter = new DrizzleQueryAdapter({\n * eq, ne, gt, gte, lt, lte, ilike, inArray, between, and, or, asc, desc,\n * })\n * ```\n */\nexport interface DrizzleOps {\n eq: (column: any, value: any) => any\n ne: (column: any, value: any) => any\n gt: (column: any, value: any) => any\n gte: (column: any, value: any) => any\n lt: (column: any, value: any) => any\n lte: (column: any, value: any) => any\n ilike: (column: any, value: string) => any\n inArray: (column: any, values: any[]) => any\n between?: (column: any, min: any, max: any) => any\n and: (...conditions: any[]) => any\n or: (...conditions: any[]) => any\n asc: (column: any) => any\n desc: (column: any) => any\n}\n\n/**\n * Translates a ParsedQuery into Drizzle-compatible query parts.\n *\n * Supports two modes:\n * 1. **String-based** (legacy): `build(parsed, { table, searchColumns })` — looks up columns by string name\n * 2. **Column-based** (recommended): `buildFromColumns(parsed, config)` — uses actual Column objects for type safety\n *\n * @example\n * ```ts\n * // String-based (legacy)\n * const query = adapter.build(parsed, { table: users, searchColumns: ['name', 'email'] })\n *\n * // Column-based (recommended)\n * const query = adapter.buildFromColumns(parsed, {\n * columns: { status: users.status, isActive: users.isActive },\n * searchColumns: [users.name, users.email],\n * baseCondition: eq(users.tenantId, tid),\n * })\n *\n * const results = await db\n * .select().from(users)\n * .where(query.where)\n * .orderBy(...query.orderBy)\n * .limit(query.limit)\n * .offset(query.offset)\n * ```\n */\nexport class DrizzleQueryAdapter implements QueryBuilderAdapter<\n DrizzleQueryResult,\n DrizzleQueryConfig\n> {\n readonly name = 'DrizzleQueryAdapter'\n\n constructor(private ops: DrizzleOps) {}\n\n /**\n * Build query from string-based config (legacy API).\n * Prefer `buildFromColumns()` for type safety.\n */\n build(\n parsed: ParsedQuery,\n config: DrizzleQueryConfig = {} as DrizzleQueryConfig,\n ): DrizzleQueryResult {\n const result: DrizzleQueryResult = {\n orderBy: [],\n limit: parsed.pagination.limit,\n offset: parsed.pagination.offset,\n }\n\n // Build where conditions\n const conditions: any[] = []\n\n // Filters\n for (const filter of parsed.filters) {\n const condition = this.buildFilter(config.table, filter)\n if (condition) conditions.push(condition)\n }\n\n // Search\n if (parsed.search && config.searchColumns && config.searchColumns.length > 0) {\n const searchConditions = config.searchColumns\n .filter((col) => config.table[col])\n .map((col) => this.ops.ilike(config.table[col], `%${parsed.search}%`))\n\n if (searchConditions.length > 0) {\n conditions.push(this.ops.or(...searchConditions))\n }\n }\n\n // Combine conditions\n if (conditions.length === 1) {\n result.where = conditions[0]\n } else if (conditions.length > 1) {\n result.where = this.ops.and(...conditions)\n }\n\n // Sort\n result.orderBy = this.buildSort(config.table, parsed.sort)\n\n return result\n }\n\n /**\n * Build query using Column objects for type-safe filtering, sorting, and search.\n *\n * Features over `build()`:\n * - Column references validated at compile time\n * - Automatic type coercion based on `column.dataType` (boolean, number, date)\n * - `baseCondition` support for tenant/workspace scoping\n * - Native `between` operator support\n * - Separate `sortable` map so filterable and sortable columns can differ\n *\n * @example\n * ```ts\n * const query = adapter.buildFromColumns(parsed, {\n * columns: { status: tasks.status, priority: tasks.priority },\n * sortable: { title: tasks.title, createdAt: tasks.createdAt },\n * searchColumns: [tasks.title, tasks.key],\n * baseCondition: eq(tasks.workspaceId, wid),\n * })\n * ```\n */\n buildFromColumns(parsed: ParsedQuery, config: DrizzleColumnQueryConfig): DrizzleQueryResult {\n const result: DrizzleQueryResult = {\n orderBy: [],\n limit: parsed.pagination.limit,\n offset: parsed.pagination.offset,\n }\n\n const conditions: any[] = []\n\n // Prepend base condition (tenant/workspace scoping)\n if (config.baseCondition) {\n conditions.push(config.baseCondition)\n }\n\n // Filters — resolve column from the columns map\n for (const filter of parsed.filters) {\n const column = config.columns[filter.field]\n if (!column) continue\n const condition = this.buildColumnFilter(column, filter)\n if (condition) conditions.push(condition)\n }\n\n // Search — use Column objects directly\n if (parsed.search && config.searchColumns && config.searchColumns.length > 0) {\n const searchConditions = config.searchColumns.map((col) =>\n this.ops.ilike(col, `%${parsed.search}%`),\n )\n if (searchConditions.length > 0) {\n conditions.push(this.ops.or(...searchConditions))\n }\n }\n\n // Combine conditions\n if (conditions.length === 1) {\n result.where = conditions[0]\n } else if (conditions.length > 1) {\n result.where = this.ops.and(...conditions)\n }\n\n // Sort — use sortable map, falling back to columns\n const sortMap = config.sortable ?? config.columns\n result.orderBy = parsed.sort\n .filter((item) => sortMap[item.field])\n .map((item) =>\n item.direction === 'desc'\n ? this.ops.desc(sortMap[item.field])\n : this.ops.asc(sortMap[item.field]),\n )\n\n return result\n }\n\n /** Map a single FilterItem to a Drizzle condition using string-based table lookup */\n private buildFilter(table: Record<string, any>, filter: FilterItem): any {\n const column = table[filter.field]\n if (!column) return null\n\n const value = this.coerce(filter.value)\n\n switch (filter.operator) {\n case 'eq':\n return this.ops.eq(column, value)\n case 'neq':\n return this.ops.ne(column, value)\n case 'gt':\n return this.ops.gt(column, value)\n case 'gte':\n return this.ops.gte(column, value)\n case 'lt':\n return this.ops.lt(column, value)\n case 'lte':\n return this.ops.lte(column, value)\n case 'contains':\n return this.ops.ilike(column, `%${filter.value}%`)\n case 'starts':\n return this.ops.ilike(column, `${filter.value}%`)\n case 'ends':\n return this.ops.ilike(column, `%${filter.value}`)\n case 'in': {\n const values = filter.value.split(',').map((v) => this.coerce(v.trim()))\n return this.ops.inArray(column, values)\n }\n case 'between': {\n const [min, max] = filter.value.split(',').map((v) => this.coerce(v.trim()))\n return this.ops.and(this.ops.gte(column, min), this.ops.lte(column, max))\n }\n default:\n return this.ops.eq(column, value)\n }\n }\n\n /**\n * Map a FilterItem to a Drizzle condition using a Column object.\n * Coerces values based on `column.dataType` for type-safe filtering.\n */\n private buildColumnFilter(column: any, filter: FilterItem): any {\n const value = this.coerceByDataType(filter.value, column.dataType)\n\n switch (filter.operator) {\n case 'eq':\n return this.ops.eq(column, value)\n case 'neq':\n return this.ops.ne(column, value)\n case 'gt':\n return this.ops.gt(column, value)\n case 'gte':\n return this.ops.gte(column, value)\n case 'lt':\n return this.ops.lt(column, value)\n case 'lte':\n return this.ops.lte(column, value)\n case 'contains':\n return this.ops.ilike(column, `%${filter.value}%`)\n case 'starts':\n return this.ops.ilike(column, `${filter.value}%`)\n case 'ends':\n return this.ops.ilike(column, `%${filter.value}`)\n case 'in': {\n const values = filter.value\n .split(',')\n .map((v) => this.coerceByDataType(v.trim(), column.dataType))\n return this.ops.inArray(column, values)\n }\n case 'between': {\n const [minStr, maxStr] = filter.value.split(',').map((v) => v.trim())\n const min = this.coerceByDataType(minStr, column.dataType)\n const max = this.coerceByDataType(maxStr, column.dataType)\n if (this.ops.between) {\n return this.ops.between(column, min, max)\n }\n return this.ops.and(this.ops.gte(column, min), this.ops.lte(column, max))\n }\n default:\n return this.ops.eq(column, value)\n }\n }\n\n /** Build Drizzle orderBy from SortItem[] */\n private buildSort(table: Record<string, any>, sort: SortItem[]): any[] {\n return sort\n .filter((item) => table[item.field])\n .map((item) =>\n item.direction === 'desc'\n ? this.ops.desc(table[item.field])\n : this.ops.asc(table[item.field]),\n )\n }\n\n /** Attempt to coerce a string value to a number or boolean if appropriate */\n private coerce(value: string): string | number | boolean {\n if (value === 'true') return true\n if (value === 'false') return false\n const num = Number(value)\n if (!Number.isNaN(num) && value.trim() !== '') return num\n return value\n }\n\n /**\n * Coerce a string value based on the column's dataType.\n *\n * - `'boolean'` → `true`/`false`\n * - `'number'` / `'bigint'` → `Number(value)`\n * - `'date'` → `new Date(value)` (ISO 8601 strings)\n * - Everything else → original string\n */\n private coerceByDataType(value: string, dataType?: string): string | number | boolean | Date {\n if (!dataType) return this.coerce(value)\n\n switch (dataType) {\n case 'boolean':\n return value === 'true' || value === '1'\n case 'number':\n case 'bigint': {\n const num = Number(value)\n return Number.isNaN(num) ? value : num\n }\n case 'date':\n case 'localDate':\n case 'localDateTime': {\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? value : date\n }\n default:\n return value\n }\n }\n}\n"],"mappings":"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","names":["Logger","Scope","DRIZZLE_DB","Symbol","log","Logger","for","DrizzleAdapter","name","db","onShutdown","options","beforeStart","_app","container","logging","info","registerFactory","DRIZZLE_DB","Scope","SINGLETON","shutdown","toQueryFieldConfig","config","filterable","Object","keys","columns","sortable","searchable","searchColumns","map","col","name","filter","Boolean","DrizzleQueryAdapter","ops","build","parsed","result","orderBy","limit","pagination","offset","conditions","filters","condition","buildFilter","table","push","search","length","searchConditions","ilike","or","where","and","buildSort","sort","buildFromColumns","baseCondition","column","field","buildColumnFilter","sortMap","item","direction","desc","asc","value","coerce","operator","eq","ne","gt","gte","lt","lte","values","split","v","trim","inArray","min","max","coerceByDataType","dataType","minStr","maxStr","between","num","Number","isNaN","date","Date","getTime"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-drizzle",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "description": "Drizzle ORM adapter with DI integration, transaction support, and query building for KickJS",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -44,8 +44,8 @@
44
44
  ],
45
45
  "dependencies": {
46
46
  "reflect-metadata": "^0.2.2",
47
- "@forinda/kickjs-core": "1.2.8",
48
- "@forinda/kickjs-http": "1.2.8"
47
+ "@forinda/kickjs-core": "1.2.10",
48
+ "@forinda/kickjs-http": "1.2.10"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "drizzle-orm": ">=0.30.0"