prisma-prefixed-ids 1.4.0 → 1.5.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.
- package/README.md +198 -4
- package/dist/index.cjs +268 -22
- package/dist/index.d.ts +11 -3
- package/dist/index.js +263 -22
- package/package.json +14 -2
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:
|
|
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^
|
|
122
|
-
- The ID length is 24 characters + prefix length (e.g., `
|
|
123
|
-
- The alphabet includes
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
...item,
|
|
39
|
-
id,
|
|
40
|
-
};
|
|
221
|
+
item.id = id;
|
|
41
222
|
}
|
|
42
223
|
}
|
|
43
224
|
return item;
|
|
44
225
|
});
|
|
45
226
|
}
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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,24 @@ type QueryArgs = {
|
|
|
9
9
|
query: (args: any) => Promise<any>;
|
|
10
10
|
model: ModelName;
|
|
11
11
|
};
|
|
12
|
-
export declare
|
|
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
27
|
export declare function extendPrismaClient<ModelName extends string = string, Client extends {
|
|
22
28
|
$extends: (extension: any) => any;
|
|
23
29
|
} = PrismaClient>(prisma: Client, config: PrefixConfig<ModelName>): ReturnType<Client["$extends"]>;
|
|
30
|
+
export declare function getDMMF(clientOrContext: PrismaClient | any): any;
|
|
31
|
+
export declare function getModelNames(prismaClient: PrismaClient): string[];
|
|
24
32
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
...item,
|
|
35
|
-
id,
|
|
36
|
-
};
|
|
212
|
+
item.id = id;
|
|
37
213
|
}
|
|
38
214
|
}
|
|
39
215
|
return item;
|
|
40
216
|
});
|
|
41
217
|
}
|
|
42
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.5.0",
|
|
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
|
-
"
|
|
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
|
}
|