befly 3.22.9 → 3.23.0

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/checks/api.js CHANGED
@@ -98,14 +98,17 @@ const apiSchema = z
98
98
  const apiListSchema = z.array(apiSchema);
99
99
 
100
100
  export async function checkApi(apis) {
101
- let hasError = false;
102
-
103
- const schemaResult = apiListSchema.safeParse(apis);
104
- if (!schemaResult.success) {
105
- const errors = formatZodIssues(schemaResult.error.issues, { items: apis, itemLabel: "api" });
106
- Logger.warn("接口校验失败", { errors: errors }, false);
107
- hasError = true;
101
+ const result = apiListSchema.safeParse(apis);
102
+ if (result.success) {
103
+ return false;
108
104
  }
109
105
 
110
- return hasError;
106
+ Logger.warn(
107
+ "接口校验失败",
108
+ {
109
+ errors: formatZodIssues(result.error.issues, { items: apis, itemLabel: "api" })
110
+ },
111
+ false
112
+ );
113
+ return true;
111
114
  }
package/checks/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
 
3
- import { Logger } from "../lib/logger.js";
4
3
  import { RUN_MODE_VALUES } from "../configs/constConfig.js";
4
+ import { Logger } from "../lib/logger.js";
5
5
  import { formatZodIssues } from "../utils/formatZodIssues.js";
6
6
  import { isNoTrimStringAllowEmpty, isValidTimeZone } from "../utils/is.js";
7
7
 
@@ -109,14 +109,17 @@ const configSchema = z
109
109
  * - checkConfig 校验的是“最终合并后的运行时配置对象”,用于阻断错误配置带病启动。
110
110
  */
111
111
  export async function checkConfig(config) {
112
- let hasError = false;
113
-
114
- const schemaResult = configSchema.safeParse(config);
115
- if (!schemaResult.success) {
116
- const errors = formatZodIssues(schemaResult.error.issues, { item: config, itemLabel: "config" });
117
- Logger.warn("配置校验失败", { errors: errors }, false);
118
- hasError = true;
112
+ const result = configSchema.safeParse(config);
113
+ if (result.success) {
114
+ return false;
119
115
  }
120
116
 
121
- return hasError;
117
+ Logger.warn(
118
+ "配置校验失败",
119
+ {
120
+ errors: formatZodIssues(result.error.issues, { item: config, itemLabel: "config" })
121
+ },
122
+ false
123
+ );
124
+ return true;
122
125
  }
package/checks/hook.js CHANGED
@@ -25,12 +25,16 @@ export async function checkHook(hooks) {
25
25
  let hasError = false;
26
26
 
27
27
  for (const hook of hooks) {
28
- const schemaResult = hookSchema.safeParse(hook);
29
- if (!schemaResult.success) {
30
- const errors = formatZodIssues(schemaResult.error.issues, { item: hook, itemLabel: "hook" });
31
- Logger.warn("钩子校验失败", { errors: errors }, false);
28
+ const result = hookSchema.safeParse(hook);
29
+ if (!result.success) {
30
+ Logger.warn(
31
+ "钩子校验失败",
32
+ {
33
+ errors: formatZodIssues(result.error.issues, { item: hook, itemLabel: "hook" })
34
+ },
35
+ false
36
+ );
32
37
  hasError = true;
33
- continue;
34
38
  }
35
39
  }
36
40
 
package/checks/menu.js CHANGED
@@ -45,14 +45,17 @@ const menuListSchema = z.array(menuSchema).superRefine((menuList, refineCtx) =>
45
45
  });
46
46
 
47
47
  export const checkMenu = async (menus) => {
48
- let hasError = false;
49
-
50
- const schemaResult = menuListSchema.safeParse(menus);
51
- if (!schemaResult.success) {
52
- const errors = formatZodIssues(schemaResult.error.issues, { items: menus, itemLabel: "menus" });
53
- Logger.warn("菜单校验失败", { errors: errors }, false);
54
- hasError = true;
48
+ const result = menuListSchema.safeParse(menus);
49
+ if (result.success) {
50
+ return false;
55
51
  }
56
52
 
57
- return hasError;
53
+ Logger.warn(
54
+ "菜单校验失败",
55
+ {
56
+ errors: formatZodIssues(result.error.issues, { items: menus, itemLabel: "menus" })
57
+ },
58
+ false
59
+ );
60
+ return true;
58
61
  };
package/checks/plugin.js CHANGED
@@ -25,12 +25,16 @@ export async function checkPlugin(plugins) {
25
25
  let hasError = false;
26
26
 
27
27
  for (const plugin of plugins) {
28
- const schemaResult = pluginSchema.safeParse(plugin);
29
- if (!schemaResult.success) {
30
- const errors = formatZodIssues(schemaResult.error.issues, { item: plugin, itemLabel: "plugin" });
31
- Logger.warn("插件校验失败", { errors: errors }, false);
28
+ const result = pluginSchema.safeParse(plugin);
29
+ if (!result.success) {
30
+ Logger.warn(
31
+ "插件校验失败",
32
+ {
33
+ errors: formatZodIssues(result.error.issues, { item: plugin, itemLabel: "plugin" })
34
+ },
35
+ false
36
+ );
32
37
  hasError = true;
33
- continue;
34
38
  }
35
39
  }
36
40
 
package/checks/table.js CHANGED
@@ -4,13 +4,23 @@ import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES
4
4
  import { Logger } from "../lib/logger.js";
5
5
  import { formatZodIssues } from "../utils/formatZodIssues.js";
6
6
  import { isNoTrimStringAllowEmpty, isNullable, isRegexInput } from "../utils/is.js";
7
+ import { snakeCase } from "../utils/util.js";
7
8
 
8
9
  z.config(z.locales.zhCN());
9
10
 
10
11
  const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
12
+ const lowerSnakeRegex = /^_?[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
11
13
  const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
12
14
  const inputSchema = z.enum(FIELD_RULE_INPUT_TYPES);
13
15
 
16
+ function isValidFieldName(fieldName) {
17
+ if (String(fieldName).includes("_")) {
18
+ return lowerSnakeRegex.test(fieldName);
19
+ }
20
+
21
+ return lowerCamelRegex.test(fieldName);
22
+ }
23
+
14
24
  function addIssue(context, path, message) {
15
25
  context.addIssue({
16
26
  path: path,
@@ -75,7 +85,21 @@ const fieldDefSchema = z
75
85
  }
76
86
  });
77
87
 
78
- const tableContentSchema = z.record(z.string().regex(lowerCamelRegex), fieldDefSchema);
88
+ const tableContentSchema = z.record(z.string().refine(isValidFieldName, "字段名必须为 lowerCamelCase 或 snake_case"), fieldDefSchema).superRefine((value, context) => {
89
+ const fieldNameMap = new Map();
90
+
91
+ for (const fieldName of Object.keys(value)) {
92
+ const dbFieldName = snakeCase(fieldName);
93
+ const previousFieldName = fieldNameMap.get(dbFieldName);
94
+
95
+ if (previousFieldName) {
96
+ addIssue(context, [fieldName], `字段名 ${fieldName} 与 ${previousFieldName} 指向同一数据库字段 ${dbFieldName}`);
97
+ continue;
98
+ }
99
+
100
+ fieldNameMap.set(dbFieldName, fieldName);
101
+ }
102
+ });
79
103
  const tableRegistrySchema = z.record(z.string().regex(lowerCamelRegex), tableContentSchema);
80
104
 
81
105
  /**
@@ -83,15 +107,20 @@ const tableRegistrySchema = z.record(z.string().regex(lowerCamelRegex), tableCon
83
107
  * @throws 当检查失败时抛出异常
84
108
  */
85
109
  export async function checkTable(tables) {
86
- // 收集所有表文件
87
- let hasError = false;
88
-
89
- const schemaResult = tableRegistrySchema.safeParse(tables);
90
- if (!schemaResult.success) {
91
- const errors = formatZodIssues(schemaResult.error.issues, { items: tables, itemLabel: "table" });
92
- Logger.warn("表结构校验失败", { errors: errors }, false);
93
- hasError = true;
110
+ const result = tableRegistrySchema.safeParse(tables);
111
+ if (result.success) {
112
+ return false;
94
113
  }
95
114
 
96
- return hasError;
115
+ Logger.warn(
116
+ "表结构校验失败",
117
+ {
118
+ errors: formatZodIssues(result.error.issues, {
119
+ items: tables,
120
+ itemLabel: "table"
121
+ })
122
+ },
123
+ false
124
+ );
125
+ return true;
97
126
  }
package/index.js CHANGED
@@ -35,35 +35,27 @@ import { deepMerge } from "./utils/deepMerge.js";
35
35
  export { syncDbApply as syncDb } from "./scripts/syncDb/index.js";
36
36
 
37
37
  function prefixMenuPaths(menus, prefix) {
38
- const output = [];
39
- for (const menu of menus) {
40
- const nextPath = menu.path === "/" ? `/${prefix}` : `/${prefix}${menu.path}`;
38
+ return menus.map((menu) => {
41
39
  const nextMenu = {
42
40
  name: menu.name,
43
- path: nextPath,
41
+ path: menu.path === "/" ? `/${prefix}` : `/${prefix}${menu.path}`,
44
42
  sort: menu.sort
45
43
  };
44
+
46
45
  if (Array.isArray(menu.children)) {
47
- const children = [];
48
- for (const child of menu.children) {
49
- children.push({
50
- name: child.name,
51
- path: child.path,
52
- sort: child.sort
53
- });
54
- }
55
- nextMenu.children = children;
46
+ nextMenu.children = menu.children.map((child) => ({
47
+ name: child.name,
48
+ path: child.path,
49
+ sort: child.sort
50
+ }));
56
51
  }
57
- output.push(nextMenu);
58
- }
59
- return output;
52
+
53
+ return nextMenu;
54
+ });
60
55
  }
61
56
 
62
57
  async function ensureSyncPrerequisites(ctx) {
63
- const missingCtxKeys = [];
64
- if (!ctx.redis) missingCtxKeys.push("ctx.redis");
65
- if (!ctx.mysql) missingCtxKeys.push("ctx.mysql");
66
- if (!ctx.cache) missingCtxKeys.push("ctx.cache");
58
+ const missingCtxKeys = ["ctx.redis", "ctx.mysql", "ctx.cache"].filter((key) => !ctx[key.slice(4)]);
67
59
  if (missingCtxKeys.length > 0) {
68
60
  throw new Error(`启动失败:${missingCtxKeys.join("、")} 未初始化`, {
69
61
  cause: null,
@@ -78,8 +70,7 @@ async function ensureSyncPrerequisites(ctx) {
78
70
  const missingTables = [];
79
71
 
80
72
  for (const table of requiredTables) {
81
- const tableExistsResult = await ctx.mysql.tableExists(table);
82
- if (!tableExistsResult.data) {
73
+ if (!(await ctx.mysql.tableExists(table)).data) {
83
74
  missingTables.push(table);
84
75
  }
85
76
  }
package/lib/connect.js CHANGED
@@ -6,6 +6,25 @@
6
6
  import { RedisClient, SQL } from "bun";
7
7
  import { Logger } from "./logger.js";
8
8
 
9
+ function getRunMode() {
10
+ return Bun.env.RUN_MODE || "unknown";
11
+ }
12
+
13
+ function buildRedisUrl(config) {
14
+ if (config.username) {
15
+ const encodedUser = encodeURIComponent(config.username);
16
+ const encodedPass = encodeURIComponent(config.password || "");
17
+ return `redis://${encodedUser}:${encodedPass}@${config.hostname}:${config.port}/${config.db}`;
18
+ }
19
+
20
+ if (config.password) {
21
+ const encodedPass = encodeURIComponent(config.password);
22
+ return `redis://:${encodedPass}@${config.hostname}:${config.port}/${config.db}`;
23
+ }
24
+
25
+ return `redis://${config.hostname}:${config.port}/${config.db}`;
26
+ }
27
+
9
28
  /**
10
29
  * 连接管理器
11
30
  */
@@ -29,13 +48,26 @@ export class Connect {
29
48
  return this.redisClient;
30
49
  }
31
50
 
51
+ static async handleConnectError(clientKey, label, subsystem, hostname, error) {
52
+ await this[clientKey]?.close();
53
+ this[clientKey] = null;
54
+ Logger.error(`${label} 连接失败 (${hostname} ${getRunMode()})`, error);
55
+ throw new Error(`${label} 连接失败 (${hostname})`, {
56
+ cause: error,
57
+ code: "runtime",
58
+ subsystem: subsystem,
59
+ operation: "connect",
60
+ runMode: getRunMode()
61
+ });
62
+ }
63
+
32
64
  /**
33
65
  * 连接数据库
34
66
  * @param config - 数据库配置
35
67
  */
36
68
  static async connectMysql(config) {
37
69
  try {
38
- const sqlConfig = {
70
+ this.mysqlClient = new SQL({
39
71
  adapter: "mysql",
40
72
  hostname: config.hostname,
41
73
  port: config.port,
@@ -44,23 +76,12 @@ export class Connect {
44
76
  password: config.password,
45
77
  max: config.max,
46
78
  bigint: true
47
- };
48
-
49
- this.mysqlClient = new SQL(sqlConfig);
79
+ });
50
80
 
51
81
  await this.mysqlClient`SELECT 1`;
52
82
  Logger.info(`Mysql 连接成功 (${config.hostname})`);
53
83
  } catch (error) {
54
- await this.mysqlClient?.close();
55
- this.mysqlClient = null;
56
- Logger.error(`Mysql 连接失败 (${config.hostname} ${Bun.env.RUN_MODE || "unkonw"})`, error);
57
- throw new Error(`Mysql 连接失败 (${config.hostname})`, {
58
- cause: error,
59
- code: "runtime",
60
- subsystem: "mysql",
61
- operation: "connect",
62
- runMode: Bun.env.RUN_MODE || "unknown"
63
- });
84
+ await this.handleConnectError("mysqlClient", "Mysql", "mysql", config.hostname, error);
64
85
  }
65
86
  }
66
87
 
@@ -70,19 +91,7 @@ export class Connect {
70
91
  */
71
92
  static async connectRedis(config) {
72
93
  try {
73
- let authPart = "";
74
- if (config.username) {
75
- const encodedUser = encodeURIComponent(config.username);
76
- const encodedPass = encodeURIComponent(config.password || "");
77
- authPart = `${encodedUser}:${encodedPass}@`;
78
- } else if (config.password) {
79
- const encodedPass = encodeURIComponent(config.password);
80
- authPart = `:${encodedPass}@`;
81
- }
82
-
83
- const redisUrl = `redis://${authPart}${config.hostname}:${config.port}/${config.db}`;
84
-
85
- this.redisClient = new RedisClient(redisUrl);
94
+ this.redisClient = new RedisClient(buildRedisUrl(config));
86
95
  // Called when successfully connected to Redis server
87
96
  this.redisClient.onconnect = () => {
88
97
  Logger.info(`Redis 连接成功 (${config.hostname})`);
@@ -100,16 +109,7 @@ export class Connect {
100
109
  // Eagerly verify the connection before returning
101
110
  await this.redisClient.ping();
102
111
  } catch (error) {
103
- await this.redisClient?.close();
104
- this.redisClient = null;
105
- Logger.error(`Redis 连接失败 (${config.hostname} ${Bun.env.RUN_MODE || "unkonw"})`, error);
106
- throw new Error(`Redis 连接失败 (${config.hostname})`, {
107
- cause: error,
108
- code: "runtime",
109
- subsystem: "redis",
110
- operation: "connect",
111
- runMode: Bun.env.RUN_MODE || "unknown"
112
- });
112
+ await this.handleConnectError("redisClient", "Redis", "redis", config.hostname, error);
113
113
  }
114
114
  }
115
115
 
package/lib/dbHelper.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fieldClear } from "../utils/fieldClear.js";
2
2
  import { isNonEmptyString, isNullable, isNumber, isPlainObject, isString } from "../utils/is.js";
3
- import { arrayKeysToCamel, canConvertToNumber, keysToCamel, keysToSnake, snakeCase } from "../utils/util.js";
3
+ import { camelCase, canConvertToNumber, keysToSnake, snakeCase } from "../utils/util.js";
4
4
  import { Logger } from "./logger.js";
5
5
  import { DbParse } from "./dbParse.js";
6
6
  import { SqlBuilder } from "./sqlBuilder.js";
@@ -26,19 +26,7 @@ function quoteIdentMySql(identifier) {
26
26
  }
27
27
 
28
28
  function normalizeSqlMetaNumber(value) {
29
- if (isNullable(value)) {
30
- return 0;
31
- }
32
-
33
- if (typeof value !== "number") {
34
- return 0;
35
- }
36
-
37
- if (!Number.isFinite(value)) {
38
- return 0;
39
- }
40
-
41
- return value;
29
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
42
30
  }
43
31
 
44
32
  function normalizeBigIntValues(value) {
@@ -232,17 +220,8 @@ function assertBatchInsertRowsConsistent(rows, options) {
232
220
  code: "validation"
233
221
  });
234
222
  }
235
- }
236
-
237
- for (const field of fields) {
238
- if (!(field in row)) {
239
- throw new Error(`批量插入缺少字段 (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
240
- cause: null,
241
- code: "validation"
242
- });
243
- }
244
- if (row[field] === undefined) {
245
- throw new Error(`批量插入字段值不能为 undefined (table: ${options.table}, rowIndex: ${i}, field: ${field})`, {
223
+ if (row[key] === undefined) {
224
+ throw new Error(`批量插入字段值不能为 undefined (table: ${options.table}, rowIndex: ${i}, field: ${key})`, {
246
225
  cause: null,
247
226
  code: "validation"
248
227
  });
@@ -294,6 +273,20 @@ class DbHelper {
294
273
  return tableInfo;
295
274
  }
296
275
 
276
+ normalizeDbFieldNames(row) {
277
+ if (!row || !isPlainObject(row)) {
278
+ return row;
279
+ }
280
+
281
+ const result = {};
282
+
283
+ for (const [key, value] of Object.entries(row)) {
284
+ result[camelCase(key)] = value;
285
+ }
286
+
287
+ return result;
288
+ }
289
+
297
290
  async execute(sql, params) {
298
291
  if (!this.sql) {
299
292
  throw new Error("数据库连接未初始化", {
@@ -422,7 +415,7 @@ class DbHelper {
422
415
  return {};
423
416
  }
424
417
 
425
- const camelRow = keysToCamel(row);
418
+ const camelRow = this.normalizeDbFieldNames(row);
426
419
  const deserialized = deserializeArrayFields(camelRow);
427
420
  if (!deserialized) {
428
421
  return {};
@@ -437,7 +430,7 @@ class DbHelper {
437
430
  }
438
431
 
439
432
  normalizeListData(list) {
440
- const camelList = arrayKeysToCamel(list);
433
+ const camelList = list.map((item) => this.normalizeDbFieldNames(item));
441
434
  const deserializedList = camelList.map((item) => deserializeArrayFields(item)).filter((item) => item !== null);
442
435
  return normalizeBigIntValues(deserializedList);
443
436
  }
@@ -709,7 +702,11 @@ class DbHelper {
709
702
  const result = listExecuteRes.data || [];
710
703
 
711
704
  if (result.length >= WARNING_LIMIT) {
712
- Logger.warn("getAll 返回数据过多,建议使用 getList 分页查询", { table: options.table, count: result.length, total: total });
705
+ Logger.warn("getAll 返回数据过多,建议使用 getList 分页查询", {
706
+ table: options.table,
707
+ count: result.length,
708
+ total: total
709
+ });
713
710
  }
714
711
 
715
712
  if (result.length >= MAX_LIMIT) {
@@ -740,47 +737,6 @@ class DbHelper {
740
737
  return { data: exists, sql: executeRes.sql };
741
738
  }
742
739
 
743
- async getFieldValue(options) {
744
- const parsed = await this.createDbParse().parseFieldValue(options);
745
- const builder = this.createSqlBuilder().select(parsed.prepared.fields).from(parsed.prepared.table).where(parsed.prepared.where);
746
- this.applyLeftJoins(builder, parsed.prepared.leftJoins);
747
- if (parsed.prepared.orderBy && parsed.prepared.orderBy.length > 0) {
748
- builder.orderBy(parsed.prepared.orderBy);
749
- }
750
- const { sql, params } = builder.toSelectSql();
751
- const executeRes = await this.execute(sql, params);
752
- const result = this.normalizeRowData(executeRes.data?.[0] || null);
753
- let value = null;
754
- let hasValue = false;
755
-
756
- if (isPlainObject(result)) {
757
- if (Object.hasOwn(result, parsed.field)) {
758
- value = result[parsed.field];
759
- hasValue = true;
760
- }
761
-
762
- if (!hasValue) {
763
- const camelField = parsed.field.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
764
- if (camelField !== parsed.field && Object.hasOwn(result, camelField)) {
765
- value = result[camelField];
766
- hasValue = true;
767
- }
768
- }
769
-
770
- if (!hasValue) {
771
- const snakeField = parsed.field.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
772
- if (snakeField !== parsed.field && Object.hasOwn(result, snakeField)) {
773
- value = result[snakeField];
774
- }
775
- }
776
- }
777
-
778
- return {
779
- data: value,
780
- sql: executeRes.sql
781
- };
782
- }
783
-
784
740
  async insData(options) {
785
741
  const parsed = this.createDbParse().parseInsert(options);
786
742
  const { table, snakeTable, data } = parsed;
@@ -939,7 +895,12 @@ class DbHelper {
939
895
  const inputData = this._prepareWriteInputData(parsed.data);
940
896
  assertWriteDataHasFields(inputData, "更新数据必须至少有一个字段", parsed.snakeTable);
941
897
 
942
- const processed = this._buildUpdateRow({ data: parsed.data, now: Date.now(), allowState: true, beflyMode: this.beflyMode });
898
+ const processed = this._buildUpdateRow({
899
+ data: parsed.data,
900
+ now: Date.now(),
901
+ allowState: true,
902
+ beflyMode: this.beflyMode
903
+ });
943
904
  assertWriteDataHasFields(processed, "更新数据必须至少有一个字段", parsed.snakeTable);
944
905
  const builder = this.createSqlBuilder().where(parsed.where);
945
906
  const { sql, params } = builder.toUpdateSql(parsed.snakeTable, processed);
@@ -954,14 +915,18 @@ class DbHelper {
954
915
 
955
916
  async delData(options) {
956
917
  const parsed = this.createDbParse().parseDelete(options, false);
957
- const now = Date.now();
958
- const processed = {
959
- state: 0,
960
- deleted_at: now
961
- };
962
-
963
- if (this.beflyMode === "auto") {
964
- processed.updated_at = now;
918
+ let processed;
919
+
920
+ if (parsed.deleteMode === "manual") {
921
+ processed = this._prepareWriteInputData(parsed.data);
922
+ assertWriteDataHasFields(processed, "delData 在 beflyMode=manual 时 data 必须至少有一个字段", parsed.snakeTable);
923
+ } else {
924
+ const now = Date.now();
925
+ processed = {
926
+ state: 0,
927
+ deleted_at: now,
928
+ updated_at: now
929
+ };
965
930
  }
966
931
 
967
932
  const builder = this.createSqlBuilder().where(parsed.where);
@@ -989,30 +954,6 @@ class DbHelper {
989
954
  };
990
955
  }
991
956
 
992
- async disableData(options) {
993
- const { table, where } = options;
994
-
995
- return await this.updData({
996
- table: table,
997
- data: {
998
- state: 2
999
- },
1000
- where: where
1001
- });
1002
- }
1003
-
1004
- async enableData(options) {
1005
- const { table, where } = options;
1006
-
1007
- return await this.updData({
1008
- table: table,
1009
- data: {
1010
- state: 1
1011
- },
1012
- where: where
1013
- });
1014
- }
1015
-
1016
957
  async increment(table, field, where, value = 1) {
1017
958
  const parsed = this.createDbParse().parseIncrement(table, field, where, value, "increment");
1018
959