├── .nvmrc ├── .eslintignore ├── .prettierignore ├── server ├── src │ ├── policies │ │ └── index.ts │ ├── content-types │ │ └── index.ts │ ├── middlewares │ │ └── index.ts │ ├── config │ │ └── index.ts │ ├── services │ │ ├── index.ts │ │ └── service.ts │ ├── controllers │ │ ├── index.ts │ │ └── controller.ts │ ├── destroy.ts │ ├── routes │ │ └── index.ts │ ├── register.ts │ ├── index.ts │ └── bootstrap.ts ├── tsconfig.json └── tsconfig.build.json ├── .npmignore ├── admin ├── src │ ├── pluginId.ts │ ├── components │ │ ├── Input │ │ │ ├── index.ts │ │ │ └── Input.tsx │ │ ├── PluginIcon │ │ │ ├── index.ts │ │ │ ├── PluginIcon.tsx │ │ │ └── icon.svg │ │ └── Initializer │ │ │ ├── index.ts │ │ │ └── Initializer.tsx │ ├── utils │ │ ├── getTranslation.ts │ │ ├── prefixPluginTranslations.ts │ │ └── helpers.ts │ ├── translations │ │ └── en.json │ └── index.ts ├── custom.d.ts ├── tsconfig.json └── tsconfig.build.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── main.yml │ └── release.yml ├── release.config.cjs ├── docs └── screenshots │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ ├── screenshot-4.png │ └── strapi-advanced-uuid.png ├── .prettierrc ├── .editorconfig ├── tsconfig.jest.json ├── jest.config.ts ├── LICENSE ├── .gitignore ├── README.md ├── package.json └── __tests__ └── bootstrap.test.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | -------------------------------------------------------------------------------- /server/src/policies/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /server/src/content-types/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /server/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | *.iml 3 | *ignore 4 | __tests__ 5 | .github -------------------------------------------------------------------------------- /admin/src/pluginId.ts: -------------------------------------------------------------------------------- 1 | export const PLUGIN_ID = 'strapi-advanced-uuid'; 2 | -------------------------------------------------------------------------------- /admin/src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | import Input from './Input'; 2 | 3 | export { Input }; 4 | -------------------------------------------------------------------------------- /server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | default: {}, 3 | validator() {}, 4 | }; 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [dulajdeshan] 2 | patreon: dulajdeshan 3 | buy_me_a_coffee: dulajdeshan 4 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ["release", { name: "next", prerelease: true }], 3 | }; 4 | -------------------------------------------------------------------------------- /server/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import service from './service'; 2 | 3 | export default { 4 | service, 5 | }; 6 | -------------------------------------------------------------------------------- /admin/src/components/PluginIcon/index.ts: -------------------------------------------------------------------------------- 1 | import PluginIcon from './PluginIcon'; 2 | 3 | export { PluginIcon }; 4 | -------------------------------------------------------------------------------- /admin/src/components/Initializer/index.ts: -------------------------------------------------------------------------------- 1 | import Initializer from './Initializer'; 2 | 3 | export { Initializer }; 4 | -------------------------------------------------------------------------------- /server/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import controller from './controller'; 2 | 3 | export default { 4 | controller, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/screenshots/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dulajdeshan/strapi-advanced-uuid/HEAD/docs/screenshots/screenshot-1.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dulajdeshan/strapi-advanced-uuid/HEAD/docs/screenshots/screenshot-2.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dulajdeshan/strapi-advanced-uuid/HEAD/docs/screenshots/screenshot-3.png -------------------------------------------------------------------------------- /docs/screenshots/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dulajdeshan/strapi-advanced-uuid/HEAD/docs/screenshots/screenshot-4.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /docs/screenshots/strapi-advanced-uuid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dulajdeshan/strapi-advanced-uuid/HEAD/docs/screenshots/strapi-advanced-uuid.png -------------------------------------------------------------------------------- /admin/src/components/PluginIcon/PluginIcon.tsx: -------------------------------------------------------------------------------- 1 | import Icon from './icon.svg' 2 | 3 | const PluginIcon = () => uuid 4 | 5 | export default PluginIcon 6 | -------------------------------------------------------------------------------- /admin/src/utils/getTranslation.ts: -------------------------------------------------------------------------------- 1 | import { PLUGIN_ID } from '../pluginId'; 2 | 3 | const getTranslation = (id: string) => `${PLUGIN_ID}.${id}`; 4 | 5 | export { getTranslation }; 6 | -------------------------------------------------------------------------------- /server/src/destroy.ts: -------------------------------------------------------------------------------- 1 | import type { Core } from '@strapi/strapi'; 2 | 3 | const destroy = ({ strapi }: { strapi: Core.Strapi }) => { 4 | // destroy phase 5 | }; 6 | 7 | export default destroy; 8 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/server", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "rootDir": "../", 6 | "baseUrl": "." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /admin/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@strapi/design-system/*'; 2 | declare module '@strapi/design-system'; 3 | 4 | declare module '*.svg' { 5 | const content: string; 6 | 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/admin", 3 | "include": ["./src", "./custom.d.ts"], 4 | "compilerOptions": { 5 | "rootDir": "../", 6 | "baseUrl": "." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["./src"], 4 | "exclude": ["**/*.test.ts"], 5 | "compilerOptions": { 6 | "rootDir": "../", 7 | "baseUrl": ".", 8 | "outDir": "./dist" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | // name of the controller file & the method. 6 | handler: 'controller.index', 7 | config: { 8 | policies: [], 9 | }, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /admin/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["./src", "./custom.d.ts"], 4 | "exclude": ["**/*.test.ts", "**/*.test.tsx"], 5 | "compilerOptions": { 6 | "rootDir": "../", 7 | "baseUrl": ".", 8 | "outDir": "./dist" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /server/src/register.ts: -------------------------------------------------------------------------------- 1 | import type { Core } from '@strapi/strapi'; 2 | import { PLUGIN_ID } from '../../admin/src/pluginId'; 3 | 4 | const register = ({ strapi }: { strapi: Core.Strapi }) => { 5 | // register phase 6 | strapi.customFields.register({ 7 | name: 'uuid', 8 | plugin: PLUGIN_ID, 9 | type: 'uid', 10 | }); 11 | }; 12 | 13 | export default register; 14 | -------------------------------------------------------------------------------- /server/src/controllers/controller.ts: -------------------------------------------------------------------------------- 1 | import type { Core } from '@strapi/strapi'; 2 | 3 | const controller = ({ strapi }: { strapi: Core.Strapi }) => ({ 4 | index(ctx) { 5 | ctx.body = strapi 6 | .plugin('strapi-advanced-uuid') 7 | // the name of the service file & the method. 8 | .service('service') 9 | .getWelcomeMessage(); 10 | }, 11 | }); 12 | 13 | export default controller; 14 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["jest"], 4 | "baseUrl": ".", 5 | "paths": { 6 | "*": ["admin/*", "server/*"] 7 | }, 8 | "module": "commonjs", // Or the module type used in both `admin` and `server` 9 | "target": "esnext", // Or the target from both `admin` and `server` 10 | "esModuleInterop": true 11 | }, 12 | "include": ["admin/**/*.ts", "server/**/*.ts", "tests/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /admin/src/components/Initializer/Initializer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | import { PLUGIN_ID } from '../../pluginId'; 4 | 5 | type InitializerProps = { 6 | setPlugin: (id: string) => void; 7 | }; 8 | 9 | export default function Initializer({ setPlugin }: InitializerProps) { 10 | const ref = useRef(setPlugin); 11 | 12 | useEffect(() => { 13 | ref.current(PLUGIN_ID); 14 | }, []); 15 | 16 | return null; 17 | } 18 | -------------------------------------------------------------------------------- /admin/src/utils/prefixPluginTranslations.ts: -------------------------------------------------------------------------------- 1 | type TradOptions = Record; 2 | 3 | const prefixPluginTranslations = (trad: TradOptions, pluginId: string): TradOptions => { 4 | if (!pluginId) { 5 | throw new TypeError("pluginId can't be empty"); 6 | } 7 | return Object.keys(trad).reduce((acc, current) => { 8 | acc[`${pluginId}.${current}`] = trad[current]; 9 | return acc; 10 | }, {} as TradOptions); 11 | }; 12 | 13 | export { prefixPluginTranslations }; 14 | -------------------------------------------------------------------------------- /admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "form.label": "Advanced UUID", 3 | "form.description": "Generates UUID v4", 4 | "form.field.generate": "Generate", 5 | "form.field.error": "The UUID format is invalid.", 6 | "form.field.uuidFormat": "UUID format", 7 | "form.field.disableRegenerate": "Disable regenerate", 8 | "form.field.disableRegenerate.description": "Disable regeneration in the UI", 9 | "form.field.disableAutoFill": "Disable Auto Fill", 10 | "form.field.disableAutoFill.description": "Disable initial auto fill of the UUID" 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from 'ts-jest'; 2 | 3 | /* 4 | * For a detailed explanation regarding each configuration property, visit: 5 | * https://jestjs.io/docs/configuration 6 | */ 7 | 8 | const config: JestConfigWithTsJest = { 9 | transform: { 10 | '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }], 11 | }, 12 | preset: 'ts-jest', 13 | coverageDirectory: './coverage', 14 | collectCoverage: true, 15 | clearMocks: true, 16 | coveragePathIgnorePatterns: ['/node_modules/', '/dist'], 17 | coverageProvider: 'v8', 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: daily 12 | commit-message: 13 | prefix: fix 14 | prefix-development: chore 15 | include: scope 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: ['*'] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [18.x, 20.x, 22.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Install dependencies 22 | run: yarn install 23 | - name: Building package 24 | run: yarn build 25 | - name: Running tests 26 | run: yarn test 27 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Application methods 3 | */ 4 | import bootstrap from './bootstrap'; 5 | import destroy from './destroy'; 6 | import register from './register'; 7 | 8 | /** 9 | * Plugin server methods 10 | */ 11 | import config from './config'; 12 | import contentTypes from './content-types'; 13 | import controllers from './controllers'; 14 | import middlewares from './middlewares'; 15 | import policies from './policies'; 16 | import routes from './routes'; 17 | import services from './services'; 18 | 19 | export default { 20 | register, 21 | bootstrap, 22 | destroy, 23 | config, 24 | controllers, 25 | routes, 26 | services, 27 | contentTypes, 28 | policies, 29 | middlewares, 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | push: 4 | branches: 5 | - release 6 | jobs: 7 | release: 8 | name: Release and Publish 9 | runs-on: ubuntu-latest 10 | if: ${{ github.ref == 'refs/heads/release' }} 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | # Setup .npmrc file to publish to npm 15 | - name: node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '20.x' 19 | - name: Install dependencies 20 | run: yarn install 21 | - name: Building package 22 | run: yarn build 23 | - name: Install semantic-release 24 | run: yarn global add semantic-release-cli semantic-release 25 | - run: semantic-release --branches release 26 | env: 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dulaj Deshan Ariyaratne 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. 22 | -------------------------------------------------------------------------------- /admin/src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { v4, validate } from 'uuid'; 2 | import { randString } from 'regex-randstr'; 3 | 4 | export const generateUUID = (format: string) => { 5 | try { 6 | if (!format) { 7 | return v4(); 8 | } 9 | const regexFormat = new RegExp(format); 10 | return randString(regexFormat); 11 | } catch (error) { 12 | return null; 13 | } 14 | }; 15 | 16 | export const validateUUID = (format: string, initialValue: string) => { 17 | const newFormat = `^${format}$`; 18 | const regexFormat = new RegExp(newFormat, 'i'); 19 | return regexFormat.exec(initialValue); 20 | }; 21 | 22 | export const getOptions = (attribute: any) => { 23 | return { 24 | disableAutoFill: (attribute.options && attribute.options['disable-auto-fill']) ?? false, 25 | disableRegenerate: (attribute.options && attribute.options['disable-regenerate']) ?? false, 26 | uuidFormat: attribute.options && attribute.options['uuid-format'], 27 | }; 28 | }; 29 | 30 | export const isValidUUIDValue = (uuidFormat: string, value: string) => { 31 | const isValidValue = uuidFormat ? validateUUID(uuidFormat, value) : validate(value); 32 | 33 | if (value && !isValidValue) { 34 | return false; 35 | } 36 | return true; 37 | }; 38 | -------------------------------------------------------------------------------- /server/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import type { Core } from '@strapi/strapi'; 2 | import { PLUGIN_ID } from '../../admin/src/pluginId'; 3 | 4 | const bootstrap = ({ strapi }: { strapi: Core.Strapi }) => { 5 | const { contentTypes } = strapi; 6 | 7 | const models = Object.keys(contentTypes).reduce((acc, key) => { 8 | const contentType = contentTypes[key]; 9 | 10 | // Filter out content types that have the custom field "plugin::strapi-advanced-uuid.uuid" 11 | const attributes = Object.keys(contentType.attributes).filter((attrKey) => { 12 | const attribute = contentType.attributes[attrKey]; 13 | if (attribute.customField === 'plugin::strapi-advanced-uuid.uuid') { 14 | return true; 15 | } 16 | }); 17 | 18 | if (attributes.length > 0) { 19 | return { ...acc, [key]: attributes }; 20 | } 21 | 22 | return acc; 23 | }, {}) as { [key: string]: string[] }; 24 | 25 | // Get the models to subscribe 26 | const modelsToSubscribe = Object.keys(models); 27 | 28 | if (strapi.db) { 29 | strapi.db.lifecycles.subscribe({ 30 | models: modelsToSubscribe, 31 | beforeCreate(event) { 32 | strapi.plugin(PLUGIN_ID).service('service').handleCRUDOperation(event); 33 | }, 34 | beforeUpdate(event) { 35 | strapi.plugin(PLUGIN_ID).service('service').handleCRUDOperation(event); 36 | }, 37 | }); 38 | } 39 | }; 40 | 41 | export default bootstrap; 42 | -------------------------------------------------------------------------------- /admin/src/components/PluginIcon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | ############################ 4 | # OS X 5 | ############################ 6 | 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | Icon 11 | .Spotlight-V100 12 | .Trashes 13 | ._* 14 | 15 | 16 | ############################ 17 | # Linux 18 | ############################ 19 | 20 | *~ 21 | 22 | 23 | ############################ 24 | # Windows 25 | ############################ 26 | 27 | Thumbs.db 28 | ehthumbs.db 29 | Desktop.ini 30 | $RECYCLE.BIN/ 31 | *.cab 32 | *.msi 33 | *.msm 34 | *.msp 35 | 36 | 37 | ############################ 38 | # Packages 39 | ############################ 40 | 41 | *.7z 42 | *.csv 43 | *.dat 44 | *.dmg 45 | *.gz 46 | *.iso 47 | *.jar 48 | *.rar 49 | *.tar 50 | *.zip 51 | *.com 52 | *.class 53 | *.dll 54 | *.exe 55 | *.o 56 | *.seed 57 | *.so 58 | *.swo 59 | *.swp 60 | *.swn 61 | *.swm 62 | *.out 63 | *.pid 64 | 65 | 66 | ############################ 67 | # Logs and databases 68 | ############################ 69 | 70 | .tmp 71 | *.log 72 | *.sql 73 | *.sqlite 74 | *.sqlite3 75 | 76 | 77 | ############################ 78 | # Misc. 79 | ############################ 80 | 81 | *# 82 | ssl 83 | .idea 84 | nbproject 85 | .tsbuildinfo 86 | .eslintcache 87 | .env 88 | 89 | 90 | ############################ 91 | # Strapi 92 | ############################ 93 | 94 | public/uploads/* 95 | !public/uploads/.gitkeep 96 | 97 | 98 | ############################ 99 | # Build 100 | ############################ 101 | 102 | dist 103 | build 104 | 105 | 106 | ############################ 107 | # Node.js 108 | ############################ 109 | 110 | lib-cov 111 | lcov.info 112 | pids 113 | logs 114 | results 115 | node_modules 116 | .node_history 117 | 118 | 119 | ############################ 120 | # Package managers 121 | ############################ 122 | 123 | .yarn/* 124 | !.yarn/cache 125 | !.yarn/unplugged 126 | !.yarn/patches 127 | !.yarn/releases 128 | !.yarn/sdks 129 | !.yarn/versions 130 | .pnp.* 131 | yarn-error.log 132 | 133 | 134 | ############################ 135 | # Tests 136 | ############################ 137 | 138 | coverage 139 | -------------------------------------------------------------------------------- /server/src/services/service.ts: -------------------------------------------------------------------------------- 1 | import type { Core } from '@strapi/strapi'; 2 | import { errors } from '@strapi/utils'; 3 | import { generateUUID, isValidUUIDValue } from '../../../admin/src/utils/helpers'; 4 | 5 | const { YupValidationError } = errors; 6 | 7 | const service = ({ strapi }: { strapi: Core.Strapi }) => ({ 8 | getWelcomeMessage() { 9 | return 'Welcome to Strapi 🚀'; 10 | }, 11 | handleCRUDOperation(event: any) { 12 | const errorMessages: any = { 13 | inner: [], 14 | }; 15 | 16 | Object.keys(event.model.attributes).forEach((attribute) => { 17 | if (event.model.attributes[attribute].customField === 'plugin::strapi-advanced-uuid.uuid') { 18 | // Get the initial value of the attribute 19 | const initialValue = event.params.data[attribute]; 20 | 21 | // Get the options of the attribute 22 | const options = event.model.attributes 23 | ? event.model.attributes[attribute]['options'] 24 | : null; 25 | 26 | // Get the uuid-format option, if it is set 27 | const uuidFormat = options ? options['uuid-format'] : null; 28 | // Get the disable-auto-fill option, if it is set 29 | const disableAutoFill = options ? options['disable-auto-fill'] : false; 30 | 31 | // If there is no initial value and disableAutoFill is not enabled, generate a new UUID 32 | if (!event.params.data[attribute] && !disableAutoFill) { 33 | event.params.data[attribute] = generateUUID(uuidFormat); 34 | } 35 | 36 | // Validation happens on following conditions: 37 | // - If disableAutoFill is not enabled 38 | // - If there is an initial value 39 | if (!disableAutoFill || initialValue) { 40 | if (!isValidUUIDValue(uuidFormat, event.params.data[attribute])) { 41 | errorMessages.inner.push({ 42 | name: 'ValidationError', // Always set to ValidationError 43 | path: attribute, // Name of field we want to show input validation on 44 | message: 'The UUID format is invalid.', // Input validation message 45 | }); 46 | } 47 | } 48 | } 49 | }); 50 | 51 | if (errorMessages.inner.length > 0) { 52 | throw new YupValidationError(errorMessages, 'You have some issues'); 53 | } 54 | }, 55 | }); 56 | 57 | export default service; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Bootstrap Icons 3 |

4 | 5 |

6 | Strapi Advanced UUID 7 |

8 | 9 |

Custom Field plugin for Strapi to generate UUID based on your regular expressions

10 | 11 |

12 | 13 | Version 14 | License 15 | 16 |

17 | 18 | The Strapi Advanced UUID Plugin is a custom plugin for Strapi that automatically generates a unique UUID for your content. It also allows you to generate UUID based on your regular expressions. 19 | 20 | ## ⚠️ Compatibility with Strapi versions 21 | 22 | Starting from version 2.0.0, the Strapi Advanced UUID plugin is compatible with Strapi 5 and can't be used in Strapi 4.4+. 23 | 24 | | Plugin version | Strapi version | 25 | | -------------- | -------------- | 26 | | 2.x.x | ≥ 5.0.0 | 27 | | 1.x.x | ≥ 4.4 | 28 | 29 | ## ⚙️ Installation 30 | 31 | To install the Strapi Advanced UUID Plugin, simply run one of the following command: 32 | 33 | ``` 34 | npm install strapi-advanced-uuid 35 | ``` 36 | 37 | ``` 38 | yarn add strapi-advanced-uuid 39 | ``` 40 | 41 | ## ⚡️ Usage 42 | 43 | ### How to Setup Advanced UUID Field 44 | 45 | After installation you will find the `Advanced UUID` at the custom fields section of the content-type builder. 46 | 47 | ![strapi advanced uuid](./docs/screenshots/screenshot-1.png) 48 | 49 | Now you can define the field attributes. `Advanced UUID` field allows you to define the custom regular expression (`UUID format`) for your field. Default UUID format will be [`UUID V4`](https://www.npmjs.com/package/uuid#uuidv4options-buffer-offset). 50 | 51 | ![strapi advanced uuid](./docs/screenshots/screenshot-2.png) 52 | 53 | ### How to Use Custom Regular Expression 54 | 55 | ![strapi advanced uuid](./docs/screenshots/screenshot-3.png) 56 | 57 | Now You can create new records via the Admin panel, API or GraphQL, and the plugin will automatically generate a UUID for each new record created. 58 | 59 | ![strapi advanced uuid](./docs/screenshots/screenshot-4.png) 60 | 61 | ## 👍 Contribute 62 | 63 | If you want to say **Thank You** and/or support the active development of `Strapi Advanced UUID`: 64 | 65 | 1. Add a [GitHub Star](https://github.com/Dulajdeshan/strapi-advanced-uuid/stargazers) to the project. 66 | 2. Support the project by donating a [cup of coffee](https://buymeacoff.ee/dulajdeshan). 67 | 68 | ## 🧾 License 69 | 70 | This plugin is licensed under the MIT License. See the [LICENSE](./LICENSE.md) file for more information. 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-advanced-uuid", 3 | "version": "2.0.1", 4 | "description": "UUID field type support to Strapi with customizations", 5 | "keywords": [ 6 | "strapi", 7 | "strapi plugin", 8 | "custom fields", 9 | "uuid", 10 | "regex" 11 | ], 12 | "homepage": "https://github.com/Dulajdeshan/strapi-advanced-uuid", 13 | "readme": "https://github.com/Dulajdeshan/strapi-advanced-uuid#readme", 14 | "bugs": { 15 | "url": "https://github.com/Dulajdeshan/strapi-advanced-uuid/issues", 16 | "email": "dulajdeshans@gmail.com" 17 | }, 18 | "repository": { 19 | "url": "git+https://github.com/Dulajdeshan/strapi-advanced-uuid.git", 20 | "type": "git", 21 | "directory": "." 22 | }, 23 | "license": "MIT", 24 | "author": { 25 | "name": "Dulaj Ariyaratne", 26 | "email": "dulajdeshans@gmail.com", 27 | "url": "https://github.com/dulajdeshan" 28 | }, 29 | "engines": { 30 | "node": ">=18.0.0 <=22.x.x", 31 | "npm": ">=6.0.0" 32 | }, 33 | "exports": { 34 | "./package.json": "./package.json", 35 | "./strapi-admin": { 36 | "types": "./dist/admin/src/index.d.ts", 37 | "source": "./admin/src/index.ts", 38 | "import": "./dist/admin/index.mjs", 39 | "require": "./dist/admin/index.js", 40 | "default": "./dist/admin/index.js" 41 | }, 42 | "./strapi-server": { 43 | "types": "./dist/server/src/index.d.ts", 44 | "source": "./server/src/index.ts", 45 | "import": "./dist/server/index.mjs", 46 | "require": "./dist/server/index.js", 47 | "default": "./dist/server/index.js" 48 | } 49 | }, 50 | "files": [ 51 | "dist" 52 | ], 53 | "scripts": { 54 | "build": "strapi-plugin build", 55 | "test": "jest", 56 | "test:ts:back": "run -T tsc -p server/tsconfig.json", 57 | "test:ts:front": "run -T tsc -p admin/tsconfig.json", 58 | "verify": "strapi-plugin verify", 59 | "watch": "strapi-plugin watch", 60 | "watch:link": "strapi-plugin watch:link" 61 | }, 62 | "dependencies": { 63 | "@strapi/design-system": "^2.0.0-rc.25", 64 | "@strapi/icons": "^2.0.0-rc.25", 65 | "react-intl": "^7.1.11", 66 | "regex-randstr": "^0.0.6", 67 | "uuid": "^11.0.2" 68 | }, 69 | "devDependencies": { 70 | "@strapi/sdk-plugin": "^5.3.2", 71 | "@strapi/strapi": "^5.15.1", 72 | "@strapi/typescript-utils": "^5.15.1", 73 | "@types/jest": "^29.5.14", 74 | "@types/node": "^22.8.6", 75 | "@types/react": "^18.3.11", 76 | "@types/react-dom": "^18.3.0", 77 | "jest": "^30.0.0", 78 | "prettier": "^3.5.3", 79 | "react": "^18.3.1", 80 | "react-dom": "^18.3.1", 81 | "react-router-dom": "^6.30.1", 82 | "styled-components": "^6.1.19", 83 | "ts-jest": "^29.2.5", 84 | "ts-node": "^10.9.2", 85 | "typescript": "^5.8.3" 86 | }, 87 | "peerDependencies": { 88 | "@strapi/sdk-plugin": "^5.3.2", 89 | "@strapi/strapi": "^5.1.0", 90 | "react": "^18.3.1", 91 | "react-dom": "^18.3.1", 92 | "react-router-dom": "^6.30.1", 93 | "styled-components": "^6.1.19" 94 | }, 95 | "strapi": { 96 | "kind": "plugin", 97 | "name": "strapi-advanced-uuid", 98 | "displayName": "Strapi Advanced UUID", 99 | "description": "UUID field type support to Strapi with customizations" 100 | }, 101 | "publishConfig": { 102 | "access": "public" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /admin/src/components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useIntl } from 'react-intl'; 4 | import { ArrowClockwise } from '@strapi/icons'; 5 | import { Field, TextInput, useComposedRefs } from '@strapi/design-system'; 6 | import { FieldValue, InputProps, useFocusInputField } from '@strapi/strapi/admin'; 7 | import { getTranslation } from '../../utils/getTranslation'; 8 | import { generateUUID, getOptions, isValidUUIDValue } from '../../utils/helpers'; 9 | import { IconButton } from '@strapi/design-system'; 10 | 11 | type TProps = InputProps & 12 | FieldValue & { 13 | labelAction?: React.ReactNode; 14 | attribute?: { 15 | disableAutoFill: boolean; 16 | disableRegenerate: boolean; 17 | uuidFormat: string; 18 | }; 19 | }; 20 | 21 | const Input = React.forwardRef( 22 | ( 23 | { 24 | hint, 25 | disabled = false, 26 | labelAction, 27 | label, 28 | name, 29 | required = false, 30 | onChange, 31 | value, 32 | error, 33 | placeholder, 34 | attribute, 35 | }, 36 | forwardedRef 37 | ) => { 38 | const { formatMessage } = useIntl(); 39 | const fieldRef = useFocusInputField(name); 40 | const composedRefs = useComposedRefs(forwardedRef, fieldRef); 41 | 42 | const [fieldError, setFieldError] = React.useState(error); 43 | 44 | const { disableAutoFill, disableRegenerate, uuidFormat } = getOptions(attribute); 45 | 46 | const getFieldError = () => 47 | formatMessage({ 48 | id: 'uuid.form.field.error', 49 | defaultMessage: 'The UUID format is invalid.', 50 | }); 51 | 52 | React.useEffect(() => { 53 | if (!value && !disableAutoFill) { 54 | const newUUID = generateUUID(uuidFormat); 55 | onChange({ target: { value: newUUID, name } } as React.ChangeEvent); 56 | } 57 | }, [value, attribute]); 58 | 59 | React.useEffect(() => { 60 | const isValid = isValidUUIDValue(uuidFormat, value); 61 | setFieldError(!isValid ? getFieldError() : undefined); 62 | }, [value]); 63 | 64 | // Helper function to handle the onChange event 65 | const handleOnChange = (e: React.ChangeEvent) => { 66 | const { value } = e.target; 67 | 68 | const isValid = isValidUUIDValue(uuidFormat, value); 69 | setFieldError(!isValid ? getFieldError() : undefined); 70 | 71 | onChange(e); 72 | }; 73 | 74 | const handleRegenerate = () => { 75 | const newUUID = generateUUID(uuidFormat); 76 | onChange({ target: { value: newUUID, name } } as React.ChangeEvent); 77 | }; 78 | 79 | return ( 80 | 81 | {label} 82 | 83 | 102 | 103 | 104 | 105 | 106 | ) 107 | } 108 | /> 109 | 110 | 111 | 112 | 113 | ); 114 | } 115 | ); 116 | 117 | export default Input; 118 | -------------------------------------------------------------------------------- /__tests__/bootstrap.test.ts: -------------------------------------------------------------------------------- 1 | import bootstrap from '../server/src/bootstrap'; 2 | import services from '../server/src/services'; 3 | 4 | describe('Strapi Lifecycle Methods for Different Models', () => { 5 | let strapiMock; 6 | 7 | beforeEach(() => { 8 | // Clear any mocks before each test 9 | jest.clearAllMocks(); 10 | 11 | // Mock the Strapi object 12 | strapiMock = { 13 | plugin: jest.fn().mockReturnValue({ 14 | ...services, 15 | }), 16 | db: { 17 | lifecycles: { 18 | subscribe: jest.fn(), 19 | }, 20 | }, 21 | contentTypes: { 22 | 'api::article.article': { 23 | attributes: { 24 | uuidField: { 25 | customField: 'plugin::strapi-advanced-uuid.uuid', 26 | options: { 'uuid-format': '^[A-Za-z0-9]{5}$', 'disable-auto-fill': false }, 27 | }, 28 | title: { 29 | type: 'string', 30 | }, 31 | }, 32 | }, 33 | 'api::product.product': { 34 | attributes: { 35 | sku: { 36 | customField: 'plugin::strapi-advanced-uuid.uuid', 37 | options: { 'uuid-format': '^[0-9a-zA-Z-]{8}$', 'disable-auto-fill': false }, 38 | }, 39 | name: { 40 | type: 'string', 41 | }, 42 | }, 43 | }, 44 | }, 45 | }; 46 | 47 | // Call the bootstrap method to set up lifecycle hooks 48 | bootstrap({ strapi: strapiMock }); 49 | }); 50 | 51 | test('should subscribe to beforeCreate and beforeUpdate hooks', () => { 52 | // Ensure the subscribe method is called 53 | expect(strapiMock.db.lifecycles.subscribe).toHaveBeenCalledWith( 54 | expect.objectContaining({ 55 | models: expect.arrayContaining(['api::article.article', 'api::product.product']), 56 | beforeCreate: expect.any(Function), 57 | beforeUpdate: expect.any(Function), 58 | }) 59 | ); 60 | }); 61 | 62 | test('beforeCreate generates UUID for article if not provided', () => { 63 | // Extract the beforeCreate hook 64 | const lifecycleHook = strapiMock.db.lifecycles.subscribe.mock.calls[0][0].beforeCreate; 65 | 66 | // Mock the event for creating an article 67 | const event = { 68 | action: 'beforeCreate', 69 | model: strapiMock.contentTypes['api::article.article'], 70 | params: { data: { title: 'New Article' } }, // uuidField not provided 71 | }; 72 | 73 | // Invoke the lifecycle hook 74 | lifecycleHook(event); 75 | 76 | // Assert that UUID is generated and matches the expected format 77 | expect(event.params.data).toMatchObject({ 78 | uuidField: expect.stringMatching(/^[A-Za-z0-9]{5}$/), 79 | title: 'New Article', 80 | }); 81 | }); 82 | 83 | test('beforeCreate validates SKU format for product', () => { 84 | // Extract the beforeCreate hook 85 | const lifecycleHook = strapiMock.db.lifecycles.subscribe.mock.calls[0][0].beforeCreate; 86 | 87 | // Mock the event for creating a product with an invalid SKU 88 | const event = { 89 | action: 'beforeCreate', 90 | model: strapiMock.contentTypes['api::product.product'], 91 | params: { data: { sku: 'invalidsku' } }, // Doesn't match format ^[0-9a-zA-Z-]{8}$ 92 | }; 93 | 94 | // Assert that YupValidationError is thrown 95 | expect(() => lifecycleHook(event)).toThrow('You have some issues'); 96 | }); 97 | 98 | test('beforeCreate does not auto-generate UUID if disableAutoFill is true', () => { 99 | // Mock model with disableAutoFill set to true 100 | const userModel = { 101 | attributes: { 102 | userId: { 103 | customField: 'plugin::strapi-advanced-uuid.uuid', 104 | options: { 'uuid-format': '^[0-9]{6}$', 'disable-auto-fill': true }, 105 | }, 106 | username: { 107 | type: 'string', 108 | }, 109 | }, 110 | }; 111 | 112 | // Update strapiMock to add the userModel 113 | strapiMock.contentTypes['api::user.user'] = userModel; 114 | 115 | // Call the bootstrap method to update lifecycle hooks 116 | bootstrap({ strapi: strapiMock }); 117 | 118 | // Extract the beforeCreate hook 119 | const lifecycleHook = strapiMock.db.lifecycles.subscribe.mock.calls[1][0].beforeCreate; 120 | 121 | // Mock the event for creating a user 122 | const event = { 123 | action: 'beforeCreate', 124 | model: userModel, 125 | params: { data: { username: 'testuser' } }, // userId not provided 126 | }; 127 | 128 | // Invoke the lifecycle hook 129 | lifecycleHook(event); 130 | 131 | // Assert that userId is not generated 132 | expect(event.params.data).not.toHaveProperty('userId'); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /admin/src/index.ts: -------------------------------------------------------------------------------- 1 | import { getTranslation } from './utils/getTranslation'; 2 | import { PLUGIN_ID } from './pluginId'; 3 | import { Initializer } from './components/Initializer'; 4 | import { PluginIcon } from './components/PluginIcon'; 5 | import { prefixPluginTranslations } from './utils/prefixPluginTranslations'; 6 | 7 | export default { 8 | register(app: any) { 9 | app.customFields.register({ 10 | name: 'uuid', 11 | pluginId: PLUGIN_ID, 12 | type: 'string', 13 | intlLabel: { 14 | id: getTranslation('form.label'), 15 | defaultMessage: 'Advanced UUID', 16 | }, 17 | intlDescription: { 18 | id: getTranslation('form.description'), 19 | defaultMessage: 'Generates a UUID v4', 20 | }, 21 | icon: PluginIcon, 22 | components: { 23 | Input: async () => 24 | import(/* webpackChunkName: "input-uuid-component" */ './components/Input/Input'), 25 | }, 26 | options: { 27 | base: [ 28 | { 29 | intlLabel: { 30 | id: getTranslation('form.field.uuidFormat'), 31 | defaultMessage: 'UUID Format', 32 | }, 33 | name: 'options.uuid-format', 34 | type: 'text', 35 | }, 36 | { 37 | sectionTitle: { 38 | id: getTranslation('form.field.options'), 39 | defaultMessage: 'Options', 40 | }, 41 | items: [ 42 | { 43 | intlLabel: { 44 | id: getTranslation('form.field.disableAutoFill'), 45 | defaultMessage: 'Disable Auto Fill', 46 | }, 47 | name: 'options.disable-auto-fill', 48 | type: 'checkbox', 49 | description: { 50 | id: 'form.field.disableAutoFill.description', 51 | defaultMessage: 52 | 'Disable initial auto fill of the UUID. UUID field will be editable when this option is enabled.', 53 | }, 54 | }, 55 | { 56 | intlLabel: { 57 | id: getTranslation('form.field.disableRegenerate'), 58 | defaultMessage: 'Disable Regenerate', 59 | }, 60 | name: 'options.disable-regenerate', 61 | type: 'checkbox', 62 | description: { 63 | id: 'form.field.disableRegenerate.description', 64 | defaultMessage: 'Disable regeneration in the UI', 65 | }, 66 | }, 67 | ], 68 | }, 69 | ], 70 | advanced: [ 71 | { 72 | sectionTitle: { 73 | id: 'global.settings', 74 | defaultMessage: 'Settings', 75 | }, 76 | items: [ 77 | { 78 | name: 'required', 79 | type: 'checkbox', 80 | intlLabel: { 81 | id: getTranslation('form.attribute.item.requiredField'), 82 | defaultMessage: 'Required field', 83 | }, 84 | description: { 85 | id: getTranslation('form.attribute.item.requiredField.description'), 86 | defaultMessage: "You won't be able to create an entry if this field is empty", 87 | }, 88 | }, 89 | { 90 | name: 'private', 91 | type: 'checkbox', 92 | intlLabel: { 93 | id: 'form.attribute.item.privateField', 94 | defaultMessage: 'Private field', 95 | }, 96 | description: { 97 | id: 'form.attribute.item.privateField.description', 98 | defaultMessage: 'This field will not show up in the API response', 99 | }, 100 | }, 101 | ], 102 | }, 103 | ], 104 | }, 105 | }); 106 | 107 | app.registerPlugin({ 108 | id: PLUGIN_ID, 109 | initializer: Initializer, 110 | isReady: false, 111 | name: PLUGIN_ID, 112 | }); 113 | }, 114 | 115 | async registerTrads(app: any) { 116 | const { locales } = app; 117 | 118 | const importedTranslations = await Promise.all( 119 | (locales as string[]).map((locale) => { 120 | return import(`./translations/${locale}.json`) 121 | .then(({ default: data }) => { 122 | return { 123 | data: prefixPluginTranslations(data, PLUGIN_ID), 124 | locale, 125 | }; 126 | }) 127 | .catch(() => { 128 | return { 129 | data: {}, 130 | locale, 131 | }; 132 | }); 133 | }) 134 | ); 135 | 136 | return importedTranslations; 137 | }, 138 | }; 139 | --------------------------------------------------------------------------------