;
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 |
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 |
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 |
14 |
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 | 
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 | 
52 |
53 | ### How to Use Custom Regular Expression
54 |
55 | 
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 | 
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 |
--------------------------------------------------------------------------------