@nocobase/database 1.7.0-beta.32 → 1.7.0-beta.34
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/lib/cursor-builder.d.ts +38 -0
- package/lib/cursor-builder.js +307 -0
- package/lib/fields/nanoid-field.js +2 -0
- package/lib/fields/uuid-field.js +2 -0
- package/lib/options-parser.d.ts +3 -1
- package/lib/options-parser.js +3 -2
- package/lib/repository.d.ts +21 -0
- package/lib/repository.js +30 -4
- package/package.json +4 -4
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import { Sequelize } from 'sequelize';
|
|
10
|
+
import { FindOptions } from './repository';
|
|
11
|
+
import { Model } from './model';
|
|
12
|
+
import { Collection } from './collection';
|
|
13
|
+
export declare class SmartCursorBuilder {
|
|
14
|
+
private sequelize;
|
|
15
|
+
private tableName;
|
|
16
|
+
private collection;
|
|
17
|
+
constructor(sequelize: Sequelize, tableName: string, collection: Collection);
|
|
18
|
+
/**
|
|
19
|
+
* 根据表结构自动选择最优游标策略
|
|
20
|
+
*/
|
|
21
|
+
private getBestCursorStrategy;
|
|
22
|
+
/**
|
|
23
|
+
* Cursor-based pagination query function.
|
|
24
|
+
* Ideal for large datasets (e.g., millions of rows)
|
|
25
|
+
* Note:
|
|
26
|
+
* 1. does not support jumping to arbitrary pages (e.g., "Page 5")
|
|
27
|
+
* 2. Requires a stable, indexed sort field (e.g. ID, createdAt)
|
|
28
|
+
* 3. If custom orderBy is used, it must match the cursor field(s) and direction, otherwise results may be incorrect or unstable.
|
|
29
|
+
* @param options
|
|
30
|
+
*/
|
|
31
|
+
chunk(options: FindOptions & {
|
|
32
|
+
chunkSize: number;
|
|
33
|
+
callback: (rows: Model[], options: FindOptions) => Promise<void>;
|
|
34
|
+
find: (options: FindOptions) => Promise<any[]>;
|
|
35
|
+
beforeFind?: (options: FindOptions) => Promise<void>;
|
|
36
|
+
afterFind?: (rows: Model[], options: FindOptions) => Promise<void>;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
var __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
38
|
+
var cursor_builder_exports = {};
|
|
39
|
+
__export(cursor_builder_exports, {
|
|
40
|
+
SmartCursorBuilder: () => SmartCursorBuilder
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(cursor_builder_exports);
|
|
43
|
+
var import_sequelize = require("sequelize");
|
|
44
|
+
var import_lodash = __toESM(require("lodash"));
|
|
45
|
+
const _SmartCursorBuilder = class _SmartCursorBuilder {
|
|
46
|
+
sequelize;
|
|
47
|
+
tableName;
|
|
48
|
+
collection;
|
|
49
|
+
constructor(sequelize, tableName, collection) {
|
|
50
|
+
this.sequelize = sequelize;
|
|
51
|
+
this.tableName = tableName;
|
|
52
|
+
this.collection = collection;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 根据表结构自动选择最优游标策略
|
|
56
|
+
*/
|
|
57
|
+
async getBestCursorStrategy() {
|
|
58
|
+
let indexInfoSql = "";
|
|
59
|
+
const dialect = this.sequelize.getDialect();
|
|
60
|
+
if (dialect === "postgres") {
|
|
61
|
+
indexInfoSql = `
|
|
62
|
+
SELECT
|
|
63
|
+
t.relname AS table_name,
|
|
64
|
+
i.relname AS index_name,
|
|
65
|
+
a.attname AS column_name,
|
|
66
|
+
array_position(ix.indkey, a.attnum) + 1 AS seq_in_index,
|
|
67
|
+
CASE
|
|
68
|
+
WHEN ix.indisprimary THEN 1
|
|
69
|
+
WHEN ix.indisunique THEN 2
|
|
70
|
+
ELSE 3
|
|
71
|
+
END AS index_type,
|
|
72
|
+
-- \u5224\u65AD\u7D22\u5F15\u6392\u5E8F\u65B9\u5411 (0=ASC, 1=DESC)
|
|
73
|
+
CASE WHEN (ix.indoption[array_position(ix.indkey, a.attnum) - 1] & 1) = 1
|
|
74
|
+
THEN 'DESC' ELSE 'ASC'
|
|
75
|
+
END AS direction
|
|
76
|
+
FROM
|
|
77
|
+
pg_class t,
|
|
78
|
+
pg_class i,
|
|
79
|
+
pg_index ix,
|
|
80
|
+
pg_attribute a,
|
|
81
|
+
pg_namespace n
|
|
82
|
+
WHERE
|
|
83
|
+
t.oid = ix.indrelid
|
|
84
|
+
AND i.oid = ix.indexrelid
|
|
85
|
+
AND a.attrelid = t.oid
|
|
86
|
+
AND t.relnamespace = n.oid
|
|
87
|
+
AND a.attnum = ANY(ix.indkey)
|
|
88
|
+
AND t.relkind = 'r'
|
|
89
|
+
AND n.nspname = current_schema()
|
|
90
|
+
AND t.relname = $1
|
|
91
|
+
ORDER BY
|
|
92
|
+
i.relname,
|
|
93
|
+
array_position(ix.indkey, a.attnum)
|
|
94
|
+
`;
|
|
95
|
+
} else if (dialect === "mariadb" || dialect === "mysql") {
|
|
96
|
+
indexInfoSql = `
|
|
97
|
+
SELECT
|
|
98
|
+
i.TABLE_NAME,
|
|
99
|
+
i.INDEX_NAME,
|
|
100
|
+
i.COLUMN_NAME,
|
|
101
|
+
i.SEQ_IN_INDEX,
|
|
102
|
+
CASE
|
|
103
|
+
WHEN i.INDEX_NAME = 'PRIMARY' THEN 1
|
|
104
|
+
WHEN i.NON_UNIQUE = 0 THEN 2
|
|
105
|
+
ELSE 3
|
|
106
|
+
END as INDEX_TYPE
|
|
107
|
+
FROM
|
|
108
|
+
information_schema.STATISTICS i
|
|
109
|
+
WHERE
|
|
110
|
+
i.TABLE_SCHEMA = DATABASE()
|
|
111
|
+
AND i.TABLE_NAME = ?
|
|
112
|
+
ORDER BY
|
|
113
|
+
i.INDEX_NAME,
|
|
114
|
+
i.SEQ_IN_INDEX;
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
const indexRows = await this.sequelize.query(indexInfoSql, {
|
|
118
|
+
type: import_sequelize.QueryTypes.SELECT,
|
|
119
|
+
replacements: [this.tableName],
|
|
120
|
+
raw: true
|
|
121
|
+
});
|
|
122
|
+
const indexes = /* @__PURE__ */ new Map();
|
|
123
|
+
const indexDirections = /* @__PURE__ */ new Map();
|
|
124
|
+
if (!indexRows || indexRows.length === 0) {
|
|
125
|
+
if (Array.isArray(this.collection.filterTargetKey)) {
|
|
126
|
+
return new CompositeKeyCursorStrategy(this.collection.filterTargetKey);
|
|
127
|
+
}
|
|
128
|
+
return new SingleColumnCursorStrategy(this.collection.filterTargetKey);
|
|
129
|
+
}
|
|
130
|
+
for (const row of indexRows) {
|
|
131
|
+
const indexName = dialect === "postgres" ? row.index_name : row.INDEX_NAME;
|
|
132
|
+
const columnName = dialect === "postgres" ? row.column_name : row.COLUMN_NAME;
|
|
133
|
+
const indexType = dialect === "postgres" ? row.index_type : row.INDEX_TYPE;
|
|
134
|
+
if (dialect === "postgres" && row.direction) {
|
|
135
|
+
if (!indexDirections.has(indexName)) {
|
|
136
|
+
indexDirections.set(indexName, /* @__PURE__ */ new Map());
|
|
137
|
+
}
|
|
138
|
+
indexDirections.get(indexName).set(columnName, row.direction);
|
|
139
|
+
}
|
|
140
|
+
if (!indexes.has(indexName)) {
|
|
141
|
+
indexes.set(indexName, {
|
|
142
|
+
name: indexName,
|
|
143
|
+
columns: [],
|
|
144
|
+
isPrimary: dialect === "postgres" ? indexType === 1 : indexName === "PRIMARY",
|
|
145
|
+
isUnique: indexType < 3
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const index = indexes.get(row.INDEX_NAME);
|
|
149
|
+
index.columns[row.SEQ_IN_INDEX - 1] = row.COLUMN_NAME;
|
|
150
|
+
}
|
|
151
|
+
for (const index of indexes.values()) {
|
|
152
|
+
if (index.isPrimary) {
|
|
153
|
+
if (index.columns.length === 1) {
|
|
154
|
+
return new SingleColumnCursorStrategy(index.columns[0]);
|
|
155
|
+
} else {
|
|
156
|
+
if (dialect === "postgres" && indexDirections.has(index.name)) {
|
|
157
|
+
const directions = index.columns.map((col) => indexDirections.get(index.name).get(col) || "ASC");
|
|
158
|
+
return new CompositeKeyCursorStrategy(index.columns, directions);
|
|
159
|
+
} else {
|
|
160
|
+
return new CompositeKeyCursorStrategy(index.columns);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
let singleColumnUniqueIndex = null;
|
|
166
|
+
let multiColumnUniqueIndex = null;
|
|
167
|
+
for (const index of indexes.values()) {
|
|
168
|
+
if (index.isUnique && !index.isPrimary) {
|
|
169
|
+
if (index.columns.length === 1 && !singleColumnUniqueIndex) {
|
|
170
|
+
singleColumnUniqueIndex = index;
|
|
171
|
+
} else if (index.columns.length > 1 && !multiColumnUniqueIndex) {
|
|
172
|
+
multiColumnUniqueIndex = index;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (singleColumnUniqueIndex) {
|
|
177
|
+
return new SingleColumnCursorStrategy(singleColumnUniqueIndex.columns[0]);
|
|
178
|
+
}
|
|
179
|
+
if (multiColumnUniqueIndex) {
|
|
180
|
+
return new CompositeKeyCursorStrategy(multiColumnUniqueIndex.columns);
|
|
181
|
+
}
|
|
182
|
+
let anyIndex = null;
|
|
183
|
+
for (const index of indexes.values()) {
|
|
184
|
+
if (index.columns.length > 0 && !index.isPrimary && !index.isUnique) {
|
|
185
|
+
anyIndex = index;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (anyIndex) {
|
|
190
|
+
if (anyIndex.columns.length === 1) {
|
|
191
|
+
return new SingleColumnCursorStrategy(anyIndex.columns[0]);
|
|
192
|
+
} else {
|
|
193
|
+
return new CompositeKeyCursorStrategy(anyIndex.columns);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Cursor-based pagination query function.
|
|
199
|
+
* Ideal for large datasets (e.g., millions of rows)
|
|
200
|
+
* Note:
|
|
201
|
+
* 1. does not support jumping to arbitrary pages (e.g., "Page 5")
|
|
202
|
+
* 2. Requires a stable, indexed sort field (e.g. ID, createdAt)
|
|
203
|
+
* 3. If custom orderBy is used, it must match the cursor field(s) and direction, otherwise results may be incorrect or unstable.
|
|
204
|
+
* @param options
|
|
205
|
+
*/
|
|
206
|
+
async chunk(options) {
|
|
207
|
+
const cursorStrategy = await this.getBestCursorStrategy();
|
|
208
|
+
let cursorRecord = null;
|
|
209
|
+
let hasMoreData = true;
|
|
210
|
+
let isFirst = true;
|
|
211
|
+
options.order = cursorStrategy.buildSort();
|
|
212
|
+
options["parseSort"] = false;
|
|
213
|
+
while (hasMoreData) {
|
|
214
|
+
if (!isFirst) {
|
|
215
|
+
options.where = cursorStrategy.buildWhere(options.where, cursorRecord);
|
|
216
|
+
}
|
|
217
|
+
if (isFirst) {
|
|
218
|
+
isFirst = false;
|
|
219
|
+
}
|
|
220
|
+
options.limit = options.chunkSize || 1e3;
|
|
221
|
+
if (options.beforeFind) {
|
|
222
|
+
await options.beforeFind(options);
|
|
223
|
+
}
|
|
224
|
+
const records = await options.find(import_lodash.default.omit(options, "callback", "beforeFind", "afterFind", "chunkSize", "find"));
|
|
225
|
+
if (options.afterFind) {
|
|
226
|
+
await options.afterFind(records, options);
|
|
227
|
+
}
|
|
228
|
+
if (records.length === 0) {
|
|
229
|
+
hasMoreData = false;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
await options.callback(records, options);
|
|
233
|
+
cursorRecord = records[records.length - 1];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
__name(_SmartCursorBuilder, "SmartCursorBuilder");
|
|
238
|
+
let SmartCursorBuilder = _SmartCursorBuilder;
|
|
239
|
+
const _SingleColumnCursorStrategy = class _SingleColumnCursorStrategy {
|
|
240
|
+
columnName;
|
|
241
|
+
constructor(columnName) {
|
|
242
|
+
this.columnName = columnName;
|
|
243
|
+
}
|
|
244
|
+
buildSort() {
|
|
245
|
+
return [[this.columnName, "ASC"]];
|
|
246
|
+
}
|
|
247
|
+
buildWhere(baseWhere, record) {
|
|
248
|
+
if (!record) {
|
|
249
|
+
return baseWhere;
|
|
250
|
+
}
|
|
251
|
+
return { ...baseWhere, [this.columnName]: { [import_sequelize.Op.gt]: record[this.columnName] } };
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
__name(_SingleColumnCursorStrategy, "SingleColumnCursorStrategy");
|
|
255
|
+
let SingleColumnCursorStrategy = _SingleColumnCursorStrategy;
|
|
256
|
+
const _CompositeKeyCursorStrategy = class _CompositeKeyCursorStrategy {
|
|
257
|
+
columns;
|
|
258
|
+
directions;
|
|
259
|
+
constructor(columns, directions) {
|
|
260
|
+
this.columns = columns;
|
|
261
|
+
this.directions = directions || Array(columns.length).fill("ASC");
|
|
262
|
+
}
|
|
263
|
+
buildSort() {
|
|
264
|
+
const orderBy = [];
|
|
265
|
+
for (let i = 0; i < this.columns.length; i++) {
|
|
266
|
+
orderBy.push([this.columns[i], this.directions[i]]);
|
|
267
|
+
}
|
|
268
|
+
return orderBy;
|
|
269
|
+
}
|
|
270
|
+
buildWhere(baseWhere, record) {
|
|
271
|
+
if (!record) {
|
|
272
|
+
return baseWhere;
|
|
273
|
+
}
|
|
274
|
+
const whereConditions = [];
|
|
275
|
+
for (let i = 0; i < this.columns.length; i++) {
|
|
276
|
+
const column = this.columns[i];
|
|
277
|
+
if (i > 0) {
|
|
278
|
+
const equalConditions = {};
|
|
279
|
+
for (let j = 0; j < i; j++) {
|
|
280
|
+
equalConditions[this.columns[j]] = record[this.columns[j]];
|
|
281
|
+
}
|
|
282
|
+
whereConditions.push({
|
|
283
|
+
...equalConditions,
|
|
284
|
+
[column]: {
|
|
285
|
+
[import_sequelize.Op.gt]: record[column]
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
whereConditions.push({
|
|
290
|
+
[column]: {
|
|
291
|
+
[import_sequelize.Op.gt]: record[column]
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const cursorCondition = {
|
|
297
|
+
[import_sequelize.Op.or]: whereConditions
|
|
298
|
+
};
|
|
299
|
+
return baseWhere ? { [import_sequelize.Op.and]: [baseWhere, cursorCondition] } : cursorCondition;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
__name(_CompositeKeyCursorStrategy, "CompositeKeyCursorStrategy");
|
|
303
|
+
let CompositeKeyCursorStrategy = _CompositeKeyCursorStrategy;
|
|
304
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
305
|
+
0 && (module.exports = {
|
|
306
|
+
SmartCursorBuilder
|
|
307
|
+
});
|
|
@@ -51,10 +51,12 @@ const _NanoidField = class _NanoidField extends import_field.Field {
|
|
|
51
51
|
bind() {
|
|
52
52
|
super.bind();
|
|
53
53
|
this.on("beforeValidate", this.listener);
|
|
54
|
+
this.on("beforeCreate", this.listener);
|
|
54
55
|
}
|
|
55
56
|
unbind() {
|
|
56
57
|
super.unbind();
|
|
57
58
|
this.off("beforeValidate", this.listener);
|
|
59
|
+
this.off("beforeCreate", this.listener);
|
|
58
60
|
}
|
|
59
61
|
};
|
|
60
62
|
__name(_NanoidField, "NanoidField");
|
package/lib/fields/uuid-field.js
CHANGED
|
@@ -49,10 +49,12 @@ const _UuidField = class _UuidField extends import_field.Field {
|
|
|
49
49
|
bind() {
|
|
50
50
|
super.bind();
|
|
51
51
|
this.on("beforeValidate", this.listener);
|
|
52
|
+
this.on("beforeCreate", this.listener);
|
|
52
53
|
}
|
|
53
54
|
unbind() {
|
|
54
55
|
super.unbind();
|
|
55
56
|
this.off("beforeValidate", this.listener);
|
|
57
|
+
this.off("beforeCreate", this.listener);
|
|
56
58
|
}
|
|
57
59
|
};
|
|
58
60
|
__name(_UuidField, "UuidField");
|
package/lib/options-parser.d.ts
CHANGED
|
@@ -27,7 +27,9 @@ export declare class OptionsParser {
|
|
|
27
27
|
isAssociation(key: string): boolean;
|
|
28
28
|
isAssociationPath(path: string): boolean;
|
|
29
29
|
filterByTkToWhereOption(): {};
|
|
30
|
-
toSequelizeParams(
|
|
30
|
+
toSequelizeParams(options?: {
|
|
31
|
+
parseSort?: boolean;
|
|
32
|
+
}): any;
|
|
31
33
|
/**
|
|
32
34
|
* parser sort options
|
|
33
35
|
* @param filterParams
|
package/lib/options-parser.js
CHANGED
|
@@ -111,7 +111,7 @@ const _OptionsParser = class _OptionsParser {
|
|
|
111
111
|
[filterTargetKey]: filterByTkOption
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
-
toSequelizeParams() {
|
|
114
|
+
toSequelizeParams(options = { parseSort: true }) {
|
|
115
115
|
var _a, _b;
|
|
116
116
|
const queryParams = this.filterParser.toSequelizeParams();
|
|
117
117
|
if ((_a = this.options) == null ? void 0 : _a.filterByTk) {
|
|
@@ -126,7 +126,8 @@ const _OptionsParser = class _OptionsParser {
|
|
|
126
126
|
}
|
|
127
127
|
queryParams.include.push(...import_lodash.default.castArray(this.options.include));
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
const fields = this.parseFields(queryParams);
|
|
130
|
+
return options.parseSort ? this.parseSort(fields) : fields;
|
|
130
131
|
}
|
|
131
132
|
/**
|
|
132
133
|
* parser sort options
|
package/lib/repository.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { HasManyRepository } from './relation-repository/hasmany-repository';
|
|
|
20
20
|
import { HasOneRepository } from './relation-repository/hasone-repository';
|
|
21
21
|
import { RelationRepository } from './relation-repository/relation-repository';
|
|
22
22
|
import { valuesToFilter } from './utils/filter-utils';
|
|
23
|
+
import { SmartCursorBuilder } from './cursor-builder';
|
|
23
24
|
interface CreateManyOptions extends BulkCreateOptions {
|
|
24
25
|
records: Values[];
|
|
25
26
|
}
|
|
@@ -145,6 +146,7 @@ export declare class Repository<TModelAttributes extends {} = any, TCreationAttr
|
|
|
145
146
|
database: Database;
|
|
146
147
|
collection: Collection;
|
|
147
148
|
model: ModelStatic<Model>;
|
|
149
|
+
cursorBuilder: SmartCursorBuilder;
|
|
148
150
|
constructor(collection: Collection);
|
|
149
151
|
static valuesToFilter: typeof valuesToFilter;
|
|
150
152
|
/**
|
|
@@ -157,6 +159,25 @@ export declare class Repository<TModelAttributes extends {} = any, TCreationAttr
|
|
|
157
159
|
chunk(options: FindOptions & {
|
|
158
160
|
chunkSize: number;
|
|
159
161
|
callback: (rows: Model[], options: FindOptions) => Promise<void>;
|
|
162
|
+
beforeFind?: (options: FindOptions) => Promise<void>;
|
|
163
|
+
afterFind?: (rows: Model[], options: FindOptions & {
|
|
164
|
+
offset: number;
|
|
165
|
+
}) => Promise<void>;
|
|
166
|
+
}): Promise<void>;
|
|
167
|
+
/**
|
|
168
|
+
* Cursor-based pagination query function.
|
|
169
|
+
* Ideal for large datasets (e.g., millions of rows)
|
|
170
|
+
* Note:
|
|
171
|
+
* 1. does not support jumping to arbitrary pages (e.g., "Page 5")
|
|
172
|
+
* 2. Requires a stable, indexed sort field (e.g. ID, createdAt)
|
|
173
|
+
* 3. If custom orderBy is used, it must match the cursor field(s) and direction, otherwise results may be incorrect or unstable.
|
|
174
|
+
* @param options
|
|
175
|
+
*/
|
|
176
|
+
chunkWithCursor(options: FindOptions & {
|
|
177
|
+
chunkSize: number;
|
|
178
|
+
callback: (rows: Model[], options: FindOptions) => Promise<void>;
|
|
179
|
+
beforeFind?: (options: FindOptions) => Promise<void>;
|
|
180
|
+
afterFind?: (rows: Model[], options: FindOptions) => Promise<void>;
|
|
160
181
|
}): Promise<void>;
|
|
161
182
|
/**
|
|
162
183
|
* find
|
package/lib/repository.js
CHANGED
|
@@ -70,6 +70,8 @@ var import_hasone_repository = require("./relation-repository/hasone-repository"
|
|
|
70
70
|
var import_update_associations = require("./update-associations");
|
|
71
71
|
var import_update_guard = require("./update-guard");
|
|
72
72
|
var import_filter_utils = require("./utils/filter-utils");
|
|
73
|
+
var import_lodash2 = __toESM(require("lodash"));
|
|
74
|
+
var import_cursor_builder = require("./cursor-builder");
|
|
73
75
|
var import_sequelize2 = require("sequelize");
|
|
74
76
|
const debug = require("debug")("noco-database");
|
|
75
77
|
const transaction = (0, import_transaction_decorator.transactionWrapperBuilder)(function() {
|
|
@@ -121,10 +123,12 @@ const _Repository = class _Repository {
|
|
|
121
123
|
database;
|
|
122
124
|
collection;
|
|
123
125
|
model;
|
|
126
|
+
cursorBuilder;
|
|
124
127
|
constructor(collection) {
|
|
125
128
|
this.database = collection.context.database;
|
|
126
129
|
this.collection = collection;
|
|
127
130
|
this.model = collection.model;
|
|
131
|
+
this.cursorBuilder = new import_cursor_builder.SmartCursorBuilder(this.database.sequelize, this.model.tableName, this.collection);
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
134
|
* return count by filter
|
|
@@ -205,18 +209,25 @@ const _Repository = class _Repository {
|
|
|
205
209
|
return await this.model.aggregate(field, method, queryOptions);
|
|
206
210
|
}
|
|
207
211
|
async chunk(options) {
|
|
208
|
-
const { chunkSize, callback, limit: overallLimit } = options;
|
|
212
|
+
const { chunkSize, callback, limit: overallLimit, beforeFind, afterFind } = options;
|
|
209
213
|
const transaction2 = await this.getTransaction(options);
|
|
210
214
|
let offset = 0;
|
|
211
215
|
let totalProcessed = 0;
|
|
212
216
|
while (true) {
|
|
213
217
|
const currentLimit = overallLimit !== void 0 ? Math.min(chunkSize, overallLimit - totalProcessed) : chunkSize;
|
|
214
|
-
const
|
|
218
|
+
const findOptions = {
|
|
215
219
|
...options,
|
|
216
220
|
limit: currentLimit,
|
|
217
221
|
offset,
|
|
218
222
|
transaction: transaction2
|
|
219
|
-
}
|
|
223
|
+
};
|
|
224
|
+
if (beforeFind) {
|
|
225
|
+
await beforeFind(findOptions);
|
|
226
|
+
}
|
|
227
|
+
const rows = await this.find(findOptions);
|
|
228
|
+
if (afterFind) {
|
|
229
|
+
await afterFind(rows, { ...findOptions, offset });
|
|
230
|
+
}
|
|
220
231
|
if (rows.length === 0) {
|
|
221
232
|
break;
|
|
222
233
|
}
|
|
@@ -228,6 +239,21 @@ const _Repository = class _Repository {
|
|
|
228
239
|
}
|
|
229
240
|
}
|
|
230
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Cursor-based pagination query function.
|
|
244
|
+
* Ideal for large datasets (e.g., millions of rows)
|
|
245
|
+
* Note:
|
|
246
|
+
* 1. does not support jumping to arbitrary pages (e.g., "Page 5")
|
|
247
|
+
* 2. Requires a stable, indexed sort field (e.g. ID, createdAt)
|
|
248
|
+
* 3. If custom orderBy is used, it must match the cursor field(s) and direction, otherwise results may be incorrect or unstable.
|
|
249
|
+
* @param options
|
|
250
|
+
*/
|
|
251
|
+
async chunkWithCursor(options) {
|
|
252
|
+
return await this.cursorBuilder.chunk({
|
|
253
|
+
...options,
|
|
254
|
+
find: this.find.bind(this)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
231
257
|
/**
|
|
232
258
|
* find
|
|
233
259
|
* @param options
|
|
@@ -535,7 +561,7 @@ const _Repository = class _Repository {
|
|
|
535
561
|
const parser = new import_options_parser.OptionsParser(options, {
|
|
536
562
|
collection: this.collection
|
|
537
563
|
});
|
|
538
|
-
const params = parser.toSequelizeParams();
|
|
564
|
+
const params = parser.toSequelizeParams({ parseSort: import_lodash2.default.isBoolean(options == null ? void 0 : options.parseSort) ? options.parseSort : true });
|
|
539
565
|
debug("sequelize query params %o", params);
|
|
540
566
|
if (options.where && params.where) {
|
|
541
567
|
params.where = {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/database",
|
|
3
|
-
"version": "1.7.0-beta.
|
|
3
|
+
"version": "1.7.0-beta.34",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
7
7
|
"license": "AGPL-3.0",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@nocobase/logger": "1.7.0-beta.
|
|
10
|
-
"@nocobase/utils": "1.7.0-beta.
|
|
9
|
+
"@nocobase/logger": "1.7.0-beta.34",
|
|
10
|
+
"@nocobase/utils": "1.7.0-beta.34",
|
|
11
11
|
"async-mutex": "^0.3.2",
|
|
12
12
|
"chalk": "^4.1.1",
|
|
13
13
|
"cron-parser": "4.4.0",
|
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
|
39
39
|
"directory": "packages/database"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "2795864162d6151f0f8cf33945b0b8b8a4cb20dc"
|
|
42
42
|
}
|