prisma-generator-express 1.18.0 → 1.20.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.
Files changed (76) hide show
  1. package/README.md +399 -194
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +1 -1
  4. package/dist/bin.js.map +1 -1
  5. package/dist/client/encodeQueryParams.d.ts +1 -0
  6. package/dist/client/encodeQueryParams.js +33 -0
  7. package/dist/client/encodeQueryParams.js.map +1 -0
  8. package/dist/constants.d.ts +1 -0
  9. package/dist/copy/misc.d.ts +5 -0
  10. package/dist/copy/misc.js +52 -0
  11. package/dist/copy/misc.js.map +1 -0
  12. package/dist/generators/generateImportPrismaStatement.d.ts +3 -0
  13. package/dist/generators/generateImportPrismaStatement.js +55 -0
  14. package/dist/generators/generateImportPrismaStatement.js.map +1 -0
  15. package/dist/generators/generateQueryBuilderHelper.d.ts +2 -0
  16. package/dist/generators/generateQueryBuilderHelper.js +139 -0
  17. package/dist/generators/generateQueryBuilderHelper.js.map +1 -0
  18. package/dist/generators/generateRouter.d.ts +6 -0
  19. package/dist/generators/generateRouter.js +340 -0
  20. package/dist/generators/generateRouter.js.map +1 -0
  21. package/dist/generators/generateUnifiedDocs.d.ts +1 -0
  22. package/dist/generators/generateUnifiedDocs.js +171 -0
  23. package/dist/generators/generateUnifiedDocs.js.map +1 -0
  24. package/dist/generators/generateUnifiedHandler.d.ts +6 -0
  25. package/dist/generators/generateUnifiedHandler.js +444 -0
  26. package/dist/generators/generateUnifiedHandler.js.map +1 -0
  27. package/dist/generators/generateUnifiedScalarUI.d.ts +5 -0
  28. package/dist/generators/generateUnifiedScalarUI.js +1390 -0
  29. package/dist/generators/generateUnifiedScalarUI.js.map +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +80 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/utils/copyFiles.d.ts +6 -0
  34. package/dist/utils/copyFiles.js +123 -21
  35. package/dist/utils/copyFiles.js.map +1 -1
  36. package/dist/utils/strings.d.ts +2 -0
  37. package/dist/utils/writeFileSafely.d.ts +10 -0
  38. package/dist/utils/writeFileSafely.js +86 -14
  39. package/dist/utils/writeFileSafely.js.map +1 -1
  40. package/package.json +59 -28
  41. package/src/bin.ts +1 -1
  42. package/src/client/encodeQueryParams.ts +56 -0
  43. package/src/copy/buildModelOpenApi.ts +1569 -0
  44. package/src/copy/misc.ts +21 -0
  45. package/src/copy/operationDefinitions.ts +96 -0
  46. package/src/copy/parseQueryParams.ts +36 -21
  47. package/src/copy/routeConfig.ts +68 -28
  48. package/src/generators/generateImportPrismaStatement.ts +78 -0
  49. package/src/generators/generateQueryBuilderHelper.ts +138 -0
  50. package/src/generators/generateRouter.ts +352 -0
  51. package/src/generators/generateUnifiedDocs.ts +168 -0
  52. package/src/generators/generateUnifiedHandler.ts +469 -0
  53. package/src/generators/generateUnifiedScalarUI.ts +1409 -0
  54. package/src/index.ts +100 -0
  55. package/src/utils/copyFiles.ts +123 -16
  56. package/src/utils/writeFileSafely.ts +79 -25
  57. package/dist/generator.js +0 -47
  58. package/dist/generator.js.map +0 -1
  59. package/dist/helpers/generateImportPrismaStatement.js +0 -25
  60. package/dist/helpers/generateImportPrismaStatement.js.map +0 -1
  61. package/dist/helpers/generateOperation.js +0 -471
  62. package/dist/helpers/generateOperation.js.map +0 -1
  63. package/dist/helpers/generateRouteFile.js +0 -210
  64. package/dist/helpers/generateRouteFile.js.map +0 -1
  65. package/dist/utils/formatFile.js +0 -26
  66. package/dist/utils/formatFile.js.map +0 -1
  67. package/src/copy/encodeQueryParams.spec.ts +0 -303
  68. package/src/copy/encodeQueryParams.ts +0 -44
  69. package/src/copy/misc.spec.ts +0 -62
  70. package/src/copy/parseQueryParams.spec.ts +0 -187
  71. package/src/copy/transformZod.spec.ts +0 -763
  72. package/src/generator.ts +0 -54
  73. package/src/helpers/generateImportPrismaStatement.ts +0 -38
  74. package/src/helpers/generateOperation.ts +0 -515
  75. package/src/helpers/generateRouteFile.ts +0 -213
  76. package/src/utils/formatFile.ts +0 -22
package/src/copy/misc.ts CHANGED
@@ -23,3 +23,24 @@ export function safeJSONparse<T>(
23
23
  export const isObject = (value: unknown): value is Record<string, unknown> => {
24
24
  return typeof value === 'object' && value !== null && !Array.isArray(value)
25
25
  }
26
+
27
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
28
+
29
+ export function isSafeKey(key: string): boolean {
30
+ return !UNSAFE_KEYS.has(key)
31
+ }
32
+
33
+ export function sanitizeKeys<T>(value: T): T {
34
+ if (Array.isArray(value)) {
35
+ return value.map(sanitizeKeys) as T
36
+ }
37
+ if (isObject(value)) {
38
+ const result: Record<string, unknown> = {}
39
+ for (const key of Object.keys(value)) {
40
+ if (!isSafeKey(key)) continue
41
+ result[key] = sanitizeKeys((value as Record<string, unknown>)[key])
42
+ }
43
+ return result as T
44
+ }
45
+ return value
46
+ }
@@ -0,0 +1,96 @@
1
+ export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'
2
+
3
+ export interface OperationDef {
4
+ name: string
5
+ method: HttpMethod
6
+ pathSuffix: string
7
+ configKey: string
8
+ }
9
+
10
+ export const OPERATION_DEFS: OperationDef[] = [
11
+ { name: 'findMany', method: 'get', pathSuffix: '', configKey: 'findMany' },
12
+ {
13
+ name: 'findUnique',
14
+ method: 'get',
15
+ pathSuffix: '/unique',
16
+ configKey: 'findUnique',
17
+ },
18
+ {
19
+ name: 'findUniqueOrThrow',
20
+ method: 'get',
21
+ pathSuffix: '/unique/strict',
22
+ configKey: 'findUniqueOrThrow',
23
+ },
24
+ {
25
+ name: 'findFirst',
26
+ method: 'get',
27
+ pathSuffix: '/first',
28
+ configKey: 'findFirst',
29
+ },
30
+ {
31
+ name: 'findFirstOrThrow',
32
+ method: 'get',
33
+ pathSuffix: '/first/strict',
34
+ configKey: 'findFirstOrThrow',
35
+ },
36
+ {
37
+ name: 'findManyPaginated',
38
+ method: 'get',
39
+ pathSuffix: '/paginated',
40
+ configKey: 'findManyPaginated',
41
+ },
42
+ { name: 'create', method: 'post', pathSuffix: '', configKey: 'create' },
43
+ {
44
+ name: 'createMany',
45
+ method: 'post',
46
+ pathSuffix: '/many',
47
+ configKey: 'createMany',
48
+ },
49
+ {
50
+ name: 'createManyAndReturn',
51
+ method: 'post',
52
+ pathSuffix: '/many/return',
53
+ configKey: 'createManyAndReturn',
54
+ },
55
+ { name: 'update', method: 'put', pathSuffix: '', configKey: 'update' },
56
+ {
57
+ name: 'updateMany',
58
+ method: 'put',
59
+ pathSuffix: '/many',
60
+ configKey: 'updateMany',
61
+ },
62
+ {
63
+ name: 'updateManyAndReturn',
64
+ method: 'put',
65
+ pathSuffix: '/many/return',
66
+ configKey: 'updateManyAndReturn',
67
+ },
68
+ { name: 'upsert', method: 'patch', pathSuffix: '', configKey: 'upsert' },
69
+ { name: 'delete', method: 'delete', pathSuffix: '', configKey: 'delete' },
70
+ {
71
+ name: 'deleteMany',
72
+ method: 'delete',
73
+ pathSuffix: '/many',
74
+ configKey: 'deleteMany',
75
+ },
76
+ { name: 'count', method: 'get', pathSuffix: '/count', configKey: 'count' },
77
+ {
78
+ name: 'aggregate',
79
+ method: 'get',
80
+ pathSuffix: '/aggregate',
81
+ configKey: 'aggregate',
82
+ },
83
+ {
84
+ name: 'groupBy',
85
+ method: 'get',
86
+ pathSuffix: '/groupby',
87
+ configKey: 'groupBy',
88
+ },
89
+ ]
90
+
91
+ export function isOperationEnabled(
92
+ config: Record<string, any>,
93
+ def: OperationDef,
94
+ ): boolean {
95
+ return !!(config.enableAll || config[def.configKey])
96
+ }
@@ -1,29 +1,38 @@
1
- import { isObject } from './misc'
2
- import { ParsedQs } from 'qs'
1
+ import { isObject, isSafeKey, sanitizeKeys } from './misc'
3
2
 
4
- /**
5
- * Type definition for possible query parameter types.
6
- */
7
- type QueryParams = string | ParsedQs | string[] | ParsedQs[] | Array<string | ParsedQs> | undefined
3
+ type QueryParams =
4
+ | string
5
+ | Record<string, unknown>
6
+ | string[]
7
+ | Record<string, unknown>[]
8
+ | undefined
8
9
 
9
- /**
10
- * Parses a query value to convert strings to their respective types.
11
- * @param {string} value - The query value to parse.
12
- * @returns {unknown} The parsed value.
13
- */
14
- const parseQueryValue = (value: string): unknown => {
10
+ const NUMERIC_KEYS = new Set(['take', 'skip'])
11
+
12
+ const parseQueryValue = (value: string, key?: string): unknown => {
13
+ if (value.startsWith('{') || value.startsWith('[') || value.startsWith('"')) {
14
+ try {
15
+ const parsed = JSON.parse(value)
16
+ return sanitizeKeys(parsed)
17
+ } catch {
18
+ // fall through
19
+ }
20
+ }
15
21
  if (value === 'true') return true
16
22
  if (value === 'false') return false
17
23
  if (value === 'null') return null
18
- if (!isNaN(Number(value))) return Number(value)
24
+ if (
25
+ key &&
26
+ NUMERIC_KEYS.has(key) &&
27
+ value !== '' &&
28
+ !isNaN(Number(value)) &&
29
+ isFinite(Number(value))
30
+ ) {
31
+ return Number(value)
32
+ }
19
33
  return value
20
34
  }
21
35
 
22
- /**
23
- * Recursively parses query parameters to convert strings to their respective types.
24
- * @param {QueryParams} params - The query parameters to parse.
25
- * @returns {unknown} The parsed query parameters.
26
- */
27
36
  export const parseQueryParams = (params: QueryParams): unknown => {
28
37
  if (typeof params === 'string') {
29
38
  return parseQueryValue(params)
@@ -33,10 +42,16 @@ export const parseQueryParams = (params: QueryParams): unknown => {
33
42
  }
34
43
  if (isObject(params)) {
35
44
  const parsedParams: Record<string, unknown> = {}
36
- for (const key in params) {
37
- parsedParams[key] = parseQueryParams(params[key])
45
+ for (const key of Object.keys(params)) {
46
+ if (!isSafeKey(key)) continue
47
+ const raw = params[key]
48
+ if (typeof raw === 'string') {
49
+ parsedParams[key] = parseQueryValue(raw, key)
50
+ } else {
51
+ parsedParams[key] = raw
52
+ }
38
53
  }
39
54
  return parsedParams
40
55
  }
41
56
  return params
42
- }
57
+ }
@@ -1,36 +1,76 @@
1
- import { RequestHandler } from 'express'
2
- import { ZodType } from 'zod'
1
+ import { RequestHandler, Request } from 'express'
3
2
 
4
- export interface ValidatorConfig {
5
- allow?: string[]
6
- forbid?: string[]
7
- schema: ZodType
3
+ export interface OperationConfig {
4
+ before?: RequestHandler[]
5
+ after?: RequestHandler[]
6
+ shape?: Record<string, any>
8
7
  }
9
8
 
10
- interface MiddlewareConfig<M> {
11
- before?: M[]
12
- after?: RequestHandler[]
13
- inputValidator?: ValidatorConfig
14
- outputValidator?: ValidatorConfig
9
+ export interface QueryBuilderConfig {
10
+ enabled?: boolean
11
+ port?: number
12
+ host?: string
13
+ schemaPath?: string
14
+ databaseUrl?: string
15
15
  }
16
16
 
17
- export interface RouteConfig<M> {
18
- findFirst?: MiddlewareConfig<M>
19
- findMany?: MiddlewareConfig<M>
20
- findUnique?: MiddlewareConfig<M>
21
- create?: MiddlewareConfig<M>
22
- createMany?: MiddlewareConfig<M>
23
- createManyAndReturn?: MiddlewareConfig<M>
24
- update?: MiddlewareConfig<M>
25
- updateMany?: MiddlewareConfig<M>
26
- updateManyAndReturn?: MiddlewareConfig<M>
27
- upsert?: MiddlewareConfig<M>
28
- delete?: MiddlewareConfig<M>
29
- deleteMany?: MiddlewareConfig<M>
30
- aggregate?: MiddlewareConfig<M>
31
- count?: MiddlewareConfig<M>
32
- groupBy?: MiddlewareConfig<M>
33
- addModelPrefix?: boolean
17
+ export interface OpenApiServerConfig {
18
+ url: string
19
+ description?: string
20
+ }
21
+
22
+ export interface OpenApiSecuritySchemeConfig {
23
+ type: string
24
+ scheme?: string
25
+ bearerFormat?: string
26
+ name?: string
27
+ in?: string
28
+ description?: string
29
+ }
30
+
31
+ export interface RouteConfig {
34
32
  enableAll?: boolean
33
+ addModelPrefix?: boolean
35
34
  customUrlPrefix?: string
35
+ specBasePath?: string
36
+ disableOpenApi?: boolean
37
+ scalarCdnUrl?: string
38
+
39
+ openApiTitle?: string
40
+ openApiDescription?: string
41
+ openApiVersion?: string
42
+ openApiServers?: OpenApiServerConfig[]
43
+ openApiSecuritySchemes?: Record<string, OpenApiSecuritySchemeConfig>
44
+ openApiSecurity?: Record<string, string[]>[]
45
+
46
+ guard?: {
47
+ resolveVariant?: (req: Request) => string | undefined
48
+ variantHeader?: string
49
+ }
50
+
51
+ queryBuilder?: QueryBuilderConfig | false
52
+
53
+ pagination?: {
54
+ defaultLimit?: number
55
+ maxLimit?: number
56
+ }
57
+
58
+ findUnique?: OperationConfig
59
+ findUniqueOrThrow?: OperationConfig
60
+ findFirst?: OperationConfig
61
+ findFirstOrThrow?: OperationConfig
62
+ findMany?: OperationConfig
63
+ findManyPaginated?: OperationConfig
64
+ create?: OperationConfig
65
+ createMany?: OperationConfig
66
+ createManyAndReturn?: OperationConfig
67
+ update?: OperationConfig
68
+ updateMany?: OperationConfig
69
+ updateManyAndReturn?: OperationConfig
70
+ upsert?: OperationConfig
71
+ delete?: OperationConfig
72
+ deleteMany?: OperationConfig
73
+ aggregate?: OperationConfig
74
+ count?: OperationConfig
75
+ groupBy?: OperationConfig
36
76
  }
@@ -0,0 +1,78 @@
1
+ import { GeneratorOptions } from '@prisma/generator-helper'
2
+ import path from 'path'
3
+
4
+ function findClientGenerator(options: GeneratorOptions) {
5
+ const byName = options.otherGenerators.find((gen) => gen.name === 'client')
6
+ if (byName) return byName
7
+
8
+ const byProvider = options.otherGenerators.find(
9
+ (gen) =>
10
+ gen.provider.value === 'prisma-client-js' ||
11
+ gen.provider.value === '@prisma/client' ||
12
+ gen.provider.value === 'prisma-client',
13
+ )
14
+ if (byProvider) return byProvider
15
+
16
+ const withOutput = options.otherGenerators.find(
17
+ (gen) =>
18
+ gen.output?.value?.includes('prisma') ||
19
+ gen.output?.value?.includes('client'),
20
+ )
21
+ return withOutput || null
22
+ }
23
+
24
+ function getRelativeImportPath(
25
+ fromDir: string,
26
+ clientOutputPath: string,
27
+ ): string {
28
+ let relativeImportPath = path.relative(fromDir, clientOutputPath)
29
+ relativeImportPath = relativeImportPath.split(path.sep).join(path.posix.sep)
30
+ if (!relativeImportPath.startsWith('.')) {
31
+ relativeImportPath = './' + relativeImportPath
32
+ }
33
+ return relativeImportPath
34
+ }
35
+
36
+ export function generateImportPrismaStatement(
37
+ generatorOptions: GeneratorOptions,
38
+ ): string {
39
+ const clientGenerator = findClientGenerator(generatorOptions)
40
+
41
+ if (!clientGenerator || !clientGenerator.output?.value) {
42
+ throw new Error(
43
+ 'Prisma client generator not found. Ensure a generator with provider "prisma-client-js" exists in your schema.',
44
+ )
45
+ }
46
+
47
+ const outputValue = generatorOptions.generator.output?.value
48
+ if (!outputValue) {
49
+ throw new Error('Generator output path not defined.')
50
+ }
51
+
52
+ const subDir = path.join(outputValue, '_relative')
53
+ const outputPath = getRelativeImportPath(subDir, clientGenerator.output.value)
54
+
55
+ return `import { Prisma, PrismaClient } from '${outputPath}';\n`
56
+ }
57
+
58
+ export function getRelativeClientPath(
59
+ generatorOptions: GeneratorOptions,
60
+ modelName: string,
61
+ ): string {
62
+ const clientGenerator = findClientGenerator(generatorOptions)
63
+
64
+ if (!clientGenerator || !clientGenerator.output?.value) {
65
+ throw new Error(
66
+ 'Prisma client generator not found. Ensure a generator with provider "prisma-client-js" exists in your schema.',
67
+ )
68
+ }
69
+
70
+ const outputValue = generatorOptions.generator.output?.value
71
+ if (!outputValue) {
72
+ throw new Error('Generator output path not defined.')
73
+ }
74
+
75
+ const routerDirPath = path.join(outputValue, modelName)
76
+
77
+ return getRelativeImportPath(routerDirPath, clientGenerator.output.value)
78
+ }
@@ -0,0 +1,138 @@
1
+ import { GeneratorOptions } from '@prisma/generator-helper'
2
+
3
+ export function generateQueryBuilderHelper(options: GeneratorOptions): string {
4
+ const schemaPath = options.schemaPath
5
+ ? JSON.stringify(options.schemaPath)
6
+ : "require('path').resolve(process.cwd(), 'prisma/schema.prisma')"
7
+
8
+ return `import { spawn } from 'child_process'
9
+ import { resolve, join, dirname } from 'path'
10
+ import { existsSync, readFileSync } from 'fs'
11
+ import { createRequire } from 'module'
12
+ import type { ChildProcess } from 'child_process'
13
+
14
+ let _process: ChildProcess | null = null
15
+ let _stopping = false
16
+ let _cleanupRegistered = false
17
+
18
+ export interface QueryBuilderOptions {
19
+ port?: number
20
+ host?: string
21
+ schemaPath?: string
22
+ databaseUrl?: string
23
+ }
24
+
25
+ function findCliPath(): string | null {
26
+ try {
27
+ const req = createRequire(resolve(process.cwd(), 'package.json'))
28
+ const pkgJsonPath = req.resolve('prisma-query-builder-ui/package.json')
29
+ const pkgDir = dirname(pkgJsonPath)
30
+ const cliPath = join(pkgDir, 'bin', 'cli.js')
31
+ if (existsSync(cliPath)) return cliPath
32
+ } catch {}
33
+
34
+ let dir = process.cwd()
35
+ const root = resolve(dir, '/')
36
+ while (dir !== root) {
37
+ const candidate = join(dir, 'node_modules', 'prisma-query-builder-ui', 'bin', 'cli.js')
38
+ if (existsSync(candidate)) return candidate
39
+ dir = dirname(dir)
40
+ }
41
+
42
+ return null
43
+ }
44
+
45
+ export function startQueryBuilder(options: QueryBuilderOptions = {}): void {
46
+ if (_process) return
47
+
48
+ const env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
49
+
50
+ if (env.NODE_ENV === 'production') return
51
+
52
+ const cliPath = findCliPath()
53
+ if (!cliPath) {
54
+ console.warn('[query-builder] prisma-query-builder-ui not found. Install: npm install prisma-query-builder-ui')
55
+ return
56
+ }
57
+
58
+ const port = options.port || 5173
59
+ const host = options.host || 'localhost'
60
+ const schemaPath = options.schemaPath || ${schemaPath}
61
+ const databaseUrl = options.databaseUrl || env.DATABASE_URL || ''
62
+
63
+ if (!existsSync(schemaPath)) {
64
+ console.error('[query-builder] Schema file not found: ' + schemaPath)
65
+ return
66
+ }
67
+
68
+ let schemaContent: string
69
+ try {
70
+ schemaContent = readFileSync(schemaPath, 'utf-8')
71
+ } catch (err) {
72
+ console.error('[query-builder] Failed to read schema:', err)
73
+ return
74
+ }
75
+
76
+ const schemaCwd = dirname(resolve(schemaPath))
77
+
78
+ _process = spawn(process.execPath, [cliPath], {
79
+ stdio: 'inherit',
80
+ env: {
81
+ ...env,
82
+ PORT: String(port),
83
+ HOST: host,
84
+ PRISMA_QUERY_BUILDER_MODE: 'embedded',
85
+ DISABLE_PERSISTENCE: 'true',
86
+ PRISMA_QUERY_BUILDER_SCHEMA_CONTENT: schemaContent,
87
+ PRISMA_QUERY_BUILDER_CWD: schemaCwd,
88
+ DATABASE_URL: databaseUrl,
89
+ },
90
+ })
91
+
92
+ _process.on('error', (err) => {
93
+ console.error('[query-builder] Failed to start:', err.message)
94
+ _process = null
95
+ })
96
+
97
+ _process.on('exit', (code) => {
98
+ const wasStopping = _stopping
99
+ _stopping = false
100
+ _process = null
101
+ if (!wasStopping && code !== 0) {
102
+ console.warn('[query-builder] Process exited with code ' + code)
103
+ }
104
+ })
105
+
106
+ if (!_cleanupRegistered) {
107
+ _cleanupRegistered = true
108
+
109
+ process.on('exit', () => {
110
+ stopQueryBuilder()
111
+ })
112
+
113
+ const handleSigint = () => {
114
+ stopQueryBuilder()
115
+ process.removeListener('SIGINT', handleSigint)
116
+ process.kill(process.pid, 'SIGINT')
117
+ }
118
+
119
+ const handleSigterm = () => {
120
+ stopQueryBuilder()
121
+ process.removeListener('SIGTERM', handleSigterm)
122
+ process.kill(process.pid, 'SIGTERM')
123
+ }
124
+
125
+ process.on('SIGINT', handleSigint)
126
+ process.on('SIGTERM', handleSigterm)
127
+ }
128
+
129
+ console.log('[query-builder] Starting on http://' + host + ':' + port)
130
+ }
131
+
132
+ export function stopQueryBuilder(): void {
133
+ if (!_process || _process.killed) return
134
+ _stopping = true
135
+ _process.kill()
136
+ }
137
+ `
138
+ }