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 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
+ [![npm version](https://img.shields.io/npm/v/prisma-prefixed-ids.svg)](https://www.npmjs.com/package/prisma-prefixed-ids)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Build Status](https://github.com/pureartisan/prisma-prefixed-ids/actions/workflows/ci.yml/badge.svg)](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
@@ -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
+ }
@@ -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
+ }