@forinda/kickjs-drizzle 3.2.0 → 4.0.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 +20 -81
- package/dist/index.d.mts +38 -52
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +106 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -9
package/README.md
CHANGED
|
@@ -1,112 +1,51 @@
|
|
|
1
1
|
# @forinda/kickjs-drizzle
|
|
2
2
|
|
|
3
|
-
Drizzle ORM adapter
|
|
3
|
+
Drizzle ORM adapter for KickJS — DI integration, lifecycle management, and a `DrizzleQueryAdapter` that translates `ParsedQuery` into Drizzle `where` / `orderBy` / `limit` / `offset`.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
# Using the KickJS CLI (recommended — auto-installs peer dependencies)
|
|
9
8
|
kick add drizzle
|
|
10
|
-
|
|
11
|
-
# Manual install
|
|
12
|
-
pnpm add @forinda/kickjs-drizzle drizzle-orm
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
- `DrizzleAdapter` — lifecycle adapter that manages the Drizzle connection
|
|
18
|
-
- `DrizzleTenantAdapter` — multi-tenant adapter with per-tenant connection caching
|
|
19
|
-
- `DRIZZLE_DB` token — singleton, provider/single-tenant database
|
|
20
|
-
- `DRIZZLE_TENANT_DB` token — transient, current tenant's database via AsyncLocalStorage
|
|
21
|
-
- `DrizzleQueryAdapter` — translates `ParsedQuery` from `@forinda/kickjs` into Drizzle queries
|
|
22
|
-
- `toQueryFieldConfig` helper for field mapping
|
|
23
|
-
|
|
24
11
|
## Quick Example
|
|
25
12
|
|
|
26
|
-
```
|
|
27
|
-
import { DrizzleAdapter, DRIZZLE_DB, DrizzleQueryAdapter } from '@forinda/kickjs-drizzle'
|
|
13
|
+
```ts
|
|
28
14
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
29
15
|
import postgres from 'postgres'
|
|
16
|
+
import { bootstrap, getEnv } from '@forinda/kickjs'
|
|
17
|
+
import { DrizzleAdapter } from '@forinda/kickjs-drizzle'
|
|
18
|
+
import * as schema from './schema'
|
|
19
|
+
import { modules } from './modules'
|
|
30
20
|
|
|
31
|
-
const client = postgres(
|
|
32
|
-
const db = drizzle(client)
|
|
21
|
+
const client = postgres(getEnv('DATABASE_URL'))
|
|
22
|
+
const db = drizzle(client, { schema })
|
|
33
23
|
|
|
34
|
-
bootstrap({
|
|
24
|
+
export const app = await bootstrap({
|
|
35
25
|
modules,
|
|
36
|
-
adapters: [
|
|
37
|
-
new DrizzleAdapter({ db }),
|
|
38
|
-
],
|
|
26
|
+
adapters: [DrizzleAdapter({ db, onShutdown: () => client.end() })],
|
|
39
27
|
})
|
|
40
|
-
|
|
41
|
-
// In a service, inject the DB
|
|
42
|
-
@Service()
|
|
43
|
-
class UserService {
|
|
44
|
-
@Inject(DRIZZLE_DB) private db!: typeof db
|
|
45
|
-
|
|
46
|
-
async findAll() {
|
|
47
|
-
return this.db.select().from(users)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
28
|
```
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Use `DrizzleTenantAdapter` alongside `TenantAdapter` for database-per-tenant:
|
|
30
|
+
Inject the typed db in services:
|
|
55
31
|
|
|
56
|
-
```
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
32
|
+
```ts
|
|
33
|
+
import { Inject, Service } from '@forinda/kickjs'
|
|
34
|
+
import { DRIZZLE_DB } from '@forinda/kickjs-drizzle'
|
|
35
|
+
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
|
36
|
+
import * as schema from './schema'
|
|
59
37
|
|
|
60
|
-
bootstrap({
|
|
61
|
-
modules,
|
|
62
|
-
adapters: [
|
|
63
|
-
new TenantAdapter({ strategy: 'subdomain' }),
|
|
64
|
-
new DrizzleTenantAdapter({
|
|
65
|
-
providerDb: drizzle(providerPool, { schema }),
|
|
66
|
-
tenantFactory: async (tenantId) => {
|
|
67
|
-
const url = await lookupTenantDbUrl(tenantId)
|
|
68
|
-
return drizzle(new Pool({ connectionString: url }), { schema })
|
|
69
|
-
},
|
|
70
|
-
onTenantShutdown: (db, tenantId) => {
|
|
71
|
-
// Close the pool when shutting down
|
|
72
|
-
},
|
|
73
|
-
}),
|
|
74
|
-
],
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// In a service — resolves to the current tenant's typed DB
|
|
78
38
|
@Service()
|
|
79
|
-
class
|
|
80
|
-
@Inject(
|
|
81
|
-
|
|
82
|
-
async findAll() {
|
|
83
|
-
return this.db.select().from(projects)
|
|
84
|
-
}
|
|
39
|
+
class UserService {
|
|
40
|
+
constructor(@Inject(DRIZZLE_DB) private db: PostgresJsDatabase<typeof schema>) {}
|
|
85
41
|
}
|
|
86
42
|
```
|
|
87
43
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
@Inject(DRIZZLE_DB) private providerDb!: typeof db // always provider
|
|
92
|
-
@Inject(DRIZZLE_TENANT_DB) private tenantDb!: typeof db // current tenant
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Query Adapter
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import { DrizzleQueryAdapter } from '@forinda/kickjs-drizzle'
|
|
99
|
-
|
|
100
|
-
const adapter = new DrizzleQueryAdapter()
|
|
101
|
-
const query = adapter.build(parsedQuery, {
|
|
102
|
-
columns: { name: users.name, email: users.email },
|
|
103
|
-
searchColumns: [users.name, users.email],
|
|
104
|
-
})
|
|
105
|
-
```
|
|
44
|
+
For the multi-tenant `DrizzleTenantAdapter` see the [examples/multi-tenant-drizzle-api](https://github.com/forinda/kick-js/tree/main/examples/multi-tenant-drizzle-api) app.
|
|
106
45
|
|
|
107
46
|
## Documentation
|
|
108
47
|
|
|
109
|
-
[
|
|
48
|
+
[forinda.github.io/kick-js/api/drizzle](https://forinda.github.io/kick-js/api/drizzle)
|
|
110
49
|
|
|
111
50
|
## License
|
|
112
51
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
import
|
|
2
|
+
import * as _$_forinda_kickjs0 from "@forinda/kickjs";
|
|
3
|
+
import { MaybePromise, ParsedQuery, QueryBuilderAdapter } from "@forinda/kickjs";
|
|
3
4
|
|
|
4
5
|
//#region src/types.d.ts
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* DI token for resolving the Drizzle database instance from the container (single-tenant).
|
|
8
|
+
*
|
|
9
|
+
* Typed as `unknown` because Drizzle's database type depends on the user's
|
|
10
|
+
* driver + schema; cast at the use site
|
|
11
|
+
* (e.g. `@Inject(DRIZZLE_DB) private db!: BetterSQLite3Database<typeof schema>`).
|
|
12
|
+
*/
|
|
13
|
+
declare const DRIZZLE_DB: _$_forinda_kickjs0.InjectionToken<unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* DI token for resolving the current tenant's Drizzle database instance (multi-tenant).
|
|
16
|
+
*
|
|
17
|
+
* Same typing caveat as {@link DRIZZLE_DB} — cast at the use site.
|
|
18
|
+
*/
|
|
19
|
+
declare const DRIZZLE_TENANT_DB: _$_forinda_kickjs0.InjectionToken<unknown>;
|
|
9
20
|
interface DrizzleAdapterOptions<TDb = unknown> {
|
|
10
21
|
/**
|
|
11
22
|
* Drizzle database instance — the return value of `drizzle()`.
|
|
@@ -19,7 +30,7 @@ interface DrizzleAdapterOptions<TDb = unknown> {
|
|
|
19
30
|
* const db = drizzle({ client: sqlite, schema })
|
|
20
31
|
* // db is BetterSQLite3Database<typeof schema>
|
|
21
32
|
*
|
|
22
|
-
*
|
|
33
|
+
* DrizzleAdapter({ db })
|
|
23
34
|
* // TDb is inferred as BetterSQLite3Database<typeof schema>
|
|
24
35
|
* ```
|
|
25
36
|
*/
|
|
@@ -36,7 +47,7 @@ interface DrizzleAdapterOptions<TDb = unknown> {
|
|
|
36
47
|
* const pool = new Pool({ connectionString: '...' })
|
|
37
48
|
* const db = drizzle(pool)
|
|
38
49
|
*
|
|
39
|
-
*
|
|
50
|
+
* DrizzleAdapter({
|
|
40
51
|
* db,
|
|
41
52
|
* onShutdown: () => pool.end(),
|
|
42
53
|
* })
|
|
@@ -85,9 +96,6 @@ interface DrizzleTenantAdapterOptions<TDb = unknown> {
|
|
|
85
96
|
* Works with any Drizzle driver: `drizzle-orm/postgres-js`, `drizzle-orm/node-postgres`,
|
|
86
97
|
* `drizzle-orm/mysql2`, `drizzle-orm/better-sqlite3`, `drizzle-orm/libsql`, etc.
|
|
87
98
|
*
|
|
88
|
-
* The adapter is generic — the db type is inferred from what you pass in,
|
|
89
|
-
* so services can inject the fully-typed database instance.
|
|
90
|
-
*
|
|
91
99
|
* @example
|
|
92
100
|
* ```ts
|
|
93
101
|
* import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
@@ -99,7 +107,7 @@ interface DrizzleTenantAdapterOptions<TDb = unknown> {
|
|
|
99
107
|
* bootstrap({
|
|
100
108
|
* modules,
|
|
101
109
|
* adapters: [
|
|
102
|
-
*
|
|
110
|
+
* DrizzleAdapter({ db, onShutdown: () => sqlite.close() }),
|
|
103
111
|
* ],
|
|
104
112
|
* })
|
|
105
113
|
* ```
|
|
@@ -115,21 +123,24 @@ interface DrizzleTenantAdapterOptions<TDb = unknown> {
|
|
|
115
123
|
* }
|
|
116
124
|
* ```
|
|
117
125
|
*/
|
|
118
|
-
declare
|
|
119
|
-
private options;
|
|
120
|
-
name: string;
|
|
121
|
-
private db;
|
|
122
|
-
private onShutdown?;
|
|
123
|
-
constructor(options: DrizzleAdapterOptions<TDb>);
|
|
124
|
-
/** Register the Drizzle db instance in the DI container */
|
|
125
|
-
beforeStart({
|
|
126
|
-
container
|
|
127
|
-
}: AdapterContext): void;
|
|
128
|
-
/** Close the underlying connection on shutdown */
|
|
129
|
-
shutdown(): Promise<void>;
|
|
130
|
-
}
|
|
126
|
+
declare const DrizzleAdapter: _$_forinda_kickjs0.AdapterFactory<DrizzleAdapterOptions<unknown>, unknown>;
|
|
131
127
|
//#endregion
|
|
132
128
|
//#region src/drizzle-tenant.adapter.d.ts
|
|
129
|
+
/**
|
|
130
|
+
* Public extension methods exposed by a DrizzleTenantAdapter instance.
|
|
131
|
+
* `getDb()` returns the (possibly newly-created) Drizzle instance for
|
|
132
|
+
* a tenant; `connectionCount` reports the cache size for monitoring.
|
|
133
|
+
*/
|
|
134
|
+
interface DrizzleTenantAdapterExtensions<TDb = unknown> {
|
|
135
|
+
/**
|
|
136
|
+
* Get the database for a specific tenant. Creates and caches the
|
|
137
|
+
* connection on first access. Returns the provider DB when tenantId
|
|
138
|
+
* is undefined/null.
|
|
139
|
+
*/
|
|
140
|
+
getDb(tenantId?: string | null): Promise<TDb>;
|
|
141
|
+
/** Number of cached tenant connections (useful for monitoring). */
|
|
142
|
+
readonly connectionCount: number;
|
|
143
|
+
}
|
|
133
144
|
/**
|
|
134
145
|
* Multi-tenant Drizzle adapter — manages per-tenant database connections
|
|
135
146
|
* with automatic caching and lifecycle management.
|
|
@@ -151,8 +162,8 @@ declare class DrizzleAdapter<TDb = unknown> implements AppAdapter {
|
|
|
151
162
|
*
|
|
152
163
|
* bootstrap({
|
|
153
164
|
* adapters: [
|
|
154
|
-
*
|
|
155
|
-
*
|
|
165
|
+
* TenantAdapter({ strategy: 'subdomain' }),
|
|
166
|
+
* DrizzleTenantAdapter({
|
|
156
167
|
* providerDb,
|
|
157
168
|
* tenantFactory: async (tenantId) => {
|
|
158
169
|
* const url = await lookupTenantDbUrl(tenantId)
|
|
@@ -171,32 +182,7 @@ declare class DrizzleAdapter<TDb = unknown> implements AppAdapter {
|
|
|
171
182
|
* }
|
|
172
183
|
* ```
|
|
173
184
|
*/
|
|
174
|
-
declare
|
|
175
|
-
name: string;
|
|
176
|
-
private readonly providerDb;
|
|
177
|
-
private readonly tenantFactory;
|
|
178
|
-
private readonly connections;
|
|
179
|
-
private readonly lastAccessed;
|
|
180
|
-
private readonly options;
|
|
181
|
-
private readonly evictionTimer?;
|
|
182
|
-
constructor(options: DrizzleTenantAdapterOptions<TDb>);
|
|
183
|
-
/**
|
|
184
|
-
* Get the database for a specific tenant.
|
|
185
|
-
* Creates and caches the connection on first access.
|
|
186
|
-
* Returns the provider DB when tenantId is undefined/null.
|
|
187
|
-
*/
|
|
188
|
-
getDb(tenantId?: string | null): Promise<TDb>;
|
|
189
|
-
/** Register DRIZZLE_TENANT_DB as a transient factory in DI */
|
|
190
|
-
beforeStart({
|
|
191
|
-
container
|
|
192
|
-
}: AdapterContext): Promise<void>;
|
|
193
|
-
/** Evict connections that haven't been accessed within cacheTtl */
|
|
194
|
-
private evictStale;
|
|
195
|
-
/** Close all tenant connections on shutdown */
|
|
196
|
-
shutdown(): Promise<void>;
|
|
197
|
-
/** Number of cached tenant connections (useful for monitoring) */
|
|
198
|
-
get connectionCount(): number;
|
|
199
|
-
}
|
|
185
|
+
declare const DrizzleTenantAdapter: _$_forinda_kickjs0.AdapterFactory<DrizzleTenantAdapterOptions<unknown>, DrizzleTenantAdapterExtensions<unknown>>;
|
|
200
186
|
//#endregion
|
|
201
187
|
//#region src/query-adapter.d.ts
|
|
202
188
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/drizzle.adapter.ts","../src/drizzle-tenant.adapter.ts","../src/query-adapter.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/drizzle.adapter.ts","../src/drizzle-tenant.adapter.ts","../src/query-adapter.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-drizzle
|
|
2
|
+
* @forinda/kickjs-drizzle v4.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,12 +8,22 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import { Logger, Scope } from "@forinda/kickjs";
|
|
11
|
+
import { Logger, Scope, createToken, defineAdapter } from "@forinda/kickjs";
|
|
12
12
|
//#region src/types.ts
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* DI token for resolving the Drizzle database instance from the container (single-tenant).
|
|
15
|
+
*
|
|
16
|
+
* Typed as `unknown` because Drizzle's database type depends on the user's
|
|
17
|
+
* driver + schema; cast at the use site
|
|
18
|
+
* (e.g. `@Inject(DRIZZLE_DB) private db!: BetterSQLite3Database<typeof schema>`).
|
|
19
|
+
*/
|
|
20
|
+
const DRIZZLE_DB = createToken("kick/drizzle/DB");
|
|
21
|
+
/**
|
|
22
|
+
* DI token for resolving the current tenant's Drizzle database instance (multi-tenant).
|
|
23
|
+
*
|
|
24
|
+
* Same typing caveat as {@link DRIZZLE_DB} — cast at the use site.
|
|
25
|
+
*/
|
|
26
|
+
const DRIZZLE_TENANT_DB = createToken("kick/drizzle/DB:tenant");
|
|
17
27
|
//#endregion
|
|
18
28
|
//#region src/drizzle.adapter.ts
|
|
19
29
|
const log$1 = Logger.for("DrizzleAdapter");
|
|
@@ -24,9 +34,6 @@ const log$1 = Logger.for("DrizzleAdapter");
|
|
|
24
34
|
* Works with any Drizzle driver: `drizzle-orm/postgres-js`, `drizzle-orm/node-postgres`,
|
|
25
35
|
* `drizzle-orm/mysql2`, `drizzle-orm/better-sqlite3`, `drizzle-orm/libsql`, etc.
|
|
26
36
|
*
|
|
27
|
-
* The adapter is generic — the db type is inferred from what you pass in,
|
|
28
|
-
* so services can inject the fully-typed database instance.
|
|
29
|
-
*
|
|
30
37
|
* @example
|
|
31
38
|
* ```ts
|
|
32
39
|
* import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
@@ -38,7 +45,7 @@ const log$1 = Logger.for("DrizzleAdapter");
|
|
|
38
45
|
* bootstrap({
|
|
39
46
|
* modules,
|
|
40
47
|
* adapters: [
|
|
41
|
-
*
|
|
48
|
+
* DrizzleAdapter({ db, onShutdown: () => sqlite.close() }),
|
|
42
49
|
* ],
|
|
43
50
|
* })
|
|
44
51
|
* ```
|
|
@@ -54,29 +61,26 @@ const log$1 = Logger.for("DrizzleAdapter");
|
|
|
54
61
|
* }
|
|
55
62
|
* ```
|
|
56
63
|
*/
|
|
57
|
-
|
|
58
|
-
name
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await this.onShutdown();
|
|
76
|
-
log$1.info("Drizzle connection closed");
|
|
77
|
-
}
|
|
64
|
+
const DrizzleAdapter = defineAdapter({
|
|
65
|
+
name: "DrizzleAdapter",
|
|
66
|
+
build: (options) => {
|
|
67
|
+
const db = options.db;
|
|
68
|
+
const onShutdown = options.onShutdown;
|
|
69
|
+
return {
|
|
70
|
+
beforeStart({ container }) {
|
|
71
|
+
if (options.logging) log$1.info("Query logging enabled");
|
|
72
|
+
container.registerFactory(DRIZZLE_DB, () => db, Scope.SINGLETON);
|
|
73
|
+
log$1.info("Drizzle database registered in DI container");
|
|
74
|
+
},
|
|
75
|
+
async shutdown() {
|
|
76
|
+
if (onShutdown) {
|
|
77
|
+
await onShutdown();
|
|
78
|
+
log$1.info("Drizzle connection closed");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
78
82
|
}
|
|
79
|
-
};
|
|
83
|
+
});
|
|
80
84
|
//#endregion
|
|
81
85
|
//#region src/drizzle-tenant.adapter.ts
|
|
82
86
|
const log = Logger.for("DrizzleTenantAdapter");
|
|
@@ -101,8 +105,8 @@ const log = Logger.for("DrizzleTenantAdapter");
|
|
|
101
105
|
*
|
|
102
106
|
* bootstrap({
|
|
103
107
|
* adapters: [
|
|
104
|
-
*
|
|
105
|
-
*
|
|
108
|
+
* TenantAdapter({ strategy: 'subdomain' }),
|
|
109
|
+
* DrizzleTenantAdapter({
|
|
106
110
|
* providerDb,
|
|
107
111
|
* tenantFactory: async (tenantId) => {
|
|
108
112
|
* const url = await lookupTenantDbUrl(tenantId)
|
|
@@ -121,90 +125,80 @@ const log = Logger.for("DrizzleTenantAdapter");
|
|
|
121
125
|
* }
|
|
122
126
|
* ```
|
|
123
127
|
*/
|
|
124
|
-
|
|
125
|
-
name
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
constructor(options) {
|
|
133
|
-
this.options = options;
|
|
134
|
-
this.providerDb = options.providerDb;
|
|
135
|
-
this.tenantFactory = options.tenantFactory;
|
|
128
|
+
const DrizzleTenantAdapter = defineAdapter({
|
|
129
|
+
name: "DrizzleTenantAdapter",
|
|
130
|
+
build: (options) => {
|
|
131
|
+
const providerDb = options.providerDb;
|
|
132
|
+
const tenantFactory = options.tenantFactory;
|
|
133
|
+
const connections = /* @__PURE__ */ new Map();
|
|
134
|
+
const lastAccessed = /* @__PURE__ */ new Map();
|
|
135
|
+
let evictionTimer;
|
|
136
136
|
if (options.cacheTtl && options.cacheTtl > 0) {
|
|
137
137
|
const interval = Math.min(options.cacheTtl, 6e4);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Get the database for a specific tenant.
|
|
144
|
-
* Creates and caches the connection on first access.
|
|
145
|
-
* Returns the provider DB when tenantId is undefined/null.
|
|
146
|
-
*/
|
|
147
|
-
async getDb(tenantId) {
|
|
148
|
-
if (!tenantId) return this.providerDb;
|
|
149
|
-
const cached = this.connections.get(tenantId);
|
|
150
|
-
if (cached) {
|
|
151
|
-
this.lastAccessed.set(tenantId, Date.now());
|
|
152
|
-
return cached;
|
|
138
|
+
evictionTimer = setInterval(() => evictStale(), interval);
|
|
139
|
+
evictionTimer.unref();
|
|
153
140
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (db && this.options.onTenantShutdown) try {
|
|
182
|
-
await this.options.onTenantShutdown(db, tenantId);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
log.error(`Failed to evict tenant DB ${tenantId}: ${err}`);
|
|
141
|
+
const getDb = async (tenantId) => {
|
|
142
|
+
if (!tenantId) return providerDb;
|
|
143
|
+
const cached = connections.get(tenantId);
|
|
144
|
+
if (cached) {
|
|
145
|
+
lastAccessed.set(tenantId, Date.now());
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
const db = await tenantFactory(tenantId);
|
|
149
|
+
connections.set(tenantId, db);
|
|
150
|
+
lastAccessed.set(tenantId, Date.now());
|
|
151
|
+
if (options.logging) log.info(`Tenant DB created: ${tenantId} (${connections.size} total)`);
|
|
152
|
+
return db;
|
|
153
|
+
};
|
|
154
|
+
async function evictStale() {
|
|
155
|
+
const ttl = options.cacheTtl;
|
|
156
|
+
if (!ttl) return;
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
for (const [tenantId, lastTime] of lastAccessed) if (now - lastTime > ttl) {
|
|
159
|
+
const db = connections.get(tenantId);
|
|
160
|
+
if (db && options.onTenantShutdown) try {
|
|
161
|
+
await options.onTenantShutdown(db, tenantId);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.error(`Failed to evict tenant DB ${tenantId}: ${err}`);
|
|
164
|
+
}
|
|
165
|
+
connections.delete(tenantId);
|
|
166
|
+
lastAccessed.delete(tenantId);
|
|
167
|
+
if (options.logging) log.info(`Tenant DB evicted (idle): ${tenantId}`);
|
|
185
168
|
}
|
|
186
|
-
this.connections.delete(tenantId);
|
|
187
|
-
this.lastAccessed.delete(tenantId);
|
|
188
|
-
if (this.options.logging) log.info(`Tenant DB evicted (idle): ${tenantId}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/** Close all tenant connections on shutdown */
|
|
192
|
-
async shutdown() {
|
|
193
|
-
if (this.evictionTimer) clearInterval(this.evictionTimer);
|
|
194
|
-
if (this.options.onTenantShutdown) for (const [tenantId, db] of this.connections) try {
|
|
195
|
-
await this.options.onTenantShutdown(db, tenantId);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
log.error(`Failed to close tenant DB ${tenantId}: ${err}`);
|
|
198
169
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
170
|
+
return {
|
|
171
|
+
getDb,
|
|
172
|
+
get connectionCount() {
|
|
173
|
+
return connections.size;
|
|
174
|
+
},
|
|
175
|
+
async beforeStart({ container }) {
|
|
176
|
+
let getCurrentTenant;
|
|
177
|
+
try {
|
|
178
|
+
getCurrentTenant = (await import("@forinda/kickjs-multi-tenant")).getCurrentTenant;
|
|
179
|
+
} catch {
|
|
180
|
+
log.warn("DrizzleTenantAdapter: @forinda/kickjs-multi-tenant not found. DRIZZLE_TENANT_DB will always resolve to the provider database.");
|
|
181
|
+
}
|
|
182
|
+
container.registerFactory(DRIZZLE_TENANT_DB, () => {
|
|
183
|
+
const tenant = getCurrentTenant?.();
|
|
184
|
+
return getDb(tenant?.id);
|
|
185
|
+
}, Scope.TRANSIENT);
|
|
186
|
+
log.info(`Drizzle tenant DB registered (${getCurrentTenant ? "multi-tenant mode" : "provider-only mode"})`);
|
|
187
|
+
},
|
|
188
|
+
async shutdown() {
|
|
189
|
+
if (evictionTimer) clearInterval(evictionTimer);
|
|
190
|
+
if (options.onTenantShutdown) for (const [tenantId, db] of connections) try {
|
|
191
|
+
await options.onTenantShutdown(db, tenantId);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log.error(`Failed to close tenant DB ${tenantId}: ${err}`);
|
|
194
|
+
}
|
|
195
|
+
connections.clear();
|
|
196
|
+
lastAccessed.clear();
|
|
197
|
+
log.info("All tenant DB connections closed");
|
|
198
|
+
}
|
|
199
|
+
};
|
|
206
200
|
}
|
|
207
|
-
};
|
|
201
|
+
});
|
|
208
202
|
//#endregion
|
|
209
203
|
//#region src/query-adapter.ts
|
|
210
204
|
/**
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["log"],"sources":["../src/types.ts","../src/drizzle.adapter.ts","../src/drizzle-tenant.adapter.ts","../src/query-adapter.ts"],"sourcesContent":["import type { MaybePromise } from '@forinda/kickjs'\n\n/** DI token for resolving the Drizzle database instance from the container (single-tenant) */\nexport const DRIZZLE_DB = Symbol('DrizzleDB')\n\n/** DI token for resolving the current tenant's Drizzle database instance (multi-tenant) */\nexport const DRIZZLE_TENANT_DB = Symbol('DrizzleTenantDB')\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\nexport interface DrizzleTenantAdapterOptions<TDb = unknown> {\n /**\n * The provider (default) database instance. Used when no tenant is\n * resolved or when accessing the tenant registry.\n */\n providerDb: TDb\n\n /**\n * Factory that creates a typed Drizzle instance for a given tenant.\n * Called once per tenant — the result is cached for subsequent requests.\n *\n * @example\n * ```ts\n * tenantFactory: async (tenantId) => {\n * const url = await lookupTenantDbUrl(tenantId)\n * return drizzle(new Pool({ connectionString: url }), { schema })\n * }\n * ```\n */\n tenantFactory: (tenantId: string) => TDb | Promise<TDb>\n\n /**\n * Optional function to close a tenant DB connection.\n * Called for each cached connection during shutdown.\n */\n onTenantShutdown?: (db: TDb, tenantId: string) => MaybePromise<any>\n\n /** Enable query logging (default: false) */\n logging?: boolean\n\n /**\n * Cache TTL in milliseconds. Tenant connections idle beyond this\n * duration are evicted. Default: no eviction (connections live until shutdown).\n */\n cacheTtl?: number\n}\n","import { Logger, type AppAdapter, type AdapterContext, Scope } from '@forinda/kickjs'\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({ container }: AdapterContext): 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 { Logger, type AppAdapter, type AdapterContext, Scope } from '@forinda/kickjs'\nimport { DRIZZLE_TENANT_DB, type DrizzleTenantAdapterOptions } from './types'\n\nconst log = Logger.for('DrizzleTenantAdapter')\n\n/**\n * Multi-tenant Drizzle adapter — manages per-tenant database connections\n * with automatic caching and lifecycle management.\n *\n * Registers `DRIZZLE_TENANT_DB` as a TRANSIENT DI token that resolves\n * to the current tenant's Drizzle instance using AsyncLocalStorage\n * (requires `TenantAdapter` to be configured).\n *\n * Works alongside `DrizzleAdapter` — use `DRIZZLE_DB` for the provider\n * database and `DRIZZLE_TENANT_DB` for the current tenant's database.\n *\n * @example\n * ```ts\n * import { drizzle } from 'drizzle-orm/node-postgres'\n * import { Pool } from 'pg'\n * import * as schema from './schema'\n *\n * const providerDb = drizzle(new Pool({ connectionString: PROVIDER_URL }), { schema })\n *\n * bootstrap({\n * adapters: [\n * new TenantAdapter({ strategy: 'subdomain' }),\n * new DrizzleTenantAdapter({\n * providerDb,\n * tenantFactory: async (tenantId) => {\n * const url = await lookupTenantDbUrl(tenantId)\n * return drizzle(new Pool({ connectionString: url }), { schema })\n * },\n * }),\n * ],\n * })\n * ```\n *\n * Inject in services:\n * ```ts\n * @Service()\n * class ProjectService {\n * constructor(@Inject(DRIZZLE_TENANT_DB) private db: NodePgDatabase<typeof schema>) {}\n * }\n * ```\n */\nexport class DrizzleTenantAdapter<TDb = unknown> implements AppAdapter {\n name = 'DrizzleTenantAdapter'\n private readonly providerDb: TDb\n private readonly tenantFactory: (tenantId: string) => TDb | Promise<TDb>\n private readonly connections = new Map<string, TDb>()\n private readonly lastAccessed = new Map<string, number>()\n private readonly options: DrizzleTenantAdapterOptions<TDb>\n private readonly evictionTimer?: ReturnType<typeof setInterval>\n\n constructor(options: DrizzleTenantAdapterOptions<TDb>) {\n this.options = options\n this.providerDb = options.providerDb\n this.tenantFactory = options.tenantFactory\n\n // Start cache eviction timer if cacheTtl is configured\n if (options.cacheTtl && options.cacheTtl > 0) {\n const interval = Math.min(options.cacheTtl, 60_000)\n this.evictionTimer = setInterval(() => this.evictStale(), interval)\n this.evictionTimer.unref()\n }\n }\n\n /**\n * Get the database for a specific tenant.\n * Creates and caches the connection on first access.\n * Returns the provider DB when tenantId is undefined/null.\n */\n async getDb(tenantId?: string | null): Promise<TDb> {\n if (!tenantId) return this.providerDb\n\n const cached = this.connections.get(tenantId)\n if (cached) {\n this.lastAccessed.set(tenantId, Date.now())\n return cached\n }\n\n const db = await this.tenantFactory(tenantId)\n this.connections.set(tenantId, db)\n this.lastAccessed.set(tenantId, Date.now())\n\n if (this.options.logging) {\n log.info(`Tenant DB created: ${tenantId} (${this.connections.size} total)`)\n }\n\n return db\n }\n\n /** Register DRIZZLE_TENANT_DB as a transient factory in DI */\n async beforeStart({ container }: AdapterContext): Promise<void> {\n // Dynamically import getCurrentTenant to avoid hard dep on multi-tenant package\n let getCurrentTenant: (() => { id: string } | undefined) | undefined\n\n try {\n const mt: any = await import('@forinda/kickjs-multi-tenant')\n getCurrentTenant = mt.getCurrentTenant\n } catch {\n log.warn(\n 'DrizzleTenantAdapter: @forinda/kickjs-multi-tenant not found. ' +\n 'DRIZZLE_TENANT_DB will always resolve to the provider database.',\n )\n }\n\n container.registerFactory(\n DRIZZLE_TENANT_DB,\n () => {\n const tenant = getCurrentTenant?.()\n return this.getDb(tenant?.id)\n },\n Scope.TRANSIENT,\n )\n\n log.info(\n `Drizzle tenant DB registered (${getCurrentTenant ? 'multi-tenant mode' : 'provider-only mode'})`,\n )\n }\n\n /** Evict connections that haven't been accessed within cacheTtl */\n private async evictStale(): Promise<void> {\n const ttl = this.options.cacheTtl\n if (!ttl) return\n\n const now = Date.now()\n for (const [tenantId, lastTime] of this.lastAccessed) {\n if (now - lastTime > ttl) {\n const db = this.connections.get(tenantId)\n if (db && this.options.onTenantShutdown) {\n try {\n await this.options.onTenantShutdown(db, tenantId)\n } catch (err) {\n log.error(`Failed to evict tenant DB ${tenantId}: ${err}`)\n }\n }\n this.connections.delete(tenantId)\n this.lastAccessed.delete(tenantId)\n\n if (this.options.logging) {\n log.info(`Tenant DB evicted (idle): ${tenantId}`)\n }\n }\n }\n }\n\n /** Close all tenant connections on shutdown */\n async shutdown(): Promise<void> {\n if (this.evictionTimer) clearInterval(this.evictionTimer)\n\n if (this.options.onTenantShutdown) {\n for (const [tenantId, db] of this.connections) {\n try {\n await this.options.onTenantShutdown(db, tenantId)\n } catch (err) {\n log.error(`Failed to close tenant DB ${tenantId}: ${err}`)\n }\n }\n }\n\n this.connections.clear()\n this.lastAccessed.clear()\n log.info('All tenant DB connections closed')\n }\n\n /** Number of cached tenant connections (useful for monitoring) */\n get connectionCount(): number {\n return this.connections.size\n }\n}\n","import type { QueryBuilderAdapter, ParsedQuery, FilterItem, SortItem } from '@forinda/kickjs'\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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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["log"],"sources":["../src/types.ts","../src/drizzle.adapter.ts","../src/drizzle-tenant.adapter.ts","../src/query-adapter.ts"],"sourcesContent":["import { createToken, type MaybePromise } from '@forinda/kickjs'\n\n/**\n * DI token for resolving the Drizzle database instance from the container (single-tenant).\n *\n * Typed as `unknown` because Drizzle's database type depends on the user's\n * driver + schema; cast at the use site\n * (e.g. `@Inject(DRIZZLE_DB) private db!: BetterSQLite3Database<typeof schema>`).\n */\nexport const DRIZZLE_DB = createToken<unknown>('kick/drizzle/DB')\n\n/**\n * DI token for resolving the current tenant's Drizzle database instance (multi-tenant).\n *\n * Same typing caveat as {@link DRIZZLE_DB} — cast at the use site.\n */\nexport const DRIZZLE_TENANT_DB = createToken<unknown>('kick/drizzle/DB:tenant')\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 * 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 * DrizzleAdapter({\n * db,\n * onShutdown: () => pool.end(),\n * })\n * ```\n */\n onShutdown?: () => MaybePromise<any>\n}\n\nexport interface DrizzleTenantAdapterOptions<TDb = unknown> {\n /**\n * The provider (default) database instance. Used when no tenant is\n * resolved or when accessing the tenant registry.\n */\n providerDb: TDb\n\n /**\n * Factory that creates a typed Drizzle instance for a given tenant.\n * Called once per tenant — the result is cached for subsequent requests.\n *\n * @example\n * ```ts\n * tenantFactory: async (tenantId) => {\n * const url = await lookupTenantDbUrl(tenantId)\n * return drizzle(new Pool({ connectionString: url }), { schema })\n * }\n * ```\n */\n tenantFactory: (tenantId: string) => TDb | Promise<TDb>\n\n /**\n * Optional function to close a tenant DB connection.\n * Called for each cached connection during shutdown.\n */\n onTenantShutdown?: (db: TDb, tenantId: string) => MaybePromise<any>\n\n /** Enable query logging (default: false) */\n logging?: boolean\n\n /**\n * Cache TTL in milliseconds. Tenant connections idle beyond this\n * duration are evicted. Default: no eviction (connections live until shutdown).\n */\n cacheTtl?: number\n}\n","import { Logger, defineAdapter, Scope } from '@forinda/kickjs'\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 * @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 * 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 const DrizzleAdapter = defineAdapter<DrizzleAdapterOptions<unknown>>({\n name: 'DrizzleAdapter',\n build: (options) => {\n const db = options.db\n const onShutdown = options.onShutdown\n\n return {\n beforeStart({ container }) {\n if (options.logging) {\n log.info('Query logging enabled')\n }\n\n container.registerFactory(DRIZZLE_DB, () => db, Scope.SINGLETON)\n\n log.info('Drizzle database registered in DI container')\n },\n\n async shutdown() {\n if (onShutdown) {\n await onShutdown()\n log.info('Drizzle connection closed')\n }\n },\n }\n },\n})\n","import { Logger, defineAdapter, Scope } from '@forinda/kickjs'\nimport { DRIZZLE_TENANT_DB, type DrizzleTenantAdapterOptions } from './types'\n\nconst log = Logger.for('DrizzleTenantAdapter')\n\n/**\n * Public extension methods exposed by a DrizzleTenantAdapter instance.\n * `getDb()` returns the (possibly newly-created) Drizzle instance for\n * a tenant; `connectionCount` reports the cache size for monitoring.\n */\nexport interface DrizzleTenantAdapterExtensions<TDb = unknown> {\n /**\n * Get the database for a specific tenant. Creates and caches the\n * connection on first access. Returns the provider DB when tenantId\n * is undefined/null.\n */\n getDb(tenantId?: string | null): Promise<TDb>\n /** Number of cached tenant connections (useful for monitoring). */\n readonly connectionCount: number\n}\n\n/**\n * Multi-tenant Drizzle adapter — manages per-tenant database connections\n * with automatic caching and lifecycle management.\n *\n * Registers `DRIZZLE_TENANT_DB` as a TRANSIENT DI token that resolves\n * to the current tenant's Drizzle instance using AsyncLocalStorage\n * (requires `TenantAdapter` to be configured).\n *\n * Works alongside `DrizzleAdapter` — use `DRIZZLE_DB` for the provider\n * database and `DRIZZLE_TENANT_DB` for the current tenant's database.\n *\n * @example\n * ```ts\n * import { drizzle } from 'drizzle-orm/node-postgres'\n * import { Pool } from 'pg'\n * import * as schema from './schema'\n *\n * const providerDb = drizzle(new Pool({ connectionString: PROVIDER_URL }), { schema })\n *\n * bootstrap({\n * adapters: [\n * TenantAdapter({ strategy: 'subdomain' }),\n * DrizzleTenantAdapter({\n * providerDb,\n * tenantFactory: async (tenantId) => {\n * const url = await lookupTenantDbUrl(tenantId)\n * return drizzle(new Pool({ connectionString: url }), { schema })\n * },\n * }),\n * ],\n * })\n * ```\n *\n * Inject in services:\n * ```ts\n * @Service()\n * class ProjectService {\n * constructor(@Inject(DRIZZLE_TENANT_DB) private db: NodePgDatabase<typeof schema>) {}\n * }\n * ```\n */\nexport const DrizzleTenantAdapter = defineAdapter<\n DrizzleTenantAdapterOptions<unknown>,\n DrizzleTenantAdapterExtensions<unknown>\n>({\n name: 'DrizzleTenantAdapter',\n build: (options) => {\n const providerDb = options.providerDb\n const tenantFactory = options.tenantFactory\n const connections = new Map<string, unknown>()\n const lastAccessed = new Map<string, number>()\n let evictionTimer: ReturnType<typeof setInterval> | undefined\n\n if (options.cacheTtl && options.cacheTtl > 0) {\n const interval = Math.min(options.cacheTtl, 60_000)\n evictionTimer = setInterval(() => evictStale(), interval)\n evictionTimer.unref()\n }\n\n const getDb = async (tenantId?: string | null): Promise<unknown> => {\n if (!tenantId) return providerDb\n\n const cached = connections.get(tenantId)\n if (cached) {\n lastAccessed.set(tenantId, Date.now())\n return cached\n }\n\n const db = await tenantFactory(tenantId)\n connections.set(tenantId, db)\n lastAccessed.set(tenantId, Date.now())\n\n if (options.logging) {\n log.info(`Tenant DB created: ${tenantId} (${connections.size} total)`)\n }\n\n return db\n }\n\n async function evictStale(): Promise<void> {\n const ttl = options.cacheTtl\n if (!ttl) return\n\n const now = Date.now()\n for (const [tenantId, lastTime] of lastAccessed) {\n if (now - lastTime > ttl) {\n const db = connections.get(tenantId)\n if (db && options.onTenantShutdown) {\n try {\n await options.onTenantShutdown(db, tenantId)\n } catch (err) {\n log.error(`Failed to evict tenant DB ${tenantId}: ${err}`)\n }\n }\n connections.delete(tenantId)\n lastAccessed.delete(tenantId)\n\n if (options.logging) {\n log.info(`Tenant DB evicted (idle): ${tenantId}`)\n }\n }\n }\n }\n\n return {\n getDb,\n get connectionCount() {\n return connections.size\n },\n\n async beforeStart({ container }) {\n // Dynamically import getCurrentTenant to avoid hard dep on multi-tenant package\n let getCurrentTenant: (() => { id: string } | undefined) | undefined\n\n try {\n const mt: any = await import('@forinda/kickjs-multi-tenant')\n getCurrentTenant = mt.getCurrentTenant\n } catch {\n log.warn(\n 'DrizzleTenantAdapter: @forinda/kickjs-multi-tenant not found. ' +\n 'DRIZZLE_TENANT_DB will always resolve to the provider database.',\n )\n }\n\n container.registerFactory(\n DRIZZLE_TENANT_DB,\n () => {\n const tenant = getCurrentTenant?.()\n return getDb(tenant?.id)\n },\n Scope.TRANSIENT,\n )\n\n log.info(\n `Drizzle tenant DB registered (${getCurrentTenant ? 'multi-tenant mode' : 'provider-only mode'})`,\n )\n },\n\n async shutdown() {\n if (evictionTimer) clearInterval(evictionTimer)\n\n if (options.onTenantShutdown) {\n for (const [tenantId, db] of connections) {\n try {\n await options.onTenantShutdown(db, tenantId)\n } catch (err) {\n log.error(`Failed to close tenant DB ${tenantId}: ${err}`)\n }\n }\n }\n\n connections.clear()\n lastAccessed.clear()\n log.info('All tenant DB connections closed')\n },\n }\n },\n})\n","import type { QueryBuilderAdapter, ParsedQuery, FilterItem, SortItem } from '@forinda/kickjs'\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-drizzle",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Drizzle ORM adapter with DI integration, transaction support, and query building for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -25,13 +25,10 @@
|
|
|
25
25
|
"@forinda/kickjs",
|
|
26
26
|
"@forinda/kickjs-auth",
|
|
27
27
|
"@forinda/kickjs-cli",
|
|
28
|
-
"@forinda/kickjs-config",
|
|
29
|
-
"@forinda/kickjs-core",
|
|
30
28
|
"@forinda/kickjs-cron",
|
|
31
29
|
"@forinda/kickjs-devtools",
|
|
32
30
|
"@forinda/kickjs-drizzle",
|
|
33
31
|
"@forinda/kickjs-graphql",
|
|
34
|
-
"@forinda/kickjs-http",
|
|
35
32
|
"@forinda/kickjs-mailer",
|
|
36
33
|
"@forinda/kickjs-multi-tenant",
|
|
37
34
|
"@forinda/kickjs-notifications",
|
|
@@ -67,9 +64,7 @@
|
|
|
67
64
|
"output": [
|
|
68
65
|
"dist/**"
|
|
69
66
|
],
|
|
70
|
-
"dependencies": [
|
|
71
|
-
"../core:build"
|
|
72
|
-
]
|
|
67
|
+
"dependencies": []
|
|
73
68
|
}
|
|
74
69
|
},
|
|
75
70
|
"dependencies": {
|
|
@@ -86,9 +81,9 @@
|
|
|
86
81
|
}
|
|
87
82
|
},
|
|
88
83
|
"devDependencies": {
|
|
89
|
-
"@types/node": "^25.
|
|
84
|
+
"@types/node": "^25.6.0",
|
|
90
85
|
"typescript": "^5.9.2",
|
|
91
|
-
"@forinda/kickjs": "
|
|
86
|
+
"@forinda/kickjs": "4.0.0"
|
|
92
87
|
},
|
|
93
88
|
"publishConfig": {
|
|
94
89
|
"access": "public"
|