@forinda/kickjs-prisma 2.3.2 → 3.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 +51 -4
- package/dist/index.d.mts +104 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +138 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@ Prisma ORM adapter for the [KickJS](https://forinda.github.io/kick-js/) framewor
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **PrismaAdapter** — registers `PrismaClient` in the DI container, handles graceful disconnect on shutdown
|
|
8
|
+
- **PrismaTenantAdapter** — multi-tenant adapter with per-tenant connection caching
|
|
8
9
|
- **PrismaQueryAdapter** — translates `ParsedQuery` into Prisma-compatible `findMany` arguments (`where`, `orderBy`, `skip`, `take`)
|
|
9
10
|
- **PrismaQueryConfig\<TModel\>** — generic type validates `searchColumns` against model field names at compile time
|
|
10
11
|
- **PrismaModelDelegate** — typed interface for Prisma model operations, eliminates `as any` in repositories
|
|
11
|
-
- **PRISMA_CLIENT** token
|
|
12
|
+
- **PRISMA_CLIENT** token — singleton, provider/single-tenant database
|
|
13
|
+
- **PRISMA_TENANT_CLIENT** token — transient, current tenant's database via AsyncLocalStorage
|
|
12
14
|
- Supports Prisma 5, 6, and 7+ (auto-detects logging method)
|
|
13
15
|
|
|
14
16
|
## Install
|
|
@@ -26,7 +28,7 @@ pnpm add @forinda/kickjs-prisma @prisma/client
|
|
|
26
28
|
```ts
|
|
27
29
|
import { PrismaClient } from '@prisma/client'
|
|
28
30
|
import { PrismaAdapter, PRISMA_CLIENT } from '@forinda/kickjs-prisma'
|
|
29
|
-
import { Inject, Service } from '@forinda/kickjs
|
|
31
|
+
import { Inject, Service } from '@forinda/kickjs'
|
|
30
32
|
|
|
31
33
|
bootstrap({
|
|
32
34
|
modules,
|
|
@@ -73,7 +75,7 @@ bootstrap({
|
|
|
73
75
|
A typed interface for common Prisma model CRUD operations. Use it to type-narrow the injected `PrismaClient` to a specific model without `as any` casts.
|
|
74
76
|
|
|
75
77
|
```ts
|
|
76
|
-
import { Repository, Inject } from '@forinda/kickjs
|
|
78
|
+
import { Repository, Inject } from '@forinda/kickjs'
|
|
77
79
|
import { PRISMA_CLIENT, type PrismaModelDelegate } from '@forinda/kickjs-prisma'
|
|
78
80
|
|
|
79
81
|
@Repository()
|
|
@@ -116,6 +118,48 @@ import type { PrismaClient } from '@prisma/client' // or '@/generated/prisma/cli
|
|
|
116
118
|
| `deleteMany` | `(args?: { where? }) => Promise<{ count }>` | Delete multiple records |
|
|
117
119
|
| `count` | `(args?: { where? }) => Promise<number>` | Count matching records |
|
|
118
120
|
|
|
121
|
+
## Multi-Tenant
|
|
122
|
+
|
|
123
|
+
Use `PrismaTenantAdapter` alongside `TenantAdapter` for database-per-tenant:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { PrismaTenantAdapter, PRISMA_TENANT_CLIENT } from '@forinda/kickjs-prisma'
|
|
127
|
+
import { TenantAdapter } from '@forinda/kickjs-multi-tenant'
|
|
128
|
+
import { PrismaClient } from '@prisma/client'
|
|
129
|
+
|
|
130
|
+
bootstrap({
|
|
131
|
+
modules,
|
|
132
|
+
adapters: [
|
|
133
|
+
new TenantAdapter({ strategy: 'subdomain' }),
|
|
134
|
+
new PrismaTenantAdapter({
|
|
135
|
+
providerDb: new PrismaClient({ datasourceUrl: PROVIDER_URL }),
|
|
136
|
+
tenantFactory: async (tenantId) => {
|
|
137
|
+
const url = await lookupTenantDbUrl(tenantId)
|
|
138
|
+
return new PrismaClient({ datasourceUrl: url })
|
|
139
|
+
},
|
|
140
|
+
onTenantShutdown: (db) => db.$disconnect(),
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// In a service — resolves to the current tenant's PrismaClient
|
|
146
|
+
@Service()
|
|
147
|
+
class ProjectService {
|
|
148
|
+
@Inject(PRISMA_TENANT_CLIENT) private prisma!: PrismaClient
|
|
149
|
+
|
|
150
|
+
async findAll() {
|
|
151
|
+
return this.prisma.project.findMany()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Use both adapters together for interop:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
@Inject(PRISMA_CLIENT) private providerPrisma!: PrismaClient // always provider
|
|
160
|
+
@Inject(PRISMA_TENANT_CLIENT) private tenantPrisma!: PrismaClient // current tenant
|
|
161
|
+
```
|
|
162
|
+
|
|
119
163
|
## Query Adapter
|
|
120
164
|
|
|
121
165
|
Translate parsed query strings into Prisma `findMany` arguments:
|
|
@@ -149,10 +193,13 @@ const config: PrismaQueryConfig = {
|
|
|
149
193
|
| Export | Type | Description |
|
|
150
194
|
|--------|------|-------------|
|
|
151
195
|
| `PrismaAdapter` | class | Lifecycle adapter for DI registration and shutdown |
|
|
196
|
+
| `PrismaTenantAdapter` | class | Multi-tenant adapter with per-tenant connection caching |
|
|
152
197
|
| `PrismaQueryAdapter` | class | Translates `ParsedQuery` to Prisma `findMany` args |
|
|
153
|
-
| `PRISMA_CLIENT` | symbol | DI token for injecting PrismaClient |
|
|
198
|
+
| `PRISMA_CLIENT` | symbol | DI token for injecting PrismaClient (single-tenant) |
|
|
199
|
+
| `PRISMA_TENANT_CLIENT` | symbol | DI token for injecting current tenant's PrismaClient |
|
|
154
200
|
| `PrismaModelDelegate` | interface | Typed CRUD operations for a single Prisma model |
|
|
155
201
|
| `PrismaAdapterOptions` | type | Options for `PrismaAdapter` constructor |
|
|
202
|
+
| `PrismaTenantAdapterOptions` | type | Options for `PrismaTenantAdapter` constructor |
|
|
156
203
|
| `PrismaQueryConfig<T>` | type | Config for `PrismaQueryAdapter.build()` with generic field validation |
|
|
157
204
|
| `PrismaQueryResult` | type | Result shape from `PrismaQueryAdapter.build()` |
|
|
158
205
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
|
|
2
|
-
import { AdapterContext, AppAdapter, ParsedQuery, QueryBuilderAdapter } from "@forinda/kickjs";
|
|
2
|
+
import { AdapterContext, AppAdapter, MaybePromise, ParsedQuery, QueryBuilderAdapter } from "@forinda/kickjs";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
5
|
+
/** DI token for resolving the current tenant's PrismaClient (multi-tenant) */
|
|
6
|
+
declare const PRISMA_TENANT_CLIENT: unique symbol;
|
|
7
|
+
interface PrismaTenantAdapterOptions<TDb = unknown> {
|
|
8
|
+
/**
|
|
9
|
+
* The provider (default) PrismaClient. Used when no tenant is
|
|
10
|
+
* resolved or when accessing the tenant registry.
|
|
11
|
+
*/
|
|
12
|
+
providerDb: TDb;
|
|
13
|
+
/**
|
|
14
|
+
* Factory that creates a PrismaClient for a given tenant.
|
|
15
|
+
* Called once per tenant — the result is cached for subsequent requests.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* tenantFactory: async (tenantId) => {
|
|
20
|
+
* const url = await lookupTenantDbUrl(tenantId)
|
|
21
|
+
* return new PrismaClient({ datasourceUrl: url })
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
tenantFactory: (tenantId: string) => TDb | Promise<TDb>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional function to close a tenant PrismaClient connection.
|
|
28
|
+
* Called for each cached connection during shutdown.
|
|
29
|
+
*/
|
|
30
|
+
onTenantShutdown?: (db: TDb, tenantId: string) => MaybePromise<any>;
|
|
31
|
+
/** Enable query logging (default: false) */
|
|
32
|
+
logging?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Cache TTL in milliseconds. Tenant connections idle beyond this
|
|
35
|
+
* duration are evicted. Default: no eviction (connections live until shutdown).
|
|
36
|
+
*/
|
|
37
|
+
cacheTtl?: number;
|
|
38
|
+
}
|
|
5
39
|
interface PrismaAdapterOptions {
|
|
6
40
|
/**
|
|
7
41
|
* PrismaClient instance — typed as `any` to avoid version coupling.
|
|
@@ -108,6 +142,74 @@ declare class PrismaAdapter implements AppAdapter {
|
|
|
108
142
|
shutdown(): Promise<void>;
|
|
109
143
|
}
|
|
110
144
|
//#endregion
|
|
145
|
+
//#region src/prisma-tenant.adapter.d.ts
|
|
146
|
+
/**
|
|
147
|
+
* Multi-tenant Prisma adapter — manages per-tenant PrismaClient connections
|
|
148
|
+
* with automatic caching and lifecycle management.
|
|
149
|
+
*
|
|
150
|
+
* Registers `PRISMA_TENANT_CLIENT` as a TRANSIENT DI token that resolves
|
|
151
|
+
* to the current tenant's PrismaClient using AsyncLocalStorage
|
|
152
|
+
* (requires `TenantAdapter` to be configured).
|
|
153
|
+
*
|
|
154
|
+
* Works alongside `PrismaAdapter` — use `PRISMA_CLIENT` for the provider
|
|
155
|
+
* database and `PRISMA_TENANT_CLIENT` for the current tenant's database.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* import { PrismaClient } from '@prisma/client'
|
|
160
|
+
*
|
|
161
|
+
* const providerDb = new PrismaClient({ datasourceUrl: PROVIDER_URL })
|
|
162
|
+
*
|
|
163
|
+
* bootstrap({
|
|
164
|
+
* adapters: [
|
|
165
|
+
* new TenantAdapter({ strategy: 'subdomain' }),
|
|
166
|
+
* new PrismaTenantAdapter({
|
|
167
|
+
* providerDb,
|
|
168
|
+
* tenantFactory: async (tenantId) => {
|
|
169
|
+
* const url = await lookupTenantDbUrl(tenantId)
|
|
170
|
+
* return new PrismaClient({ datasourceUrl: url })
|
|
171
|
+
* },
|
|
172
|
+
* onTenantShutdown: (db) => db.$disconnect(),
|
|
173
|
+
* }),
|
|
174
|
+
* ],
|
|
175
|
+
* })
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* Inject in services:
|
|
179
|
+
* ```ts
|
|
180
|
+
* @Service()
|
|
181
|
+
* class ProjectService {
|
|
182
|
+
* constructor(@Inject(PRISMA_TENANT_CLIENT) private prisma: PrismaClient) {}
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
declare class PrismaTenantAdapter<TDb = unknown> implements AppAdapter {
|
|
187
|
+
name: string;
|
|
188
|
+
private readonly providerDb;
|
|
189
|
+
private readonly tenantFactory;
|
|
190
|
+
private readonly connections;
|
|
191
|
+
private readonly lastAccessed;
|
|
192
|
+
private readonly options;
|
|
193
|
+
private readonly evictionTimer?;
|
|
194
|
+
constructor(options: PrismaTenantAdapterOptions<TDb>);
|
|
195
|
+
/**
|
|
196
|
+
* Get the PrismaClient for a specific tenant.
|
|
197
|
+
* Creates and caches the connection on first access.
|
|
198
|
+
* Returns the provider DB when tenantId is undefined/null.
|
|
199
|
+
*/
|
|
200
|
+
getDb(tenantId?: string | null): Promise<TDb>;
|
|
201
|
+
/** Register PRISMA_TENANT_CLIENT as a transient factory in DI */
|
|
202
|
+
beforeStart({
|
|
203
|
+
container
|
|
204
|
+
}: AdapterContext): Promise<void>;
|
|
205
|
+
/** Evict connections that haven't been accessed within cacheTtl */
|
|
206
|
+
private evictStale;
|
|
207
|
+
/** Close all tenant connections on shutdown */
|
|
208
|
+
shutdown(): Promise<void>;
|
|
209
|
+
/** Number of cached tenant connections (useful for monitoring) */
|
|
210
|
+
get connectionCount(): number;
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
111
213
|
//#region src/query-adapter.d.ts
|
|
112
214
|
/**
|
|
113
215
|
* Configuration for the Prisma query builder adapter.
|
|
@@ -168,5 +270,5 @@ declare class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResul
|
|
|
168
270
|
private coerce;
|
|
169
271
|
}
|
|
170
272
|
//#endregion
|
|
171
|
-
export { PRISMA_CLIENT, PrismaAdapter, type PrismaAdapterOptions, type PrismaModelDelegate, PrismaQueryAdapter, type PrismaQueryConfig, type PrismaQueryResult };
|
|
273
|
+
export { PRISMA_CLIENT, PRISMA_TENANT_CLIENT, PrismaAdapter, type PrismaAdapterOptions, type PrismaModelDelegate, PrismaQueryAdapter, type PrismaQueryConfig, type PrismaQueryResult, PrismaTenantAdapter, type PrismaTenantAdapterOptions };
|
|
172
274
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/prisma.adapter.ts","../src/query-adapter.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/prisma.adapter.ts","../src/prisma-tenant.adapter.ts","../src/query-adapter.ts"],"mappings":";;;;;cAGa,oBAAA;AAAA,UAEI,0BAAA;EAF2C;;;;EAO1D,UAAA,EAAY,GAAA;EAL6B;;;;;;;;;;;;EAmBzC,aAAA,GAAgB,QAAA,aAAqB,GAAA,GAAM,OAAA,CAAQ,GAAA;EAAnD;;;;EAMA,gBAAA,IAAoB,EAAA,EAAI,GAAA,EAAK,QAAA,aAAqB,YAAA;EAAlD;EAGA,OAAA;EAHoB;;;;EASpB,QAAA;AAAA;AAAA,UAGe,oBAAA;EAAA;;;;;AAgBjB;EATE,MAAA;;;;AA4BF;EAvBE,OAAA;AAAA;;cAIW,aAAA;;;;;;;;;;;;;;;;;;UAmBI,mBAAA;EACf,UAAA,CAAW,IAAA;IACT,KAAA,EAAO,MAAA;IACP,OAAA,GAAU,MAAA;EAAA,IACR,OAAA;EACJ,SAAA,EAAW,IAAA,GAAO,MAAA,oBAA0B,OAAA;EAC5C,QAAA,CAAS,IAAA,GAAO,MAAA,oBAA0B,OAAA;EAC1C,MAAA,CAAO,IAAA;IAAQ,IAAA,EAAM,MAAA;EAAA,IAA4B,OAAA;EACjD,MAAA,CAAO,IAAA;IAAQ,KAAA,EAAO,MAAA;IAAyB,IAAA,EAAM,MAAA;EAAA,IAA4B,OAAA;EACjF,MAAA,CAAO,IAAA;IAAQ,KAAA,EAAO,MAAA;EAAA,IAA4B,OAAA;EAClD,UAAA,CAAW,IAAA;IAAS,KAAA,GAAQ,MAAA;EAAA,IAA4B,OAAA;IAAU,KAAA;EAAA;EAClE,KAAA,CAAM,IAAA;IAAS,KAAA,GAAQ,MAAA;EAAA,IAA4B,OAAA;AAAA;;;;AArFrD;;;;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;cCgCa,aAAA,YAAyB,UAAA;EAAA,QAIhB,OAAA;EAHpB,IAAA;EAAA,QACQ,MAAA;cAEY,OAAA,EAAS,oBAAA;EDQ7B;ECHA,WAAA,CAAA;IAAc;EAAA,GAAa,cAAA;EDYhB;ECsBL,QAAA,CAAA,GAAY,OAAA;AAAA;;;;AD7EpB;;;;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA;;;;;AAgBA;cEba,mBAAA,2BAA8C,UAAA;EACzD,IAAA;EAAA,iBACiB,UAAA;EAAA,iBACA,aAAA;EAAA,iBACA,WAAA;EAAA,iBACA,YAAA;EAAA,iBACA,OAAA;EAAA,iBACA,aAAA;cAEL,OAAA,EAAS,0BAAA,CAA2B,GAAA;EF2B5C;;;;;EEVE,KAAA,CAAM,QAAA,mBAA2B,OAAA,CAAQ,GAAA;EFaE;EEQ3C,WAAA,CAAA;IAAc;EAAA,GAAa,cAAA,GAAiB,OAAA;EFPG;EAAA,QEqCvC,UAAA;EFpCQ;EE8DhB,QAAA,CAAA,GAAY,OAAA;EF7DU;EAAA,IEgFxB,eAAA,CAAA;AAAA;;;;;AFpKN;;;;;AAEA;;;;;;;;;;;;;UGiBiB,iBAAA,UAA2B,MAAA;EHE1C;EGAA,aAAA,UAAuB,MAAA;AAAA;;UAIR,iBAAA;EACf,KAAA,GAAQ,MAAA;EACR,OAAA,GAAU,MAAA;EACV,IAAA;EACA,IAAA;AAAA;;;;;AHUF;;;;;AAgBA;;;;;AAmBA;cG3Ba,kBAAA,YAA8B,mBAAA,CACzC,iBAAA,EACA,iBAAA;EAAA,SAES,IAAA;EAET,KAAA,UAAe,MAAA,cAAA,CACb,MAAA,EAAQ,WAAA,EACR,MAAA,GAAQ,iBAAA,CAAkB,MAAA,IACzB,iBAAA;EHqBS;EAAA,QGaJ,YAAA;EHXU;EAAA,QGiDV,SAAA;EHhDQ;EAAA,QGqDR,WAAA;EHpDa;EAAA,QGiEb,MAAA;AAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-prisma
|
|
2
|
+
* @forinda/kickjs-prisma v3.0.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { Logger, Scope } from "@forinda/kickjs";
|
|
12
12
|
//#region src/types.ts
|
|
13
|
+
/** DI token for resolving the current tenant's PrismaClient (multi-tenant) */
|
|
14
|
+
const PRISMA_TENANT_CLIENT = Symbol("PrismaTenantDB");
|
|
13
15
|
/** DI token for resolving the PrismaClient from the container */
|
|
14
16
|
const PRISMA_CLIENT = Symbol("PrismaClient");
|
|
15
17
|
//#endregion
|
|
16
18
|
//#region src/prisma.adapter.ts
|
|
17
|
-
const log = Logger.for("PrismaAdapter");
|
|
19
|
+
const log$1 = Logger.for("PrismaAdapter");
|
|
18
20
|
/**
|
|
19
21
|
* Prisma adapter — registers a PrismaClient in the DI container and manages
|
|
20
22
|
* its lifecycle (connection setup and teardown).
|
|
@@ -58,31 +60,158 @@ var PrismaAdapter = class {
|
|
|
58
60
|
beforeStart({ container }) {
|
|
59
61
|
if (this.options.logging) {
|
|
60
62
|
if (typeof this.client.$on === "function") this.client.$on("query", (event) => {
|
|
61
|
-
log.debug(`Query: ${event.query}`);
|
|
62
|
-
log.debug(`Params: ${event.params}`);
|
|
63
|
-
log.debug(`Duration: ${event.duration}ms`);
|
|
63
|
+
log$1.debug(`Query: ${event.query}`);
|
|
64
|
+
log$1.debug(`Params: ${event.params}`);
|
|
65
|
+
log$1.debug(`Duration: ${event.duration}ms`);
|
|
64
66
|
});
|
|
65
67
|
else if (typeof this.client.$extends === "function") this.client = this.client.$extends({ query: { $allOperations({ operation, model, args, query }) {
|
|
66
68
|
const start = performance.now();
|
|
67
69
|
return query(args).then((result) => {
|
|
68
70
|
const duration = Math.round(performance.now() - start);
|
|
69
|
-
log.debug(`${model}.${operation} — ${duration}ms`);
|
|
71
|
+
log$1.debug(`${model}.${operation} — ${duration}ms`);
|
|
70
72
|
return result;
|
|
71
73
|
});
|
|
72
74
|
} } });
|
|
73
75
|
}
|
|
74
76
|
container.registerFactory(PRISMA_CLIENT, () => this.client, Scope.SINGLETON);
|
|
75
|
-
log.info("PrismaClient registered in DI container");
|
|
77
|
+
log$1.info("PrismaClient registered in DI container");
|
|
76
78
|
}
|
|
77
79
|
/** Disconnect the PrismaClient on shutdown */
|
|
78
80
|
async shutdown() {
|
|
79
81
|
if (typeof this.client.$disconnect === "function") {
|
|
80
82
|
await this.client.$disconnect();
|
|
81
|
-
log.info("PrismaClient disconnected");
|
|
83
|
+
log$1.info("PrismaClient disconnected");
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
87
|
//#endregion
|
|
88
|
+
//#region src/prisma-tenant.adapter.ts
|
|
89
|
+
const log = Logger.for("PrismaTenantAdapter");
|
|
90
|
+
/**
|
|
91
|
+
* Multi-tenant Prisma adapter — manages per-tenant PrismaClient connections
|
|
92
|
+
* with automatic caching and lifecycle management.
|
|
93
|
+
*
|
|
94
|
+
* Registers `PRISMA_TENANT_CLIENT` as a TRANSIENT DI token that resolves
|
|
95
|
+
* to the current tenant's PrismaClient using AsyncLocalStorage
|
|
96
|
+
* (requires `TenantAdapter` to be configured).
|
|
97
|
+
*
|
|
98
|
+
* Works alongside `PrismaAdapter` — use `PRISMA_CLIENT` for the provider
|
|
99
|
+
* database and `PRISMA_TENANT_CLIENT` for the current tenant's database.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* import { PrismaClient } from '@prisma/client'
|
|
104
|
+
*
|
|
105
|
+
* const providerDb = new PrismaClient({ datasourceUrl: PROVIDER_URL })
|
|
106
|
+
*
|
|
107
|
+
* bootstrap({
|
|
108
|
+
* adapters: [
|
|
109
|
+
* new TenantAdapter({ strategy: 'subdomain' }),
|
|
110
|
+
* new PrismaTenantAdapter({
|
|
111
|
+
* providerDb,
|
|
112
|
+
* tenantFactory: async (tenantId) => {
|
|
113
|
+
* const url = await lookupTenantDbUrl(tenantId)
|
|
114
|
+
* return new PrismaClient({ datasourceUrl: url })
|
|
115
|
+
* },
|
|
116
|
+
* onTenantShutdown: (db) => db.$disconnect(),
|
|
117
|
+
* }),
|
|
118
|
+
* ],
|
|
119
|
+
* })
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* Inject in services:
|
|
123
|
+
* ```ts
|
|
124
|
+
* @Service()
|
|
125
|
+
* class ProjectService {
|
|
126
|
+
* constructor(@Inject(PRISMA_TENANT_CLIENT) private prisma: PrismaClient) {}
|
|
127
|
+
* }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
var PrismaTenantAdapter = class {
|
|
131
|
+
name = "PrismaTenantAdapter";
|
|
132
|
+
providerDb;
|
|
133
|
+
tenantFactory;
|
|
134
|
+
connections = /* @__PURE__ */ new Map();
|
|
135
|
+
lastAccessed = /* @__PURE__ */ new Map();
|
|
136
|
+
options;
|
|
137
|
+
evictionTimer;
|
|
138
|
+
constructor(options) {
|
|
139
|
+
this.options = options;
|
|
140
|
+
this.providerDb = options.providerDb;
|
|
141
|
+
this.tenantFactory = options.tenantFactory;
|
|
142
|
+
if (options.cacheTtl && options.cacheTtl > 0) {
|
|
143
|
+
const interval = Math.min(options.cacheTtl, 6e4);
|
|
144
|
+
this.evictionTimer = setInterval(() => this.evictStale(), interval);
|
|
145
|
+
this.evictionTimer.unref();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get the PrismaClient for a specific tenant.
|
|
150
|
+
* Creates and caches the connection on first access.
|
|
151
|
+
* Returns the provider DB when tenantId is undefined/null.
|
|
152
|
+
*/
|
|
153
|
+
async getDb(tenantId) {
|
|
154
|
+
if (!tenantId) return this.providerDb;
|
|
155
|
+
const cached = this.connections.get(tenantId);
|
|
156
|
+
if (cached) {
|
|
157
|
+
this.lastAccessed.set(tenantId, Date.now());
|
|
158
|
+
return cached;
|
|
159
|
+
}
|
|
160
|
+
const db = await this.tenantFactory(tenantId);
|
|
161
|
+
this.connections.set(tenantId, db);
|
|
162
|
+
this.lastAccessed.set(tenantId, Date.now());
|
|
163
|
+
if (this.options.logging) log.info(`Tenant DB created: ${tenantId} (${this.connections.size} total)`);
|
|
164
|
+
return db;
|
|
165
|
+
}
|
|
166
|
+
/** Register PRISMA_TENANT_CLIENT as a transient factory in DI */
|
|
167
|
+
async beforeStart({ container }) {
|
|
168
|
+
let getCurrentTenant;
|
|
169
|
+
try {
|
|
170
|
+
getCurrentTenant = (await import("@forinda/kickjs-multi-tenant")).getCurrentTenant;
|
|
171
|
+
} catch {
|
|
172
|
+
log.warn("PrismaTenantAdapter: @forinda/kickjs-multi-tenant not found. PRISMA_TENANT_CLIENT will always resolve to the provider database.");
|
|
173
|
+
}
|
|
174
|
+
container.registerFactory(PRISMA_TENANT_CLIENT, () => {
|
|
175
|
+
const tenant = getCurrentTenant?.();
|
|
176
|
+
return this.getDb(tenant?.id);
|
|
177
|
+
}, Scope.TRANSIENT);
|
|
178
|
+
log.info(`Prisma tenant DB registered (${getCurrentTenant ? "multi-tenant mode" : "provider-only mode"})`);
|
|
179
|
+
}
|
|
180
|
+
/** Evict connections that haven't been accessed within cacheTtl */
|
|
181
|
+
async evictStale() {
|
|
182
|
+
const ttl = this.options.cacheTtl;
|
|
183
|
+
if (!ttl) return;
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
for (const [tenantId, lastTime] of this.lastAccessed) if (now - lastTime > ttl) {
|
|
186
|
+
const db = this.connections.get(tenantId);
|
|
187
|
+
if (db && this.options.onTenantShutdown) try {
|
|
188
|
+
await this.options.onTenantShutdown(db, tenantId);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
log.error(`Failed to evict tenant DB ${tenantId}: ${err}`);
|
|
191
|
+
}
|
|
192
|
+
this.connections.delete(tenantId);
|
|
193
|
+
this.lastAccessed.delete(tenantId);
|
|
194
|
+
if (this.options.logging) log.info(`Tenant DB evicted (idle): ${tenantId}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Close all tenant connections on shutdown */
|
|
198
|
+
async shutdown() {
|
|
199
|
+
if (this.evictionTimer) clearInterval(this.evictionTimer);
|
|
200
|
+
if (this.options.onTenantShutdown) for (const [tenantId, db] of this.connections) try {
|
|
201
|
+
await this.options.onTenantShutdown(db, tenantId);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
log.error(`Failed to close tenant DB ${tenantId}: ${err}`);
|
|
204
|
+
}
|
|
205
|
+
this.connections.clear();
|
|
206
|
+
this.lastAccessed.clear();
|
|
207
|
+
log.info("All tenant DB connections closed");
|
|
208
|
+
}
|
|
209
|
+
/** Number of cached tenant connections (useful for monitoring) */
|
|
210
|
+
get connectionCount() {
|
|
211
|
+
return this.connections.size;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
//#endregion
|
|
86
215
|
//#region src/query-adapter.ts
|
|
87
216
|
/**
|
|
88
217
|
* Translates a ParsedQuery into Prisma-compatible `findMany` arguments.
|
|
@@ -177,6 +306,6 @@ var PrismaQueryAdapter = class {
|
|
|
177
306
|
}
|
|
178
307
|
};
|
|
179
308
|
//#endregion
|
|
180
|
-
export { PRISMA_CLIENT, PrismaAdapter, PrismaQueryAdapter };
|
|
309
|
+
export { PRISMA_CLIENT, PRISMA_TENANT_CLIENT, PrismaAdapter, PrismaQueryAdapter, PrismaTenantAdapter };
|
|
181
310
|
|
|
182
311
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/types.ts","../src/prisma.adapter.ts","../src/query-adapter.ts"],"sourcesContent":["export interface PrismaAdapterOptions {\n /**\n * PrismaClient instance — typed as `any` to avoid version coupling.\n *\n * Prisma 5/6: `new PrismaClient()` from `@prisma/client`\n * Prisma 7+: `new PrismaClient({ adapter })` from your generated output path\n */\n client: any\n /**\n * Enable query logging (default: false).\n * Uses `$on('query', ...)` for Prisma 5/6 and `$extends` for Prisma 7+.\n */\n logging?: boolean\n}\n\n/** DI token for resolving the PrismaClient from the container */\nexport const PRISMA_CLIENT = Symbol('PrismaClient')\n\n/**\n * Common Prisma model delegate operations.\n * Use this to type-narrow the injected PrismaClient to a specific model\n * without needing `as any` casts in repositories.\n *\n * @example\n * ```ts\n * @Repository()\n * export class PrismaUserRepository {\n * @Inject(PRISMA_CLIENT) private prisma!: { user: PrismaModelDelegate }\n *\n * async findById(id: string) {\n * return this.prisma.user.findUnique({ where: { id } })\n * }\n * }\n * ```\n */\nexport interface PrismaModelDelegate {\n findUnique(args: {\n where: Record<string, unknown>\n include?: Record<string, unknown>\n }): Promise<unknown>\n findFirst?(args?: Record<string, unknown>): Promise<unknown>\n findMany(args?: Record<string, unknown>): Promise<unknown[]>\n create(args: { data: Record<string, unknown> }): Promise<unknown>\n update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<unknown>\n delete(args: { where: Record<string, unknown> }): Promise<unknown>\n deleteMany(args?: { where?: Record<string, unknown> }): Promise<{ count: number }>\n count(args?: { where?: Record<string, unknown> }): Promise<number>\n}\n","import { Logger, type AppAdapter, type AdapterContext, Scope } from '@forinda/kickjs'\nimport { PRISMA_CLIENT, type PrismaAdapterOptions } from './types'\n\nconst log = Logger.for('PrismaAdapter')\n\n/**\n * Prisma adapter — registers a PrismaClient in the DI container and manages\n * its lifecycle (connection setup and teardown).\n *\n * Works with Prisma 5, 6, and 7+.\n *\n * @example Prisma 5/6\n * ```ts\n * import { PrismaClient } from '@prisma/client'\n *\n * new PrismaAdapter({ client: new PrismaClient(), logging: true })\n * ```\n *\n * @example Prisma 7+ (driver adapters)\n * ```ts\n * import { PrismaClient } from './generated/prisma'\n * import { PrismaPg } from '@prisma/adapter-pg'\n * import pg from 'pg'\n *\n * const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })\n * const client = new PrismaClient({ adapter: new PrismaPg(pool) })\n * new PrismaAdapter({ client, logging: true })\n * ```\n *\n * Inject the client in services:\n * ```ts\n * @Service()\n * class UserService {\n * @Inject(PRISMA_CLIENT) private prisma!: PrismaClient\n * }\n * ```\n */\nexport class PrismaAdapter implements AppAdapter {\n name = 'PrismaAdapter'\n private client: any\n\n constructor(private options: PrismaAdapterOptions) {\n this.client = options.client\n }\n\n /** Register the PrismaClient in the DI container */\n beforeStart({ container }: AdapterContext): void {\n // Set up query logging if requested\n if (this.options.logging) {\n if (typeof this.client.$on === 'function') {\n // Prisma 5/6: event-based logging\n this.client.$on('query', (event: any) => {\n log.debug(`Query: ${event.query}`)\n log.debug(`Params: ${event.params}`)\n log.debug(`Duration: ${event.duration}ms`)\n })\n } else if (typeof this.client.$extends === 'function') {\n // Prisma 7+: Client Extensions for logging ($on removed)\n this.client = this.client.$extends({\n query: {\n $allOperations({ operation, model, args, query }: any) {\n const start = performance.now()\n return query(args).then((result: any) => {\n const duration = Math.round(performance.now() - start)\n log.debug(`${model}.${operation} — ${duration}ms`)\n return result\n })\n },\n },\n })\n }\n }\n\n // Register the client instance as a singleton factory in the container\n container.registerFactory(PRISMA_CLIENT, () => this.client, Scope.SINGLETON)\n\n log.info('PrismaClient registered in DI container')\n }\n\n /** Disconnect the PrismaClient on shutdown */\n async shutdown(): Promise<void> {\n if (typeof this.client.$disconnect === 'function') {\n await this.client.$disconnect()\n log.info('PrismaClient disconnected')\n }\n }\n}\n","import type { QueryBuilderAdapter, ParsedQuery, FilterItem, SortItem } from '@forinda/kickjs'\n\n/**\n * Configuration for the Prisma query builder adapter.\n *\n * Use the generic parameter to constrain `searchColumns` to actual model field names:\n *\n * @example\n * ```ts\n * import type { User } from '@prisma/client'\n *\n * // Type-safe — only User field names are accepted\n * const config: PrismaQueryConfig<User> = {\n * searchColumns: ['name', 'email'], // ✓ valid User fields\n * }\n *\n * // Without generic — accepts any string (backward compatible)\n * const config: PrismaQueryConfig = {\n * searchColumns: ['name', 'email'],\n * }\n * ```\n */\nexport interface PrismaQueryConfig<TModel = Record<string, any>> {\n /** Columns to search across when a search string is provided */\n searchColumns?: (keyof TModel & string)[]\n}\n\n/** Result shape matching Prisma's findMany arguments */\nexport interface PrismaQueryResult {\n where?: Record<string, any>\n orderBy?: Record<string, 'asc' | 'desc'>[]\n skip?: number\n take?: number\n}\n\n/**\n * Translates a ParsedQuery into Prisma-compatible `findMany` arguments.\n *\n * @example\n * ```ts\n * import type { User } from '@prisma/client'\n *\n * const adapter = new PrismaQueryAdapter()\n * const parsed = parseQuery(req.query)\n *\n * // Type-safe — only User fields allowed in searchColumns\n * const args = adapter.build(parsed, { searchColumns: ['name', 'email'] } satisfies PrismaQueryConfig<User>)\n * const users = await prisma.user.findMany(args)\n * ```\n */\nexport class PrismaQueryAdapter implements QueryBuilderAdapter<\n PrismaQueryResult,\n PrismaQueryConfig<any>\n> {\n readonly name = 'PrismaQueryAdapter'\n\n build<TModel = Record<string, any>>(\n parsed: ParsedQuery,\n config: PrismaQueryConfig<TModel> = {},\n ): PrismaQueryResult {\n const result: PrismaQueryResult = {}\n\n // Build where clause from filters and search\n const whereConditions = this.buildFilters(parsed.filters)\n const searchCondition = this.buildSearch(parsed.search, config.searchColumns)\n\n if (whereConditions.length > 0 || searchCondition) {\n const andClauses: any[] = []\n\n if (whereConditions.length > 0) {\n andClauses.push(...whereConditions)\n }\n if (searchCondition) {\n andClauses.push(searchCondition)\n }\n\n result.where = andClauses.length === 1 ? andClauses[0] : { AND: andClauses }\n }\n\n // Build orderBy\n const orderBy = this.buildSort(parsed.sort)\n if (orderBy.length > 0) {\n result.orderBy = orderBy\n }\n\n // Build pagination\n result.skip = parsed.pagination.offset\n result.take = parsed.pagination.limit\n\n return result\n }\n\n /** Map FilterItem[] to Prisma where conditions */\n private buildFilters(filters: FilterItem[]): Record<string, any>[] {\n return filters.map((filter) => {\n const { field, operator, value } = filter\n\n switch (operator) {\n case 'eq':\n return { [field]: { equals: this.coerce(value) } }\n case 'neq':\n return { [field]: { not: this.coerce(value) } }\n case 'gt':\n return { [field]: { gt: this.coerce(value) } }\n case 'gte':\n return { [field]: { gte: this.coerce(value) } }\n case 'lt':\n return { [field]: { lt: this.coerce(value) } }\n case 'lte':\n return { [field]: { lte: this.coerce(value) } }\n case 'contains':\n return { [field]: { contains: value, mode: 'insensitive' } }\n case 'starts':\n return { [field]: { startsWith: value, mode: 'insensitive' } }\n case 'ends':\n return { [field]: { endsWith: value, mode: 'insensitive' } }\n case 'in': {\n const values = value.split(',').map((v) => this.coerce(v.trim()))\n return { [field]: { in: values } }\n }\n case 'between': {\n const [min, max] = value.split(',').map((v) => this.coerce(v.trim()))\n return { [field]: { gte: min, lte: max } }\n }\n default:\n return { [field]: { equals: this.coerce(value) } }\n }\n })\n }\n\n /** Build Prisma orderBy from SortItem[] */\n private buildSort(sort: SortItem[]): Record<string, 'asc' | 'desc'>[] {\n return sort.map((item) => ({ [item.field]: item.direction }))\n }\n\n /** Build a search condition using OR + contains across multiple columns */\n private buildSearch(search: string, searchColumns?: string[]): Record<string, any> | null {\n if (!search || !searchColumns || searchColumns.length === 0) {\n return null\n }\n\n return {\n OR: searchColumns.map((column) => ({\n [column]: { contains: search, mode: 'insensitive' },\n })),\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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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["log"],"sources":["../src/types.ts","../src/prisma.adapter.ts","../src/prisma-tenant.adapter.ts","../src/query-adapter.ts"],"sourcesContent":["import type { MaybePromise } from '@forinda/kickjs'\n\n/** DI token for resolving the current tenant's PrismaClient (multi-tenant) */\nexport const PRISMA_TENANT_CLIENT = Symbol('PrismaTenantDB')\n\nexport interface PrismaTenantAdapterOptions<TDb = unknown> {\n /**\n * The provider (default) PrismaClient. Used when no tenant is\n * resolved or when accessing the tenant registry.\n */\n providerDb: TDb\n\n /**\n * Factory that creates a PrismaClient 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 new PrismaClient({ datasourceUrl: url })\n * }\n * ```\n */\n tenantFactory: (tenantId: string) => TDb | Promise<TDb>\n\n /**\n * Optional function to close a tenant PrismaClient 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\nexport interface PrismaAdapterOptions {\n /**\n * PrismaClient instance — typed as `any` to avoid version coupling.\n *\n * Prisma 5/6: `new PrismaClient()` from `@prisma/client`\n * Prisma 7+: `new PrismaClient({ adapter })` from your generated output path\n */\n client: any\n /**\n * Enable query logging (default: false).\n * Uses `$on('query', ...)` for Prisma 5/6 and `$extends` for Prisma 7+.\n */\n logging?: boolean\n}\n\n/** DI token for resolving the PrismaClient from the container */\nexport const PRISMA_CLIENT = Symbol('PrismaClient')\n\n/**\n * Common Prisma model delegate operations.\n * Use this to type-narrow the injected PrismaClient to a specific model\n * without needing `as any` casts in repositories.\n *\n * @example\n * ```ts\n * @Repository()\n * export class PrismaUserRepository {\n * @Inject(PRISMA_CLIENT) private prisma!: { user: PrismaModelDelegate }\n *\n * async findById(id: string) {\n * return this.prisma.user.findUnique({ where: { id } })\n * }\n * }\n * ```\n */\nexport interface PrismaModelDelegate {\n findUnique(args: {\n where: Record<string, unknown>\n include?: Record<string, unknown>\n }): Promise<unknown>\n findFirst?(args?: Record<string, unknown>): Promise<unknown>\n findMany(args?: Record<string, unknown>): Promise<unknown[]>\n create(args: { data: Record<string, unknown> }): Promise<unknown>\n update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<unknown>\n delete(args: { where: Record<string, unknown> }): Promise<unknown>\n deleteMany(args?: { where?: Record<string, unknown> }): Promise<{ count: number }>\n count(args?: { where?: Record<string, unknown> }): Promise<number>\n}\n","import { Logger, type AppAdapter, type AdapterContext, Scope } from '@forinda/kickjs'\nimport { PRISMA_CLIENT, type PrismaAdapterOptions } from './types'\n\nconst log = Logger.for('PrismaAdapter')\n\n/**\n * Prisma adapter — registers a PrismaClient in the DI container and manages\n * its lifecycle (connection setup and teardown).\n *\n * Works with Prisma 5, 6, and 7+.\n *\n * @example Prisma 5/6\n * ```ts\n * import { PrismaClient } from '@prisma/client'\n *\n * new PrismaAdapter({ client: new PrismaClient(), logging: true })\n * ```\n *\n * @example Prisma 7+ (driver adapters)\n * ```ts\n * import { PrismaClient } from './generated/prisma'\n * import { PrismaPg } from '@prisma/adapter-pg'\n * import pg from 'pg'\n *\n * const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })\n * const client = new PrismaClient({ adapter: new PrismaPg(pool) })\n * new PrismaAdapter({ client, logging: true })\n * ```\n *\n * Inject the client in services:\n * ```ts\n * @Service()\n * class UserService {\n * @Inject(PRISMA_CLIENT) private prisma!: PrismaClient\n * }\n * ```\n */\nexport class PrismaAdapter implements AppAdapter {\n name = 'PrismaAdapter'\n private client: any\n\n constructor(private options: PrismaAdapterOptions) {\n this.client = options.client\n }\n\n /** Register the PrismaClient in the DI container */\n beforeStart({ container }: AdapterContext): void {\n // Set up query logging if requested\n if (this.options.logging) {\n if (typeof this.client.$on === 'function') {\n // Prisma 5/6: event-based logging\n this.client.$on('query', (event: any) => {\n log.debug(`Query: ${event.query}`)\n log.debug(`Params: ${event.params}`)\n log.debug(`Duration: ${event.duration}ms`)\n })\n } else if (typeof this.client.$extends === 'function') {\n // Prisma 7+: Client Extensions for logging ($on removed)\n this.client = this.client.$extends({\n query: {\n $allOperations({ operation, model, args, query }: any) {\n const start = performance.now()\n return query(args).then((result: any) => {\n const duration = Math.round(performance.now() - start)\n log.debug(`${model}.${operation} — ${duration}ms`)\n return result\n })\n },\n },\n })\n }\n }\n\n // Register the client instance as a singleton factory in the container\n container.registerFactory(PRISMA_CLIENT, () => this.client, Scope.SINGLETON)\n\n log.info('PrismaClient registered in DI container')\n }\n\n /** Disconnect the PrismaClient on shutdown */\n async shutdown(): Promise<void> {\n if (typeof this.client.$disconnect === 'function') {\n await this.client.$disconnect()\n log.info('PrismaClient disconnected')\n }\n }\n}\n","import { Logger, type AppAdapter, type AdapterContext, Scope } from '@forinda/kickjs'\nimport { PRISMA_TENANT_CLIENT, type PrismaTenantAdapterOptions } from './types'\n\nconst log = Logger.for('PrismaTenantAdapter')\n\n/**\n * Multi-tenant Prisma adapter — manages per-tenant PrismaClient connections\n * with automatic caching and lifecycle management.\n *\n * Registers `PRISMA_TENANT_CLIENT` as a TRANSIENT DI token that resolves\n * to the current tenant's PrismaClient using AsyncLocalStorage\n * (requires `TenantAdapter` to be configured).\n *\n * Works alongside `PrismaAdapter` — use `PRISMA_CLIENT` for the provider\n * database and `PRISMA_TENANT_CLIENT` for the current tenant's database.\n *\n * @example\n * ```ts\n * import { PrismaClient } from '@prisma/client'\n *\n * const providerDb = new PrismaClient({ datasourceUrl: PROVIDER_URL })\n *\n * bootstrap({\n * adapters: [\n * new TenantAdapter({ strategy: 'subdomain' }),\n * new PrismaTenantAdapter({\n * providerDb,\n * tenantFactory: async (tenantId) => {\n * const url = await lookupTenantDbUrl(tenantId)\n * return new PrismaClient({ datasourceUrl: url })\n * },\n * onTenantShutdown: (db) => db.$disconnect(),\n * }),\n * ],\n * })\n * ```\n *\n * Inject in services:\n * ```ts\n * @Service()\n * class ProjectService {\n * constructor(@Inject(PRISMA_TENANT_CLIENT) private prisma: PrismaClient) {}\n * }\n * ```\n */\nexport class PrismaTenantAdapter<TDb = unknown> implements AppAdapter {\n name = 'PrismaTenantAdapter'\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: PrismaTenantAdapterOptions<TDb>\n private readonly evictionTimer?: ReturnType<typeof setInterval>\n\n constructor(options: PrismaTenantAdapterOptions<TDb>) {\n this.options = options\n this.providerDb = options.providerDb\n this.tenantFactory = options.tenantFactory\n\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 PrismaClient 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 PRISMA_TENANT_CLIENT 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 // @ts-expect-error optional peer dependency\n const mt = await import('@forinda/kickjs-multi-tenant')\n getCurrentTenant = mt.getCurrentTenant\n } catch {\n log.warn(\n 'PrismaTenantAdapter: @forinda/kickjs-multi-tenant not found. ' +\n 'PRISMA_TENANT_CLIENT will always resolve to the provider database.',\n )\n }\n\n container.registerFactory(\n PRISMA_TENANT_CLIENT,\n () => {\n const tenant = getCurrentTenant?.()\n return this.getDb(tenant?.id)\n },\n Scope.TRANSIENT,\n )\n\n log.info(\n `Prisma 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 Prisma query builder adapter.\n *\n * Use the generic parameter to constrain `searchColumns` to actual model field names:\n *\n * @example\n * ```ts\n * import type { User } from '@prisma/client'\n *\n * // Type-safe — only User field names are accepted\n * const config: PrismaQueryConfig<User> = {\n * searchColumns: ['name', 'email'], // ✓ valid User fields\n * }\n *\n * // Without generic — accepts any string (backward compatible)\n * const config: PrismaQueryConfig = {\n * searchColumns: ['name', 'email'],\n * }\n * ```\n */\nexport interface PrismaQueryConfig<TModel = Record<string, any>> {\n /** Columns to search across when a search string is provided */\n searchColumns?: (keyof TModel & string)[]\n}\n\n/** Result shape matching Prisma's findMany arguments */\nexport interface PrismaQueryResult {\n where?: Record<string, any>\n orderBy?: Record<string, 'asc' | 'desc'>[]\n skip?: number\n take?: number\n}\n\n/**\n * Translates a ParsedQuery into Prisma-compatible `findMany` arguments.\n *\n * @example\n * ```ts\n * import type { User } from '@prisma/client'\n *\n * const adapter = new PrismaQueryAdapter()\n * const parsed = parseQuery(req.query)\n *\n * // Type-safe — only User fields allowed in searchColumns\n * const args = adapter.build(parsed, { searchColumns: ['name', 'email'] } satisfies PrismaQueryConfig<User>)\n * const users = await prisma.user.findMany(args)\n * ```\n */\nexport class PrismaQueryAdapter implements QueryBuilderAdapter<\n PrismaQueryResult,\n PrismaQueryConfig<any>\n> {\n readonly name = 'PrismaQueryAdapter'\n\n build<TModel = Record<string, any>>(\n parsed: ParsedQuery,\n config: PrismaQueryConfig<TModel> = {},\n ): PrismaQueryResult {\n const result: PrismaQueryResult = {}\n\n // Build where clause from filters and search\n const whereConditions = this.buildFilters(parsed.filters)\n const searchCondition = this.buildSearch(parsed.search, config.searchColumns)\n\n if (whereConditions.length > 0 || searchCondition) {\n const andClauses: any[] = []\n\n if (whereConditions.length > 0) {\n andClauses.push(...whereConditions)\n }\n if (searchCondition) {\n andClauses.push(searchCondition)\n }\n\n result.where = andClauses.length === 1 ? andClauses[0] : { AND: andClauses }\n }\n\n // Build orderBy\n const orderBy = this.buildSort(parsed.sort)\n if (orderBy.length > 0) {\n result.orderBy = orderBy\n }\n\n // Build pagination\n result.skip = parsed.pagination.offset\n result.take = parsed.pagination.limit\n\n return result\n }\n\n /** Map FilterItem[] to Prisma where conditions */\n private buildFilters(filters: FilterItem[]): Record<string, any>[] {\n return filters.map((filter) => {\n const { field, operator, value } = filter\n\n switch (operator) {\n case 'eq':\n return { [field]: { equals: this.coerce(value) } }\n case 'neq':\n return { [field]: { not: this.coerce(value) } }\n case 'gt':\n return { [field]: { gt: this.coerce(value) } }\n case 'gte':\n return { [field]: { gte: this.coerce(value) } }\n case 'lt':\n return { [field]: { lt: this.coerce(value) } }\n case 'lte':\n return { [field]: { lte: this.coerce(value) } }\n case 'contains':\n return { [field]: { contains: value, mode: 'insensitive' } }\n case 'starts':\n return { [field]: { startsWith: value, mode: 'insensitive' } }\n case 'ends':\n return { [field]: { endsWith: value, mode: 'insensitive' } }\n case 'in': {\n const values = value.split(',').map((v) => this.coerce(v.trim()))\n return { [field]: { in: values } }\n }\n case 'between': {\n const [min, max] = value.split(',').map((v) => this.coerce(v.trim()))\n return { [field]: { gte: min, lte: max } }\n }\n default:\n return { [field]: { equals: this.coerce(value) } }\n }\n })\n }\n\n /** Build Prisma orderBy from SortItem[] */\n private buildSort(sort: SortItem[]): Record<string, 'asc' | 'desc'>[] {\n return sort.map((item) => ({ [item.field]: item.direction }))\n }\n\n /** Build a search condition using OR + contains across multiple columns */\n private buildSearch(search: string, searchColumns?: string[]): Record<string, any> | null {\n if (!search || !searchColumns || searchColumns.length === 0) {\n return null\n }\n\n return {\n OR: searchColumns.map((column) => ({\n [column]: { contains: search, mode: 'insensitive' },\n })),\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":";;;;;;;;;;;;;AAGA,MAAa,uBAAuB,OAAO,iBAAiB;;AAuD5D,MAAa,gBAAgB,OAAO,eAAe;;;ACvDnD,MAAMA,QAAM,OAAO,IAAI,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCvC,IAAa,gBAAb,MAAiD;CAC/C,OAAO;CACP;CAEA,YAAY,SAAuC;AAA/B,OAAA,UAAA;AAClB,OAAK,SAAS,QAAQ;;;CAIxB,YAAY,EAAE,aAAmC;AAE/C,MAAI,KAAK,QAAQ;OACX,OAAO,KAAK,OAAO,QAAQ,WAE7B,MAAK,OAAO,IAAI,UAAU,UAAe;AACvC,UAAI,MAAM,UAAU,MAAM,QAAQ;AAClC,UAAI,MAAM,WAAW,MAAM,SAAS;AACpC,UAAI,MAAM,aAAa,MAAM,SAAS,IAAI;KAC1C;YACO,OAAO,KAAK,OAAO,aAAa,WAEzC,MAAK,SAAS,KAAK,OAAO,SAAS,EACjC,OAAO,EACL,eAAe,EAAE,WAAW,OAAO,MAAM,SAAc;IACrD,MAAM,QAAQ,YAAY,KAAK;AAC/B,WAAO,MAAM,KAAK,CAAC,MAAM,WAAgB;KACvC,MAAM,WAAW,KAAK,MAAM,YAAY,KAAK,GAAG,MAAM;AACtD,WAAI,MAAM,GAAG,MAAM,GAAG,UAAU,KAAK,SAAS,IAAI;AAClD,YAAO;MACP;MAEL,EACF,CAAC;;AAKN,YAAU,gBAAgB,qBAAqB,KAAK,QAAQ,MAAM,UAAU;AAE5E,QAAI,KAAK,0CAA0C;;;CAIrD,MAAM,WAA0B;AAC9B,MAAI,OAAO,KAAK,OAAO,gBAAgB,YAAY;AACjD,SAAM,KAAK,OAAO,aAAa;AAC/B,SAAI,KAAK,4BAA4B;;;;;;AChF3C,MAAM,MAAM,OAAO,IAAI,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0C7C,IAAa,sBAAb,MAAsE;CACpE,OAAO;CACP;CACA;CACA,8BAA+B,IAAI,KAAkB;CACrD,+BAAgC,IAAI,KAAqB;CACzD;CACA;CAEA,YAAY,SAA0C;AACpD,OAAK,UAAU;AACf,OAAK,aAAa,QAAQ;AAC1B,OAAK,gBAAgB,QAAQ;AAE7B,MAAI,QAAQ,YAAY,QAAQ,WAAW,GAAG;GAC5C,MAAM,WAAW,KAAK,IAAI,QAAQ,UAAU,IAAO;AACnD,QAAK,gBAAgB,kBAAkB,KAAK,YAAY,EAAE,SAAS;AACnE,QAAK,cAAc,OAAO;;;;;;;;CAS9B,MAAM,MAAM,UAAwC;AAClD,MAAI,CAAC,SAAU,QAAO,KAAK;EAE3B,MAAM,SAAS,KAAK,YAAY,IAAI,SAAS;AAC7C,MAAI,QAAQ;AACV,QAAK,aAAa,IAAI,UAAU,KAAK,KAAK,CAAC;AAC3C,UAAO;;EAGT,MAAM,KAAK,MAAM,KAAK,cAAc,SAAS;AAC7C,OAAK,YAAY,IAAI,UAAU,GAAG;AAClC,OAAK,aAAa,IAAI,UAAU,KAAK,KAAK,CAAC;AAE3C,MAAI,KAAK,QAAQ,QACf,KAAI,KAAK,sBAAsB,SAAS,IAAI,KAAK,YAAY,KAAK,SAAS;AAG7E,SAAO;;;CAIT,MAAM,YAAY,EAAE,aAA4C;EAE9D,IAAI;AAEJ,MAAI;AAGF,uBADW,MAAM,OAAO,iCACF;UAChB;AACN,OAAI,KACF,kIAED;;AAGH,YAAU,gBACR,4BACM;GACJ,MAAM,SAAS,oBAAoB;AACnC,UAAO,KAAK,MAAM,QAAQ,GAAG;KAE/B,MAAM,UACP;AAED,MAAI,KACF,gCAAgC,mBAAmB,sBAAsB,qBAAqB,GAC/F;;;CAIH,MAAc,aAA4B;EACxC,MAAM,MAAM,KAAK,QAAQ;AACzB,MAAI,CAAC,IAAK;EAEV,MAAM,MAAM,KAAK,KAAK;AACtB,OAAK,MAAM,CAAC,UAAU,aAAa,KAAK,aACtC,KAAI,MAAM,WAAW,KAAK;GACxB,MAAM,KAAK,KAAK,YAAY,IAAI,SAAS;AACzC,OAAI,MAAM,KAAK,QAAQ,iBACrB,KAAI;AACF,UAAM,KAAK,QAAQ,iBAAiB,IAAI,SAAS;YAC1C,KAAK;AACZ,QAAI,MAAM,6BAA6B,SAAS,IAAI,MAAM;;AAG9D,QAAK,YAAY,OAAO,SAAS;AACjC,QAAK,aAAa,OAAO,SAAS;AAElC,OAAI,KAAK,QAAQ,QACf,KAAI,KAAK,6BAA6B,WAAW;;;;CAOzD,MAAM,WAA0B;AAC9B,MAAI,KAAK,cAAe,eAAc,KAAK,cAAc;AAEzD,MAAI,KAAK,QAAQ,iBACf,MAAK,MAAM,CAAC,UAAU,OAAO,KAAK,YAChC,KAAI;AACF,SAAM,KAAK,QAAQ,iBAAiB,IAAI,SAAS;WAC1C,KAAK;AACZ,OAAI,MAAM,6BAA6B,SAAS,IAAI,MAAM;;AAKhE,OAAK,YAAY,OAAO;AACxB,OAAK,aAAa,OAAO;AACzB,MAAI,KAAK,mCAAmC;;;CAI9C,IAAI,kBAA0B;AAC5B,SAAO,KAAK,YAAY;;;;;;;;;;;;;;;;;;;;ACtH5B,IAAa,qBAAb,MAGE;CACA,OAAgB;CAEhB,MACE,QACA,SAAoC,EAAE,EACnB;EACnB,MAAM,SAA4B,EAAE;EAGpC,MAAM,kBAAkB,KAAK,aAAa,OAAO,QAAQ;EACzD,MAAM,kBAAkB,KAAK,YAAY,OAAO,QAAQ,OAAO,cAAc;AAE7E,MAAI,gBAAgB,SAAS,KAAK,iBAAiB;GACjD,MAAM,aAAoB,EAAE;AAE5B,OAAI,gBAAgB,SAAS,EAC3B,YAAW,KAAK,GAAG,gBAAgB;AAErC,OAAI,gBACF,YAAW,KAAK,gBAAgB;AAGlC,UAAO,QAAQ,WAAW,WAAW,IAAI,WAAW,KAAK,EAAE,KAAK,YAAY;;EAI9E,MAAM,UAAU,KAAK,UAAU,OAAO,KAAK;AAC3C,MAAI,QAAQ,SAAS,EACnB,QAAO,UAAU;AAInB,SAAO,OAAO,OAAO,WAAW;AAChC,SAAO,OAAO,OAAO,WAAW;AAEhC,SAAO;;;CAIT,aAAqB,SAA8C;AACjE,SAAO,QAAQ,KAAK,WAAW;GAC7B,MAAM,EAAE,OAAO,UAAU,UAAU;AAEnC,WAAQ,UAAR;IACE,KAAK,KACH,QAAO,GAAG,QAAQ,EAAE,QAAQ,KAAK,OAAO,MAAM,EAAE,EAAE;IACpD,KAAK,MACH,QAAO,GAAG,QAAQ,EAAE,KAAK,KAAK,OAAO,MAAM,EAAE,EAAE;IACjD,KAAK,KACH,QAAO,GAAG,QAAQ,EAAE,IAAI,KAAK,OAAO,MAAM,EAAE,EAAE;IAChD,KAAK,MACH,QAAO,GAAG,QAAQ,EAAE,KAAK,KAAK,OAAO,MAAM,EAAE,EAAE;IACjD,KAAK,KACH,QAAO,GAAG,QAAQ,EAAE,IAAI,KAAK,OAAO,MAAM,EAAE,EAAE;IAChD,KAAK,MACH,QAAO,GAAG,QAAQ,EAAE,KAAK,KAAK,OAAO,MAAM,EAAE,EAAE;IACjD,KAAK,WACH,QAAO,GAAG,QAAQ;KAAE,UAAU;KAAO,MAAM;KAAe,EAAE;IAC9D,KAAK,SACH,QAAO,GAAG,QAAQ;KAAE,YAAY;KAAO,MAAM;KAAe,EAAE;IAChE,KAAK,OACH,QAAO,GAAG,QAAQ;KAAE,UAAU;KAAO,MAAM;KAAe,EAAE;IAC9D,KAAK,MAAM;KACT,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,KAAK,MAAM,KAAK,OAAO,EAAE,MAAM,CAAC,CAAC;AACjE,YAAO,GAAG,QAAQ,EAAE,IAAI,QAAQ,EAAE;;IAEpC,KAAK,WAAW;KACd,MAAM,CAAC,KAAK,OAAO,MAAM,MAAM,IAAI,CAAC,KAAK,MAAM,KAAK,OAAO,EAAE,MAAM,CAAC,CAAC;AACrE,YAAO,GAAG,QAAQ;MAAE,KAAK;MAAK,KAAK;MAAK,EAAE;;IAE5C,QACE,QAAO,GAAG,QAAQ,EAAE,QAAQ,KAAK,OAAO,MAAM,EAAE,EAAE;;IAEtD;;;CAIJ,UAAkB,MAAoD;AACpE,SAAO,KAAK,KAAK,UAAU,GAAG,KAAK,QAAQ,KAAK,WAAW,EAAE;;;CAI/D,YAAoB,QAAgB,eAAsD;AACxF,MAAI,CAAC,UAAU,CAAC,iBAAiB,cAAc,WAAW,EACxD,QAAO;AAGT,SAAO,EACL,IAAI,cAAc,KAAK,YAAY,GAChC,SAAS;GAAE,UAAU;GAAQ,MAAM;GAAe,EACpD,EAAE,EACJ;;;CAIH,OAAe,OAA0C;AACvD,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,UAAU,QAAS,QAAO;EAC9B,MAAM,MAAM,OAAO,MAAM;AACzB,MAAI,CAAC,OAAO,MAAM,IAAI,IAAI,MAAM,MAAM,KAAK,GAAI,QAAO;AACtD,SAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-prisma",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Prisma ORM adapter with DI integration, transaction support, and query building for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -73,14 +73,18 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"reflect-metadata": "^0.2.2",
|
|
76
|
-
"@forinda/kickjs": "
|
|
76
|
+
"@forinda/kickjs": "3.0.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@prisma/client": ">=5.0.0"
|
|
79
|
+
"@prisma/client": ">=5.0.0",
|
|
80
|
+
"@forinda/kickjs-multi-tenant": ">=2.0.0"
|
|
80
81
|
},
|
|
81
82
|
"peerDependenciesMeta": {
|
|
82
83
|
"@prisma/client": {
|
|
83
84
|
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@forinda/kickjs-multi-tenant": {
|
|
87
|
+
"optional": true
|
|
84
88
|
}
|
|
85
89
|
},
|
|
86
90
|
"devDependencies": {
|
|
@@ -107,6 +111,7 @@
|
|
|
107
111
|
"scripts": {
|
|
108
112
|
"build": "wireit",
|
|
109
113
|
"dev": "tsdown --watch",
|
|
114
|
+
"test": "vitest run --passWithNoTests",
|
|
110
115
|
"typecheck": "tsc --noEmit",
|
|
111
116
|
"clean": "rm -rf dist .wireit",
|
|
112
117
|
"lint": "tsc --noEmit"
|