prisma-prefixed-ids 1.0.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/.eslintrc.json +32 -0
- package/.github/workflows/ci.yml +39 -0
- package/.prettierrc +1 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +54 -0
- package/jest.config.cjs +16 -0
- package/package.json +53 -0
- package/src/__tests__/index.test.ts +137 -0
- package/src/index.ts +84 -0
- package/tsconfig.json +15 -0
- package/tsconfig.test.json +11 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"node": true,
|
|
4
|
+
"es2021": true,
|
|
5
|
+
"jest": true
|
|
6
|
+
},
|
|
7
|
+
"extends": [
|
|
8
|
+
"eslint:recommended",
|
|
9
|
+
"plugin:@typescript-eslint/recommended",
|
|
10
|
+
"prettier"
|
|
11
|
+
],
|
|
12
|
+
"parser": "@typescript-eslint/parser",
|
|
13
|
+
"parserOptions": {
|
|
14
|
+
"ecmaVersion": "latest",
|
|
15
|
+
"sourceType": "module"
|
|
16
|
+
},
|
|
17
|
+
"plugins": ["@typescript-eslint"],
|
|
18
|
+
"rules": {
|
|
19
|
+
"@typescript-eslint/explicit-function-return-type": "error",
|
|
20
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
21
|
+
"@typescript-eslint/no-unused-vars": [
|
|
22
|
+
"error",
|
|
23
|
+
{ "argsIgnorePattern": "^_" }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"ignorePatterns": [
|
|
27
|
+
"**/*.test.ts",
|
|
28
|
+
"**/*.spec.ts",
|
|
29
|
+
"dist/**/*",
|
|
30
|
+
"node_modules/**/*"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [18.x, 20.x]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: ${{ matrix.node-version }}
|
|
24
|
+
cache: 'npm'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Check formatting
|
|
30
|
+
run: npm run format:check
|
|
31
|
+
|
|
32
|
+
- name: Run linter
|
|
33
|
+
run: npm run lint
|
|
34
|
+
|
|
35
|
+
- name: Build
|
|
36
|
+
run: npm run build
|
|
37
|
+
|
|
38
|
+
- name: Run tests with coverage
|
|
39
|
+
run: npm run test:coverage
|
package/.prettierrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Prageeth Silva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Prisma Prefixed IDs
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/prisma-prefixed-ids)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/pureartisan/prisma-prefixed-ids/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
A Prisma extension that automatically adds prefixed IDs to your models. This package allows you to configure custom prefixes for your models and even customize how the IDs are generated.
|
|
8
|
+
|
|
9
|
+
## Why nanoid instead of UUID v4?
|
|
10
|
+
|
|
11
|
+
This package uses [nanoid](https://github.com/ai/nanoid) for ID generation instead of UUID v4 for several reasons:
|
|
12
|
+
|
|
13
|
+
1. **Better Collision Resistance**: While UUID v4 has a 122-bit random component, nanoid with 24 characters (using 36 possible characters) provides approximately 128 bits of entropy, making it even more collision-resistant than UUID v4.
|
|
14
|
+
|
|
15
|
+
2. **Smaller Size**: A UUID v4 is 36 characters long (including hyphens), while a nanoid with 24 characters is more compact. When combined with a prefix (e.g., `usr_`), the total length is still shorter than a UUID v4.
|
|
16
|
+
|
|
17
|
+
3. **URL-Safe**: nanoid uses URL-safe characters by default, making it suitable for use in URLs without encoding.
|
|
18
|
+
|
|
19
|
+
4. **Customizable**: nanoid allows you to customize the alphabet and length, giving you more control over the ID format.
|
|
20
|
+
|
|
21
|
+
5. **Better Performance**: nanoid is optimized for performance and generates IDs faster than UUID v4.
|
|
22
|
+
|
|
23
|
+
For example, with a 24-character nanoid:
|
|
24
|
+
- The chance of a collision is approximately 1 in 2^128 (same as UUID v4)
|
|
25
|
+
- The ID length is 24 characters + prefix length (e.g., `usr_abc123...`)
|
|
26
|
+
- The alphabet includes 36 characters (0-9, a-z), making it both readable and compact
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install prisma-prefixed-ids
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { type Prisma, PrismaClient } from "@prisma/client";
|
|
38
|
+
import { extendPrismaClient } from 'prisma-prefixed-ids';
|
|
39
|
+
|
|
40
|
+
type ModelName = Prisma.ModelName;
|
|
41
|
+
// NOTE: is your Prisma.ModelName is not available in your setup,
|
|
42
|
+
// simply use the following instead:
|
|
43
|
+
// type ModelName = string;
|
|
44
|
+
|
|
45
|
+
// Create your Prisma client
|
|
46
|
+
const prisma = new PrismaClient();
|
|
47
|
+
|
|
48
|
+
// Define your model prefixes
|
|
49
|
+
const prefixes: Partial<Record<ModelName, string>> = {
|
|
50
|
+
Organization: 'org',
|
|
51
|
+
User: 'usr',
|
|
52
|
+
// Add more model prefixes as needed
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Extend the client with prefixed IDs
|
|
56
|
+
const extendedPrisma = extendPrismaClient(prisma, {
|
|
57
|
+
prefixes,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Use the extended client
|
|
61
|
+
const organization = await extendedPrisma.organization.create({
|
|
62
|
+
data: {
|
|
63
|
+
name: 'My Organization',
|
|
64
|
+
// id will be automatically generated with prefix 'org_'
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
console.log(organization.id); // e.g., 'org_abc123...'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Custom ID Generation
|
|
72
|
+
|
|
73
|
+
You can customize how IDs are generated by providing your own ID generator function:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { extendPrismaClient } from 'prisma-prefixed-ids';
|
|
77
|
+
import { customAlphabet } from 'nanoid';
|
|
78
|
+
|
|
79
|
+
// Create a custom ID generator
|
|
80
|
+
const customIdGenerator = (prefix: string) => {
|
|
81
|
+
const nanoid = customAlphabet('1234567890abcdef', 10);
|
|
82
|
+
return `${prefix}_${nanoid()}`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const extendedPrisma = extendPrismaClient(prisma, {
|
|
86
|
+
prefixes: {
|
|
87
|
+
Organization: 'org',
|
|
88
|
+
User: 'usr',
|
|
89
|
+
},
|
|
90
|
+
idGenerator: customIdGenerator,
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration Options
|
|
95
|
+
|
|
96
|
+
The extension accepts the following configuration:
|
|
97
|
+
|
|
98
|
+
- `prefixes`: A record mapping model names to their prefixes (required)
|
|
99
|
+
- `idGenerator`: A function that generates IDs (optional, defaults to using nanoid)
|
|
100
|
+
|
|
101
|
+
### Prefixes Configuration
|
|
102
|
+
|
|
103
|
+
The `prefixes` configuration is a simple object where:
|
|
104
|
+
- Keys are your Prisma model names (case sensitive)
|
|
105
|
+
- Values are the prefixes you want to use (without the underscore, which is added automatically)
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
```typescript
|
|
109
|
+
const prefixes = {
|
|
110
|
+
Organization: 'org',
|
|
111
|
+
User: 'usr',
|
|
112
|
+
Post: 'post',
|
|
113
|
+
Comment: 'cmt',
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### ID Generator Function
|
|
118
|
+
|
|
119
|
+
The `idGenerator` function should:
|
|
120
|
+
- Accept a prefix as its only parameter
|
|
121
|
+
- Return a string that will be used as the ID
|
|
122
|
+
|
|
123
|
+
The default generator uses nanoid with a 24-character length and alphanumeric characters.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
type ModelName = string;
|
|
3
|
+
export type PrefixConfig<ModelName extends string> = {
|
|
4
|
+
prefixes: Record<ModelName, string>;
|
|
5
|
+
idGenerator?: (prefix: string) => string;
|
|
6
|
+
};
|
|
7
|
+
type QueryArgs = {
|
|
8
|
+
args: any;
|
|
9
|
+
query: (args: any) => Promise<any>;
|
|
10
|
+
model: ModelName;
|
|
11
|
+
};
|
|
12
|
+
export declare function createPrefixedIdsExtension<ModelName extends string>(config: PrefixConfig<ModelName>): {
|
|
13
|
+
name: string;
|
|
14
|
+
query: {
|
|
15
|
+
$allModels: {
|
|
16
|
+
create: (args: QueryArgs) => Promise<any>;
|
|
17
|
+
createMany: (args: QueryArgs) => Promise<any>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function extendPrismaClient<ModelName extends string = string>(prisma: PrismaClient, config: PrefixConfig<ModelName>): PrismaClient;
|
|
22
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPrefixedIdsExtension = createPrefixedIdsExtension;
|
|
4
|
+
exports.extendPrismaClient = extendPrismaClient;
|
|
5
|
+
const nanoid_1 = require("nanoid");
|
|
6
|
+
const defaultIdGenerator = (prefix) => {
|
|
7
|
+
const nanoid = (0, nanoid_1.customAlphabet)("0123456789abcdefghijklmnopqrstuvwxyz", 24);
|
|
8
|
+
return `${prefix}_${nanoid()}`;
|
|
9
|
+
};
|
|
10
|
+
function createPrefixedIdsExtension(config) {
|
|
11
|
+
const { prefixes, idGenerator = defaultIdGenerator } = config;
|
|
12
|
+
const prefixedId = (modelName) => {
|
|
13
|
+
if (modelName in prefixes) {
|
|
14
|
+
return idGenerator(prefixes[modelName]);
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
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
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return query(args);
|
|
30
|
+
},
|
|
31
|
+
createMany: ({ args, query, model }) => {
|
|
32
|
+
if (model in prefixes && args.data && Array.isArray(args.data)) {
|
|
33
|
+
args.data = args.data.map((item) => {
|
|
34
|
+
if (!item.id) {
|
|
35
|
+
const id = prefixedId(model);
|
|
36
|
+
if (id) {
|
|
37
|
+
return {
|
|
38
|
+
...item,
|
|
39
|
+
id,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return item;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return query(args);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function extendPrismaClient(prisma, config) {
|
|
53
|
+
return prisma.$extends(createPrefixedIdsExtension(config));
|
|
54
|
+
}
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: "ts-jest",
|
|
3
|
+
testEnvironment: "node",
|
|
4
|
+
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
|
5
|
+
transform: {
|
|
6
|
+
"^.+\\.ts$": [
|
|
7
|
+
"ts-jest",
|
|
8
|
+
{
|
|
9
|
+
tsconfig: "tsconfig.test.json",
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
moduleNameMapper: {
|
|
14
|
+
"^(\\.{1,2}/.*)\\.js$": "$1",
|
|
15
|
+
},
|
|
16
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prisma-prefixed-ids",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Prisma extension that adds prefixed IDs to your models",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/pureartisan/prisma-prefixed-ids"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:watch": "jest --watch",
|
|
17
|
+
"test:coverage": "jest --coverage --collectCoverageFrom='src/**/*.ts' --coveragePathIgnorePatterns='src/__tests__'",
|
|
18
|
+
"lint": "eslint . --ext .ts",
|
|
19
|
+
"lint:fix": "eslint . --ext .ts --fix",
|
|
20
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
21
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
22
|
+
"git:push": "git push && git push --tags"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"prisma",
|
|
26
|
+
"extension",
|
|
27
|
+
"ids",
|
|
28
|
+
"prefixed",
|
|
29
|
+
"nanoid"
|
|
30
|
+
],
|
|
31
|
+
"author": "Prageeth Silva <prageethsilva@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@prisma/client": "^5.0.0",
|
|
35
|
+
"nanoid": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/jest": "^29.0.0",
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"@types/nanoid": "^2.0.0",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
42
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
43
|
+
"eslint": "^8.56.0",
|
|
44
|
+
"eslint-config-prettier": "^9.0.0",
|
|
45
|
+
"jest": "^29.0.0",
|
|
46
|
+
"prettier": "^3.0.0",
|
|
47
|
+
"ts-jest": "^29.0.0",
|
|
48
|
+
"typescript": "^5.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@prisma/client": "^5.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { PrismaClient } from "@prisma/client";
|
|
4
|
+
import { createPrefixedIdsExtension, extendPrismaClient } from "../index";
|
|
5
|
+
|
|
6
|
+
// Mock PrismaClient
|
|
7
|
+
jest.mock("@prisma/client", () => {
|
|
8
|
+
return {
|
|
9
|
+
PrismaClient: jest.fn().mockImplementation(() => ({
|
|
10
|
+
$extends: jest.fn(),
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Mock nanoid
|
|
16
|
+
jest.mock("nanoid", () => ({
|
|
17
|
+
customAlphabet: jest.fn().mockImplementation(() => () => "mock_nanoid_value"),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe("PrefixedIdsExtension", () => {
|
|
21
|
+
let prisma: PrismaClient;
|
|
22
|
+
const mockQuery = jest.fn((args: any) => Promise.resolve(args));
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
prisma = new PrismaClient();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("createPrefixedIdsExtension", () => {
|
|
30
|
+
it("should create an extension with the correct name", () => {
|
|
31
|
+
const extension = createPrefixedIdsExtension({
|
|
32
|
+
prefixes: {
|
|
33
|
+
Test: "test",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(extension.name).toBe("prefixedIds");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should use default idGenerator if none provided", async () => {
|
|
41
|
+
const extension = createPrefixedIdsExtension({
|
|
42
|
+
prefixes: {
|
|
43
|
+
Test: "test",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const result = await extension.query.$allModels.create({
|
|
48
|
+
args: { data: {} },
|
|
49
|
+
query: mockQuery,
|
|
50
|
+
model: "Test",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result).toBeDefined();
|
|
54
|
+
expect(mockQuery).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should use custom idGenerator if provided", async () => {
|
|
58
|
+
const customIdGenerator = jest.fn(
|
|
59
|
+
(prefix: string) => `${prefix}_custom_id`,
|
|
60
|
+
);
|
|
61
|
+
const extension = createPrefixedIdsExtension({
|
|
62
|
+
prefixes: {
|
|
63
|
+
Test: "test",
|
|
64
|
+
},
|
|
65
|
+
idGenerator: customIdGenerator,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await extension.query.$allModels.create({
|
|
69
|
+
args: { data: {} },
|
|
70
|
+
query: mockQuery,
|
|
71
|
+
model: "Test",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(customIdGenerator).toHaveBeenCalledWith("test");
|
|
75
|
+
expect(mockQuery).toHaveBeenCalledWith({
|
|
76
|
+
data: { id: "test_custom_id" },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should not modify args if model has no prefix", async () => {
|
|
81
|
+
const extension = createPrefixedIdsExtension({
|
|
82
|
+
prefixes: {
|
|
83
|
+
Test: "test",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const originalArgs = { data: {} };
|
|
88
|
+
const result = await extension.query.$allModels.create({
|
|
89
|
+
args: originalArgs,
|
|
90
|
+
query: mockQuery,
|
|
91
|
+
model: "UnknownModel",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result).toBeDefined();
|
|
95
|
+
expect(result.data).not.toHaveProperty("id");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should handle createMany operation", async () => {
|
|
99
|
+
const extension = createPrefixedIdsExtension({
|
|
100
|
+
prefixes: {
|
|
101
|
+
Test: "test",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await extension.query.$allModels.createMany({
|
|
106
|
+
args: {
|
|
107
|
+
data: [{}, {}],
|
|
108
|
+
},
|
|
109
|
+
query: mockQuery,
|
|
110
|
+
model: "Test",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(result).toBeDefined();
|
|
114
|
+
expect(result.data).toHaveLength(2);
|
|
115
|
+
expect(result.data[0]).toHaveProperty("id");
|
|
116
|
+
expect(result.data[1]).toHaveProperty("id");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("extendPrismaClient", () => {
|
|
121
|
+
it("should extend the Prisma client with the extension", () => {
|
|
122
|
+
const extendedPrisma = extendPrismaClient(prisma, {
|
|
123
|
+
prefixes: {
|
|
124
|
+
Test: "test",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(prisma.$extends).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should not throw error if prefixes are not provided", () => {
|
|
132
|
+
expect(() => {
|
|
133
|
+
extendPrismaClient(prisma, {} as any);
|
|
134
|
+
}).not.toThrow();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { PrismaClient } from "@prisma/client";
|
|
2
|
+
import { customAlphabet } from "nanoid";
|
|
3
|
+
|
|
4
|
+
// Define ModelName type based on Prisma's model names
|
|
5
|
+
type ModelName = string;
|
|
6
|
+
|
|
7
|
+
export type PrefixConfig<ModelName extends string> = {
|
|
8
|
+
prefixes: Record<ModelName, string>;
|
|
9
|
+
idGenerator?: (prefix: string) => string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const defaultIdGenerator = (prefix: string): string => {
|
|
13
|
+
const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 24);
|
|
14
|
+
return `${prefix}_${nanoid()}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type QueryArgs = {
|
|
18
|
+
args: any;
|
|
19
|
+
query: (args: any) => Promise<any>;
|
|
20
|
+
model: ModelName;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createPrefixedIdsExtension<ModelName extends string>(
|
|
24
|
+
config: PrefixConfig<ModelName>,
|
|
25
|
+
): {
|
|
26
|
+
name: string;
|
|
27
|
+
query: {
|
|
28
|
+
$allModels: {
|
|
29
|
+
create: (args: QueryArgs) => Promise<any>;
|
|
30
|
+
createMany: (args: QueryArgs) => Promise<any>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
} {
|
|
34
|
+
const { prefixes, idGenerator = defaultIdGenerator } = config;
|
|
35
|
+
|
|
36
|
+
const prefixedId = (modelName: ModelName): string | null => {
|
|
37
|
+
if (modelName in prefixes) {
|
|
38
|
+
return idGenerator(prefixes[modelName]);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
name: "prefixedIds",
|
|
45
|
+
query: {
|
|
46
|
+
$allModels: {
|
|
47
|
+
create: ({ args, query, model }: QueryArgs): Promise<any> => {
|
|
48
|
+
if (args.data && !args.data.id) {
|
|
49
|
+
const id = prefixedId(model as ModelName);
|
|
50
|
+
if (id) {
|
|
51
|
+
args.data.id = id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return query(args);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
createMany: ({ args, query, model }: QueryArgs): Promise<any> => {
|
|
58
|
+
if (model in prefixes && args.data && Array.isArray(args.data)) {
|
|
59
|
+
args.data = (args.data as Record<string, any>[]).map((item) => {
|
|
60
|
+
if (!item.id) {
|
|
61
|
+
const id = prefixedId(model as ModelName);
|
|
62
|
+
if (id) {
|
|
63
|
+
return {
|
|
64
|
+
...item,
|
|
65
|
+
id,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return item;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return query(args);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function extendPrismaClient<ModelName extends string = string>(
|
|
80
|
+
prisma: PrismaClient,
|
|
81
|
+
config: PrefixConfig<ModelName>,
|
|
82
|
+
): PrismaClient {
|
|
83
|
+
return prisma.$extends(createPrefixedIdsExtension(config));
|
|
84
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"moduleResolution": "node"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"types": ["jest", "node"],
|
|
7
|
+
"esModuleInterop": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*.test.ts", "src/**/__tests__/**/*"],
|
|
10
|
+
"exclude": ["node_modules"]
|
|
11
|
+
}
|