@nocobase/plugin-field-sequence 3.0.0-alpha.5 → 3.0.0-alpha.7

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.
@@ -9,18 +9,18 @@
9
9
 
10
10
  module.exports = {
11
11
  "react": "18.2.0",
12
- "@nocobase/client": "3.0.0-alpha.5",
13
- "@nocobase/flow-engine": "3.0.0-alpha.5",
12
+ "@nocobase/client": "3.0.0-alpha.7",
13
+ "@nocobase/flow-engine": "3.0.0-alpha.7",
14
14
  "@formily/antd-v5": "1.2.3",
15
15
  "@formily/core": "2.3.7",
16
16
  "@formily/react": "2.3.7",
17
- "@nocobase/utils": "3.0.0-alpha.5",
17
+ "@nocobase/utils": "3.0.0-alpha.7",
18
18
  "antd": "5.24.2",
19
19
  "react-i18next": "11.18.6",
20
20
  "@ant-design/icons": "5.6.1",
21
- "@nocobase/client-v2": "3.0.0-alpha.5",
21
+ "@nocobase/client-v2": "3.0.0-alpha.7",
22
22
  "lodash": "4.18.1",
23
- "@nocobase/server": "3.0.0-alpha.5",
24
- "@nocobase/database": "3.0.0-alpha.5",
23
+ "@nocobase/server": "3.0.0-alpha.7",
24
+ "@nocobase/database": "3.0.0-alpha.7",
25
25
  "dayjs": "1.11.13"
26
26
  };
@@ -1 +1 @@
1
- {"name":"cron-parser","version":"4.4.0","description":"Node.js library for parsing crontab instructions","main":"lib/parser.js","types":"index.d.ts","typesVersions":{"<4.1":{"*":["types/ts3/*"]}},"directories":{"test":"test"},"scripts":{"test:tsd":"tsd","test:unit":"TZ=UTC tap ./test/*.js","test:cover":"TZ=UTC tap --coverage-report=html ./test/*.js","lint":"eslint .","lint:fix":"eslint --fix .","test":"npm run lint && npm run test:unit && npm run test:tsd"},"repository":{"type":"git","url":"https://github.com/harrisiirak/cron-parser.git"},"keywords":["cron","crontab","parser"],"author":"Harri Siirak","contributors":["Nicholas Clawson","Daniel Prentis <daniel@salsitasoft.com>","Renault John Lecoultre","Richard Astbury <richard.astbury@gmail.com>","Meaglin Wasabi <Meaglin.wasabi@gmail.com>","Mike Kusold <hello@mikekusold.com>","Alex Kit <alex.kit@atmajs.com>","Santiago Gimeno <santiago.gimeno@gmail.com>","Daniel <darc.tec@gmail.com>","Christian Steininger <christian.steininger.cs@gmail.com>","Mykola Piskovyi <m.piskovyi@gmail.com>","Brian Vaughn <brian.david.vaughn@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Yasuhiroki <yasuhiroki.duck@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Brendan Warkentin <faazshift@gmail.com>","Charlie Fish <fishcharlie.code@gmail.com>","Ian Graves <ian+diskimage@iangrav.es>","Andy Thompson <me@andytson.com>","Regev Brody <regevbr@gmail.com>"],"license":"MIT","dependencies":{"luxon":"^1.28.0"},"devDependencies":{"eslint":"^8.2.0","sinon":"^10.0.0","tap":"^16.0.1","tsd":"^0.19.0"},"engines":{"node":">=0.8"},"browser":{"fs":false},"tap":{"check-coverage":false},"tsd":{"directory":"test","compilerOptions":{"lib":["es2017","dom"]}},"_lastModified":"2026-08-01T20:52:30.511Z"}
1
+ {"name":"cron-parser","version":"4.4.0","description":"Node.js library for parsing crontab instructions","main":"lib/parser.js","types":"index.d.ts","typesVersions":{"<4.1":{"*":["types/ts3/*"]}},"directories":{"test":"test"},"scripts":{"test:tsd":"tsd","test:unit":"TZ=UTC tap ./test/*.js","test:cover":"TZ=UTC tap --coverage-report=html ./test/*.js","lint":"eslint .","lint:fix":"eslint --fix .","test":"npm run lint && npm run test:unit && npm run test:tsd"},"repository":{"type":"git","url":"https://github.com/harrisiirak/cron-parser.git"},"keywords":["cron","crontab","parser"],"author":"Harri Siirak","contributors":["Nicholas Clawson","Daniel Prentis <daniel@salsitasoft.com>","Renault John Lecoultre","Richard Astbury <richard.astbury@gmail.com>","Meaglin Wasabi <Meaglin.wasabi@gmail.com>","Mike Kusold <hello@mikekusold.com>","Alex Kit <alex.kit@atmajs.com>","Santiago Gimeno <santiago.gimeno@gmail.com>","Daniel <darc.tec@gmail.com>","Christian Steininger <christian.steininger.cs@gmail.com>","Mykola Piskovyi <m.piskovyi@gmail.com>","Brian Vaughn <brian.david.vaughn@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Yasuhiroki <yasuhiroki.duck@gmail.com>","Nicholas Clawson <nickclaw@gmail.com>","Brendan Warkentin <faazshift@gmail.com>","Charlie Fish <fishcharlie.code@gmail.com>","Ian Graves <ian+diskimage@iangrav.es>","Andy Thompson <me@andytson.com>","Regev Brody <regevbr@gmail.com>"],"license":"MIT","dependencies":{"luxon":"^1.28.0"},"devDependencies":{"eslint":"^8.2.0","sinon":"^10.0.0","tap":"^16.0.1","tsd":"^0.19.0"},"engines":{"node":">=0.8"},"browser":{"fs":false},"tap":{"check-coverage":false},"tsd":{"directory":"test","compilerOptions":{"lib":["es2017","dom"]}},"_lastModified":"2026-08-05T22:31:38.309Z"}
@@ -152,15 +152,11 @@ class PluginFieldSequenceServer extends import_server.Plugin {
152
152
  );
153
153
  return;
154
154
  }
155
- const [record] = await collection.model.findAll({
156
- order: [[(autoIncrementField == null ? void 0 : autoIncrementField.name) ?? (createAtField == null ? void 0 : createAtField.name), "DESC"]],
157
- limit: 1
158
- });
159
- if (!record) {
160
- app.log.warn(`Collection [${collection.name}] has no records. Skipping sequences repair.`);
161
- return;
162
- }
163
155
  const sequencesFieldSet = import_lodash.default.uniq(sequencesList.map(({ field }) => field));
156
+ const primaryKeyFields = fields.filter((field) => field.options.primaryKey);
157
+ const sortablePrimaryKeyFields = primaryKeyFields.filter(
158
+ (field) => field.type === "bigInt" || field.type === "snowflakeId"
159
+ );
164
160
  for (const sequencesField of sequencesFieldSet) {
165
161
  const field = fieldMap[sequencesField];
166
162
  app.log.info(
@@ -178,7 +174,89 @@ class PluginFieldSequenceServer extends import_server.Plugin {
178
174
  );
179
175
  continue;
180
176
  }
181
- await field.update(record, { overwrite: true });
177
+ const selectedFields = import_lodash.default.uniq([
178
+ field.name,
179
+ ...primaryKeyFields.map((primaryKeyField) => primaryKeyField.name),
180
+ ...createAtField ? [createAtField.name] : []
181
+ ]);
182
+ const latestRecordOrderField = createAtField ?? autoIncrementField;
183
+ const latestRecordOrder = [
184
+ [latestRecordOrderField.name, "DESC"],
185
+ ...sortablePrimaryKeyFields.filter((primaryKeyField) => primaryKeyField !== latestRecordOrderField).map((primaryKeyField) => [primaryKeyField.name, "DESC"])
186
+ ];
187
+ const latestRecordFilter = {
188
+ [field.name]: { $ne: null },
189
+ ...createAtField ? { [createAtField.name]: { $ne: null } } : {}
190
+ };
191
+ const chunkSize = 1e3;
192
+ const latestRecordQuery = {
193
+ fields: selectedFields,
194
+ filter: latestRecordFilter,
195
+ order: latestRecordOrder
196
+ };
197
+ latestRecordQuery["parseSort"] = false;
198
+ const [latestCandidate] = await collection.repository.find({ ...latestRecordQuery, limit: 1 });
199
+ let latestRecord = latestCandidate && field.match(latestCandidate.get(field.name)) ? latestCandidate : void 0;
200
+ let offset = 1;
201
+ while (!latestRecord && latestCandidate) {
202
+ const records = await collection.repository.find({
203
+ ...latestRecordQuery,
204
+ limit: chunkSize,
205
+ offset
206
+ });
207
+ latestRecord = records.find((record) => field.match(record.get(field.name)));
208
+ if (latestRecord || records.length < chunkSize) {
209
+ break;
210
+ }
211
+ offset += chunkSize;
212
+ }
213
+ if (!latestRecord) {
214
+ app.log.warn(
215
+ `Collection [${collection.name}] field [${field.name}] has no valid sequence records. Skipping sequences repair.`
216
+ );
217
+ continue;
218
+ }
219
+ const state = field.createRepairState();
220
+ if (!createAtField) {
221
+ field.collectRepairState(latestRecord, state);
222
+ await field.saveRepairState(state);
223
+ continue;
224
+ }
225
+ const latestCreatedAt = latestRecord.get(createAtField.name);
226
+ if (!(latestCreatedAt instanceof Date) || Number.isNaN(latestCreatedAt.getTime())) {
227
+ app.log.warn(
228
+ `Collection [${collection.name}] field [${field.name}] latest record has an invalid createdAt. Skipping sequences repair.`
229
+ );
230
+ continue;
231
+ }
232
+ const secondStart = new Date(Math.floor(latestCreatedAt.getTime() / 1e3) * 1e3);
233
+ const secondEnd = new Date(secondStart.getTime() + 1e3);
234
+ const latestBatchQuery = {
235
+ fields: selectedFields,
236
+ filter: {
237
+ [field.name]: { $ne: null },
238
+ [createAtField.name]: {
239
+ $gte: secondStart,
240
+ $lt: secondEnd
241
+ }
242
+ }
243
+ };
244
+ const collectRecords = async (records) => {
245
+ for (const record of records) {
246
+ field.collectRepairState(record, state, createAtField.name);
247
+ }
248
+ };
249
+ if (!app.db.inDialect("sqlite") && primaryKeyFields.length && primaryKeyFields.length === sortablePrimaryKeyFields.length) {
250
+ await collection.repository.chunkWithCursor({
251
+ ...latestBatchQuery,
252
+ chunkSize,
253
+ callback: collectRecords
254
+ });
255
+ } else {
256
+ latestBatchQuery["parseSort"] = false;
257
+ await collectRecords(await collection.repository.find(latestBatchQuery));
258
+ }
259
+ await field.saveRepairState(state);
182
260
  }
183
261
  });
184
262
  }
@@ -23,6 +23,13 @@ export interface Pattern {
23
23
  }): Promise<void>;
24
24
  }
25
25
  export declare const sequencePatterns: Registry<Pattern>;
26
+ export interface IntegerSequenceRepairState {
27
+ current: bigint;
28
+ lastGeneratedAt: Date;
29
+ cycleStart?: Date;
30
+ cycleEnd?: Date;
31
+ }
32
+ export type SequenceRepairState = Map<number, IntegerSequenceRepairState>;
26
33
  interface PatternConfig {
27
34
  type: string;
28
35
  title?: string;
@@ -41,6 +48,9 @@ export declare class SequenceField extends Field {
41
48
  setGroupValue: (instances: Model[], options: any) => Promise<void>;
42
49
  cleanHook: (_: any, options: any) => void;
43
50
  match(value: any): RegExpMatchArray;
51
+ createRepairState(): SequenceRepairState;
52
+ collectRepairState(instance: Model, state: SequenceRepairState, createdAtField?: string): void;
53
+ saveRepairState(state: SequenceRepairState): Promise<void>;
44
54
  update(instance: Model, options: any): Promise<void>;
45
55
  bind(): void;
46
56
  unbind(): void;
@@ -64,6 +64,47 @@ function parseBigInt(str, radix = 10) {
64
64
  }
65
65
  return negative ? -result : result;
66
66
  }
67
+ function getCycleBounds(recordTime, cycle) {
68
+ const interval = import_cron_parser.default.parseExpression(cycle, { currentDate: recordTime });
69
+ const previous = interval.prev();
70
+ const next = interval.next();
71
+ if (next.getTime() === recordTime.getTime()) {
72
+ return {
73
+ cycleStart: new Date(next.getTime()),
74
+ cycleEnd: new Date(interval.next().getTime())
75
+ };
76
+ }
77
+ return {
78
+ cycleStart: new Date(previous.getTime()),
79
+ cycleEnd: new Date(next.getTime())
80
+ };
81
+ }
82
+ function mergeSequenceRepairState(state, current, recordTime, cycle) {
83
+ if (!state) {
84
+ return {
85
+ current,
86
+ lastGeneratedAt: recordTime,
87
+ ...cycle ? getCycleBounds(recordTime, cycle) : {}
88
+ };
89
+ }
90
+ if (cycle) {
91
+ const cycleBounds = state.cycleStart && state.cycleEnd ? { cycleStart: state.cycleStart, cycleEnd: state.cycleEnd } : getCycleBounds(state.lastGeneratedAt, cycle);
92
+ if (recordTime.getTime() >= cycleBounds.cycleEnd.getTime()) {
93
+ return {
94
+ current,
95
+ lastGeneratedAt: recordTime,
96
+ ...getCycleBounds(recordTime, cycle)
97
+ };
98
+ }
99
+ if (recordTime.getTime() < cycleBounds.cycleStart.getTime()) {
100
+ return state;
101
+ }
102
+ }
103
+ if (current > state.current) {
104
+ return { ...state, current, lastGeneratedAt: recordTime };
105
+ }
106
+ return state;
107
+ }
67
108
  sequencePatterns.register("string", {
68
109
  validate(options) {
69
110
  if (!(options == null ? void 0 : options.value)) {
@@ -445,6 +486,54 @@ class SequenceField extends import_database.Field {
445
486
  match(value) {
446
487
  return typeof value === "string" ? value.match(this.matcher) : null;
447
488
  }
489
+ createRepairState() {
490
+ return /* @__PURE__ */ new Map();
491
+ }
492
+ collectRepairState(instance, state, createdAtField) {
493
+ const { name, patterns } = this.options;
494
+ const matched = this.match(instance.get(name));
495
+ if (!matched) {
496
+ return;
497
+ }
498
+ const recordTime = instance.get(createdAtField ?? "createdAt");
499
+ const generatedAt = recordTime ?? /* @__PURE__ */ new Date();
500
+ patterns.forEach((pattern, index) => {
501
+ if (pattern.type !== "integer") {
502
+ return;
503
+ }
504
+ const { base = 10, cycle, key } = pattern.options;
505
+ const current = parseBigInt(matched[index + 1], base);
506
+ state.set(key, mergeSequenceRepairState(state.get(key), current, generatedAt, cycle));
507
+ });
508
+ }
509
+ async saveRepairState(state) {
510
+ const SeqRepo = this.database.getRepository("sequences");
511
+ for (const [key, values] of state) {
512
+ const lastSeq = await SeqRepo.findOne({
513
+ filter: {
514
+ collection: this.collection.name,
515
+ field: this.name,
516
+ key
517
+ }
518
+ });
519
+ if (lastSeq) {
520
+ await lastSeq.update({
521
+ current: values.current,
522
+ lastGeneratedAt: values.lastGeneratedAt
523
+ });
524
+ } else {
525
+ await SeqRepo.create({
526
+ values: {
527
+ collection: this.collection.name,
528
+ field: this.name,
529
+ key,
530
+ current: values.current,
531
+ lastGeneratedAt: values.lastGeneratedAt
532
+ }
533
+ });
534
+ }
535
+ }
536
+ }
448
537
  async update(instance, options) {
449
538
  const { name, patterns } = this.options;
450
539
  const matched = this.match(instance.get(name));
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.",
7
7
  "description.ru-RU": "Автоматически генерирует коды по заданным правилам с поддержкой комбинаций дат, чисел и текста.",
8
8
  "description.zh-CN": "根据配置的规则自动生成编码,支持日期、数字、文本的组合。",
9
- "version": "3.0.0-alpha.5",
9
+ "version": "3.0.0-alpha.7",
10
10
  "license": "Apache-2.0",
11
11
  "main": "./dist/server/index.js",
12
12
  "homepage": "https://docs.nocobase.com/handbook/field-sequence",
@@ -33,7 +33,7 @@
33
33
  "@nocobase/test": "2.x",
34
34
  "@nocobase/utils": "2.x"
35
35
  },
36
- "gitHead": "82c7ed1cdb2f247c190582e34ee37f154cac0968",
36
+ "gitHead": "19cd28c710f3b3c614ebe2fd7d5a415d527ee559",
37
37
  "keywords": [
38
38
  "Collection fields"
39
39
  ]