@forinda/kickjs-prisma 1.2.13 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # @forinda/kickjs-prisma
2
2
 
3
- Prisma ORM adapter for the [KickJS](https://forinda.github.io/kick-js/) framework. Provides DI integration, lifecycle management, and query building.
3
+ Prisma ORM adapter for the [KickJS](https://forinda.github.io/kick-js/) framework. Provides DI integration, lifecycle management, type-safe query building, and a `PrismaModelDelegate` interface for cast-free repositories.
4
4
 
5
5
  ## Features
6
6
 
7
- - **PrismaAdapter** - Registers a `PrismaClient` in the DI container and handles graceful disconnect on shutdown.
8
- - **PrismaQueryAdapter** - Translates framework-agnostic `ParsedQuery` objects into Prisma-compatible `findMany` arguments (`where`, `orderBy`, `skip`, `take`).
9
- - Optional query logging.
7
+ - **PrismaAdapter** registers `PrismaClient` in the DI container, handles graceful disconnect on shutdown
8
+ - **PrismaQueryAdapter** translates `ParsedQuery` into Prisma-compatible `findMany` arguments (`where`, `orderBy`, `skip`, `take`)
9
+ - **PrismaQueryConfig\<TModel\>** generic type validates `searchColumns` against model field names at compile time
10
+ - **PrismaModelDelegate** — typed interface for Prisma model operations, eliminates `as any` in repositories
11
+ - **PRISMA_CLIENT** token for DI injection
12
+ - Supports Prisma 5, 6, and 7+ (auto-detects logging method)
10
13
 
11
- ## Installation
14
+ ## Install
12
15
 
13
16
  ```bash
14
17
  # Using the KickJS CLI (recommended — auto-installs peer dependencies)
@@ -18,13 +21,13 @@ kick add prisma
18
21
  pnpm add @forinda/kickjs-prisma @prisma/client
19
22
  ```
20
23
 
21
- ## Usage
24
+ ## Quick Example (Prisma 5/6)
22
25
 
23
26
  ```ts
24
27
  import { PrismaClient } from '@prisma/client'
25
28
  import { PrismaAdapter, PRISMA_CLIENT } from '@forinda/kickjs-prisma'
29
+ import { Inject, Service } from '@forinda/kickjs-core'
26
30
 
27
- // Register the adapter
28
31
  bootstrap({
29
32
  modules,
30
33
  adapters: [
@@ -32,13 +35,131 @@ bootstrap({
32
35
  ],
33
36
  })
34
37
 
35
- // Inject in services
36
38
  @Service()
37
39
  class UserService {
38
- @Inject(PRISMA_CLIENT) private prisma: PrismaClient
40
+ @Inject(PRISMA_CLIENT) private prisma!: PrismaClient
41
+
42
+ async findAll() {
43
+ return this.prisma.user.findMany()
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Quick Example (Prisma 7+)
49
+
50
+ Prisma 7 uses driver adapters and generates the client to a custom output path:
51
+
52
+ ```ts
53
+ import { PrismaClient } from './generated/prisma/client'
54
+ import { PrismaPg } from '@prisma/adapter-pg'
55
+ import pg from 'pg'
56
+ import { PrismaAdapter, PRISMA_CLIENT } from '@forinda/kickjs-prisma'
57
+
58
+ const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
59
+ const client = new PrismaClient({ adapter: new PrismaPg(pool) })
60
+
61
+ bootstrap({
62
+ modules,
63
+ adapters: [
64
+ new PrismaAdapter({ client, logging: true }),
65
+ ],
66
+ })
67
+ ```
68
+
69
+ > Logging uses `$on('query', ...)` for Prisma 5/6 and `$extends` for Prisma 7+ automatically.
70
+
71
+ ## PrismaModelDelegate
72
+
73
+ 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
+
75
+ ```ts
76
+ import { Repository, Inject } from '@forinda/kickjs-core'
77
+ import { PRISMA_CLIENT, type PrismaModelDelegate } from '@forinda/kickjs-prisma'
78
+
79
+ @Repository()
80
+ export class PrismaUserRepository {
81
+ // Type-safe: only exposes CRUD operations for the 'user' model
82
+ @Inject(PRISMA_CLIENT) private prisma!: { user: PrismaModelDelegate }
83
+
84
+ async findById(id: string) {
85
+ return this.prisma.user.findUnique({ where: { id } })
86
+ }
87
+
88
+ async findAll() {
89
+ return this.prisma.user.findMany()
90
+ }
91
+
92
+ async create(dto: CreateUserDTO) {
93
+ return this.prisma.user.create({ data: dto as Record<string, unknown> })
94
+ }
39
95
  }
40
96
  ```
41
97
 
98
+ This is what `kick g module --repo prisma` generates by default. For full Prisma field-level type safety, replace `PrismaModelDelegate` with your actual PrismaClient type:
99
+
100
+ ```ts
101
+ // Full type safety (optional upgrade)
102
+ import type { PrismaClient } from '@prisma/client' // or '@/generated/prisma/client' for v7
103
+ @Inject(PRISMA_CLIENT) private prisma!: PrismaClient
104
+ ```
105
+
106
+ ### PrismaModelDelegate API
107
+
108
+ | Method | Signature | Description |
109
+ |--------|-----------|-------------|
110
+ | `findUnique` | `(args: { where, include? }) => Promise<unknown>` | Find a single record by unique field |
111
+ | `findFirst` | `(args?) => Promise<unknown>` | Find the first matching record |
112
+ | `findMany` | `(args?) => Promise<unknown[]>` | Find multiple records |
113
+ | `create` | `(args: { data }) => Promise<unknown>` | Create a new record |
114
+ | `update` | `(args: { where, data }) => Promise<unknown>` | Update an existing record |
115
+ | `delete` | `(args: { where }) => Promise<unknown>` | Delete a single record |
116
+ | `deleteMany` | `(args?: { where? }) => Promise<{ count }>` | Delete multiple records |
117
+ | `count` | `(args?: { where? }) => Promise<number>` | Count matching records |
118
+
119
+ ## Query Adapter
120
+
121
+ Translate parsed query strings into Prisma `findMany` arguments:
122
+
123
+ ```ts
124
+ import type { User } from '@prisma/client'
125
+ import { PrismaQueryAdapter, type PrismaQueryConfig } from '@forinda/kickjs-prisma'
126
+
127
+ const adapter = new PrismaQueryAdapter()
128
+
129
+ // Type-safe — only User field names accepted in searchColumns
130
+ const config: PrismaQueryConfig<User> = {
131
+ searchColumns: ['name', 'email'],
132
+ }
133
+
134
+ const args = adapter.build(parsed, config)
135
+ const users = await prisma.user.findMany(args)
136
+ // args = { where: { OR: [...] }, orderBy: [...], skip: 0, take: 20 }
137
+ ```
138
+
139
+ Without the generic, `searchColumns` accepts any string (backward compatible):
140
+
141
+ ```ts
142
+ const config: PrismaQueryConfig = {
143
+ searchColumns: ['name', 'email'],
144
+ }
145
+ ```
146
+
147
+ ## Exports
148
+
149
+ | Export | Type | Description |
150
+ |--------|------|-------------|
151
+ | `PrismaAdapter` | class | Lifecycle adapter for DI registration and shutdown |
152
+ | `PrismaQueryAdapter` | class | Translates `ParsedQuery` to Prisma `findMany` args |
153
+ | `PRISMA_CLIENT` | symbol | DI token for injecting PrismaClient |
154
+ | `PrismaModelDelegate` | interface | Typed CRUD operations for a single Prisma model |
155
+ | `PrismaAdapterOptions` | type | Options for `PrismaAdapter` constructor |
156
+ | `PrismaQueryConfig<T>` | type | Config for `PrismaQueryAdapter.build()` with generic field validation |
157
+ | `PrismaQueryResult` | type | Result shape from `PrismaQueryAdapter.build()` |
158
+
159
+ ## Documentation
160
+
161
+ [Full documentation](https://forinda.github.io/kick-js/)
162
+
42
163
  ## License
43
164
 
44
165
  MIT
package/dist/index.d.ts CHANGED
@@ -2,36 +2,94 @@ import { AppAdapter, Container } from '@forinda/kickjs-core';
2
2
  import { QueryBuilderAdapter, ParsedQuery } from '@forinda/kickjs-http';
3
3
 
4
4
  interface PrismaAdapterOptions {
5
- /** PrismaClient instance - typed as `any` to avoid version coupling */
5
+ /**
6
+ * PrismaClient instance — typed as `any` to avoid version coupling.
7
+ *
8
+ * Prisma 5/6: `new PrismaClient()` from `@prisma/client`
9
+ * Prisma 7+: `new PrismaClient({ adapter })` from your generated output path
10
+ */
6
11
  client: any;
7
- /** Enable query logging (default: false) */
12
+ /**
13
+ * Enable query logging (default: false).
14
+ * Uses `$on('query', ...)` for Prisma 5/6 and `$extends` for Prisma 7+.
15
+ */
8
16
  logging?: boolean;
9
17
  }
10
18
  /** DI token for resolving the PrismaClient from the container */
11
19
  declare const PRISMA_CLIENT: unique symbol;
20
+ /**
21
+ * Common Prisma model delegate operations.
22
+ * Use this to type-narrow the injected PrismaClient to a specific model
23
+ * without needing `as any` casts in repositories.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * @Repository()
28
+ * export class PrismaUserRepository {
29
+ * @Inject(PRISMA_CLIENT) private prisma!: { user: PrismaModelDelegate }
30
+ *
31
+ * async findById(id: string) {
32
+ * return this.prisma.user.findUnique({ where: { id } })
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ interface PrismaModelDelegate {
38
+ findUnique(args: {
39
+ where: Record<string, unknown>;
40
+ include?: Record<string, unknown>;
41
+ }): Promise<unknown>;
42
+ findFirst?(args?: Record<string, unknown>): Promise<unknown>;
43
+ findMany(args?: Record<string, unknown>): Promise<unknown[]>;
44
+ create(args: {
45
+ data: Record<string, unknown>;
46
+ }): Promise<unknown>;
47
+ update(args: {
48
+ where: Record<string, unknown>;
49
+ data: Record<string, unknown>;
50
+ }): Promise<unknown>;
51
+ delete(args: {
52
+ where: Record<string, unknown>;
53
+ }): Promise<unknown>;
54
+ deleteMany(args?: {
55
+ where?: Record<string, unknown>;
56
+ }): Promise<{
57
+ count: number;
58
+ }>;
59
+ count(args?: {
60
+ where?: Record<string, unknown>;
61
+ }): Promise<number>;
62
+ }
12
63
 
13
64
  /**
14
65
  * Prisma adapter — registers a PrismaClient in the DI container and manages
15
66
  * its lifecycle (connection setup and teardown).
16
67
  *
17
- * @example
68
+ * Works with Prisma 5, 6, and 7+.
69
+ *
70
+ * @example Prisma 5/6
18
71
  * ```ts
19
72
  * import { PrismaClient } from '@prisma/client'
20
- * import { PrismaAdapter } from '@forinda/kickjs-prisma'
21
73
  *
22
- * bootstrap({
23
- * modules,
24
- * adapters: [
25
- * new PrismaAdapter({ client: new PrismaClient(), logging: true }),
26
- * ],
27
- * })
74
+ * new PrismaAdapter({ client: new PrismaClient(), logging: true })
75
+ * ```
76
+ *
77
+ * @example Prisma 7+ (driver adapters)
78
+ * ```ts
79
+ * import { PrismaClient } from './generated/prisma'
80
+ * import { PrismaPg } from '@prisma/adapter-pg'
81
+ * import pg from 'pg'
82
+ *
83
+ * const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
84
+ * const client = new PrismaClient({ adapter: new PrismaPg(pool) })
85
+ * new PrismaAdapter({ client, logging: true })
28
86
  * ```
29
87
  *
30
88
  * Inject the client in services:
31
89
  * ```ts
32
90
  * @Service()
33
91
  * class UserService {
34
- * @Inject(PRISMA_CLIENT) private prisma: PrismaClient
92
+ * @Inject(PRISMA_CLIENT) private prisma!: PrismaClient
35
93
  * }
36
94
  * ```
37
95
  */
@@ -46,10 +104,29 @@ declare class PrismaAdapter implements AppAdapter {
46
104
  shutdown(): Promise<void>;
47
105
  }
48
106
 
49
- /** Configuration for the Prisma query builder adapter */
50
- interface PrismaQueryConfig {
107
+ /**
108
+ * Configuration for the Prisma query builder adapter.
109
+ *
110
+ * Use the generic parameter to constrain `searchColumns` to actual model field names:
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * import type { User } from '@prisma/client'
115
+ *
116
+ * // Type-safe — only User field names are accepted
117
+ * const config: PrismaQueryConfig<User> = {
118
+ * searchColumns: ['name', 'email'], // ✓ valid User fields
119
+ * }
120
+ *
121
+ * // Without generic — accepts any string (backward compatible)
122
+ * const config: PrismaQueryConfig = {
123
+ * searchColumns: ['name', 'email'],
124
+ * }
125
+ * ```
126
+ */
127
+ interface PrismaQueryConfig<TModel = Record<string, any>> {
51
128
  /** Columns to search across when a search string is provided */
52
- searchColumns?: string[];
129
+ searchColumns?: (keyof TModel & string)[];
53
130
  }
54
131
  /** Result shape matching Prisma's findMany arguments */
55
132
  interface PrismaQueryResult {
@@ -63,15 +140,19 @@ interface PrismaQueryResult {
63
140
  *
64
141
  * @example
65
142
  * ```ts
143
+ * import type { User } from '@prisma/client'
144
+ *
66
145
  * const adapter = new PrismaQueryAdapter()
67
146
  * const parsed = parseQuery(req.query)
68
- * const args = adapter.build(parsed, { searchColumns: ['name', 'email'] })
147
+ *
148
+ * // Type-safe — only User fields allowed in searchColumns
149
+ * const args = adapter.build(parsed, { searchColumns: ['name', 'email'] } satisfies PrismaQueryConfig<User>)
69
150
  * const users = await prisma.user.findMany(args)
70
151
  * ```
71
152
  */
72
- declare class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResult, PrismaQueryConfig> {
153
+ declare class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResult, PrismaQueryConfig<any>> {
73
154
  readonly name = "PrismaQueryAdapter";
74
- build(parsed: ParsedQuery, config?: PrismaQueryConfig): PrismaQueryResult;
155
+ build<TModel = Record<string, any>>(parsed: ParsedQuery, config?: PrismaQueryConfig<TModel>): PrismaQueryResult;
75
156
  /** Map FilterItem[] to Prisma where conditions */
76
157
  private buildFilters;
77
158
  /** Build Prisma orderBy from SortItem[] */
@@ -82,4 +163,4 @@ declare class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResul
82
163
  private coerce;
83
164
  }
84
165
 
85
- export { PRISMA_CLIENT, PrismaAdapter, type PrismaAdapterOptions, PrismaQueryAdapter, type PrismaQueryConfig, type PrismaQueryResult };
166
+ export { PRISMA_CLIENT, PrismaAdapter, type PrismaAdapterOptions, type PrismaModelDelegate, PrismaQueryAdapter, type PrismaQueryConfig, type PrismaQueryResult };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var f=Object.defineProperty;var a=(u,t)=>f(u,"name",{value:t,configurable:!0});import{Logger as p,Scope as g}from"@forinda/kickjs-core";var l=Symbol("PrismaClient");var c=p.for("PrismaAdapter"),h=class{static{a(this,"PrismaAdapter")}options;name="PrismaAdapter";client;constructor(t){this.options=t,this.client=t.client}beforeStart(t,r){this.options.logging&&typeof this.client.$on=="function"&&this.client.$on("query",e=>{c.debug(`Query: ${e.query}`),c.debug(`Params: ${e.params}`),c.debug(`Duration: ${e.duration}ms`)}),r.registerFactory(l,()=>this.client,g.SINGLETON),c.info("PrismaClient registered in DI container")}async shutdown(){typeof this.client.$disconnect=="function"&&(await this.client.$disconnect(),c.info("PrismaClient disconnected"))}};var m=class{static{a(this,"PrismaQueryAdapter")}name="PrismaQueryAdapter";build(t,r={}){let e={},o=this.buildFilters(t.filters),i=this.buildSearch(t.search,r.searchColumns);if(o.length>0||i){let n=[];o.length>0&&n.push(...o),i&&n.push(i),e.where=n.length===1?n[0]:{AND:n}}let s=this.buildSort(t.sort);return s.length>0&&(e.orderBy=s),e.skip=t.pagination.offset,e.take=t.pagination.limit,e}buildFilters(t){return t.map(r=>{let{field:e,operator:o,value:i}=r;switch(o){case"eq":return{[e]:{equals:this.coerce(i)}};case"neq":return{[e]:{not:this.coerce(i)}};case"gt":return{[e]:{gt:this.coerce(i)}};case"gte":return{[e]:{gte:this.coerce(i)}};case"lt":return{[e]:{lt:this.coerce(i)}};case"lte":return{[e]:{lte:this.coerce(i)}};case"contains":return{[e]:{contains:i,mode:"insensitive"}};case"starts":return{[e]:{startsWith:i,mode:"insensitive"}};case"ends":return{[e]:{endsWith:i,mode:"insensitive"}};case"in":{let s=i.split(",").map(n=>this.coerce(n.trim()));return{[e]:{in:s}}}case"between":{let[s,n]=i.split(",").map(d=>this.coerce(d.trim()));return{[e]:{gte:s,lte:n}}}default:return{[e]:{equals:this.coerce(i)}}}})}buildSort(t){return t.map(r=>({[r.field]:r.direction}))}buildSearch(t,r){return!t||!r||r.length===0?null:{OR:r.map(e=>({[e]:{contains:t,mode:"insensitive"}}))}}coerce(t){if(t==="true")return!0;if(t==="false")return!1;let r=Number(t);return!Number.isNaN(r)&&t.trim()!==""?r:t}};export{l as PRISMA_CLIENT,h as PrismaAdapter,m as PrismaQueryAdapter};
1
+ var p=Object.defineProperty;var a=(u,e)=>p(u,"name",{value:e,configurable:!0});import{Logger as g,Scope as b}from"@forinda/kickjs-core";var h=Symbol("PrismaClient");var c=g.for("PrismaAdapter"),m=class{static{a(this,"PrismaAdapter")}options;name="PrismaAdapter";client;constructor(e){this.options=e,this.client=e.client}beforeStart(e,i){this.options.logging&&(typeof this.client.$on=="function"?this.client.$on("query",t=>{c.debug(`Query: ${t.query}`),c.debug(`Params: ${t.params}`),c.debug(`Duration: ${t.duration}ms`)}):typeof this.client.$extends=="function"&&(this.client=this.client.$extends({query:{$allOperations({operation:t,model:o,args:r,query:s}){let n=performance.now();return s(r).then(l=>{let d=Math.round(performance.now()-n);return c.debug(`${o}.${t} \u2014 ${d}ms`),l})}}}))),i.registerFactory(h,()=>this.client,b.SINGLETON),c.info("PrismaClient registered in DI container")}async shutdown(){typeof this.client.$disconnect=="function"&&(await this.client.$disconnect(),c.info("PrismaClient disconnected"))}};var f=class{static{a(this,"PrismaQueryAdapter")}name="PrismaQueryAdapter";build(e,i={}){let t={},o=this.buildFilters(e.filters),r=this.buildSearch(e.search,i.searchColumns);if(o.length>0||r){let n=[];o.length>0&&n.push(...o),r&&n.push(r),t.where=n.length===1?n[0]:{AND:n}}let s=this.buildSort(e.sort);return s.length>0&&(t.orderBy=s),t.skip=e.pagination.offset,t.take=e.pagination.limit,t}buildFilters(e){return e.map(i=>{let{field:t,operator:o,value:r}=i;switch(o){case"eq":return{[t]:{equals:this.coerce(r)}};case"neq":return{[t]:{not:this.coerce(r)}};case"gt":return{[t]:{gt:this.coerce(r)}};case"gte":return{[t]:{gte:this.coerce(r)}};case"lt":return{[t]:{lt:this.coerce(r)}};case"lte":return{[t]:{lte:this.coerce(r)}};case"contains":return{[t]:{contains:r,mode:"insensitive"}};case"starts":return{[t]:{startsWith:r,mode:"insensitive"}};case"ends":return{[t]:{endsWith:r,mode:"insensitive"}};case"in":{let s=r.split(",").map(n=>this.coerce(n.trim()));return{[t]:{in:s}}}case"between":{let[s,n]=r.split(",").map(l=>this.coerce(l.trim()));return{[t]:{gte:s,lte:n}}}default:return{[t]:{equals:this.coerce(r)}}}})}buildSort(e){return e.map(i=>({[i.field]:i.direction}))}buildSearch(e,i){return!e||!i||i.length===0?null:{OR:i.map(t=>({[t]:{contains:e,mode:"insensitive"}}))}}coerce(e){if(e==="true")return!0;if(e==="false")return!1;let i=Number(e);return!Number.isNaN(i)&&e.trim()!==""?i:e}};export{h as PRISMA_CLIENT,m as PrismaAdapter,f as PrismaQueryAdapter};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forinda/kickjs-prisma",
3
- "version": "1.2.13",
3
+ "version": "1.3.1",
4
4
  "description": "Prisma ORM adapter with DI integration, transaction support, and query building for KickJS",
5
5
  "keywords": [
6
6
  "kickjs",
@@ -16,14 +16,27 @@
16
16
  "orm",
17
17
  "database",
18
18
  "query-builder",
19
- "adapter",
19
+ "postgresql",
20
+ "mysql",
21
+ "sqlite",
22
+ "prisma7",
20
23
  "@forinda/kickjs-core",
21
24
  "@forinda/kickjs-http",
22
25
  "@forinda/kickjs-config",
23
26
  "@forinda/kickjs-cli",
24
27
  "@forinda/kickjs-swagger",
25
28
  "@forinda/kickjs-testing",
26
- "@forinda/kickjs-prisma"
29
+ "@forinda/kickjs-ws",
30
+ "@forinda/kickjs-drizzle",
31
+ "@forinda/kickjs-otel",
32
+ "@forinda/kickjs-graphql",
33
+ "@forinda/kickjs-auth",
34
+ "@forinda/kickjs-cron",
35
+ "@forinda/kickjs-mailer",
36
+ "@forinda/kickjs-queue",
37
+ "@forinda/kickjs-multi-tenant",
38
+ "@forinda/kickjs-devtools",
39
+ "@forinda/kickjs-notifications"
27
40
  ],
28
41
  "type": "module",
29
42
  "main": "dist/index.js",
@@ -39,12 +52,17 @@
39
52
  ],
40
53
  "dependencies": {
41
54
  "reflect-metadata": "^0.2.2",
42
- "@forinda/kickjs-core": "1.2.13",
43
- "@forinda/kickjs-http": "1.2.13"
55
+ "@forinda/kickjs-core": "1.3.1",
56
+ "@forinda/kickjs-http": "1.3.1"
44
57
  },
45
58
  "peerDependencies": {
46
59
  "@prisma/client": ">=5.0.0"
47
60
  },
61
+ "peerDependenciesMeta": {
62
+ "@prisma/client": {
63
+ "optional": true
64
+ }
65
+ },
48
66
  "devDependencies": {
49
67
  "@types/node": "^24.5.2",
50
68
  "tsup": "^8.5.0",