@nocobase/test 1.8.25 → 1.8.26
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/package.json +3 -3
- package/perf/collections/categories.ts +209 -0
- package/perf/collections/comments.ts +268 -0
- package/perf/collections/posts.ts +253 -0
- package/perf/collections/tags.ts +242 -0
- package/perf/scenarios/blog/create-single_x100.ts +69 -0
- package/perf/scenarios/blog/flow-login-list-create-list-with-delay.ts +823 -0
- package/perf/scenarios/blog/flow-login-list-create-list-with-static.ts +2555 -0
- package/perf/scenarios/blog/flow-login-list-create-list.ts +767 -0
- package/perf/scenarios/blog/get-by-pk-appends_x100.ts +34 -0
- package/perf/scenarios/blog/get-by-pk_x100.ts +33 -0
- package/perf/scenarios/blog/init.ts +203 -0
- package/perf/scenarios/blog/list-by-title-includes-page-appends_x100.ts +43 -0
- package/perf/scenarios/blog/list-by-title-includes_x100.ts +42 -0
- package/perf/scenarios/blog/setup.js +16 -0
- package/perf/scenarios/blog/update-single_x100.ts +50 -0
- package/perf/utils.js +19 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// __benchmarks__/k6/write-single.js
|
|
2
|
+
import http from 'k6/http';
|
|
3
|
+
import { check, sleep } from 'k6';
|
|
4
|
+
|
|
5
|
+
export { setup } from './setup.js';
|
|
6
|
+
|
|
7
|
+
export const options = {
|
|
8
|
+
stages: [
|
|
9
|
+
{ duration: '1s', target: 100 },
|
|
10
|
+
{ duration: '59s', target: 100 },
|
|
11
|
+
],
|
|
12
|
+
thresholds: {
|
|
13
|
+
http_req_duration: ['p(95)<500'], // 95% 请求 < 500ms
|
|
14
|
+
http_req_failed: ['rate<0.01'], // 失败率 < 1%
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function ({ token }) {
|
|
19
|
+
const id = Math.floor(Math.random() * 1000000) + 1;
|
|
20
|
+
const url = `${__ENV.TARGET_ORIGIN}/api/posts:get/${id}?appends[]=comments&appends[]=category&appends[]=tags&appends[]=createdBy&appends[]=comments`;
|
|
21
|
+
const params = {
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
Authorization: `Bearer ${token}`,
|
|
25
|
+
'X-Role': 'admin',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const res = http.get(url, params);
|
|
30
|
+
|
|
31
|
+
check(res, {
|
|
32
|
+
'status is 200': (r) => r.status === 200,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// __benchmarks__/k6/write-single.js
|
|
2
|
+
import http from 'k6/http';
|
|
3
|
+
import { check, sleep } from 'k6';
|
|
4
|
+
|
|
5
|
+
export { setup } from './setup.js';
|
|
6
|
+
|
|
7
|
+
export const options = {
|
|
8
|
+
stages: [
|
|
9
|
+
{ duration: '1s', target: 100 },
|
|
10
|
+
{ duration: '59s', target: 100 },
|
|
11
|
+
],
|
|
12
|
+
thresholds: {
|
|
13
|
+
http_req_duration: ['p(95)<200'], // 95% 请求 < 200ms
|
|
14
|
+
http_req_failed: ['rate<0.01'], // 失败率 < 1%
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function ({ token }) {
|
|
19
|
+
const url = `${__ENV.TARGET_ORIGIN}/api/posts:get/${Math.floor(Math.random() * 1000000) + 1}`; // 你的写接口
|
|
20
|
+
const params = {
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Authorization: `Bearer ${token}`,
|
|
24
|
+
'X-Role': 'admin',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const res = http.get(url, params);
|
|
29
|
+
|
|
30
|
+
check(res, {
|
|
31
|
+
'status is 200': (r) => r.status === 200,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { faker } from '@faker-js/faker';
|
|
3
|
+
import { createMockServer } from '@nocobase/test';
|
|
4
|
+
import { uid } from '@nocobase/utils';
|
|
5
|
+
import { CollectionRepository } from '@nocobase/plugin-data-source-main';
|
|
6
|
+
|
|
7
|
+
const CATEGORIES_COUNT = 100;
|
|
8
|
+
const TAGS_COUNT = 1000;
|
|
9
|
+
const POSTS_COUNT = 1000000;
|
|
10
|
+
const COMMENTS_COUNT = 10000000;
|
|
11
|
+
const BATCH_SIZE = 100000;
|
|
12
|
+
|
|
13
|
+
export default async function main() {
|
|
14
|
+
const app = await createMockServer({
|
|
15
|
+
plugins: ['nocobase'],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const db = app.db;
|
|
19
|
+
await db.import({ directory: path.resolve(__dirname, 'collections') });
|
|
20
|
+
await db.sync();
|
|
21
|
+
const CollectionRepo = db.getRepository('collections') as CollectionRepository;
|
|
22
|
+
await CollectionRepo.db2cm('categories');
|
|
23
|
+
await CollectionRepo.db2cm('posts');
|
|
24
|
+
await CollectionRepo.db2cm('tags');
|
|
25
|
+
await CollectionRepo.db2cm('comments');
|
|
26
|
+
|
|
27
|
+
const CategoriesModel = db.getModel('categories');
|
|
28
|
+
const PostsModel = db.getModel('posts');
|
|
29
|
+
const TagsModel = db.getModel('tags');
|
|
30
|
+
const CommentsModel = db.getModel('comments');
|
|
31
|
+
const PostTagsModel = db.getModel('postTags');
|
|
32
|
+
|
|
33
|
+
console.log('Destroying existing data...');
|
|
34
|
+
await PostTagsModel.destroy({ truncate: true, cascade: true });
|
|
35
|
+
await CommentsModel.destroy({ truncate: true, cascade: true });
|
|
36
|
+
await PostsModel.destroy({ truncate: true, cascade: true });
|
|
37
|
+
await TagsModel.destroy({ truncate: true, cascade: true });
|
|
38
|
+
await CategoriesModel.destroy({ truncate: true, cascade: true });
|
|
39
|
+
console.log('Existing data destroyed.');
|
|
40
|
+
|
|
41
|
+
console.log(`Creating ${CATEGORIES_COUNT} categories...`);
|
|
42
|
+
const categories = [];
|
|
43
|
+
const categorySlugsSet = new Set<string>();
|
|
44
|
+
while (categorySlugsSet.size < CATEGORIES_COUNT) {
|
|
45
|
+
categorySlugsSet.add(uid());
|
|
46
|
+
}
|
|
47
|
+
const categorySlugs = Array.from(categorySlugsSet);
|
|
48
|
+
for (let i = 0; i < CATEGORIES_COUNT; i++) {
|
|
49
|
+
categories.push({
|
|
50
|
+
title: faker.lorem.words(3),
|
|
51
|
+
subTitle: faker.lorem.sentence(),
|
|
52
|
+
description: faker.lorem.paragraph(),
|
|
53
|
+
slug: categorySlugs[i],
|
|
54
|
+
sort: i,
|
|
55
|
+
coverImageUrl: faker.image.url(),
|
|
56
|
+
url: faker.internet.url(),
|
|
57
|
+
followerCount: faker.number.int({ min: 0, max: 10000 }),
|
|
58
|
+
articleCount: 0,
|
|
59
|
+
hidden: faker.datatype.boolean(),
|
|
60
|
+
themeColor: faker.internet.color(),
|
|
61
|
+
icon: faker.internet.emoji(),
|
|
62
|
+
allowComments: faker.datatype.boolean(),
|
|
63
|
+
createdById: 1,
|
|
64
|
+
updatedById: 1,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const createdCategories = await CategoriesModel.bulkCreate(categories);
|
|
68
|
+
console.log('Categories created.');
|
|
69
|
+
|
|
70
|
+
console.log(`Creating ${TAGS_COUNT} tags...`);
|
|
71
|
+
const tags = [];
|
|
72
|
+
const tagSlugsSet = new Set<string>();
|
|
73
|
+
while (tagSlugsSet.size < TAGS_COUNT) {
|
|
74
|
+
tagSlugsSet.add(uid());
|
|
75
|
+
}
|
|
76
|
+
const tagSlugs = Array.from(tagSlugsSet);
|
|
77
|
+
const tagNamesSet = new Set<string>();
|
|
78
|
+
while (tagNamesSet.size < TAGS_COUNT) {
|
|
79
|
+
tagNamesSet.add(uid());
|
|
80
|
+
}
|
|
81
|
+
const tagNames = Array.from(tagNamesSet);
|
|
82
|
+
for (let i = 0; i < TAGS_COUNT; i++) {
|
|
83
|
+
tags.push({
|
|
84
|
+
name: tagNames[i],
|
|
85
|
+
description: faker.lorem.sentence(),
|
|
86
|
+
color: faker.internet.color(),
|
|
87
|
+
icon: faker.internet.emoji(),
|
|
88
|
+
slug: tagSlugs[i],
|
|
89
|
+
isFeatured: faker.datatype.boolean(),
|
|
90
|
+
usageCount: 0,
|
|
91
|
+
followerCount: faker.number.int({ min: 0, max: 5000 }),
|
|
92
|
+
isOfficial: faker.datatype.boolean(),
|
|
93
|
+
moderationStatus: 'approved',
|
|
94
|
+
group: faker.lorem.word(),
|
|
95
|
+
seoTitle: faker.lorem.sentence(),
|
|
96
|
+
seoDescription: faker.lorem.paragraph(),
|
|
97
|
+
createdById: 1,
|
|
98
|
+
updatedById: 1,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const createdTags = await TagsModel.bulkCreate(tags);
|
|
102
|
+
console.log('Tags created.');
|
|
103
|
+
|
|
104
|
+
console.log(`Creating ${POSTS_COUNT} posts in batches of ${BATCH_SIZE}...`);
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < POSTS_COUNT; i += BATCH_SIZE) {
|
|
107
|
+
const posts = [];
|
|
108
|
+
const postSlugsSet = new Set<string>();
|
|
109
|
+
|
|
110
|
+
console.time('mock posts data in a batch');
|
|
111
|
+
while (postSlugsSet.size < BATCH_SIZE) {
|
|
112
|
+
postSlugsSet.add(uid());
|
|
113
|
+
}
|
|
114
|
+
const postSlugs = Array.from(postSlugsSet);
|
|
115
|
+
for (let j = 0; j < BATCH_SIZE; j++) {
|
|
116
|
+
const post = {
|
|
117
|
+
title: uid(),
|
|
118
|
+
subTitle: uid(),
|
|
119
|
+
content: uid(),
|
|
120
|
+
slug: postSlugs[j],
|
|
121
|
+
musicUrl: uid(),
|
|
122
|
+
excerpt: uid(),
|
|
123
|
+
coverImage: uid(),
|
|
124
|
+
status: 'published',
|
|
125
|
+
allowComments: faker.datatype.boolean(),
|
|
126
|
+
featured: faker.datatype.boolean(),
|
|
127
|
+
publishedAt: faker.date.past(),
|
|
128
|
+
viewCount: faker.number.int({ min: 0, max: 100000 }),
|
|
129
|
+
read: faker.number.int({ min: 0, max: 100000 }),
|
|
130
|
+
score: faker.number.float({ min: 0, max: 5, precision: 0.1 }),
|
|
131
|
+
categoryId: createdCategories[faker.number.int({ min: 0, max: CATEGORIES_COUNT - 1 })].id,
|
|
132
|
+
createdById: 1,
|
|
133
|
+
updatedById: 1,
|
|
134
|
+
};
|
|
135
|
+
posts.push(post);
|
|
136
|
+
}
|
|
137
|
+
console.timeEnd('mock posts data in a batch');
|
|
138
|
+
|
|
139
|
+
console.time('insert posts data in a batch');
|
|
140
|
+
const createdPosts = await PostsModel.bulkCreate(posts);
|
|
141
|
+
console.timeEnd('insert posts data in a batch');
|
|
142
|
+
|
|
143
|
+
console.time('mock post-tags relations in a batch');
|
|
144
|
+
const postTags = new Map();
|
|
145
|
+
for (const post of createdPosts) {
|
|
146
|
+
const numTags = faker.number.int({ min: 1, max: 5 });
|
|
147
|
+
for (let k = 0; k < numTags; k++) {
|
|
148
|
+
const postId = post.id;
|
|
149
|
+
const tagId = createdTags[faker.number.int({ min: 0, max: TAGS_COUNT - 1 })].id;
|
|
150
|
+
postTags.set(`${postId}-${tagId}`, { postId, tagId });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
console.timeEnd('mock post-tags relations in a batch');
|
|
154
|
+
|
|
155
|
+
console.time(`insert post-tags relations in a batch (${postTags.size})`);
|
|
156
|
+
await PostTagsModel.bulkCreate(Array.from(postTags.values()));
|
|
157
|
+
console.timeEnd(`insert post-tags relations in a batch (${postTags.size})`);
|
|
158
|
+
|
|
159
|
+
console.log(`Created posts ${i + 1}-${i + BATCH_SIZE}`);
|
|
160
|
+
}
|
|
161
|
+
console.log('Posts created.');
|
|
162
|
+
|
|
163
|
+
console.log(`Creating ${COMMENTS_COUNT} comments in batches of ${BATCH_SIZE}...`);
|
|
164
|
+
const allPosts = await PostsModel.findAll({ attributes: ['id'] });
|
|
165
|
+
for (let i = 0; i < COMMENTS_COUNT; i += BATCH_SIZE) {
|
|
166
|
+
const comments = [];
|
|
167
|
+
for (let j = 0; j < BATCH_SIZE; j++) {
|
|
168
|
+
comments.push({
|
|
169
|
+
content: uid,
|
|
170
|
+
authorName: uid,
|
|
171
|
+
authorEmail: uid,
|
|
172
|
+
authorUrl: uid,
|
|
173
|
+
ipAddress: faker.internet.ip(),
|
|
174
|
+
userAgent: uid,
|
|
175
|
+
status: 'approved',
|
|
176
|
+
likeCount: faker.number.int({ min: 0, max: 1000 }),
|
|
177
|
+
dislikeCount: faker.number.int({ min: 0, max: 200 }),
|
|
178
|
+
rating: faker.number.int({ min: 1, max: 5 }),
|
|
179
|
+
isApproved: true,
|
|
180
|
+
isFeatured: faker.datatype.boolean(),
|
|
181
|
+
isSticky: faker.datatype.boolean(),
|
|
182
|
+
postId: allPosts[faker.number.int({ min: 0, max: allPosts.length - 1 })].id,
|
|
183
|
+
createdById: 1,
|
|
184
|
+
updatedById: 1,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
await CommentsModel.bulkCreate(comments);
|
|
188
|
+
console.log(`Created comments ${i + 1}-${i + BATCH_SIZE}`);
|
|
189
|
+
}
|
|
190
|
+
console.log('Comments created.');
|
|
191
|
+
|
|
192
|
+
await app.destroy();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main()
|
|
196
|
+
.then(() => {
|
|
197
|
+
console.log('Data generation completed.');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
})
|
|
200
|
+
.catch((error) => {
|
|
201
|
+
console.error('Error during data generation:', error);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// __benchmarks__/k6/write-single.js
|
|
2
|
+
import http from 'k6/http';
|
|
3
|
+
import { check, sleep } from 'k6';
|
|
4
|
+
|
|
5
|
+
import { uid } from '../../utils.js';
|
|
6
|
+
export { setup } from './setup.js';
|
|
7
|
+
|
|
8
|
+
export const options = {
|
|
9
|
+
stages: [
|
|
10
|
+
// { duration: '1s', target: 1 },
|
|
11
|
+
{ duration: '1s', target: 100 },
|
|
12
|
+
{ duration: '59s', target: 100 },
|
|
13
|
+
],
|
|
14
|
+
thresholds: {
|
|
15
|
+
http_req_duration: ['p(95)<400'], // 95% 请求 < 400ms
|
|
16
|
+
http_req_failed: ['rate<0.01'], // 失败率 < 1%
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function ({ token }) {
|
|
21
|
+
const keyword = uid(1);
|
|
22
|
+
const url = `${__ENV.TARGET_ORIGIN}/api/posts:list?filter=${JSON.stringify({
|
|
23
|
+
$and: [{ title: { $includes: keyword } }],
|
|
24
|
+
})}&sort=-createdAt&pageSize=50&page=${
|
|
25
|
+
Math.floor(Math.random() * 2000) + 1
|
|
26
|
+
}&appends[]=category&appends[]=tags&appends[]=createdBy&appends[]=updatedBy&appends[]=comments`;
|
|
27
|
+
const params = {
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
'X-Role': 'admin',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const res = http.get(url, params);
|
|
36
|
+
|
|
37
|
+
check(res, {
|
|
38
|
+
'status is 200': (r) => r.status === 200,
|
|
39
|
+
'has data': (r) => r.json('data').length > 1,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
sleep(1);
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// __benchmarks__/k6/write-single.js
|
|
2
|
+
import http from 'k6/http';
|
|
3
|
+
import { check, sleep } from 'k6';
|
|
4
|
+
|
|
5
|
+
import { uid } from '../../utils.js';
|
|
6
|
+
export { setup } from './setup.js';
|
|
7
|
+
|
|
8
|
+
export const options = {
|
|
9
|
+
stages: [
|
|
10
|
+
{ duration: '1s', target: 100 },
|
|
11
|
+
{ duration: '59s', target: 100 },
|
|
12
|
+
],
|
|
13
|
+
thresholds: {
|
|
14
|
+
http_req_duration: ['p(95)<500'], // 95% 请求 < 200ms
|
|
15
|
+
http_req_failed: ['rate<0.01'], // 失败率 < 1%
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function ({ token }) {
|
|
20
|
+
const keyword = uid(2);
|
|
21
|
+
const url = `${__ENV.TARGET_ORIGIN}/api/posts:list`;
|
|
22
|
+
const params = {
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
Authorization: `Bearer ${token}`,
|
|
26
|
+
'X-Role': 'admin',
|
|
27
|
+
},
|
|
28
|
+
queries: {
|
|
29
|
+
filter: JSON.stringify({ $and: [{ title: { $includes: keyword } }] }),
|
|
30
|
+
sort: '-createdAt',
|
|
31
|
+
pageSize: 50,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const res = http.get(url, params);
|
|
36
|
+
|
|
37
|
+
check(res, {
|
|
38
|
+
'status is 200': (r) => r.status === 200,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// sleep(1);
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const http = require('k6/http');
|
|
2
|
+
const { check } = require('k6');
|
|
3
|
+
|
|
4
|
+
export function setup() {
|
|
5
|
+
// 在测试开始前执行一次
|
|
6
|
+
let res = http.post(`${__ENV.TARGET_ORIGIN}/api/auth:signIn`, {
|
|
7
|
+
account: 'nocobase',
|
|
8
|
+
password: 'admin123',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
check(res, { 'login succeeded': (r) => r.status === 200 });
|
|
12
|
+
|
|
13
|
+
// 假设返回里有 token
|
|
14
|
+
let data = res.json('data');
|
|
15
|
+
return { token: data.token };
|
|
16
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// __benchmarks__/k6/write-single.js
|
|
2
|
+
import http from 'k6/http';
|
|
3
|
+
import { check, sleep } from 'k6';
|
|
4
|
+
|
|
5
|
+
export { setup } from './setup.js';
|
|
6
|
+
|
|
7
|
+
export const options = {
|
|
8
|
+
stages: [
|
|
9
|
+
{ duration: '1s', target: 100 },
|
|
10
|
+
{ duration: '59s', target: 100 },
|
|
11
|
+
],
|
|
12
|
+
thresholds: {
|
|
13
|
+
http_req_duration: ['p(95)<400'], // 95% 请求 < 400ms
|
|
14
|
+
http_req_failed: ['rate<0.01'], // 失败率 < 1%
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function ({ token }) {
|
|
19
|
+
const url = `${__ENV.API_BASE_URL}/posts:update/${Math.floor(Math.random() * 1000000) + 1}`;
|
|
20
|
+
const params = {
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Authorization: `Bearer ${token}`,
|
|
24
|
+
'X-Role': 'admin',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const payload = JSON.stringify({
|
|
28
|
+
title: `Title ${__VU}-${__ITER} ${Math.random().toString(36)}`,
|
|
29
|
+
content: 'Some content',
|
|
30
|
+
categoryId: 1,
|
|
31
|
+
tags: [1, 2],
|
|
32
|
+
publishedAt: new Date().toISOString(),
|
|
33
|
+
status: 'published',
|
|
34
|
+
allowComments: true,
|
|
35
|
+
featured: false,
|
|
36
|
+
viewCount: Math.floor(Math.random() * 1000),
|
|
37
|
+
excerpt: 'This is a short excerpt of the post.',
|
|
38
|
+
musicUrl: 'https://example.com/music.mp3',
|
|
39
|
+
coverImage: 'https://example.com/cover.jpg',
|
|
40
|
+
slug: `title-${__VU}-${__ITER}-${Math.random().toString(36)}`,
|
|
41
|
+
read: 1,
|
|
42
|
+
score: 4.5,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const res = http.post(url, payload, params);
|
|
46
|
+
|
|
47
|
+
check(res, {
|
|
48
|
+
'status is 200': (r) => r.status === 200,
|
|
49
|
+
});
|
|
50
|
+
}
|
package/perf/utils.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let IDX = 36,
|
|
11
|
+
HEX = '';
|
|
12
|
+
while (IDX--) HEX += IDX.toString(36);
|
|
13
|
+
|
|
14
|
+
export function uid(len) {
|
|
15
|
+
let str = '',
|
|
16
|
+
num = len || 11;
|
|
17
|
+
while (num--) str += HEX[(Math.random() * 36) | 0];
|
|
18
|
+
return str;
|
|
19
|
+
}
|