@baseplate-dev/fastify-generators 0.6.3 → 0.6.5

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/CHANGELOG.md +30 -0
  2. package/dist/constants/fastify-packages.d.ts +3 -3
  3. package/dist/constants/fastify-packages.js +3 -3
  4. package/dist/generators/auth/auth-roles/auth-roles.generator.d.ts +1 -0
  5. package/dist/generators/auth/auth-roles/auth-roles.generator.d.ts.map +1 -1
  6. package/dist/generators/auth/auth-roles/auth-roles.generator.js +5 -1
  7. package/dist/generators/auth/auth-roles/auth-roles.generator.js.map +1 -1
  8. package/dist/generators/auth/auth-roles/templates/module/constants/auth-roles.constants.ts +4 -3
  9. package/dist/generators/pothos/pothos-prisma-enum/pothos-prisma-enum.generator.d.ts +1 -0
  10. package/dist/generators/pothos/pothos-prisma-enum/pothos-prisma-enum.generator.d.ts.map +1 -1
  11. package/dist/generators/pothos/pothos-prisma-enum/pothos-prisma-enum.generator.js +11 -2
  12. package/dist/generators/pothos/pothos-prisma-enum/pothos-prisma-enum.generator.js.map +1 -1
  13. package/dist/generators/prisma/_shared/build-data-helpers/build-schema-fragments.d.ts +47 -0
  14. package/dist/generators/prisma/_shared/build-data-helpers/build-schema-fragments.d.ts.map +1 -0
  15. package/dist/generators/prisma/_shared/build-data-helpers/build-schema-fragments.js +56 -0
  16. package/dist/generators/prisma/_shared/build-data-helpers/build-schema-fragments.js.map +1 -0
  17. package/dist/generators/prisma/_shared/build-data-helpers/build-transform-operation-parts.d.ts +71 -0
  18. package/dist/generators/prisma/_shared/build-data-helpers/build-transform-operation-parts.d.ts.map +1 -0
  19. package/dist/generators/prisma/_shared/build-data-helpers/build-transform-operation-parts.js +153 -0
  20. package/dist/generators/prisma/_shared/build-data-helpers/build-transform-operation-parts.js.map +1 -0
  21. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorization-statements.d.ts +46 -0
  22. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorization-statements.d.ts.map +1 -0
  23. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorization-statements.js +44 -0
  24. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorization-statements.js.map +1 -0
  25. package/dist/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.d.ts +6 -0
  26. package/dist/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.d.ts.map +1 -1
  27. package/dist/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.js +13 -1
  28. package/dist/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.js.map +1 -1
  29. package/dist/generators/prisma/_shared/build-data-helpers/generate-where-type.d.ts +24 -0
  30. package/dist/generators/prisma/_shared/build-data-helpers/generate-where-type.d.ts.map +1 -0
  31. package/dist/generators/prisma/_shared/build-data-helpers/generate-where-type.js +49 -0
  32. package/dist/generators/prisma/_shared/build-data-helpers/generate-where-type.js.map +1 -0
  33. package/dist/generators/prisma/_shared/field-definition-generators/generate-scalar-input-field.d.ts +1 -4
  34. package/dist/generators/prisma/_shared/field-definition-generators/generate-scalar-input-field.d.ts.map +1 -1
  35. package/dist/generators/prisma/_shared/field-definition-generators/generate-scalar-input-field.js +10 -2
  36. package/dist/generators/prisma/_shared/field-definition-generators/generate-scalar-input-field.js.map +1 -1
  37. package/dist/generators/prisma/_shared/field-definition-generators/types.d.ts +47 -1
  38. package/dist/generators/prisma/_shared/field-definition-generators/types.d.ts.map +1 -1
  39. package/dist/generators/prisma/data-utils/data-utils.generator.d.ts +17 -74
  40. package/dist/generators/prisma/data-utils/data-utils.generator.d.ts.map +1 -1
  41. package/dist/generators/prisma/data-utils/generated/index.d.ts +50 -145
  42. package/dist/generators/prisma/data-utils/generated/index.d.ts.map +1 -1
  43. package/dist/generators/prisma/data-utils/generated/template-paths.d.ts +5 -6
  44. package/dist/generators/prisma/data-utils/generated/template-paths.d.ts.map +1 -1
  45. package/dist/generators/prisma/data-utils/generated/template-paths.js +5 -6
  46. package/dist/generators/prisma/data-utils/generated/template-paths.js.map +1 -1
  47. package/dist/generators/prisma/data-utils/generated/template-renderers.d.ts +0 -21
  48. package/dist/generators/prisma/data-utils/generated/template-renderers.d.ts.map +1 -1
  49. package/dist/generators/prisma/data-utils/generated/template-renderers.js +1 -7
  50. package/dist/generators/prisma/data-utils/generated/template-renderers.js.map +1 -1
  51. package/dist/generators/prisma/data-utils/generated/ts-import-providers.d.ts +51 -159
  52. package/dist/generators/prisma/data-utils/generated/ts-import-providers.d.ts.map +1 -1
  53. package/dist/generators/prisma/data-utils/generated/ts-import-providers.js +32 -64
  54. package/dist/generators/prisma/data-utils/generated/ts-import-providers.js.map +1 -1
  55. package/dist/generators/prisma/data-utils/generated/typed-templates.d.ts +66 -142
  56. package/dist/generators/prisma/data-utils/generated/typed-templates.d.ts.map +1 -1
  57. package/dist/generators/prisma/data-utils/generated/typed-templates.js +47 -98
  58. package/dist/generators/prisma/data-utils/generated/typed-templates.js.map +1 -1
  59. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/define-transformer.ts +130 -0
  60. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/execute-transform-plan.ts +108 -0
  61. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/nested-transformers.ts +364 -0
  62. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/prepare-transformers.ts +73 -0
  63. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/prisma-types.ts +6 -83
  64. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/transformer-types.ts +118 -0
  65. package/dist/generators/prisma/prisma-authorizer-utils/generated/index.d.ts +34 -106
  66. package/dist/generators/prisma/prisma-authorizer-utils/generated/index.d.ts.map +1 -1
  67. package/dist/generators/prisma/prisma-authorizer-utils/generated/template-renderers.d.ts +17 -53
  68. package/dist/generators/prisma/prisma-authorizer-utils/generated/template-renderers.d.ts.map +1 -1
  69. package/dist/generators/prisma/prisma-authorizer-utils/generated/typed-templates.d.ts +34 -106
  70. package/dist/generators/prisma/prisma-authorizer-utils/generated/typed-templates.d.ts.map +1 -1
  71. package/dist/generators/prisma/prisma-authorizer-utils/prisma-authorizer-utils.generator.d.ts +17 -53
  72. package/dist/generators/prisma/prisma-authorizer-utils/prisma-authorizer-utils.generator.d.ts.map +1 -1
  73. package/dist/generators/prisma/prisma-authorizer-utils/templates/src/utils/authorizers.ts +11 -11
  74. package/dist/generators/prisma/prisma-data-create/prisma-data-create.generator.d.ts +39 -49
  75. package/dist/generators/prisma/prisma-data-create/prisma-data-create.generator.d.ts.map +1 -1
  76. package/dist/generators/prisma/prisma-data-create/prisma-data-create.generator.js +82 -45
  77. package/dist/generators/prisma/prisma-data-create/prisma-data-create.generator.js.map +1 -1
  78. package/dist/generators/prisma/prisma-data-delete/prisma-data-delete.generator.d.ts +39 -52
  79. package/dist/generators/prisma/prisma-data-delete/prisma-data-delete.generator.d.ts.map +1 -1
  80. package/dist/generators/prisma/prisma-data-delete/prisma-data-delete.generator.js +48 -32
  81. package/dist/generators/prisma/prisma-data-delete/prisma-data-delete.generator.js.map +1 -1
  82. package/dist/generators/prisma/prisma-data-nested-field/nested-field-writer.d.ts +16 -2
  83. package/dist/generators/prisma/prisma-data-nested-field/nested-field-writer.d.ts.map +1 -1
  84. package/dist/generators/prisma/prisma-data-nested-field/nested-field-writer.js +282 -138
  85. package/dist/generators/prisma/prisma-data-nested-field/nested-field-writer.js.map +1 -1
  86. package/dist/generators/prisma/prisma-data-nested-field/prisma-data-nested-field.generator.d.ts +19 -55
  87. package/dist/generators/prisma/prisma-data-nested-field/prisma-data-nested-field.generator.d.ts.map +1 -1
  88. package/dist/generators/prisma/prisma-data-nested-field/prisma-data-nested-field.generator.js +18 -4
  89. package/dist/generators/prisma/prisma-data-nested-field/prisma-data-nested-field.generator.js.map +1 -1
  90. package/dist/generators/prisma/prisma-data-service/prisma-data-service.generator.d.ts +37 -92
  91. package/dist/generators/prisma/prisma-data-service/prisma-data-service.generator.d.ts.map +1 -1
  92. package/dist/generators/prisma/prisma-data-service/prisma-data-service.generator.js +86 -29
  93. package/dist/generators/prisma/prisma-data-service/prisma-data-service.generator.js.map +1 -1
  94. package/dist/generators/prisma/prisma-data-update/prisma-data-update.generator.d.ts +40 -52
  95. package/dist/generators/prisma/prisma-data-update/prisma-data-update.generator.d.ts.map +1 -1
  96. package/dist/generators/prisma/prisma-data-update/prisma-data-update.generator.js +102 -49
  97. package/dist/generators/prisma/prisma-data-update/prisma-data-update.generator.js.map +1 -1
  98. package/dist/generators/prisma/prisma-query-filter-utils/generated/index.d.ts +51 -159
  99. package/dist/generators/prisma/prisma-query-filter-utils/generated/index.d.ts.map +1 -1
  100. package/dist/generators/prisma/prisma-query-filter-utils/generated/template-renderers.d.ts +17 -53
  101. package/dist/generators/prisma/prisma-query-filter-utils/generated/template-renderers.d.ts.map +1 -1
  102. package/dist/generators/prisma/prisma-query-filter-utils/generated/typed-templates.d.ts +68 -212
  103. package/dist/generators/prisma/prisma-query-filter-utils/generated/typed-templates.d.ts.map +1 -1
  104. package/dist/generators/prisma/prisma-query-filter-utils/prisma-query-filter-utils.generator.d.ts +17 -53
  105. package/dist/generators/prisma/prisma-query-filter-utils/prisma-query-filter-utils.generator.d.ts.map +1 -1
  106. package/dist/generators/prisma/prisma-relation-field/prisma-relation-field.generator.js +1 -1
  107. package/dist/generators/prisma/prisma-relation-field/prisma-relation-field.generator.js.map +1 -1
  108. package/dist/writers/prisma-schema/fields.d.ts +5 -1
  109. package/dist/writers/prisma-schema/fields.d.ts.map +1 -1
  110. package/dist/writers/prisma-schema/fields.js +8 -2
  111. package/dist/writers/prisma-schema/fields.js.map +1 -1
  112. package/package.json +8 -8
  113. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorize-fragment.d.ts +0 -20
  114. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorize-fragment.d.ts.map +0 -1
  115. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorize-fragment.js +0 -28
  116. package/dist/generators/prisma/_shared/build-data-helpers/generate-authorize-fragment.js.map +0 -1
  117. package/dist/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.d.ts +0 -130
  118. package/dist/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.d.ts.map +0 -1
  119. package/dist/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.js +0 -221
  120. package/dist/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.js.map +0 -1
  121. package/dist/generators/prisma/_shared/build-data-helpers/index.d.ts +0 -4
  122. package/dist/generators/prisma/_shared/build-data-helpers/index.d.ts.map +0 -1
  123. package/dist/generators/prisma/_shared/build-data-helpers/index.js +0 -4
  124. package/dist/generators/prisma/_shared/build-data-helpers/index.js.map +0 -1
  125. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/commit-operations.ts +0 -366
  126. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/compose-operations.ts +0 -131
  127. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/field-definitions.ts +0 -777
  128. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/field-utils.ts +0 -201
  129. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/prisma-utils.ts +0 -90
  130. package/dist/generators/prisma/data-utils/templates/src/utils/data-operations/types.ts +0 -721
@@ -1,777 +0,0 @@
1
- // @ts-nocheck
2
-
3
- import type {
4
- CreateInput,
5
- GetPayload,
6
- ModelPropName,
7
- UpdateInput,
8
- WhereInput,
9
- WhereUniqueInput,
10
- } from '$prismaTypes';
11
- import type {
12
- AnyFieldDefinition,
13
- FieldDefinition,
14
- InferFieldsCreateOutput,
15
- InferFieldsUpdateOutput,
16
- InferInput,
17
- InferInputSchema,
18
- OperationContext,
19
- TransactionalOperationContext,
20
- } from '$types';
21
- import type { Payload } from '@prisma/client/runtime/client';
22
- import type { z } from 'zod';
23
-
24
- import {
25
- generateCreateSchema,
26
- invokeHooks,
27
- transformFields,
28
- } from '$fieldUtils';
29
- import { makeGenericPrismaDelegate } from '$prismaUtils';
30
- import { prisma } from '%prismaImports';
31
-
32
- /**
33
- * Create a simple scalar field with validation
34
- *
35
- * This helper creates a field definition that validates input using a Zod schema.
36
- * The validated value is used directly for both create and update operations.
37
- *
38
- * @template TSchema - The Zod schema type for validation
39
- * @param schema - Zod schema for validation
40
- * @returns Scalar field definition that maps validated input directly to Prisma create/update data
41
- *
42
- * @example
43
- * ```typescript
44
- * const fields = {
45
- * title: scalarField(z.string()),
46
- * email: scalarField(z.string().email()),
47
- * age: scalarField(z.number().int().positive()),
48
- * };
49
- * ```
50
- */
51
- export function scalarField<TSchema extends z.ZodType>(
52
- schema: TSchema,
53
- ): FieldDefinition<TSchema, z.output<TSchema>, z.output<TSchema>> {
54
- return {
55
- schema,
56
- processInput: (value) => ({
57
- data: { create: value, update: value },
58
- }),
59
- };
60
- }
61
-
62
- /**
63
- * =========================================
64
- * Nested Field Handlers
65
- * =========================================
66
- */
67
-
68
- /**
69
- * Configuration for a parent model in nested field definitions.
70
- *
71
- * Used to establish the relationship between a parent and child model
72
- * in nested one-to-one and one-to-many field handlers.
73
- *
74
- * @template TModelName - Prisma model name
75
- */
76
- export interface ParentModelConfig<TModelName extends ModelPropName> {
77
- /** Prisma model name of the parent */
78
- model: TModelName;
79
- /** Function to extract unique identifier from parent model instance */
80
- getWhereUnique: (
81
- parentModel: GetPayload<TModelName>,
82
- ) => WhereUniqueInput<TModelName>;
83
- }
84
-
85
- /**
86
- * Creates a parent model configuration for use in nested field definitions.
87
- *
88
- * @template TModelName - Prisma model name
89
- * @param model - Prisma model name
90
- * @param getWhereUnique - Function to extract unique identifier from parent model
91
- * @returns Parent model configuration object
92
- *
93
- * @example
94
- * ```typescript
95
- * const parentModel = createParentModelConfig('user', (user) => ({
96
- * id: user.id,
97
- * }));
98
- * ```
99
- */
100
- export function createParentModelConfig<TModelName extends ModelPropName>(
101
- model: TModelName,
102
- getWhereUnique: (
103
- parentModel: GetPayload<TModelName>,
104
- ) => WhereUniqueInput<TModelName>,
105
- ): ParentModelConfig<TModelName> {
106
- return {
107
- model,
108
- getWhereUnique,
109
- };
110
- }
111
-
112
- type RelationName<TModelName extends ModelPropName> = keyof Payload<
113
- (typeof prisma)[TModelName]
114
- >['objects'];
115
-
116
- /**
117
- * Configuration for defining a nested one-to-one relationship field.
118
- *
119
- * One-to-one fields represent a single related entity that can be created,
120
- * updated, or deleted along with the parent entity. The field handler manages
121
- * the lifecycle of the nested entity automatically.
122
- *
123
- * @template TParentModelName - Parent model name
124
- * @template TModelName - Child model name
125
- * @template TRelationName - Relation field name on the child model
126
- * @template TFields - Field definitions for the nested entity
127
- */
128
- export interface NestedOneToOneFieldConfig<
129
- TParentModelName extends ModelPropName,
130
- TModelName extends ModelPropName,
131
- TRelationName extends RelationName<TModelName>,
132
- TFields extends Record<string, AnyFieldDefinition>,
133
- > {
134
- /**
135
- * Prisma model name of parent model
136
- */
137
- parentModel: ParentModelConfig<TParentModelName>;
138
-
139
- /**
140
- * Prisma model name of the child model
141
- */
142
- model: TModelName;
143
-
144
- /**
145
- * Relation name of the parent model from the child model
146
- */
147
- relationName: TRelationName;
148
-
149
- /**
150
- * Field definitions for the nested entity
151
- */
152
- fields: TFields;
153
-
154
- /**
155
- * Extract where unique from parent model
156
- */
157
- getWhereUnique: (
158
- parentModel: GetPayload<TParentModelName>,
159
- ) => WhereUniqueInput<TModelName>;
160
-
161
- /**
162
- * Transform validated create field data into final Prisma structure
163
- */
164
- buildCreateData: (
165
- data: InferFieldsCreateOutput<TFields> &
166
- Record<TRelationName, { connect: WhereUniqueInput<TParentModelName> }>,
167
- parentModel: GetPayload<TParentModelName>,
168
- ctx: TransactionalOperationContext<
169
- GetPayload<TModelName>,
170
- { hasResult: false }
171
- >,
172
- ) => CreateInput<TModelName> | Promise<CreateInput<TModelName>>;
173
-
174
- /**
175
- * Transform validated update field data into final Prisma structure
176
- */
177
- buildUpdateData: (
178
- data: InferFieldsUpdateOutput<TFields>,
179
- parentModel: GetPayload<TParentModelName>,
180
- ctx: TransactionalOperationContext<
181
- GetPayload<TModelName>,
182
- { hasResult: false }
183
- >,
184
- ) => UpdateInput<TModelName> | Promise<UpdateInput<TModelName>>;
185
- }
186
-
187
- /**
188
- * Create a nested one-to-one relationship field handler
189
- *
190
- * This helper creates a field definition for managing one-to-one nested relationships.
191
- * It handles nested field validation, transformation, and supports both create and update operations.
192
- *
193
- * The nested entity is created/updated via afterExecute hooks, allowing it to reference
194
- * the parent entity after it has been created.
195
- *
196
- * Behavior:
197
- * - **Provided value**: Upserts the nested entity (creates if new, updates if exists)
198
- * - **null**: Deletes the nested entity (update only)
199
- * - **undefined**: No change to nested entity
200
- *
201
- * @param config - Configuration object
202
- * @returns Field definition
203
- *
204
- * @example
205
- * ```typescript
206
- * const fields = {
207
- * userProfile: nestedOneToOneField({
208
- * parentModel: createParentModelConfig('user', (user) => ({ id: user.id })),
209
- * model: 'userProfile',
210
- * relationName: 'user',
211
- * fields: {
212
- * bio: scalarField(z.string()),
213
- * avatar: fileField(avatarFileCategory),
214
- * },
215
- * getWhereUnique: (parent) => ({ userId: parent.id }),
216
- * buildCreateData: (data) => ({
217
- * bio: data.bio,
218
- * avatar: data.avatar ? { connect: { id: data.avatar } } : undefined,
219
- * }),
220
- * buildUpdateData: (data) => ({
221
- * bio: data.bio,
222
- * avatar: data.avatar ? { connect: { id: data.avatar } } : undefined,
223
- * }),
224
- * }),
225
- * };
226
- * ```
227
- */
228
- export function nestedOneToOneField<
229
- TParentModelName extends ModelPropName,
230
- TModelName extends ModelPropName,
231
- TRelationName extends RelationName<TModelName>,
232
- TFields extends Record<string, AnyFieldDefinition>,
233
- >(
234
- config: NestedOneToOneFieldConfig<
235
- TParentModelName,
236
- TModelName,
237
- TRelationName,
238
- TFields
239
- >,
240
- ): FieldDefinition<
241
- z.ZodOptional<z.ZodNullable<InferInputSchema<TFields>>>,
242
- undefined,
243
- undefined
244
- > {
245
- return {
246
- schema: generateCreateSchema(config.fields).nullish(),
247
- processInput: async (value, processCtx) => {
248
- // Handle null - delete the relation if it exists
249
- if (value === null) {
250
- return {
251
- data: {
252
- create: undefined,
253
- update: undefined,
254
- },
255
- hooks: {
256
- afterExecute: [
257
- async (ctx) => {
258
- const whereUnique = config.getWhereUnique(
259
- ctx.result as GetPayload<TParentModelName>,
260
- );
261
- const prismaDelegate = makeGenericPrismaDelegate(
262
- ctx.tx,
263
- config.model,
264
- );
265
- // Use deleteMany which is idempotent - won't error if no record exists
266
- await prismaDelegate.deleteMany({
267
- where: expandWhereUnique(whereUnique),
268
- });
269
- },
270
- ],
271
- },
272
- };
273
- }
274
-
275
- // Handle undefined - no change
276
- if (value === undefined) {
277
- return { data: { create: undefined, update: undefined } };
278
- }
279
-
280
- let cachedExisting: GetPayload<TModelName> | undefined;
281
- async function loadExisting(): Promise<
282
- GetPayload<TModelName> | undefined
283
- > {
284
- if (cachedExisting) return cachedExisting;
285
- const existingParent = await processCtx.loadExisting();
286
- if (!existingParent) return undefined;
287
- const whereUnique = config.getWhereUnique(
288
- existingParent as GetPayload<TParentModelName>,
289
- );
290
- const prismaDelegate = makeGenericPrismaDelegate(prisma, config.model);
291
- cachedExisting =
292
- ((await prismaDelegate.findUnique({
293
- where: whereUnique,
294
- })) as GetPayload<TModelName> | null) ?? undefined;
295
- return cachedExisting;
296
- }
297
-
298
- // Process nested fields
299
- const { data, hooks } = await transformFields(config.fields, value, {
300
- serviceContext: processCtx.serviceContext,
301
- operation: 'upsert',
302
- allowOptionalFields: false,
303
- loadExisting: loadExisting as () => Promise<object | undefined>,
304
- });
305
-
306
- let newModelResult: GetPayload<TModelName> | undefined;
307
-
308
- return {
309
- data: {},
310
- hooks: {
311
- beforeExecute: [
312
- (ctx) => invokeHooks(hooks.beforeExecute, { ...ctx, loadExisting }),
313
- ],
314
- afterExecute: [
315
- async (ctx) => {
316
- const whereUnique = config.getWhereUnique(
317
- ctx.result as GetPayload<TParentModelName>,
318
- );
319
- const parentWhereUnique = config.parentModel.getWhereUnique(
320
- ctx.result as GetPayload<TParentModelName>,
321
- );
322
- const sharedCtx = {
323
- ...ctx,
324
- operation: 'upsert' as const,
325
- loadExisting,
326
- };
327
- const [builtCreate, builtUpdate] = await Promise.all([
328
- config.buildCreateData(
329
- {
330
- ...data.create,
331
- ...({
332
- [config.relationName]: { connect: parentWhereUnique },
333
- } as Record<
334
- TRelationName,
335
- { connect: WhereUniqueInput<TParentModelName> }
336
- >),
337
- },
338
- ctx.result as GetPayload<TParentModelName>,
339
- sharedCtx,
340
- ),
341
- config.buildUpdateData(
342
- data.update,
343
- ctx.result as GetPayload<TParentModelName>,
344
- sharedCtx,
345
- ),
346
- ]);
347
- const prismaDelegate = makeGenericPrismaDelegate(
348
- ctx.tx,
349
- config.model,
350
- );
351
-
352
- newModelResult = await prismaDelegate.upsert({
353
- where: whereUnique,
354
- create: builtCreate,
355
- update: builtUpdate,
356
- });
357
-
358
- await invokeHooks(hooks.afterExecute, {
359
- ...ctx,
360
- loadExisting,
361
- result: newModelResult,
362
- });
363
- },
364
- ],
365
- afterCommit: [
366
- async (ctx) => {
367
- await invokeHooks(hooks.afterCommit, {
368
- ...ctx,
369
- loadExisting,
370
- result: newModelResult,
371
- });
372
- },
373
- ],
374
- },
375
- };
376
- },
377
- };
378
- }
379
-
380
- /**
381
- * Configuration for defining a nested one-to-many relationship field.
382
- *
383
- * One-to-many fields represent a collection of related entities that are synchronized
384
- * with the input array. The handler automatically:
385
- * - Creates new items without unique identifiers
386
- * - Updates existing items with unique identifiers
387
- * - Deletes items not present in the input array
388
- *
389
- * @template TParentModelName - Parent model name
390
- * @template TModelName - Child model name
391
- * @template TRelationName - Relation field name on the child model
392
- * @template TFields - Field definitions for each item in the collection
393
- */
394
- export interface NestedOneToManyFieldConfig<
395
- TParentModelName extends ModelPropName,
396
- TModelName extends ModelPropName,
397
- TRelationName extends RelationName<TModelName>,
398
- TFields extends Record<string, AnyFieldDefinition>,
399
- > {
400
- /**
401
- * Prisma model name of parent model
402
- */
403
- parentModel: ParentModelConfig<TParentModelName>;
404
-
405
- /**
406
- * Prisma model name of the child model
407
- */
408
- model: TModelName;
409
-
410
- /**
411
- * Relation name of the parent model from the child model
412
- */
413
- relationName: TRelationName;
414
-
415
- /**
416
- * Field definitions for the nested entity
417
- */
418
- fields: TFields;
419
-
420
- /**
421
- * Function to extract a unique where clause from the input data for a child item and
422
- * the parent model.
423
- * If it returns undefined, the item is considered a new item to be created.
424
- */
425
- getWhereUnique: (
426
- input: InferInput<TFields>,
427
- originalModel: GetPayload<TParentModelName>,
428
- ) => WhereUniqueInput<TModelName> | undefined;
429
-
430
- /**
431
- * Transform validated create field data into final Prisma structure for a single item.
432
- * The parent relation connect is included in the data parameter. You can pass it
433
- * through directly (e.g., `(data) => data`) or restructure as needed.
434
- */
435
- buildCreateData: (
436
- data: InferFieldsCreateOutput<TFields> &
437
- Record<TRelationName, { connect: WhereUniqueInput<TParentModelName> }>,
438
- parentModel: GetPayload<TParentModelName>,
439
- ctx: TransactionalOperationContext<
440
- GetPayload<TModelName> | undefined,
441
- { hasResult: false }
442
- >,
443
- ) => CreateInput<TModelName> | Promise<CreateInput<TModelName>>;
444
-
445
- /**
446
- * Transform validated update field data into final Prisma structure for a single item.
447
- */
448
- buildUpdateData: (
449
- data: InferFieldsUpdateOutput<TFields>,
450
- parentModel: GetPayload<TParentModelName>,
451
- ctx: TransactionalOperationContext<
452
- GetPayload<TModelName> | undefined,
453
- { hasResult: false }
454
- >,
455
- ) => UpdateInput<TModelName> | Promise<UpdateInput<TModelName>>;
456
- }
457
-
458
- /**
459
- * Converts a Prisma `WhereUniqueInput` into a plain `WhereInput`.
460
- *
461
- * Compound unique constraints arrive as synthetic keys (e.g., `userId_role: { userId, role }`),
462
- * while generic `where` filters need the flattened field structure. This normalization allows
463
- * composing unique constraints with parent-level filters when constructing delete conditions
464
- * in one-to-many relationships.
465
- *
466
- * @template TModelName - Prisma model name
467
- * @param whereUnique - Unique filter returned by `getWhereUnique`, or undefined for new items
468
- * @returns Normalized where filter or undefined if no usable fields exist
469
- *
470
- * @internal This function is used internally by nestedOneToManyField
471
- */
472
- function expandWhereUnique<TModelName extends ModelPropName>(
473
- whereUnique: WhereUniqueInput<TModelName> | undefined,
474
- ): WhereInput<TModelName> | undefined {
475
- if (!whereUnique) return undefined;
476
-
477
- const entries = Object.entries(whereUnique as Record<string, unknown>).filter(
478
- ([, value]) => value !== undefined && value !== null,
479
- );
480
-
481
- if (entries.length === 0) return undefined;
482
-
483
- const [[key, value]] = entries;
484
-
485
- if (typeof value === 'object' && !Array.isArray(value)) {
486
- return value as WhereInput<TModelName>;
487
- }
488
-
489
- return { [key]: value as unknown } as WhereInput<TModelName>;
490
- }
491
-
492
- /**
493
- * Creates a nested one-to-many relationship field handler.
494
- *
495
- * This helper manages collections of child entities by synchronizing them with the input array.
496
- * The synchronization logic:
497
- * - **Update**: Items with unique identifiers (from `getWhereUnique`) are updated
498
- * - **Create**: Items without unique identifiers are created as new records
499
- * - **Delete**: Existing items not present in the input array are removed
500
- * - **No Change**: Passing `undefined` leaves the collection unchanged
501
- *
502
- * **Important:** When a value is provided (not `undefined`), the input array is treated
503
- * as the complete collection. Existing items not present in the array will be deleted.
504
- *
505
- * All operations are performed atomically within the parent operation's transaction,
506
- * ensuring data consistency even if the operation fails.
507
- *
508
- * @template TParentModelName - Parent model name
509
- * @template TModelName - Child model name
510
- * @template TRelationName - Relation field name on child model
511
- * @template TFields - Field definitions for each child item
512
- * @param config - Configuration object for the one-to-many relationship
513
- * @returns Field definition for use with `composeCreate`/`composeUpdate` and `commitCreate`/`commitUpdate`
514
- *
515
- * @example
516
- * ```typescript
517
- * const fields = {
518
- * images: nestedOneToManyField({
519
- * parentModel: createParentModelConfig('user', (user) => ({ id: user.id })),
520
- * model: 'userImage',
521
- * relationName: 'user',
522
- * fields: {
523
- * id: scalarField(z.string()),
524
- * caption: scalarField(z.string()),
525
- * },
526
- * getWhereUnique: (input) => input.id ? { id: input.id } : undefined,
527
- * buildCreateData: (data) => ({ caption: data.caption }),
528
- * buildUpdateData: (data) => ({ caption: data.caption }),
529
- * }),
530
- * };
531
- *
532
- * // Create user with images
533
- * await createUser({
534
- * data: {
535
- * name: 'John',
536
- * images: [
537
- * { caption: 'First image' },
538
- * { caption: 'Second image' },
539
- * ],
540
- * },
541
- * context: ctx,
542
- * });
543
- *
544
- * // Update user images (creates new, updates existing, deletes removed)
545
- * await updateUser({
546
- * where: { id: userId },
547
- * data: {
548
- * images: [
549
- * { id: 'img-1', caption: 'Updated caption' }, // Updates existing
550
- * { caption: 'New image' }, // Creates new
551
- * // img-2 not in array, will be deleted
552
- * ],
553
- * },
554
- * context: ctx,
555
- * });
556
- * ```
557
- */
558
- export function nestedOneToManyField<
559
- TParentModelName extends ModelPropName,
560
- TModelName extends ModelPropName,
561
- TRelationName extends RelationName<TModelName>,
562
- TFields extends Record<string, AnyFieldDefinition>,
563
- >(
564
- config: NestedOneToManyFieldConfig<
565
- TParentModelName,
566
- TModelName,
567
- TRelationName,
568
- TFields
569
- >,
570
- ): FieldDefinition<
571
- z.ZodOptional<z.ZodArray<InferInputSchema<TFields>>>,
572
- undefined,
573
- undefined | { deleteMany: Record<never, never> }
574
- > {
575
- const getWhereUnique = (
576
- input: InferInput<TFields>,
577
- originalModel: GetPayload<TParentModelName>,
578
- ): WhereUniqueInput<TModelName> | undefined => {
579
- const whereUnique = config.getWhereUnique(input, originalModel);
580
- if (whereUnique && Object.values(whereUnique).includes(undefined)) {
581
- throw new Error(
582
- 'getWhereUnique cannot return any undefined values in the object',
583
- );
584
- }
585
- return whereUnique;
586
- };
587
-
588
- return {
589
- schema: generateCreateSchema(config.fields).array().optional(),
590
- processInput: async (value, processCtx) => {
591
- const { serviceContext, loadExisting } = processCtx;
592
-
593
- if (value === undefined) {
594
- return { data: { create: undefined, update: undefined } };
595
- }
596
-
597
- const existingModel = (await loadExisting()) as
598
- | GetPayload<TParentModelName>
599
- | undefined;
600
-
601
- // Filter objects that relate to parent model only
602
- const whereFromOriginalModel = existingModel && {
603
- [config.relationName]: expandWhereUnique(
604
- config.parentModel.getWhereUnique(existingModel),
605
- ),
606
- };
607
- // Handle list of items
608
- const delegate = makeGenericPrismaDelegate(prisma, config.model);
609
-
610
- const cachedLoadExisting = value.map((itemInput) => {
611
- let cachedExisting: GetPayload<TModelName> | undefined;
612
- const whereUnique =
613
- existingModel && getWhereUnique(itemInput, existingModel);
614
-
615
- return async (): Promise<GetPayload<TModelName> | undefined> => {
616
- if (cachedExisting) return cachedExisting;
617
- if (!whereUnique) return undefined;
618
- cachedExisting =
619
- ((await delegate.findUnique({
620
- where: { ...whereUnique, ...whereFromOriginalModel },
621
- })) as GetPayload<TModelName> | null) ?? undefined;
622
- return cachedExisting;
623
- };
624
- });
625
-
626
- const processedItems = await Promise.all(
627
- value.map(async (itemInput, idx) => {
628
- const whereUnique =
629
- existingModel && getWhereUnique(itemInput, existingModel);
630
-
631
- const { data, hooks } = await transformFields(
632
- config.fields,
633
- itemInput,
634
- {
635
- serviceContext,
636
- operation: 'upsert',
637
- allowOptionalFields: false,
638
- loadExisting: cachedLoadExisting[idx] as () => Promise<
639
- object | undefined
640
- >,
641
- },
642
- );
643
-
644
- return { whereUnique, data, hooks };
645
- }),
646
- );
647
-
648
- const beforeExecuteHook = async (
649
- ctx: TransactionalOperationContext<
650
- GetPayload<TParentModelName>,
651
- { hasResult: false }
652
- >,
653
- ): Promise<void> => {
654
- await Promise.all(
655
- processedItems.map((item, idx) =>
656
- invokeHooks(item.hooks.beforeExecute, {
657
- ...ctx,
658
- loadExisting: cachedLoadExisting[idx],
659
- }),
660
- ),
661
- );
662
- };
663
-
664
- const results: (GetPayload<TModelName> | undefined)[] = Array.from(
665
- { length: value.length },
666
- () => undefined,
667
- );
668
- const afterExecuteHook = async (
669
- ctx: TransactionalOperationContext<
670
- GetPayload<TParentModelName>,
671
- { hasResult: true }
672
- >,
673
- ): Promise<void> => {
674
- const prismaDelegate = makeGenericPrismaDelegate(ctx.tx, config.model);
675
-
676
- // Delete items not in the input
677
- if (whereFromOriginalModel) {
678
- const keepFilters = processedItems
679
- .map((item) => expandWhereUnique(item.whereUnique))
680
- .filter(
681
- (where): where is WhereInput<TModelName> => where !== undefined,
682
- )
683
- .map((where) => ({ NOT: where }));
684
-
685
- const deleteWhere =
686
- keepFilters.length === 0
687
- ? whereFromOriginalModel
688
- : ({
689
- AND: [whereFromOriginalModel, ...keepFilters],
690
- } as WhereInput<TModelName>);
691
-
692
- await prismaDelegate.deleteMany({ where: deleteWhere });
693
- }
694
-
695
- // Upsert items
696
- await Promise.all(
697
- processedItems.map(async (item, idx) => {
698
- const parentWhereUnique = config.parentModel.getWhereUnique(
699
- ctx.result,
700
- );
701
-
702
- const sharedCtx: TransactionalOperationContext<
703
- GetPayload<TModelName> | undefined,
704
- { hasResult: false }
705
- > = {
706
- ...ctx,
707
- operation: item.whereUnique ? 'update' : 'create',
708
- loadExisting: cachedLoadExisting[idx],
709
- result: undefined,
710
- };
711
- const [builtCreate, builtUpdate] = await Promise.all([
712
- config.buildCreateData(
713
- {
714
- ...item.data.create,
715
- ...({
716
- [config.relationName]: { connect: parentWhereUnique },
717
- } as Record<
718
- TRelationName,
719
- { connect: WhereUniqueInput<TParentModelName> }
720
- >),
721
- },
722
- ctx.result,
723
- sharedCtx,
724
- ),
725
- config.buildUpdateData(item.data.update, ctx.result, sharedCtx),
726
- ]);
727
-
728
- results[idx] = item.whereUnique
729
- ? await prismaDelegate.upsert({
730
- where: {
731
- ...item.whereUnique,
732
- ...whereFromOriginalModel,
733
- } as WhereUniqueInput<TModelName>,
734
- create: builtCreate,
735
- update: builtUpdate,
736
- })
737
- : await prismaDelegate.create({
738
- data: builtCreate,
739
- });
740
-
741
- await invokeHooks(item.hooks.afterExecute, {
742
- ...ctx,
743
- result: results[idx],
744
- loadExisting: cachedLoadExisting[idx],
745
- });
746
- }),
747
- );
748
- };
749
-
750
- const afterCommitHook = async (
751
- ctx: OperationContext<
752
- GetPayload<TParentModelName>,
753
- { hasResult: true }
754
- >,
755
- ): Promise<void> => {
756
- await Promise.all(
757
- processedItems.map((item, idx) =>
758
- invokeHooks(item.hooks.afterCommit, {
759
- ...ctx,
760
- result: results[idx],
761
- loadExisting: cachedLoadExisting[idx],
762
- }),
763
- ),
764
- );
765
- };
766
-
767
- return {
768
- data: {},
769
- hooks: {
770
- beforeExecute: [beforeExecuteHook],
771
- afterExecute: [afterExecuteHook],
772
- afterCommit: [afterCommitHook],
773
- },
774
- };
775
- },
776
- };
777
- }