prisma-prefixed-ids 1.4.0 → 1.5.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
@@ -19,7 +19,7 @@ import { type Prisma, PrismaClient } from "@prisma/client";
19
19
  import { extendPrismaClient } from 'prisma-prefixed-ids';
20
20
 
21
21
  type ModelName = Prisma.ModelName;
22
- // NOTE: is your Prisma.ModelName is not available in your setup,
22
+ // NOTE: if your Prisma.ModelName is not available in your setup,
23
23
  // simply use the following instead:
24
24
  // type ModelName = string;
25
25
 
@@ -49,6 +49,179 @@ const organization = await extendedPrisma.organization.create({
49
49
  console.log(organization.id); // e.g., 'org_abc123...'
50
50
  ```
51
51
 
52
+ ## Nested Writes Support (v1.5.0+)
53
+
54
+ Since version 1.5.0, this package fully supports **nested writes** with automatic ID generation for all related records. This includes complex relationship operations like:
55
+
56
+ ### Basic Nested Creates
57
+
58
+ ```typescript
59
+ // Create a user with nested posts
60
+ const userWithPosts = await extendedPrisma.user.create({
61
+ data: {
62
+ name: 'John Doe',
63
+ email: 'john@example.com',
64
+ posts: {
65
+ create: [
66
+ {
67
+ title: 'My First Post',
68
+ content: 'Hello world!',
69
+ published: true,
70
+ },
71
+ {
72
+ title: 'Draft Post',
73
+ content: 'Work in progress...',
74
+ published: false,
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ include: { posts: true },
80
+ });
81
+
82
+ // Result:
83
+ // - User gets ID: usr_abc123...
84
+ // - Posts get IDs: pst_def456..., pst_ghi789...
85
+ ```
86
+
87
+ ### Deep Nested Writes
88
+
89
+ ```typescript
90
+ // Create deeply nested structures with automatic ID generation
91
+ const complexUser = await extendedPrisma.user.create({
92
+ data: {
93
+ name: 'Jane Smith',
94
+ email: 'jane@example.com',
95
+ posts: {
96
+ create: {
97
+ title: 'Post with Categories and Comments',
98
+ content: 'A comprehensive post...',
99
+ published: true,
100
+ categories: {
101
+ create: {
102
+ name: 'Technology',
103
+ description: 'Tech-related posts',
104
+ },
105
+ },
106
+ comments: {
107
+ createMany: {
108
+ data: [
109
+ { content: 'Great post!', authorName: 'Reader 1' },
110
+ { content: 'Very informative', authorName: 'Reader 2' },
111
+ ],
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ include: {
118
+ posts: {
119
+ include: {
120
+ categories: true,
121
+ comments: true,
122
+ },
123
+ },
124
+ },
125
+ });
126
+
127
+ // All related records automatically get prefixed IDs:
128
+ // - User: usr_...
129
+ // - Post: pst_...
130
+ // - Category: cat_...
131
+ // - Comments: cmt_..., cmt_...
132
+ ```
133
+
134
+ ### Update Operations with Nested Creates
135
+
136
+ ```typescript
137
+ // Update existing records and create new related records
138
+ const updatedUser = await extendedPrisma.user.update({
139
+ where: { id: 'usr_existing123' },
140
+ data: {
141
+ name: 'Updated Name',
142
+ posts: {
143
+ create: [
144
+ {
145
+ title: 'New Post After Update',
146
+ content: 'Added after user update',
147
+ },
148
+ ],
149
+ },
150
+ },
151
+ include: { posts: true },
152
+ });
153
+
154
+ // Existing user keeps original ID, new posts get fresh prefixed IDs
155
+ ```
156
+
157
+ ### Upsert Operations
158
+
159
+ ```typescript
160
+ // Upsert with nested creates
161
+ const upsertedUser = await extendedPrisma.user.upsert({
162
+ where: { email: 'maybe@example.com' },
163
+ create: {
164
+ name: 'New User',
165
+ email: 'maybe@example.com',
166
+ posts: {
167
+ create: {
168
+ title: 'First Post',
169
+ content: 'Created during upsert',
170
+ },
171
+ },
172
+ },
173
+ update: {
174
+ name: 'Updated Existing User',
175
+ },
176
+ include: { posts: true },
177
+ });
178
+
179
+ // IDs are generated only for the create branch if record doesn't exist
180
+ ```
181
+
182
+ ### ConnectOrCreate Operations
183
+
184
+ ```typescript
185
+ // Connect to existing or create new with automatic ID generation
186
+ const postWithCategories = await extendedPrisma.post.create({
187
+ data: {
188
+ title: 'Post with Mixed Categories',
189
+ content: 'Some categories exist, others will be created',
190
+ categories: {
191
+ connectOrCreate: [
192
+ {
193
+ where: { name: 'Existing Category' },
194
+ create: { name: 'Should not be created' },
195
+ },
196
+ {
197
+ where: { name: 'New Category' },
198
+ create: {
199
+ name: 'New Category',
200
+ description: 'Freshly created category',
201
+ },
202
+ },
203
+ ],
204
+ },
205
+ },
206
+ include: { categories: true },
207
+ });
208
+
209
+ // New categories get prefixed IDs, existing ones are connected as-is
210
+ ```
211
+
212
+ ### Supported Nested Operations
213
+
214
+ The extension supports all Prisma nested write operations:
215
+
216
+ - ✅ **`create`** - Single nested record creation
217
+ - ✅ **`createMany`** - Multiple nested records creation
218
+ - ✅ **`connectOrCreate`** - Connect existing or create new records
219
+ - ✅ **`upsert`** - Update existing or create new records
220
+ - ✅ **Deeply nested structures** - Multiple levels of relationships
221
+ - ✅ **Mixed operations** - Combining create, connect, disconnect in single query
222
+
223
+ All nested records that are created (not connected to existing ones) will automatically receive prefixed IDs according to your configuration.
224
+
52
225
  ## Custom ID Generation
53
226
 
54
227
  You can customize how IDs are generated by providing your own ID generator function:
@@ -118,9 +291,30 @@ This package uses [nanoid](https://github.com/ai/nanoid) for ID generation inste
118
291
  5. **Better Performance**: nanoid is optimized for performance and generates IDs faster than UUID v4.
119
292
 
120
293
  For example, with a 24-character nanoid:
121
- - The chance of a collision is approximately 1 in 2^128 (same as UUID v4)
122
- - The ID length is 24 characters + prefix length (e.g., `usr_abc123...`)
123
- - The alphabet includes 36 characters (0-9, a-z), making it both readable and compact
294
+ - The chance of a collision is approximately 1 in 2^142 (even better than UUID v4's 2^122)
295
+ - The ID length is 24 characters + prefix length (e.g., `usr_abc123DEF...`)
296
+ - The alphabet includes 62 characters (0-9, a-z, A-Z), providing high entropy while remaining readable
297
+
298
+ ## Development
299
+
300
+ ### Running Tests
301
+
302
+ This project includes comprehensive unit and integration tests:
303
+
304
+ ```bash
305
+ # Run all tests
306
+ npm test
307
+
308
+ # Run only unit tests (fast, no database)
309
+ npm run test:unit
310
+
311
+ # Run only integration tests (uses real SQLite database)
312
+ npm run test:integration
313
+ ```
314
+
315
+ The integration tests automatically download the Prisma query engine and set up a SQLite database as needed. No additional setup is required.
316
+
317
+ For more detailed development information, see [DEVELOPMENT.md](./DEVELOPMENT.md).
124
318
 
125
319
  ## License
126
320
 
package/dist/index.cjs CHANGED
@@ -1,13 +1,175 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processNestedData = exports.findRelationModel = void 0;
3
4
  exports.createPrefixedIdsExtension = createPrefixedIdsExtension;
4
5
  exports.extendPrismaClient = extendPrismaClient;
6
+ exports.getDMMF = getDMMF;
7
+ exports.getModelNames = getModelNames;
5
8
  const nanoid_1 = require("nanoid");
6
9
  const defaultIdGenerator = (prefix) => {
7
10
  const nanoid = (0, nanoid_1.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 24);
8
11
  return `${prefix}_${nanoid()}`;
9
12
  };
10
- function createPrefixedIdsExtension(config) {
13
+ // All possible relation operations in Prisma
14
+ const RELATION_OPERATIONS = [
15
+ "create",
16
+ "createMany",
17
+ "connectOrCreate",
18
+ "upsert",
19
+ "update",
20
+ "updateMany",
21
+ ];
22
+ // Helper to find the relation model from DMMF
23
+ const findRelationModel = (dmmf, parentModel, fieldName) => {
24
+ // Find the model in DMMF
25
+ const model = dmmf.datamodel.models.find((m) => m.name === parentModel);
26
+ if (!model) {
27
+ return null;
28
+ }
29
+ // Find the field that matches the relation name
30
+ const field = model.fields.find((f) => f.name === fieldName);
31
+ if (!field || field.kind !== "object") {
32
+ return null;
33
+ }
34
+ // Return the related model name
35
+ return field.type;
36
+ };
37
+ exports.findRelationModel = findRelationModel;
38
+ // Helper function to check if key is a relation operation
39
+ const isRelationOperation = (key) => {
40
+ return RELATION_OPERATIONS.includes(key);
41
+ };
42
+ // Helper function to process nested data with proper model detection
43
+ const processNestedData = (data, model, prefixedId, dmmf, shouldAddRootId = true) => {
44
+ if (!data) {
45
+ return data;
46
+ }
47
+ // Handle array of items
48
+ if (Array.isArray(data)) {
49
+ return data.map((item) => (0, exports.processNestedData)(item, model, prefixedId, dmmf, shouldAddRootId));
50
+ }
51
+ // Handle object
52
+ if (typeof data === "object") {
53
+ const result = { ...data };
54
+ // Generate ID for the current model if needed (for nested creates)
55
+ if (shouldAddRootId && !result.id) {
56
+ const id = prefixedId(model);
57
+ if (id) {
58
+ result.id = id;
59
+ }
60
+ }
61
+ // Process nested relations by checking each key in the object
62
+ for (const [key, value] of Object.entries(result)) {
63
+ if (!value || typeof value !== "object")
64
+ continue;
65
+ // CASE 1: Key itself is a relation operation (root level operation)
66
+ if (isRelationOperation(key)) {
67
+ // Find which field this operation belongs to by looking at non-operation keys
68
+ const relationFields = Object.keys(result).filter((k) => !isRelationOperation(k));
69
+ for (const relationField of relationFields) {
70
+ const relatedModel = (0, exports.findRelationModel)(dmmf, model, relationField);
71
+ if (relatedModel) {
72
+ if (key === "createMany" && "data" in value) {
73
+ // Handle createMany operation
74
+ const createManyOp = value;
75
+ result[key] = {
76
+ ...value,
77
+ data: (0, exports.processNestedData)(createManyOp.data, relatedModel, prefixedId, dmmf),
78
+ };
79
+ }
80
+ else if (key === "upsert") {
81
+ // Handle upsert operation (has create and update)
82
+ result[key] = {
83
+ ...value,
84
+ create: value.create
85
+ ? (0, exports.processNestedData)(value.create, relatedModel, prefixedId, dmmf)
86
+ : value.create,
87
+ update: value.update,
88
+ };
89
+ }
90
+ else if (key === "connectOrCreate") {
91
+ // Handle connectOrCreate operation
92
+ result[key] = {
93
+ ...value,
94
+ create: value.create
95
+ ? (0, exports.processNestedData)(value.create, relatedModel, prefixedId, dmmf)
96
+ : value.create,
97
+ };
98
+ }
99
+ else if (key === "create" || key === "createMany") {
100
+ // Only process create operations with ID generation
101
+ result[key] = (0, exports.processNestedData)(value, relatedModel, prefixedId, dmmf);
102
+ }
103
+ else {
104
+ // For other operations like update, just pass through the value
105
+ result[key] = value;
106
+ }
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ // CASE 2: Key might be a relation field that contains operations
112
+ else {
113
+ const relatedModel = (0, exports.findRelationModel)(dmmf, model, key);
114
+ if (!relatedModel)
115
+ continue;
116
+ // Process all possible operations in this relation field
117
+ const updatedValue = { ...value };
118
+ // Process each operation type
119
+ for (const op of RELATION_OPERATIONS) {
120
+ if (!(op in value))
121
+ continue;
122
+ if (op === "createMany" && "data" in value[op]) {
123
+ updatedValue[op] = {
124
+ ...value[op],
125
+ data: (0, exports.processNestedData)(value[op].data, relatedModel, prefixedId, dmmf),
126
+ };
127
+ }
128
+ else if (op === "upsert") {
129
+ updatedValue[op] = {
130
+ ...value[op],
131
+ create: value[op].create
132
+ ? (0, exports.processNestedData)(value[op].create, relatedModel, prefixedId, dmmf)
133
+ : value[op].create,
134
+ update: value[op].update, // Don't process update
135
+ };
136
+ }
137
+ else if (op === "connectOrCreate") {
138
+ // Special handling for connectOrCreate - it's an array where each item has where/create
139
+ if (Array.isArray(value[op])) {
140
+ updatedValue[op] = value[op].map((connectOrCreateItem) => ({
141
+ ...connectOrCreateItem,
142
+ create: connectOrCreateItem.create
143
+ ? (0, exports.processNestedData)(connectOrCreateItem.create, relatedModel, prefixedId, dmmf, true)
144
+ : connectOrCreateItem.create,
145
+ }));
146
+ }
147
+ else {
148
+ // Fallback for non-array connectOrCreate (shouldn't happen in normal usage)
149
+ updatedValue[op] = value[op];
150
+ }
151
+ }
152
+ else if (op === "create" || op === "createMany") {
153
+ // Only process create operations with ID generation
154
+ updatedValue[op] = (0, exports.processNestedData)(value[op], relatedModel, prefixedId, dmmf);
155
+ }
156
+ else {
157
+ // For other operations like update, just pass through the value
158
+ updatedValue[op] = value[op];
159
+ }
160
+ }
161
+ result[key] = updatedValue;
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+ return data;
167
+ };
168
+ exports.processNestedData = processNestedData;
169
+ function createPrefixedIdsExtension(config, dmmf) {
170
+ if (!dmmf) {
171
+ throw new Error("DMMF is required for prefixed IDs extension");
172
+ }
11
173
  const { prefixes, idGenerator = defaultIdGenerator } = config;
12
174
  const prefixedId = (modelName) => {
13
175
  if (modelName in prefixes) {
@@ -15,40 +177,124 @@ function createPrefixedIdsExtension(config) {
15
177
  }
16
178
  return null;
17
179
  };
18
- return {
19
- name: "prefixedIds",
20
- query: {
21
- $allModels: {
22
- create: ({ args, query, model }) => {
23
- if (args.data && !args.data.id) {
24
- const id = prefixedId(model);
25
- if (id) {
26
- args.data.id = id;
27
- }
180
+ const createOperationHandler = (operation) => {
181
+ return ({ args, query, model }) => {
182
+ if (operation === "upsert") {
183
+ // For upsert operations, add ID to create branch only
184
+ if (args.create && !args.create.id) {
185
+ const id = prefixedId(model);
186
+ if (id) {
187
+ args.create.id = id;
188
+ }
189
+ }
190
+ // Process nested data in both create and update branches
191
+ if (dmmf) {
192
+ if (args.create) {
193
+ args.create = (0, exports.processNestedData)(args.create, model, prefixedId, dmmf, true);
28
194
  }
29
- return query(args);
30
- },
31
- createMany: ({ args, query, model }) => {
32
- if (model in prefixes && args.data && Array.isArray(args.data)) {
195
+ if (args.update) {
196
+ args.update = (0, exports.processNestedData)(args.update, model, prefixedId, dmmf, false);
197
+ }
198
+ }
199
+ }
200
+ else if (operation === "connectOrCreate") {
201
+ // For connectOrCreate operations, add ID to create branch only
202
+ if (args.create && !args.create.id) {
203
+ const id = prefixedId(model);
204
+ if (id) {
205
+ args.create.id = id;
206
+ }
207
+ }
208
+ // Process nested data in create branch
209
+ if (dmmf && args.create) {
210
+ args.create = (0, exports.processNestedData)(args.create, model, prefixedId, dmmf, true);
211
+ }
212
+ }
213
+ else if (args.data) {
214
+ if (operation === "createMany") {
215
+ // For createMany, data is an array
216
+ if (Array.isArray(args.data)) {
33
217
  args.data = args.data.map((item) => {
34
218
  if (!item.id) {
35
219
  const id = prefixedId(model);
36
220
  if (id) {
37
- return {
38
- ...item,
39
- id,
40
- };
221
+ item.id = id;
41
222
  }
42
223
  }
43
224
  return item;
44
225
  });
45
226
  }
46
- return query(args);
47
- },
227
+ }
228
+ else if (operation === "create") {
229
+ // For regular create operations only
230
+ if (!args.data.id) {
231
+ const id = prefixedId(model);
232
+ if (id) {
233
+ args.data.id = id;
234
+ }
235
+ }
236
+ // Process nested data to add IDs to nested creates
237
+ if (dmmf) {
238
+ args.data = (0, exports.processNestedData)(args.data, model, prefixedId, dmmf, true);
239
+ }
240
+ }
241
+ else if (operation === "update" || operation === "updateMany") {
242
+ // For update operations, only process nested creates, don't add ID to root
243
+ if (dmmf) {
244
+ args.data = (0, exports.processNestedData)(args.data, model, prefixedId, dmmf, false);
245
+ }
246
+ }
247
+ }
248
+ return query(args);
249
+ };
250
+ };
251
+ return {
252
+ name: "prefixedIds",
253
+ query: {
254
+ $allModels: {
255
+ create: createOperationHandler("create"),
256
+ createMany: createOperationHandler("createMany"),
257
+ update: createOperationHandler("update"),
258
+ updateMany: createOperationHandler("updateMany"),
259
+ upsert: createOperationHandler("upsert"),
260
+ connectOrCreate: createOperationHandler("connectOrCreate"),
48
261
  },
49
262
  },
50
263
  };
51
264
  }
52
265
  function extendPrismaClient(prisma, config) {
53
- return prisma.$extends(createPrefixedIdsExtension(config));
266
+ const dmmf = getDMMF(prisma);
267
+ return prisma.$extends(createPrefixedIdsExtension(config, dmmf));
268
+ }
269
+ // Helper function to get DMMF from a Prisma Client instance or query context
270
+ function getDMMF(clientOrContext) {
271
+ // Try newer structure first (_runtimeDataModel)
272
+ if (clientOrContext._runtimeDataModel) {
273
+ const modelsEntries = Object.entries(clientOrContext._runtimeDataModel.models);
274
+ return {
275
+ datamodel: {
276
+ models: modelsEntries.map(([name, model]) => ({
277
+ name: name,
278
+ fields: model.fields.map((field) => ({
279
+ name: field.name,
280
+ kind: field.relationName ? "object" : "scalar",
281
+ type: field.type,
282
+ isList: field.isList,
283
+ })),
284
+ })),
285
+ },
286
+ };
287
+ }
288
+ // Fallback to older structures
289
+ return (clientOrContext._baseDmmf ||
290
+ clientOrContext._dmmf ||
291
+ clientOrContext._client?._baseDmmf ||
292
+ clientOrContext._client?._dmmf);
293
+ }
294
+ // Helper function to get all model names from a Prisma Client instance
295
+ function getModelNames(prismaClient) {
296
+ const dmmf = getDMMF(prismaClient);
297
+ if (!dmmf)
298
+ return [];
299
+ return dmmf.datamodel.models.map((model) => model.name);
54
300
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { PrismaClient } from "@prisma/client";
2
- type ModelName = string;
1
+ import { type PrismaClient } from "@prisma/client";
2
+ export type ModelName = string;
3
3
  export type PrefixConfig<ModelName extends string> = {
4
4
  prefixes: Partial<Record<ModelName, string>>;
5
5
  idGenerator?: (prefix: string) => string;
@@ -9,16 +9,22 @@ type QueryArgs = {
9
9
  query: (args: any) => Promise<any>;
10
10
  model: ModelName;
11
11
  };
12
- export declare function createPrefixedIdsExtension<ModelName extends string>(config: PrefixConfig<ModelName>): {
12
+ export declare const findRelationModel: (dmmf: any, parentModel: string, fieldName: string) => string | null;
13
+ export declare const processNestedData: <T extends ModelName>(data: any, model: T, prefixedId: (model: T) => string | null, dmmf: any, shouldAddRootId?: boolean) => any;
14
+ export declare function createPrefixedIdsExtension<ModelName extends string>(config: PrefixConfig<ModelName>, dmmf: any): {
13
15
  name: string;
14
16
  query: {
15
17
  $allModels: {
16
18
  create: (args: QueryArgs) => Promise<any>;
17
19
  createMany: (args: QueryArgs) => Promise<any>;
20
+ update: (args: QueryArgs) => Promise<any>;
21
+ updateMany: (args: QueryArgs) => Promise<any>;
22
+ upsert: (args: QueryArgs) => Promise<any>;
23
+ connectOrCreate: (args: QueryArgs) => Promise<any>;
18
24
  };
19
25
  };
20
26
  };
21
- export declare function extendPrismaClient<ModelName extends string = string, Client extends {
22
- $extends: (extension: any) => any;
23
- } = PrismaClient>(prisma: Client, config: PrefixConfig<ModelName>): ReturnType<Client["$extends"]>;
27
+ export declare function extendPrismaClient<ModelName extends string = string, Client extends PrismaClient = PrismaClient>(prisma: Client, config: PrefixConfig<ModelName>): Client;
28
+ export declare function getDMMF(clientOrContext: PrismaClient | any): any;
29
+ export declare function getModelNames(prismaClient: PrismaClient): string[];
24
30
  export {};
package/dist/index.js CHANGED
@@ -3,7 +3,164 @@ const defaultIdGenerator = (prefix) => {
3
3
  const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 24);
4
4
  return `${prefix}_${nanoid()}`;
5
5
  };
6
- export function createPrefixedIdsExtension(config) {
6
+ // All possible relation operations in Prisma
7
+ const RELATION_OPERATIONS = [
8
+ "create",
9
+ "createMany",
10
+ "connectOrCreate",
11
+ "upsert",
12
+ "update",
13
+ "updateMany",
14
+ ];
15
+ // Helper to find the relation model from DMMF
16
+ export const findRelationModel = (dmmf, parentModel, fieldName) => {
17
+ // Find the model in DMMF
18
+ const model = dmmf.datamodel.models.find((m) => m.name === parentModel);
19
+ if (!model) {
20
+ return null;
21
+ }
22
+ // Find the field that matches the relation name
23
+ const field = model.fields.find((f) => f.name === fieldName);
24
+ if (!field || field.kind !== "object") {
25
+ return null;
26
+ }
27
+ // Return the related model name
28
+ return field.type;
29
+ };
30
+ // Helper function to check if key is a relation operation
31
+ const isRelationOperation = (key) => {
32
+ return RELATION_OPERATIONS.includes(key);
33
+ };
34
+ // Helper function to process nested data with proper model detection
35
+ export const processNestedData = (data, model, prefixedId, dmmf, shouldAddRootId = true) => {
36
+ if (!data) {
37
+ return data;
38
+ }
39
+ // Handle array of items
40
+ if (Array.isArray(data)) {
41
+ return data.map((item) => processNestedData(item, model, prefixedId, dmmf, shouldAddRootId));
42
+ }
43
+ // Handle object
44
+ if (typeof data === "object") {
45
+ const result = { ...data };
46
+ // Generate ID for the current model if needed (for nested creates)
47
+ if (shouldAddRootId && !result.id) {
48
+ const id = prefixedId(model);
49
+ if (id) {
50
+ result.id = id;
51
+ }
52
+ }
53
+ // Process nested relations by checking each key in the object
54
+ for (const [key, value] of Object.entries(result)) {
55
+ if (!value || typeof value !== "object")
56
+ continue;
57
+ // CASE 1: Key itself is a relation operation (root level operation)
58
+ if (isRelationOperation(key)) {
59
+ // Find which field this operation belongs to by looking at non-operation keys
60
+ const relationFields = Object.keys(result).filter((k) => !isRelationOperation(k));
61
+ for (const relationField of relationFields) {
62
+ const relatedModel = findRelationModel(dmmf, model, relationField);
63
+ if (relatedModel) {
64
+ if (key === "createMany" && "data" in value) {
65
+ // Handle createMany operation
66
+ const createManyOp = value;
67
+ result[key] = {
68
+ ...value,
69
+ data: processNestedData(createManyOp.data, relatedModel, prefixedId, dmmf),
70
+ };
71
+ }
72
+ else if (key === "upsert") {
73
+ // Handle upsert operation (has create and update)
74
+ result[key] = {
75
+ ...value,
76
+ create: value.create
77
+ ? processNestedData(value.create, relatedModel, prefixedId, dmmf)
78
+ : value.create,
79
+ update: value.update,
80
+ };
81
+ }
82
+ else if (key === "connectOrCreate") {
83
+ // Handle connectOrCreate operation
84
+ result[key] = {
85
+ ...value,
86
+ create: value.create
87
+ ? processNestedData(value.create, relatedModel, prefixedId, dmmf)
88
+ : value.create,
89
+ };
90
+ }
91
+ else if (key === "create" || key === "createMany") {
92
+ // Only process create operations with ID generation
93
+ result[key] = processNestedData(value, relatedModel, prefixedId, dmmf);
94
+ }
95
+ else {
96
+ // For other operations like update, just pass through the value
97
+ result[key] = value;
98
+ }
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ // CASE 2: Key might be a relation field that contains operations
104
+ else {
105
+ const relatedModel = findRelationModel(dmmf, model, key);
106
+ if (!relatedModel)
107
+ continue;
108
+ // Process all possible operations in this relation field
109
+ const updatedValue = { ...value };
110
+ // Process each operation type
111
+ for (const op of RELATION_OPERATIONS) {
112
+ if (!(op in value))
113
+ continue;
114
+ if (op === "createMany" && "data" in value[op]) {
115
+ updatedValue[op] = {
116
+ ...value[op],
117
+ data: processNestedData(value[op].data, relatedModel, prefixedId, dmmf),
118
+ };
119
+ }
120
+ else if (op === "upsert") {
121
+ updatedValue[op] = {
122
+ ...value[op],
123
+ create: value[op].create
124
+ ? processNestedData(value[op].create, relatedModel, prefixedId, dmmf)
125
+ : value[op].create,
126
+ update: value[op].update, // Don't process update
127
+ };
128
+ }
129
+ else if (op === "connectOrCreate") {
130
+ // Special handling for connectOrCreate - it's an array where each item has where/create
131
+ if (Array.isArray(value[op])) {
132
+ updatedValue[op] = value[op].map((connectOrCreateItem) => ({
133
+ ...connectOrCreateItem,
134
+ create: connectOrCreateItem.create
135
+ ? processNestedData(connectOrCreateItem.create, relatedModel, prefixedId, dmmf, true)
136
+ : connectOrCreateItem.create,
137
+ }));
138
+ }
139
+ else {
140
+ // Fallback for non-array connectOrCreate (shouldn't happen in normal usage)
141
+ updatedValue[op] = value[op];
142
+ }
143
+ }
144
+ else if (op === "create" || op === "createMany") {
145
+ // Only process create operations with ID generation
146
+ updatedValue[op] = processNestedData(value[op], relatedModel, prefixedId, dmmf);
147
+ }
148
+ else {
149
+ // For other operations like update, just pass through the value
150
+ updatedValue[op] = value[op];
151
+ }
152
+ }
153
+ result[key] = updatedValue;
154
+ }
155
+ }
156
+ return result;
157
+ }
158
+ return data;
159
+ };
160
+ export function createPrefixedIdsExtension(config, dmmf) {
161
+ if (!dmmf) {
162
+ throw new Error("DMMF is required for prefixed IDs extension");
163
+ }
7
164
  const { prefixes, idGenerator = defaultIdGenerator } = config;
8
165
  const prefixedId = (modelName) => {
9
166
  if (modelName in prefixes) {
@@ -11,40 +168,124 @@ export function createPrefixedIdsExtension(config) {
11
168
  }
12
169
  return null;
13
170
  };
14
- return {
15
- name: "prefixedIds",
16
- query: {
17
- $allModels: {
18
- create: ({ args, query, model }) => {
19
- if (args.data && !args.data.id) {
20
- const id = prefixedId(model);
21
- if (id) {
22
- args.data.id = id;
23
- }
171
+ const createOperationHandler = (operation) => {
172
+ return ({ args, query, model }) => {
173
+ if (operation === "upsert") {
174
+ // For upsert operations, add ID to create branch only
175
+ if (args.create && !args.create.id) {
176
+ const id = prefixedId(model);
177
+ if (id) {
178
+ args.create.id = id;
179
+ }
180
+ }
181
+ // Process nested data in both create and update branches
182
+ if (dmmf) {
183
+ if (args.create) {
184
+ args.create = processNestedData(args.create, model, prefixedId, dmmf, true);
24
185
  }
25
- return query(args);
26
- },
27
- createMany: ({ args, query, model }) => {
28
- if (model in prefixes && args.data && Array.isArray(args.data)) {
186
+ if (args.update) {
187
+ args.update = processNestedData(args.update, model, prefixedId, dmmf, false);
188
+ }
189
+ }
190
+ }
191
+ else if (operation === "connectOrCreate") {
192
+ // For connectOrCreate operations, add ID to create branch only
193
+ if (args.create && !args.create.id) {
194
+ const id = prefixedId(model);
195
+ if (id) {
196
+ args.create.id = id;
197
+ }
198
+ }
199
+ // Process nested data in create branch
200
+ if (dmmf && args.create) {
201
+ args.create = processNestedData(args.create, model, prefixedId, dmmf, true);
202
+ }
203
+ }
204
+ else if (args.data) {
205
+ if (operation === "createMany") {
206
+ // For createMany, data is an array
207
+ if (Array.isArray(args.data)) {
29
208
  args.data = args.data.map((item) => {
30
209
  if (!item.id) {
31
210
  const id = prefixedId(model);
32
211
  if (id) {
33
- return {
34
- ...item,
35
- id,
36
- };
212
+ item.id = id;
37
213
  }
38
214
  }
39
215
  return item;
40
216
  });
41
217
  }
42
- return query(args);
43
- },
218
+ }
219
+ else if (operation === "create") {
220
+ // For regular create operations only
221
+ if (!args.data.id) {
222
+ const id = prefixedId(model);
223
+ if (id) {
224
+ args.data.id = id;
225
+ }
226
+ }
227
+ // Process nested data to add IDs to nested creates
228
+ if (dmmf) {
229
+ args.data = processNestedData(args.data, model, prefixedId, dmmf, true);
230
+ }
231
+ }
232
+ else if (operation === "update" || operation === "updateMany") {
233
+ // For update operations, only process nested creates, don't add ID to root
234
+ if (dmmf) {
235
+ args.data = processNestedData(args.data, model, prefixedId, dmmf, false);
236
+ }
237
+ }
238
+ }
239
+ return query(args);
240
+ };
241
+ };
242
+ return {
243
+ name: "prefixedIds",
244
+ query: {
245
+ $allModels: {
246
+ create: createOperationHandler("create"),
247
+ createMany: createOperationHandler("createMany"),
248
+ update: createOperationHandler("update"),
249
+ updateMany: createOperationHandler("updateMany"),
250
+ upsert: createOperationHandler("upsert"),
251
+ connectOrCreate: createOperationHandler("connectOrCreate"),
44
252
  },
45
253
  },
46
254
  };
47
255
  }
48
256
  export function extendPrismaClient(prisma, config) {
49
- return prisma.$extends(createPrefixedIdsExtension(config));
257
+ const dmmf = getDMMF(prisma);
258
+ return prisma.$extends(createPrefixedIdsExtension(config, dmmf));
259
+ }
260
+ // Helper function to get DMMF from a Prisma Client instance or query context
261
+ export function getDMMF(clientOrContext) {
262
+ // Try newer structure first (_runtimeDataModel)
263
+ if (clientOrContext._runtimeDataModel) {
264
+ const modelsEntries = Object.entries(clientOrContext._runtimeDataModel.models);
265
+ return {
266
+ datamodel: {
267
+ models: modelsEntries.map(([name, model]) => ({
268
+ name: name,
269
+ fields: model.fields.map((field) => ({
270
+ name: field.name,
271
+ kind: field.relationName ? "object" : "scalar",
272
+ type: field.type,
273
+ isList: field.isList,
274
+ })),
275
+ })),
276
+ },
277
+ };
278
+ }
279
+ // Fallback to older structures
280
+ return (clientOrContext._baseDmmf ||
281
+ clientOrContext._dmmf ||
282
+ clientOrContext._client?._baseDmmf ||
283
+ clientOrContext._client?._dmmf);
284
+ }
285
+ // Helper function to get all model names from a Prisma Client instance
286
+ export function getModelNames(prismaClient) {
287
+ const dmmf = getDMMF(prismaClient);
288
+ if (!dmmf)
289
+ return [];
290
+ return dmmf.datamodel.models.map((model) => model.name);
50
291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prisma-prefixed-ids",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "A Prisma extension that adds prefixed IDs to your models",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,9 +25,17 @@
25
25
  "build:esm": "tsc -p tsconfig.json",
26
26
  "build:cjs": "tsc -p tsconfig.cjs.json && mv dist/cjs/index.js dist/index.cjs && rm -rf dist/cjs",
27
27
  "prepare": "npm run build",
28
- "test": "jest",
28
+ "postinstall": "npm run clean:test-artifacts",
29
+ "clean:test-artifacts": "rm -rf test-client prisma/test.db prisma/test.db-journal",
30
+ "test": "npm run test:unit && npm run test:integration",
31
+ "test:unit": "jest --testPathIgnorePatterns=integration",
32
+ "test:integration": "npm run db:setup && jest --testPathPattern=integration",
29
33
  "test:watch": "jest --watch",
30
34
  "test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.ts' --coveragePathIgnorePatterns='src/__tests__'",
35
+ "db:setup": "npm run db:generate && npm run db:push",
36
+ "db:generate": "prisma generate",
37
+ "db:push": "prisma db push",
38
+ "db:reset": "prisma migrate reset --force",
31
39
  "lint": "eslint . --ext .ts",
32
40
  "lint:fix": "eslint . --ext .ts --fix",
33
41
  "format": "prettier --write \"src/**/*.ts\"",
@@ -57,10 +65,14 @@
57
65
  "eslint-config-prettier": "^9.0.0",
58
66
  "jest": "^29.0.0",
59
67
  "prettier": "^3.0.0",
68
+ "prisma": "^5.0.0",
60
69
  "ts-jest": "^29.0.0",
61
70
  "typescript": "^5.0.0"
62
71
  },
63
72
  "peerDependencies": {
64
73
  "@prisma/client": "^5.0.0 || ^6.0.0"
74
+ },
75
+ "prisma": {
76
+ "schema": "tests/prisma/schema.prisma"
65
77
  }
66
78
  }