prisma-generator-express 1.66.0 → 1.68.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 (130) hide show
  1. package/README.md +188 -3432
  2. package/dist/client/encodeQueryParams.js +3 -1
  3. package/dist/client/encodeQueryParams.js.map +1 -1
  4. package/dist/copy/misc.d.ts +1 -0
  5. package/dist/copy/misc.js +14 -0
  6. package/dist/copy/misc.js.map +1 -1
  7. package/dist/copy/operationDefinitions.d.ts +4 -1
  8. package/dist/copy/operationDefinitions.js +133 -9
  9. package/dist/copy/operationDefinitions.js.map +1 -1
  10. package/dist/generators/generateFastifyHandler.d.ts +2 -2
  11. package/dist/generators/generateFastifyHandler.js +68 -12
  12. package/dist/generators/generateFastifyHandler.js.map +1 -1
  13. package/dist/generators/generateHonoHandler.d.ts +2 -2
  14. package/dist/generators/generateHonoHandler.js +53 -6
  15. package/dist/generators/generateHonoHandler.js.map +1 -1
  16. package/dist/generators/generateHonoOpenApi.d.ts +13 -0
  17. package/dist/generators/generateHonoOpenApi.js +70 -0
  18. package/dist/generators/generateHonoOpenApi.js.map +1 -0
  19. package/dist/generators/generateImportPrismaStatement.d.ts +1 -0
  20. package/dist/generators/generateImportPrismaStatement.js +6 -0
  21. package/dist/generators/generateImportPrismaStatement.js.map +1 -1
  22. package/dist/generators/generateMcpApp.d.ts +10 -0
  23. package/dist/generators/generateMcpApp.js +113 -0
  24. package/dist/generators/generateMcpApp.js.map +1 -0
  25. package/dist/generators/generateModelMcp.d.ts +9 -0
  26. package/dist/generators/generateModelMcp.js +145 -0
  27. package/dist/generators/generateModelMcp.js.map +1 -0
  28. package/dist/generators/generateModelMetadata.js +29 -15
  29. package/dist/generators/generateModelMetadata.js.map +1 -1
  30. package/dist/generators/generateOperationCore.d.ts +3 -3
  31. package/dist/generators/generateOperationCore.js +51 -16
  32. package/dist/generators/generateOperationCore.js.map +1 -1
  33. package/dist/generators/generateRelationMeta.js +2 -2
  34. package/dist/generators/generateRelationMeta.js.map +1 -1
  35. package/dist/generators/generateRouteConfigType.d.ts +2 -2
  36. package/dist/generators/generateRouteConfigType.js +29 -11
  37. package/dist/generators/generateRouteConfigType.js.map +1 -1
  38. package/dist/generators/generateRouter.d.ts +7 -4
  39. package/dist/generators/generateRouter.js +164 -65
  40. package/dist/generators/generateRouter.js.map +1 -1
  41. package/dist/generators/generateRouterFastify.d.ts +7 -4
  42. package/dist/generators/generateRouterFastify.js +106 -86
  43. package/dist/generators/generateRouterFastify.js.map +1 -1
  44. package/dist/generators/generateRouterHono.d.ts +7 -4
  45. package/dist/generators/generateRouterHono.js +59 -414
  46. package/dist/generators/generateRouterHono.js.map +1 -1
  47. package/dist/generators/generateRouterPartsHono.d.ts +14 -0
  48. package/dist/generators/generateRouterPartsHono.js +180 -0
  49. package/dist/generators/generateRouterPartsHono.js.map +1 -0
  50. package/dist/generators/generateUnifiedDocs.d.ts +1 -1
  51. package/dist/generators/generateUnifiedDocs.js +15 -8
  52. package/dist/generators/generateUnifiedDocs.js.map +1 -1
  53. package/dist/generators/generateUnifiedHandler.d.ts +2 -2
  54. package/dist/generators/generateUnifiedHandler.js +42 -18
  55. package/dist/generators/generateUnifiedHandler.js.map +1 -1
  56. package/dist/guardOptions.js +12 -7
  57. package/dist/guardOptions.js.map +1 -1
  58. package/dist/index.d.ts +3 -1
  59. package/dist/index.js +123 -4
  60. package/dist/index.js.map +1 -1
  61. package/dist/utils/copyFiles.d.ts +1 -1
  62. package/dist/utils/copyFiles.js +42 -7
  63. package/dist/utils/copyFiles.js.map +1 -1
  64. package/dist/utils/importExt.d.ts +1 -1
  65. package/dist/utils/mcpGate.d.ts +10 -0
  66. package/dist/utils/mcpGate.js +118 -0
  67. package/dist/utils/mcpGate.js.map +1 -0
  68. package/dist/utils/pathCasing.d.ts +4 -0
  69. package/dist/utils/pathCasing.js +40 -0
  70. package/dist/utils/pathCasing.js.map +1 -0
  71. package/dist/utils/resolveImportStyle.js +13 -5
  72. package/dist/utils/resolveImportStyle.js.map +1 -1
  73. package/dist/utils/writeFileSafely.js +6 -0
  74. package/dist/utils/writeFileSafely.js.map +1 -1
  75. package/package.json +22 -5
  76. package/src/bin.ts +1 -1
  77. package/src/client/encodeQueryParams.ts +39 -35
  78. package/src/constants.ts +1 -1
  79. package/src/copy/autoIncludePlanner.ts +93 -27
  80. package/src/copy/autoIncludePlannerGuarded.ts +171 -48
  81. package/src/copy/autoIncludeRuntimeGuarded.ts +25 -7
  82. package/src/copy/buildModelOpenApi.ts +229 -176
  83. package/src/copy/concurrency.ts +10 -8
  84. package/src/copy/docsRenderer.ts +14 -15
  85. package/src/copy/errorMapper.ts +68 -19
  86. package/src/copy/guardHelpers.ts +30 -9
  87. package/src/copy/materializedCount.ts +17 -5
  88. package/src/copy/mcpRuntime.ts +1017 -0
  89. package/src/copy/misc.ts +88 -68
  90. package/src/copy/ndjson.ts +72 -0
  91. package/src/copy/operationDefinitions.ts +177 -25
  92. package/src/copy/operationPipeline.ts +267 -0
  93. package/src/copy/operationRuntime.ts +124 -26
  94. package/src/copy/operationSchemas.ts +2773 -0
  95. package/src/copy/pagination.ts +35 -13
  96. package/src/copy/parseQueryParams.ts +47 -48
  97. package/src/copy/projectionDefaults.ts +6 -4
  98. package/src/copy/routeConfig.express.ts +16 -4
  99. package/src/copy/routeConfig.fastify.ts +16 -2
  100. package/src/copy/routeConfig.hono.ts +46 -14
  101. package/src/copy/routeConfig.ts +175 -40
  102. package/src/copy/routerParts.ts +524 -0
  103. package/src/copy/scalarTypes.ts +7 -2
  104. package/src/copy/sse.ts +72 -19
  105. package/src/generators/generateFastifyHandler.ts +77 -20
  106. package/src/generators/generateHonoHandler.ts +62 -14
  107. package/src/generators/generateHonoOpenApi.ts +93 -0
  108. package/src/generators/generateImportPrismaStatement.ts +93 -81
  109. package/src/generators/generateMcpApp.ts +133 -0
  110. package/src/generators/generateModelMcp.ts +208 -0
  111. package/src/generators/generateModelMetadata.ts +40 -22
  112. package/src/generators/generateOperationCore.ts +81 -33
  113. package/src/generators/generateRelationMeta.ts +23 -11
  114. package/src/generators/generateRouteConfigType.ts +39 -13
  115. package/src/generators/generateRouter.ts +171 -68
  116. package/src/generators/generateRouterFastify.ts +117 -89
  117. package/src/generators/generateRouterHono.ts +84 -464
  118. package/src/generators/generateRouterPartsHono.ts +220 -0
  119. package/src/generators/generateUnifiedDocs.ts +29 -8
  120. package/src/generators/generateUnifiedHandler.ts +47 -24
  121. package/src/generators/generateUnifiedScalarUI.ts +1 -1
  122. package/src/guardOptions.ts +105 -92
  123. package/src/index.ts +161 -6
  124. package/src/utils/copyFiles.ts +235 -168
  125. package/src/utils/importExt.ts +2 -2
  126. package/src/utils/mcpGate.ts +157 -0
  127. package/src/utils/pathCasing.ts +42 -0
  128. package/src/utils/resolveImportStyle.ts +37 -15
  129. package/src/utils/strings.ts +2 -2
  130. package/src/utils/writeFileSafely.ts +12 -2
@@ -8,13 +8,15 @@ export async function mapLimited<T>(
8
8
  const workerCount = Math.min(limit, items.length)
9
9
  const workers: Promise<void>[] = []
10
10
  for (let w = 0; w < workerCount; w++) {
11
- workers.push((async () => {
12
- for (;;) {
13
- const i = index++
14
- if (i >= items.length) return
15
- await fn(items[i], i)
16
- }
17
- })())
11
+ workers.push(
12
+ (async () => {
13
+ for (;;) {
14
+ const i = index++
15
+ if (i >= items.length) return
16
+ await fn(items[i], i)
17
+ }
18
+ })(),
19
+ )
18
20
  }
19
21
  await Promise.all(workers)
20
- }
22
+ }
@@ -127,7 +127,7 @@ function exampleValue(ctx: DocsModelContext, fieldName: string): unknown {
127
127
 
128
128
  function compoundWhereExample(
129
129
  ctx: DocsModelContext,
130
- ): Record<string, any> | null {
130
+ ): Record<string, unknown> | null {
131
131
  if (ctx.compoundId) {
132
132
  const keyName = ctx.compoundId.fields.join('_')
133
133
  const val: Record<string, unknown> = {}
@@ -426,7 +426,7 @@ export function renderDocs(
426
426
  const singleRelations = relationFields.filter((f) => !f.isList)
427
427
 
428
428
  const getOps = OPERATION_METADATA.filter((meta) =>
429
- isOperationEnabled(config as Record<string, any>, meta),
429
+ isOperationEnabled(config, meta),
430
430
  )
431
431
  .filter((meta) => !isOpHiddenByStrategy(meta.name, writeStrategy))
432
432
  .map((meta) => {
@@ -453,9 +453,7 @@ export function renderDocs(
453
453
 
454
454
  const postReadOps = postReadsEnabled
455
455
  ? OPERATION_METADATA.filter((meta) => READ_OPERATION_NAMES.has(meta.name))
456
- .filter((meta) =>
457
- isOperationEnabled(config as Record<string, any>, meta),
458
- )
456
+ .filter((meta) => isOperationEnabled(config, meta))
459
457
  .map((meta) => {
460
458
  const detail = detailForOp(meta.name)
461
459
  const postPath =
@@ -499,8 +497,8 @@ export function renderDocs(
499
497
  const firstStringField = scalarFields.find((f) => f.type === 'String')
500
498
  const firstBooleanField = scalarFields.find((f) => f.type === 'Boolean')
501
499
 
502
- const whereExample: Record<string, any> = {}
503
- const andClauses: Record<string, any>[] = []
500
+ const whereExample: Record<string, unknown> = {}
501
+ const andClauses: Record<string, unknown>[] = []
504
502
  if (firstFilterFieldName) {
505
503
  andClauses.push({
506
504
  [firstFilterFieldName]: {
@@ -517,19 +515,19 @@ export function renderDocs(
517
515
  if (firstBooleanField)
518
516
  whereExample.OR = [{ [firstBooleanField.name]: { equals: true } }]
519
517
 
520
- const selectExample: any = {}
518
+ const selectExample: Record<string, unknown> = {}
521
519
  for (const f of scalarFields.slice(0, 10)) selectExample[f.name] = true
522
520
 
523
- const includeExample: any = {}
521
+ const includeExample: Record<string, unknown> = {}
524
522
  for (const f of relationFields.slice(0, 6)) includeExample[f.name] = true
525
523
 
526
- const omitExample: any = {}
524
+ const omitExample: Record<string, unknown> = {}
527
525
  const omitCandidates = scalarFields.filter((f) => !f.isId && !f.isUnique)
528
526
  for (const f of omitCandidates.slice(0, 3)) omitExample[f.name] = true
529
527
 
530
528
  const orderByField = firstUnique ? firstUnique.name : firstFilterFieldName
531
529
 
532
- const findManyQueryArgs: any = {
530
+ const findManyQueryArgs: Record<string, unknown> = {
533
531
  where: whereExample,
534
532
  select: selectExample,
535
533
  orderBy: orderByField ? { [orderByField]: 'asc' } : undefined,
@@ -575,7 +573,7 @@ export function renderDocs(
575
573
  'const data = await res.json()'
576
574
  : null
577
575
 
578
- const createBodyExample: any = { data: {} }
576
+ const createBodyExample: { data: Record<string, unknown> } = { data: {} }
579
577
  for (const f of requiredCreateFields.slice(0, 5)) {
580
578
  createBodyExample.data[f.name] = exampleValue(ctx, f.name)
581
579
  }
@@ -592,9 +590,10 @@ export function renderDocs(
592
590
  '})\n' +
593
591
  'const created = await res.json()'
594
592
 
595
- const updateBodyExample: any = uniqueWhereExample
596
- ? { where: uniqueWhereExample, data: {} }
597
- : null
593
+ const updateBodyExample: {
594
+ where: Record<string, unknown>
595
+ data: Record<string, unknown>
596
+ } | null = uniqueWhereExample ? { where: uniqueWhereExample, data: {} } : null
598
597
  if (updateBodyExample) {
599
598
  const firstEditableString = scalarFields.find(
600
599
  (sf) => sf.type === 'String' && !sf.isId,
@@ -36,10 +36,19 @@ const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
36
36
  P2023: { status: 500, message: 'Inconsistent column data' },
37
37
  P2024: { status: 503, message: 'Connection pool timeout' },
38
38
  P2025: { status: 404, message: 'Record not found' },
39
- P2026: { status: 501, message: 'Feature not supported by the current database provider' },
40
- P2027: { status: 400, message: 'Multiple errors occurred during transaction execution' },
39
+ P2026: {
40
+ status: 501,
41
+ message: 'Feature not supported by the current database provider',
42
+ },
43
+ P2027: {
44
+ status: 400,
45
+ message: 'Multiple errors occurred during transaction execution',
46
+ },
41
47
  P2028: { status: 500, message: 'Transaction API error' },
42
- P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
48
+ P2030: {
49
+ status: 400,
50
+ message: 'Cannot find a fulltext index for the search',
51
+ },
43
52
  P2033: { status: 400, message: 'Number out of range for the field type' },
44
53
  P2034: { status: 409, message: 'Transaction conflict, please retry' },
45
54
  }
@@ -59,11 +68,20 @@ function asErrorShape(error: unknown): ErrorShape {
59
68
  }
60
69
 
61
70
  function isProduction(): boolean {
62
- return typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production'
71
+ return (
72
+ typeof process !== 'undefined' &&
73
+ process.env &&
74
+ process.env.NODE_ENV === 'production'
75
+ )
63
76
  }
64
77
 
65
78
  function isHttpStatus(value: unknown): value is number {
66
- return typeof value === 'number' && Number.isInteger(value) && value >= 400 && value <= 599
79
+ return (
80
+ typeof value === 'number' &&
81
+ Number.isInteger(value) &&
82
+ value >= 400 &&
83
+ value <= 599
84
+ )
67
85
  }
68
86
 
69
87
  export function mapError(error: unknown): HttpError {
@@ -77,18 +95,27 @@ export function mapError(error: unknown): HttpError {
77
95
  ? e.statusCode
78
96
  : null
79
97
  if (duckStatus !== null) {
80
- const detail = typeof e.message === 'string' && e.message ? e.message : 'Internal server error'
98
+ const detail =
99
+ typeof e.message === 'string' && e.message
100
+ ? e.message
101
+ : 'Internal server error'
81
102
  return new HttpError(duckStatus, detail)
82
103
  }
83
104
 
84
- if (e.name === 'ShapeError') return new HttpError(400, e.message || 'Shape validation failed')
85
- if (e.name === 'CallerError') return new HttpError(400, e.message || 'Caller validation failed')
86
- if (e.name === 'PolicyError') return new HttpError(403, e.message || 'Policy denied')
105
+ if (e.name === 'ShapeError')
106
+ return new HttpError(400, e.message || 'Shape validation failed')
107
+ if (e.name === 'CallerError')
108
+ return new HttpError(400, e.message || 'Caller validation failed')
109
+ if (e.name === 'PolicyError')
110
+ return new HttpError(403, e.message || 'Policy denied')
87
111
  if (e.name === 'ZodError') {
88
112
  const issues = e.issues
89
113
  const message = Array.isArray(issues)
90
- ? (issues as Array<{ message?: string }>).map((i) => i.message ?? '').filter(Boolean).join('; ')
91
- : (e.message || 'Validation failed')
114
+ ? (issues as Array<{ message?: string }>)
115
+ .map((i) => i.message ?? '')
116
+ .filter(Boolean)
117
+ .join('; ')
118
+ : e.message || 'Validation failed'
92
119
  return new HttpError(400, message)
93
120
  }
94
121
  if (typeof e.code === 'string') {
@@ -98,7 +125,9 @@ export function mapError(error: unknown): HttpError {
98
125
  const shouldStripDetail = isProd && mapped.status >= 500
99
126
  return new HttpError(
100
127
  mapped.status,
101
- !shouldStripDetail && detail ? mapped.message + ': ' + detail : mapped.message,
128
+ !shouldStripDetail && detail
129
+ ? mapped.message + ': ' + detail
130
+ : mapped.message,
102
131
  )
103
132
  }
104
133
  if (e.code.startsWith('P')) {
@@ -108,19 +137,39 @@ export function mapError(error: unknown): HttpError {
108
137
  }
109
138
  }
110
139
  if (typeof e.name === 'string') {
111
- if (e.name === 'PrismaClientValidationError') return new HttpError(400, e.message || 'Invalid query parameters')
112
- if (e.name === 'PrismaClientKnownRequestError') return new HttpError(400, e.message || 'Database request error')
140
+ if (e.name === 'PrismaClientValidationError')
141
+ return new HttpError(400, e.message || 'Invalid query parameters')
142
+ if (e.name === 'PrismaClientKnownRequestError')
143
+ return new HttpError(400, e.message || 'Database request error')
113
144
  if (e.name === 'PrismaClientInitializationError') {
114
- return new HttpError(503, isProd ? 'Service unavailable' : (e.message || 'Database connection failed'))
145
+ return new HttpError(
146
+ 503,
147
+ isProd
148
+ ? 'Service unavailable'
149
+ : e.message || 'Database connection failed',
150
+ )
115
151
  }
116
152
  if (e.name === 'PrismaClientRustPanicError') {
117
- return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Internal database engine error'))
153
+ return new HttpError(
154
+ 500,
155
+ isProd
156
+ ? 'Internal server error'
157
+ : e.message || 'Internal database engine error',
158
+ )
118
159
  }
119
160
  if (e.name === 'PrismaClientUnknownRequestError') {
120
- return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Unknown database error'))
161
+ return new HttpError(
162
+ 500,
163
+ isProd
164
+ ? 'Internal server error'
165
+ : e.message || 'Unknown database error',
166
+ )
121
167
  }
122
168
  }
123
169
  const msg = error instanceof Error ? error.message : String(error)
124
170
  console.error(LOG_PREFIX, 'Unhandled error:', error)
125
- return new HttpError(500, isProd ? 'Internal server error' : (msg || 'Internal server error'))
126
- }
171
+ return new HttpError(
172
+ 500,
173
+ isProd ? 'Internal server error' : msg || 'Internal server error',
174
+ )
175
+ }
@@ -3,12 +3,17 @@ import { HttpError } from './errorMapper'
3
3
  export type PrismaDelegateLike = {
4
4
  count: (args?: unknown) => Promise<unknown>
5
5
  findMany: (args?: unknown) => Promise<unknown>
6
- guard?: (shape: Record<string, unknown>, caller?: string) => PrismaDelegateLike
6
+ guard?: (
7
+ shape: Record<string, unknown>,
8
+ caller?: string,
9
+ ) => PrismaDelegateLike
7
10
  }
8
11
 
9
- export function assertGuard(
10
- delegate: PrismaDelegateLike,
11
- ): asserts delegate is PrismaDelegateLike & { guard: NonNullable<PrismaDelegateLike['guard']> } {
12
+ export function assertGuard<T extends PrismaDelegateLike>(
13
+ delegate: T,
14
+ ): asserts delegate is T & {
15
+ guard: NonNullable<T['guard']>
16
+ } {
12
17
  if (typeof delegate.guard !== 'function') {
13
18
  throw new HttpError(
14
19
  500,
@@ -18,9 +23,24 @@ export function assertGuard(
18
23
  }
19
24
 
20
25
  export const GUARD_SHAPE_CONFIG_KEYS = new Set([
21
- 'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
22
- 'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
23
- '_sum', '_min', '_max', 'by',
26
+ 'data',
27
+ 'create',
28
+ 'update',
29
+ 'where',
30
+ 'include',
31
+ 'select',
32
+ 'orderBy',
33
+ 'cursor',
34
+ 'take',
35
+ 'skip',
36
+ 'distinct',
37
+ 'having',
38
+ '_count',
39
+ '_avg',
40
+ '_sum',
41
+ '_min',
42
+ '_max',
43
+ 'by',
24
44
  ])
25
45
 
26
46
  function keepWhereOnly(obj: Record<string, unknown>): Record<string, unknown> {
@@ -39,7 +59,8 @@ export function buildCountShape(
39
59
  return (...args: unknown[]) => keepWhereOnly(fn(...args))
40
60
  }
41
61
  const keys = Object.keys(shape)
42
- const isSingleShape = keys.length === 0 || keys.every((k) => GUARD_SHAPE_CONFIG_KEYS.has(k))
62
+ const isSingleShape =
63
+ keys.length === 0 || keys.every((k) => GUARD_SHAPE_CONFIG_KEYS.has(k))
43
64
  if (isSingleShape) return keepWhereOnly(shape)
44
65
  const result: Record<string, unknown> = {}
45
66
  for (const [key, variant] of Object.entries(shape)) {
@@ -53,4 +74,4 @@ export function buildCountShape(
53
74
  }
54
75
  }
55
76
  return result
56
- }
77
+ }
@@ -4,7 +4,10 @@ import type { PaginationCountSource } from './routeConfig'
4
4
  export const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
5
5
 
6
6
  type PrismaRawClient = {
7
- $queryRawUnsafe?: <T = unknown>(sql: string, ...values: unknown[]) => Promise<T>
7
+ $queryRawUnsafe?: <T = unknown>(
8
+ sql: string,
9
+ ...values: unknown[]
10
+ ) => Promise<T>
8
11
  }
9
12
 
10
13
  export function quoteIdent(name: string): string {
@@ -47,7 +50,10 @@ export async function countFromMaterializedView(
47
50
  ): Promise<number> {
48
51
  const raw = client as PrismaRawClient
49
52
  if (typeof raw.$queryRawUnsafe !== 'function') {
50
- throw new HttpError(500, 'Materialized count source requires $queryRawUnsafe on the Prisma client')
53
+ throw new HttpError(
54
+ 500,
55
+ 'Materialized count source requires $queryRawUnsafe on the Prisma client',
56
+ )
51
57
  }
52
58
  const column = source.column ?? 'total'
53
59
  const where = buildMaterializedCountWhere(source.where)
@@ -58,11 +64,17 @@ export async function countFromMaterializedView(
58
64
  buildMaterializedCountFqn(source) +
59
65
  where.sql +
60
66
  ' LIMIT 1'
61
- const rows = await raw.$queryRawUnsafe<Array<{ total: unknown }>>(sql, ...where.values)
67
+ const rows = await raw.$queryRawUnsafe<Array<{ total: unknown }>>(
68
+ sql,
69
+ ...where.values,
70
+ )
62
71
  const value = rows[0]?.total
63
72
  const total = Number(value)
64
73
  if (!Number.isFinite(total)) {
65
- throw new HttpError(500, 'Materialized count source did not return a numeric total')
74
+ throw new HttpError(
75
+ 500,
76
+ 'Materialized count source did not return a numeric total',
77
+ )
66
78
  }
67
79
  return Math.trunc(total)
68
- }
80
+ }