@nocobase/database 1.9.0-beta.8 → 2.0.0-alpha.2

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.
@@ -81,13 +81,22 @@ export declare class Collection<TModelAttributes extends {} = any, TCreationAttr
81
81
  model: ModelStatic<Model>;
82
82
  repository: Repository<TModelAttributes, TCreationAttributes>;
83
83
  constructor(options: CollectionOptions, context: CollectionContext);
84
+ get underscored(): boolean;
84
85
  get filterTargetKey(): string | string[];
85
86
  get name(): string;
86
87
  get origin(): string;
87
88
  get titleField(): string;
89
+ get titleFieldInstance(): Field;
88
90
  get db(): Database;
89
91
  get treeParentField(): BelongsToField | null;
90
92
  get treeChildrenField(): HasManyField | null;
93
+ validate(options: {
94
+ values: Record<string, any> | Record<string, any>[];
95
+ operation: 'create' | 'update';
96
+ context: {
97
+ t: Function;
98
+ };
99
+ }): void;
91
100
  isMultiFilterTargetKey(): boolean;
92
101
  tableName(): any;
93
102
  /**
@@ -148,6 +157,7 @@ export declare class Collection<TModelAttributes extends {} = any, TCreationAttr
148
157
  modelName: string;
149
158
  sequelize: import("sequelize").Sequelize;
150
159
  tableName: any;
160
+ underscored: boolean;
151
161
  };
152
162
  protected bindFieldEventListener(): void;
153
163
  private checkOptions;
package/lib/collection.js CHANGED
@@ -53,9 +53,11 @@ var import_events = require("events");
53
53
  var import_lodash = __toESM(require("lodash"));
54
54
  var import_safe_json_stringify = __toESM(require("safe-json-stringify"));
55
55
  var import_sequelize = require("sequelize");
56
+ var import_fields = require("./fields");
56
57
  var import_model = require("./model");
57
58
  var import_repository = require("./repository");
58
59
  var import_utils = require("./utils");
60
+ var import_field_validation = require("./utils/field-validation");
59
61
  function EnsureAtomicity(target, propertyKey, descriptor) {
60
62
  const originalMethod = descriptor.value;
61
63
  descriptor.value = function(...args) {
@@ -109,6 +111,9 @@ const _Collection = class _Collection extends import_events.EventEmitter {
109
111
  this.setRepository(options.repository);
110
112
  this.setSortable(options.sortable);
111
113
  }
114
+ get underscored() {
115
+ return this.options.underscored;
116
+ }
112
117
  get filterTargetKey() {
113
118
  var _a;
114
119
  const targetKey = (_a = this.options) == null ? void 0 : _a.filterTargetKey;
@@ -135,6 +140,9 @@ const _Collection = class _Collection extends import_events.EventEmitter {
135
140
  get titleField() {
136
141
  return this.options.titleField || this.model.primaryKeyAttribute;
137
142
  }
143
+ get titleFieldInstance() {
144
+ return this.getField(this.titleField);
145
+ }
138
146
  get db() {
139
147
  return this.context.database;
140
148
  }
@@ -152,13 +160,78 @@ const _Collection = class _Collection extends import_events.EventEmitter {
152
160
  }
153
161
  }
154
162
  }
163
+ validate(options) {
164
+ const { values: updateValues, context, operation } = options;
165
+ if (!updateValues) {
166
+ return;
167
+ }
168
+ const values = Array.isArray(updateValues) ? updateValues : [updateValues];
169
+ const { t } = context || { t: /* @__PURE__ */ __name((key, options2) => key, "t") };
170
+ const unwrapTplLabel = /* @__PURE__ */ __name((label) => {
171
+ if (typeof label !== "string") return label;
172
+ const m = label.match(/^[\s\t]*\{\{\s*t\(\s*(['"])(.*?)\1(?:\s*,[\s\S]*)?\)\s*\}\}[\s\t]*$/);
173
+ return m ? m[2] : label;
174
+ }, "unwrapTplLabel");
175
+ const helper = /* @__PURE__ */ __name((field, value) => {
176
+ var _a, _b, _c;
177
+ const val = value[field.name];
178
+ if (!field.options.validation) {
179
+ return;
180
+ }
181
+ const fieldLabel = unwrapTplLabel(((_a = field.options.uiSchema) == null ? void 0 : _a.title) || field.name);
182
+ if (field instanceof import_fields.RelationField) {
183
+ if ((_b = field.options) == null ? void 0 : _b.validation.rules) {
184
+ const isRequired = (_c = field.options) == null ? void 0 : _c.validation.rules.some((rule) => rule.name === "required");
185
+ if (isRequired && !val) {
186
+ throw new Error(
187
+ t("{{#label}} is required", {
188
+ ns: "client",
189
+ "#label": `${t("Collection", { ns: "client" })}: ${this.name}, ${t("Field", { ns: "client" })}: ${t(
190
+ fieldLabel,
191
+ { ns: "client" }
192
+ )}`
193
+ })
194
+ );
195
+ }
196
+ }
197
+ return;
198
+ }
199
+ const joiSchema = (0, import_field_validation.buildJoiSchema)(field.options.validation, {
200
+ label: `${t("Collection", { ns: "client" })}: ${this.name}, ${t("Field", { ns: "client" })}: ${t(fieldLabel, {
201
+ ns: "client"
202
+ })}`,
203
+ value: val
204
+ });
205
+ const { error } = joiSchema.validate(val, {
206
+ messages: (0, import_field_validation.getJoiErrorMessage)(t)
207
+ });
208
+ if (error) {
209
+ throw error;
210
+ }
211
+ }, "helper");
212
+ for (const value of values) {
213
+ if (operation === "create") {
214
+ for (const [, field] of this.fields) {
215
+ helper(field, value);
216
+ }
217
+ }
218
+ if (operation === "update") {
219
+ for (const key of Object.keys(value)) {
220
+ const field = this.getField(key);
221
+ if (field) {
222
+ helper(field, value);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
155
228
  isMultiFilterTargetKey() {
156
229
  return Array.isArray(this.filterTargetKey) && this.filterTargetKey.length > 1;
157
230
  }
158
231
  tableName() {
159
232
  const { name, tableName } = this.options;
160
233
  const tName = tableName || name;
161
- return this.options.underscored ? (0, import_utils.snakeCase)(tName) : tName;
234
+ return this.underscored ? (0, import_utils.snakeCase)(tName) : tName;
162
235
  }
163
236
  /**
164
237
  * @internal
@@ -271,7 +344,7 @@ const _Collection = class _Collection extends import_events.EventEmitter {
271
344
  return this.setField(name, options);
272
345
  }
273
346
  checkFieldType(name, options) {
274
- if (!this.options.underscored) {
347
+ if (!this.underscored) {
275
348
  return;
276
349
  }
277
350
  const fieldName = options.field || (0, import_utils.snakeCase)(name);
@@ -416,7 +489,7 @@ const _Collection = class _Collection extends import_events.EventEmitter {
416
489
  }
417
490
  if (this.model.options.timestamps !== false) {
418
491
  let timestampsFields = ["createdAt", "updatedAt", "deletedAt"];
419
- if (this.db.options.underscored) {
492
+ if (this.underscored) {
420
493
  timestampsFields = timestampsFields.map((fieldName) => (0, import_utils.snakeCase)(fieldName));
421
494
  }
422
495
  if (timestampsFields.includes(field.columnName())) {
@@ -709,7 +782,8 @@ const _Collection = class _Collection extends import_events.EventEmitter {
709
782
  ...import_lodash.default.omit(this.options, ["name", "fields", "model", "targetKey"]),
710
783
  modelName: name,
711
784
  sequelize: this.context.database.sequelize,
712
- tableName: this.tableName()
785
+ tableName: this.tableName(),
786
+ underscored: this.underscored
713
787
  };
714
788
  return attr;
715
789
  }
package/lib/database.d.ts CHANGED
@@ -17,6 +17,7 @@ import { Collection, CollectionOptions, RepositoryType } from './collection';
17
17
  import { CollectionFactory } from './collection-factory';
18
18
  import { ImportFileExtension } from './collection-importer';
19
19
  import DatabaseUtils from './database-utils';
20
+ import { BaseDialect } from './dialects/base-dialect';
20
21
  import ReferencesMap from './features/references-map';
21
22
  import { ArrayFieldRepository } from './field-repository/array-field-repository';
22
23
  import * as FieldTypes from './fields';
@@ -31,7 +32,6 @@ import { RelationRepository } from './relation-repository/relation-repository';
31
32
  import { Repository, TargetKey } from './repository';
32
33
  import { AfterDefineCollectionListener, BeforeDefineCollectionListener, CreateListener, CreateWithAssociationsListener, DatabaseAfterDefineCollectionEventType, DatabaseAfterRemoveCollectionEventType, DatabaseBeforeDefineCollectionEventType, DatabaseBeforeRemoveCollectionEventType, DestroyListener, EventType, ModelCreateEventTypes, ModelCreateWithAssociationsEventTypes, ModelDestroyEventTypes, ModelSaveEventTypes, ModelSaveWithAssociationsEventTypes, ModelUpdateEventTypes, ModelUpdateWithAssociationsEventTypes, ModelValidateEventTypes, RemoveCollectionListener, SaveListener, SaveWithAssociationsListener, SyncListener, UpdateListener, UpdateWithAssociationsListener, ValidateListener } from './types';
33
34
  import { BaseValueParser } from './value-parsers';
34
- import { BaseDialect } from './dialects/base-dialect';
35
35
  export type MergeOptions = merge.Options;
36
36
  export interface PendingOptions {
37
37
  field: RelationField;
@@ -67,6 +67,11 @@ export type AddMigrationsOptions = {
67
67
  directory: string;
68
68
  };
69
69
  type OperatorFunc = (value: any, ctx?: RegisterOperatorsContext) => any;
70
+ type RunSQLOptions = {
71
+ filter?: Record<string, any>;
72
+ bind?: Record<string, any> | Array<any>;
73
+ type?: 'selectVar' | 'selectRow' | 'selectRows';
74
+ } & Transactionable;
70
75
  export declare class Database extends EventEmitter implements AsyncEmitter {
71
76
  static dialects: Map<string, typeof BaseDialect>;
72
77
  sequelize: Sequelize;
@@ -118,7 +123,7 @@ export declare class Database extends EventEmitter implements AsyncEmitter {
118
123
  /**
119
124
  * @internal
120
125
  */
121
- sequelizeOptions(options: any): IDatabaseOptions;
126
+ sequelizeOptions(options: DatabaseOptions): IDatabaseOptions;
122
127
  /**
123
128
  * @internal
124
129
  */
@@ -128,6 +133,7 @@ export declare class Database extends EventEmitter implements AsyncEmitter {
128
133
  inDialect(...dialect: string[]): boolean;
129
134
  isMySQLCompatibleDialect(): boolean;
130
135
  isPostgresCompatibleDialect(): boolean;
136
+ wrapSequelizeRunForMySQL(): void;
131
137
  /**
132
138
  * Add collection to database
133
139
  * @param options
@@ -207,6 +213,9 @@ export declare class Database extends EventEmitter implements AsyncEmitter {
207
213
  extensions?: ImportFileExtension[];
208
214
  }): Promise<Map<string, Collection>>;
209
215
  private registerCollectionType;
216
+ quoteIdentifier(identifier: string): string;
217
+ quoteTable(tableName: string): string;
218
+ runSQL(sql: string, options?: RunSQLOptions): Promise<any>;
210
219
  }
211
220
  export declare function extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): {
212
221
  collectionOptions: CollectionOptions;
package/lib/database.js CHANGED
@@ -58,6 +58,7 @@ var import_path = require("path");
58
58
  var import_safe_json_stringify = __toESM(require("safe-json-stringify"));
59
59
  var import_sequelize = require("sequelize");
60
60
  var import_umzug = require("umzug");
61
+ var import_collection = require("./collection");
61
62
  var import_collection_factory = require("./collection-factory");
62
63
  var import_collection_importer = require("./collection-importer");
63
64
  var import_database_utils = __toESM(require("./database-utils"));
@@ -176,6 +177,9 @@ const _Database = class _Database extends import_events.EventEmitter {
176
177
  );
177
178
  const sequelizeOptions = this.sequelizeOptions(this.options);
178
179
  this.sequelize = new import_sequelize.Sequelize(sequelizeOptions);
180
+ if (options.dialect === "mysql") {
181
+ this.wrapSequelizeRunForMySQL();
182
+ }
179
183
  this.queryInterface = (0, import_query_interface_builder.default)(this);
180
184
  this.collections = /* @__PURE__ */ new Map();
181
185
  this.modelHook = new import_model_hook.ModelHook(this);
@@ -397,14 +401,40 @@ const _Database = class _Database extends import_events.EventEmitter {
397
401
  isPostgresCompatibleDialect() {
398
402
  return this.inDialect("postgres");
399
403
  }
404
+ /*
405
+ * https://github.com/sidorares/node-mysql2/issues/1239#issuecomment-766867699
406
+ * https://github.com/sidorares/node-mysql2/pull/1407#issuecomment-1325789581
407
+ * > I'm starting to think simple "always send (parameter.toString()) as VAR_STRING" unless the type is explicitly specified by user" might be actually the best behaviour
408
+ */
409
+ wrapSequelizeRunForMySQL() {
410
+ const that = this;
411
+ const run = this.sequelize.dialect.Query.prototype.run;
412
+ this.sequelize.dialect.Query.prototype.run = function(sql, parameters) {
413
+ if (!/^update\s+/i.test(sql.trim()) || !(parameters == null ? void 0 : parameters.length)) {
414
+ return run.apply(this, [sql, parameters]);
415
+ }
416
+ try {
417
+ parameters.forEach((p, index) => {
418
+ if (typeof p === "number") {
419
+ parameters[index] = p.toString();
420
+ }
421
+ });
422
+ } catch (err) {
423
+ that.logger.error(err);
424
+ }
425
+ return run.apply(this, [sql, parameters]);
426
+ };
427
+ }
400
428
  /**
401
429
  * Add collection to database
402
430
  * @param options
403
431
  */
404
432
  collection(options) {
405
433
  options = import_lodash.default.cloneDeep(options);
406
- if (this.options.underscored) {
407
- options.underscored = true;
434
+ if (typeof options.underscored !== "boolean") {
435
+ if (this.options.underscored) {
436
+ options.underscored = true;
437
+ }
408
438
  }
409
439
  this.logger.trace(`beforeDefineCollection: ${(0, import_safe_json_stringify.default)(options)}`, {
410
440
  databaseInstanceId: this.instanceId
@@ -754,6 +784,57 @@ const _Database = class _Database extends import_events.EventEmitter {
754
784
  }
755
785
  });
756
786
  }
787
+ // 如果需要手动引用标识符,可以添加辅助方法
788
+ quoteIdentifier(identifier) {
789
+ return this.sequelize.getQueryInterface().queryGenerator["quoteIdentifier"](identifier);
790
+ }
791
+ quoteTable(tableName) {
792
+ return this.sequelize.getQueryInterface().quoteIdentifiers(tableName);
793
+ }
794
+ async runSQL(sql, options = {}) {
795
+ const { filter, bind, type, transaction } = options;
796
+ let finalSQL = sql;
797
+ if (!finalSQL.replace(/\s+/g, " ").trim()) {
798
+ throw new Error("SQL cannot be empty");
799
+ }
800
+ if (filter) {
801
+ let where = {};
802
+ const tmpCollection = new import_collection.Collection({ name: "tmp", underscored: false }, { database: this });
803
+ const r = tmpCollection.repository;
804
+ where = r.buildQueryOptions({ filter }).where;
805
+ const queryGenerator = this.sequelize.getQueryInterface().queryGenerator;
806
+ const wSQL = queryGenerator.getWhereConditions(where, null, null, { bindParam: true });
807
+ if (wSQL) {
808
+ let normalizedSQL = sql.replace(/\s+/g, " ").trim();
809
+ if (normalizedSQL.endsWith(";")) {
810
+ normalizedSQL = normalizedSQL.slice(0, -1).trim();
811
+ }
812
+ finalSQL = `SELECT * FROM (${normalizedSQL}) AS tmp WHERE ${wSQL}`;
813
+ }
814
+ }
815
+ this.logger.debug("runSQL", { finalSQL });
816
+ const result = await this.sequelize.query(finalSQL, { bind, transaction });
817
+ let data = result[0];
818
+ if (type === "selectVar") {
819
+ if (Array.isArray(data)) {
820
+ data = data[0];
821
+ }
822
+ return Object.values(data || {}).shift();
823
+ }
824
+ if (type === "selectRow") {
825
+ if (Array.isArray(data)) {
826
+ data = data[0];
827
+ }
828
+ return data || null;
829
+ }
830
+ if (type === "selectRows") {
831
+ if (!Array.isArray(data)) {
832
+ return [data].filter(Boolean);
833
+ }
834
+ return data;
835
+ }
836
+ return data;
837
+ }
757
838
  };
758
839
  __name(_Database, "Database");
759
840
  __publicField(_Database, "dialects", /* @__PURE__ */ new Map());
@@ -7,6 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { BaseDialect } from './base-dialect';
10
+ import { DatabaseOptions } from '../database';
10
11
  export declare class MysqlDialect extends BaseDialect {
11
12
  static dialectName: string;
12
13
  getVersionGuard(): {
@@ -14,5 +15,5 @@ export declare class MysqlDialect extends BaseDialect {
14
15
  get: (v: string) => string;
15
16
  version: string;
16
17
  };
17
- getSequelizeOptions(options: any): any;
18
+ getSequelizeOptions(options: DatabaseOptions): import("../database").IDatabaseOptions;
18
19
  }
@@ -32,7 +32,6 @@ __export(mysql_dialect_exports, {
32
32
  MysqlDialect: () => MysqlDialect
33
33
  });
34
34
  module.exports = __toCommonJS(mysql_dialect_exports);
35
- var import_utils = require("@nocobase/utils");
36
35
  var import_base_dialect = require("./base-dialect");
37
36
  const _MysqlDialect = class _MysqlDialect extends import_base_dialect.BaseDialect {
38
37
  getVersionGuard() {
@@ -46,7 +45,11 @@ const _MysqlDialect = class _MysqlDialect extends import_base_dialect.BaseDialec
46
45
  };
47
46
  }
48
47
  getSequelizeOptions(options) {
49
- import_utils.lodash.set(options, "dialectOptions.multipleStatements", true);
48
+ const dialectOptions = {
49
+ ...options.dialectOptions || {},
50
+ multipleStatements: true
51
+ };
52
+ options.dialectOptions = dialectOptions;
50
53
  return options;
51
54
  }
52
55
  };
@@ -10,17 +10,39 @@ import { DataType, ModelAttributeColumnOptions, ModelIndexesOptions, SyncOptions
10
10
  import { Collection } from '../collection';
11
11
  import { Database } from '../database';
12
12
  import { ModelEventTypes } from '../types';
13
+ import { BasicType, BooleanSchema, NumberSchema, ObjectSchema, StringSchema } from 'joi';
13
14
  export interface FieldContext {
14
15
  database: Database;
15
16
  collection: Collection;
16
17
  }
17
- export interface BaseFieldOptions {
18
+ type RuleSchemaMap = {
19
+ string: StringSchema;
20
+ boolean: BooleanSchema;
21
+ number: NumberSchema;
22
+ object: ObjectSchema;
23
+ };
24
+ export type FieldValidationRuleName<T extends BasicType> = T extends keyof RuleSchemaMap ? keyof RuleSchemaMap[T] : never;
25
+ export interface FieldValidationRule<T extends BasicType> {
26
+ key: string;
27
+ name: FieldValidationRuleName<T>;
28
+ args?: {
29
+ [key: string]: any;
30
+ };
31
+ paramsType?: 'object';
32
+ }
33
+ export interface ValidationOptions<T extends BasicType = BasicType> {
34
+ type: T;
35
+ rules: FieldValidationRule<T>[];
36
+ [key: string]: any;
37
+ }
38
+ export interface BaseFieldOptions<T extends BasicType = BasicType> {
18
39
  name?: string;
19
40
  hidden?: boolean;
20
41
  translation?: boolean;
42
+ validation?: ValidationOptions<T>;
21
43
  [key: string]: any;
22
44
  }
23
- export interface BaseColumnFieldOptions extends BaseFieldOptions, Omit<ModelAttributeColumnOptions, 'type'> {
45
+ export interface BaseColumnFieldOptions<T extends BasicType = BasicType> extends BaseFieldOptions<T>, Omit<ModelAttributeColumnOptions, 'type'> {
24
46
  dataType?: DataType;
25
47
  index?: boolean | ModelIndexesOptions;
26
48
  }
@@ -50,3 +72,4 @@ export declare abstract class Field {
50
72
  additionalSequelizeOptions(): {};
51
73
  typeToString(): any;
52
74
  }
75
+ export {};
@@ -32,6 +32,7 @@ import { UnixTimestampFieldOptions } from './unix-timestamp-field';
32
32
  import { DateOnlyFieldOptions } from './date-only-field';
33
33
  import { DatetimeNoTzFieldOptions } from './datetime-no-tz-field';
34
34
  import { DatetimeTzFieldOptions } from './datetime-tz-field';
35
+ import { SnowflakeIdFieldOptions } from './snowflake-id-field';
35
36
  export * from './array-field';
36
37
  export * from './belongs-to-field';
37
38
  export * from './belongs-to-many-field';
@@ -60,4 +61,5 @@ export * from './virtual-field';
60
61
  export * from './nanoid-field';
61
62
  export * from './encryption-field';
62
63
  export * from './unix-timestamp-field';
63
- export type FieldOptions = BaseFieldOptions | StringFieldOptions | IntegerFieldOptions | FloatFieldOptions | DecimalFieldOptions | DoubleFieldOptions | RealFieldOptions | JsonFieldOptions | JsonbFieldOptions | BooleanFieldOptions | RadioFieldOptions | TextFieldOptions | VirtualFieldOptions | ArrayFieldOptions | SetFieldOptions | TimeFieldOptions | DateFieldOptions | DatetimeTzFieldOptions | DatetimeNoTzFieldOptions | DateOnlyFieldOptions | UnixTimestampFieldOptions | UidFieldOptions | UUIDFieldOptions | NanoidFieldOptions | PasswordFieldOptions | ContextFieldOptions | BelongsToFieldOptions | HasOneFieldOptions | HasManyFieldOptions | BelongsToManyFieldOptions | EncryptionField;
64
+ export * from './snowflake-id-field';
65
+ export type FieldOptions = BaseFieldOptions | StringFieldOptions | IntegerFieldOptions | FloatFieldOptions | DecimalFieldOptions | DoubleFieldOptions | RealFieldOptions | JsonFieldOptions | JsonbFieldOptions | BooleanFieldOptions | RadioFieldOptions | TextFieldOptions | VirtualFieldOptions | ArrayFieldOptions | SetFieldOptions | TimeFieldOptions | DateFieldOptions | DatetimeTzFieldOptions | DatetimeNoTzFieldOptions | DateOnlyFieldOptions | UnixTimestampFieldOptions | UidFieldOptions | UUIDFieldOptions | NanoidFieldOptions | PasswordFieldOptions | ContextFieldOptions | BelongsToFieldOptions | HasOneFieldOptions | HasManyFieldOptions | BelongsToManyFieldOptions | EncryptionField | SnowflakeIdFieldOptions;
@@ -51,6 +51,7 @@ __reExport(fields_exports, require("./virtual-field"), module.exports);
51
51
  __reExport(fields_exports, require("./nanoid-field"), module.exports);
52
52
  __reExport(fields_exports, require("./encryption-field"), module.exports);
53
53
  __reExport(fields_exports, require("./unix-timestamp-field"), module.exports);
54
+ __reExport(fields_exports, require("./snowflake-id-field"), module.exports);
54
55
  // Annotate the CommonJS export names for ESM import in node:
55
56
  0 && (module.exports = {
56
57
  ...require("./array-field"),
@@ -80,5 +81,6 @@ __reExport(fields_exports, require("./unix-timestamp-field"), module.exports);
80
81
  ...require("./virtual-field"),
81
82
  ...require("./nanoid-field"),
82
83
  ...require("./encryption-field"),
83
- ...require("./unix-timestamp-field")
84
+ ...require("./unix-timestamp-field"),
85
+ ...require("./snowflake-id-field")
84
86
  });
@@ -13,7 +13,7 @@ export declare abstract class NumberField extends Field {
13
13
  export declare class IntegerField extends NumberField {
14
14
  get dataType(): DataTypes.IntegerDataTypeConstructor;
15
15
  }
16
- export interface IntegerFieldOptions extends BaseColumnFieldOptions {
16
+ export interface IntegerFieldOptions extends BaseColumnFieldOptions<'number'> {
17
17
  type: 'integer';
18
18
  }
19
19
  export declare class BigIntField extends NumberField {
@@ -0,0 +1,29 @@
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 { DataTypes } from 'sequelize';
10
+ import { BaseColumnFieldOptions, Field } from './field';
11
+ interface Application {
12
+ snowflakeIdGenerator: {
13
+ generate(): number | BigInt;
14
+ };
15
+ }
16
+ export declare class SnowflakeIdField extends Field {
17
+ private static app;
18
+ static setApp(app: Application): void;
19
+ get dataType(): DataTypes.BigIntDataTypeConstructor;
20
+ private setId;
21
+ init(): void;
22
+ bind(): void;
23
+ unbind(): void;
24
+ }
25
+ export interface SnowflakeIdFieldOptions extends BaseColumnFieldOptions {
26
+ type: 'snowflakeId';
27
+ epoch?: number;
28
+ }
29
+ export {};
@@ -0,0 +1,79 @@
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 __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") {
22
+ for (let key of __getOwnPropNames(from))
23
+ if (!__hasOwnProp.call(to, key) && key !== except)
24
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
25
+ }
26
+ return to;
27
+ };
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
30
+ var snowflake_id_field_exports = {};
31
+ __export(snowflake_id_field_exports, {
32
+ SnowflakeIdField: () => SnowflakeIdField
33
+ });
34
+ module.exports = __toCommonJS(snowflake_id_field_exports);
35
+ var import_sequelize = require("sequelize");
36
+ var import_field = require("./field");
37
+ const _SnowflakeIdField = class _SnowflakeIdField extends import_field.Field {
38
+ static setApp(app) {
39
+ this.app = app;
40
+ }
41
+ get dataType() {
42
+ return import_sequelize.DataTypes.BIGINT;
43
+ }
44
+ setId(name, instance) {
45
+ const value = instance.get(name);
46
+ if (!value) {
47
+ const generator = this.constructor.app.snowflakeIdGenerator;
48
+ instance.set(name, generator.generate());
49
+ }
50
+ }
51
+ init() {
52
+ const { name } = this.options;
53
+ this.listener = (instance) => this.setId(name, instance);
54
+ this.bulkListener = async (instances) => {
55
+ for (const instance of instances) {
56
+ this.setId(name, instance);
57
+ }
58
+ };
59
+ }
60
+ bind() {
61
+ super.bind();
62
+ this.on("beforeValidate", this.listener);
63
+ this.on("beforeSave", this.listener);
64
+ this.on("beforeBulkCreate", this.bulkListener);
65
+ }
66
+ unbind() {
67
+ super.unbind();
68
+ this.off("beforeValidate", this.listener);
69
+ this.off("beforeSave", this.listener);
70
+ this.off("beforeBulkCreate", this.bulkListener);
71
+ }
72
+ };
73
+ __name(_SnowflakeIdField, "SnowflakeIdField");
74
+ __publicField(_SnowflakeIdField, "app");
75
+ let SnowflakeIdField = _SnowflakeIdField;
76
+ // Annotate the CommonJS export names for ESM import in node:
77
+ 0 && (module.exports = {
78
+ SnowflakeIdField
79
+ });
@@ -14,7 +14,7 @@ export declare class StringField extends Field {
14
14
  set(value: any): void;
15
15
  };
16
16
  }
17
- export interface StringFieldOptions extends BaseColumnFieldOptions {
17
+ export interface StringFieldOptions extends BaseColumnFieldOptions<'string'> {
18
18
  type: 'string';
19
19
  length?: number;
20
20
  trim?: boolean;
@@ -77,6 +77,11 @@ const _InheritedSyncRunner = class _InheritedSyncRunner {
77
77
  let maxSequenceName;
78
78
  if (childAttributes.id && childAttributes.id.autoIncrement) {
79
79
  for (const parent of parents) {
80
+ const attributes2 = parent.model.getAttributes();
81
+ const parentIdField = attributes2["id"];
82
+ if (!(parentIdField == null ? void 0 : parentIdField.autoIncrement)) {
83
+ continue;
84
+ }
80
85
  const sequenceNameResult = await queryInterface.sequelize.query(
81
86
  `SELECT column_default
82
87
  FROM information_schema.columns
@@ -146,8 +151,14 @@ const _InheritedSyncRunner = class _InheritedSyncRunner {
146
151
  const columns = await queryInterface.describeTable(tableName, options);
147
152
  for (const attribute in childAttributes) {
148
153
  const columnName = childAttributes[attribute].field;
154
+ const fieldName = childAttributes[attribute].fieldName;
149
155
  if (!columns[columnName]) {
150
- await queryInterface.addColumn(tableName, columnName, childAttributes[columnName], options);
156
+ await queryInterface.addColumn(
157
+ tableName,
158
+ columnName,
159
+ childAttributes[columnName] || childAttributes[fieldName],
160
+ options
161
+ );
151
162
  }
152
163
  }
153
164
  }
@@ -86,6 +86,11 @@ const _BelongsToManyRepository = class _BelongsToManyRepository extends import_m
86
86
  through: values[this.throughName()],
87
87
  transaction: transaction2
88
88
  };
89
+ this.collection.validate({
90
+ values,
91
+ operation: "create",
92
+ context: options.context
93
+ });
89
94
  const instance = await sourceModel[createAccessor](values, createOptions);
90
95
  await (0, import_update_associations.updateAssociations)(instance, values, { ...options, transaction: transaction2 });
91
96
  return instance;
@@ -178,6 +178,7 @@ const _RelationRepository = class _RelationRepository {
178
178
  const values = options.values;
179
179
  const transaction2 = await this.getTransaction(options);
180
180
  const sourceModel = await this.getSourceModel(transaction2);
181
+ this.collection.validate({ values, context: options.context, operation: "create" });
181
182
  const instance = await sourceModel[createAccessor](guard.sanitize(options.values), { ...options, transaction: transaction2 });
182
183
  await (0, import_update_associations.updateAssociations)(instance, values, { ...options, transaction: transaction2 });
183
184
  if (options.hooks !== false) {
@@ -209,6 +209,7 @@ export declare class Repository<TModelAttributes extends {} = any, TCreationAttr
209
209
  */
210
210
  firstOrCreate(options: FirstOrCreateOptions): Promise<any>;
211
211
  updateOrCreate(options: FirstOrCreateOptions): Promise<any>;
212
+ private validate;
212
213
  /**
213
214
  * Save instance to database
214
215
  *
package/lib/repository.js CHANGED
@@ -436,6 +436,9 @@ const _Repository = class _Repository {
436
436
  }
437
437
  return this.create({ values, transaction: transaction2, context, ...rest });
438
438
  }
439
+ validate(options) {
440
+ this.collection.validate(options);
441
+ }
439
442
  async create(options) {
440
443
  if (Array.isArray(options.values)) {
441
444
  return this.createMany({
@@ -450,6 +453,7 @@ const _Repository = class _Repository {
450
453
  underscored: this.collection.options.underscored
451
454
  });
452
455
  const values = this.model.callSetters(guard.sanitize(options.values || {}), options);
456
+ this.validate({ values, context: options.context, operation: "create" });
453
457
  const instance = await this.model.create(values, {
454
458
  ...options,
455
459
  transaction: transaction2
@@ -494,6 +498,7 @@ const _Repository = class _Repository {
494
498
  const transaction2 = await this.getTransaction(options);
495
499
  const guard = import_update_guard.UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored });
496
500
  const values = this.model.callSetters(guard.sanitize(options.values || {}), options);
501
+ this.validate({ values, context: options.context, operation: "update" });
497
502
  if (options.individualHooks === false) {
498
503
  const { model: Model2 } = this.collection;
499
504
  const primaryKeyField = Model2.primaryKeyField || Model2.primaryKeyAttribute;
@@ -79,6 +79,11 @@ async function updateModelByValues(instance, values, options) {
79
79
  guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
80
80
  values = guard.sanitize(values);
81
81
  }
82
+ instance.constructor.collection.validate({
83
+ values,
84
+ operation: "update",
85
+ context: options == null ? void 0 : options.context
86
+ });
82
87
  await instance.update(values, options);
83
88
  await updateAssociations(instance, values, options);
84
89
  }
@@ -266,6 +271,11 @@ async function updateSingleAssociation(model, key, value, options = {}) {
266
271
  return true;
267
272
  }
268
273
  }
274
+ association.target.collection.validate({
275
+ values: value,
276
+ context: options.context,
277
+ operation: "create"
278
+ });
269
279
  const instance = await model[createAccessor](value, { context, transaction });
270
280
  await updateAssociations(instance, value, {
271
281
  ...options,
@@ -356,6 +366,11 @@ async function updateMultipleAssociation(model, key, value, options = {}) {
356
366
  throw new Error(`${targetKey} field value is empty`);
357
367
  }
358
368
  if ((0, import_utils.isUndefinedOrNull)(item[targetKey])) {
369
+ association.target.collection.validate({
370
+ values: item,
371
+ context: options.context,
372
+ operation: "create"
373
+ });
359
374
  const instance = await model[createAccessor](item, accessorOptions);
360
375
  await updateAssociations(instance, item, {
361
376
  ...options,
@@ -373,6 +388,11 @@ async function updateMultipleAssociation(model, key, value, options = {}) {
373
388
  transaction
374
389
  });
375
390
  if (!instance) {
391
+ association.target.collection.validate({
392
+ values: item,
393
+ context: options.context,
394
+ operation: "create"
395
+ });
376
396
  instance = await model[createAccessor](item, accessorOptions);
377
397
  await updateAssociations(instance, item, {
378
398
  ...options,
@@ -392,6 +412,11 @@ async function updateMultipleAssociation(model, key, value, options = {}) {
392
412
  if (association.associationType === "HasMany") {
393
413
  delete item[association.foreignKey];
394
414
  }
415
+ association.target.collection.validate({
416
+ values: item,
417
+ context: options.context,
418
+ operation: "update"
419
+ });
395
420
  await instance.update(item, { ...options, transaction });
396
421
  }
397
422
  await updateAssociations(instance, item, {
@@ -0,0 +1,59 @@
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 { AnySchema } from 'joi';
10
+ import { ValidationOptions } from '../fields';
11
+ export declare function buildJoiSchema(validation: ValidationOptions, options: {
12
+ label?: string;
13
+ value: string;
14
+ }): AnySchema;
15
+ export declare function getJoiErrorMessage(t: Function): {
16
+ 'string.base': any;
17
+ 'string.empty': any;
18
+ 'string.min': any;
19
+ 'string.max': any;
20
+ 'string.length': any;
21
+ 'string.alphanum': any;
22
+ 'string.token': any;
23
+ 'string.regex': any;
24
+ 'string.email': any;
25
+ 'string.uri': any;
26
+ 'string.uriCustomScheme': any;
27
+ 'string.isoDate': any;
28
+ 'string.guid': any;
29
+ 'string.hex': any;
30
+ 'string.hostname': any;
31
+ 'string.lowercase': any;
32
+ 'string.uppercase': any;
33
+ 'string.trim': any;
34
+ 'string.creditCard': any;
35
+ 'string.pattern.base': any;
36
+ 'string.pattern.name': any;
37
+ 'string.pattern.invert.base': any;
38
+ 'string.pattern.invert.name': any;
39
+ 'any.required': any;
40
+ 'number.base': any;
41
+ 'number.min': any;
42
+ 'number.max': any;
43
+ 'number.less': any;
44
+ 'number.greater': any;
45
+ 'number.float': any;
46
+ 'number.integer': any;
47
+ 'number.negative': any;
48
+ 'number.positive': any;
49
+ 'number.precision': any;
50
+ 'number.multiple': any;
51
+ 'number.port': any;
52
+ 'number.unsafe': any;
53
+ 'date.base': any;
54
+ 'date.format': any;
55
+ 'date.greater': any;
56
+ 'date.less': any;
57
+ 'date.max': any;
58
+ 'date.min': any;
59
+ };
@@ -0,0 +1,143 @@
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 field_validation_exports = {};
39
+ __export(field_validation_exports, {
40
+ buildJoiSchema: () => buildJoiSchema,
41
+ getJoiErrorMessage: () => getJoiErrorMessage
42
+ });
43
+ module.exports = __toCommonJS(field_validation_exports);
44
+ var import_joi = __toESM(require("joi"));
45
+ var import_lodash = __toESM(require("lodash"));
46
+ function buildJoiSchema(validation, options) {
47
+ const { type, rules } = validation;
48
+ const { label, value } = options;
49
+ if (!type || typeof type !== "string" || !(type in import_joi.default)) {
50
+ throw new Error(`Invalid validation type: "${type}". Type must be a valid Joi schema type.`);
51
+ }
52
+ let schema = import_joi.default[type]();
53
+ const isRequired = rules.some((rule) => rule.name === "required");
54
+ if (isRequired) {
55
+ schema = schema.empty(null);
56
+ } else {
57
+ schema = schema.allow(null, "");
58
+ if ([null, ""].includes(value)) return schema;
59
+ }
60
+ if (rules) {
61
+ rules.forEach((rule) => {
62
+ if (!import_lodash.default.isEmpty(rule.args)) {
63
+ if (rule.name === "pattern" && !import_lodash.default.isRegExp(rule.args.regex)) {
64
+ const lastSlash = rule.args.regex.lastIndexOf("/");
65
+ const isRegExpStr = rule.args.regex.startsWith("/") && lastSlash > 0;
66
+ if (isRegExpStr) {
67
+ rule.args.regex = rule.args.regex.slice(1, lastSlash);
68
+ }
69
+ rule.args.regex = new RegExp(rule.args.regex);
70
+ }
71
+ schema = rule.paramsType === "object" ? schema[rule.name](rule.args) : schema[rule.name](...Object.values(rule.args));
72
+ } else {
73
+ schema = schema[rule.name]();
74
+ }
75
+ });
76
+ }
77
+ if (label) {
78
+ schema = schema.label(label);
79
+ }
80
+ return schema;
81
+ }
82
+ __name(buildJoiSchema, "buildJoiSchema");
83
+ function getJoiErrorMessage(t) {
84
+ const tOptions = { ns: "client" };
85
+ const JoiErrorMessages = {
86
+ "string.base": t("{{#label}} must be a string", tOptions),
87
+ "string.empty": t("{{#label}} is not allowed to be empty", tOptions),
88
+ "string.min": t("{{#label}} length must be at least {{#limit}} characters long", tOptions),
89
+ "string.max": t("{{#label}} length must be less than or equal to {{#limit}} characters long", tOptions),
90
+ "string.length": t("{{#label}} length must be {{#limit}} characters long", tOptions),
91
+ "string.alphanum": t("{{#label}} must only contain alpha-numeric characters", tOptions),
92
+ "string.token": t("{{#label}} must only contain alpha-numeric and underscore characters", tOptions),
93
+ "string.regex": t("{{#label}} with value {{#value}} fails to match the required pattern", tOptions),
94
+ "string.email": t("{{#label}} email address doesn\u2019t meet the required format", tOptions),
95
+ "string.uri": t("{{#label}} must be a valid uri", tOptions),
96
+ "string.uriCustomScheme": t(
97
+ "{{#label}} must be a valid uri with a scheme matching the {{#scheme}} pattern",
98
+ tOptions
99
+ ),
100
+ "string.isoDate": t("{{#label}} must be a valid ISO 8601 date", tOptions),
101
+ "string.guid": t("{{#label}} must be a valid UUID", tOptions),
102
+ "string.hex": t("{{#label}} must only contain hexadecimal characters", tOptions),
103
+ "string.hostname": t("{{#label}} must be a valid hostname", tOptions),
104
+ "string.lowercase": t("{{#label}} must only contain lowercase characters", tOptions),
105
+ "string.uppercase": t("{{#label}} must only contain uppercase characters", tOptions),
106
+ "string.trim": t("{{#label}} must not have leading or trailing whitespace", tOptions),
107
+ "string.creditCard": t("{{#label}} must be a credit card", tOptions),
108
+ "string.pattern.base": t('{{#label}} with value "{{#value}}" fails to match the required pattern', tOptions),
109
+ "string.pattern.name": t('{{#label}} with value "{{#value}}" fails to match the {{#name}} pattern', tOptions),
110
+ "string.pattern.invert.base": t('{{#label}} with value "{{#value}}" matches the inverted pattern', tOptions),
111
+ "string.pattern.invert.name": t(
112
+ '{{#label}} with value "{{#value}}" matches the inverted {{#name}} pattern',
113
+ tOptions
114
+ ),
115
+ "any.required": t("{{#label}} is required", tOptions),
116
+ "number.base": t("{{#label}} must be a number", tOptions),
117
+ "number.min": t("{{#label}} must be greater than or equal to {{#limit}}", tOptions),
118
+ "number.max": t("{{#label}} must be less than or equal to {{#limit}}", tOptions),
119
+ "number.less": t("{{#label}} must be less than {{#limit}}", tOptions),
120
+ "number.greater": t("{{#label}} must be greater than {{#limit}}", tOptions),
121
+ "number.float": t("{{#label}} must be a float or double", tOptions),
122
+ "number.integer": t("{{#label}} must be an integer", tOptions),
123
+ "number.negative": t("{{#label}} must be a negative number", tOptions),
124
+ "number.positive": t("{{#label}} must be a positive number", tOptions),
125
+ "number.precision": t("{{#label}} must not have more than {{#limit}} decimal places", tOptions),
126
+ "number.multiple": t("{{#label}} must be a multiple of {{#multiple}}", tOptions),
127
+ "number.port": t("{{#label}} must be a valid port", tOptions),
128
+ "number.unsafe": t("{{#label}} must be a safe number", tOptions),
129
+ "date.base": t("{{#label}} must be a valid date", tOptions),
130
+ "date.format": t("{{#label}} must be in {{#format}} format", tOptions),
131
+ "date.greater": t("{{#label}} must be greater than {{#limit}}", tOptions),
132
+ "date.less": t("{{#label}} must be less than {{#limit}}", tOptions),
133
+ "date.max": t("{{#label}} must be less than or equal to {{#limit}}", tOptions),
134
+ "date.min": t("{{#label}} must be greater than or equal to {{#limit}}", tOptions)
135
+ };
136
+ return JoiErrorMessages;
137
+ }
138
+ __name(getJoiErrorMessage, "getJoiErrorMessage");
139
+ // Annotate the CommonJS export names for ESM import in node:
140
+ 0 && (module.exports = {
141
+ buildJoiSchema,
142
+ getJoiErrorMessage
143
+ });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@nocobase/database",
3
- "version": "1.9.0-beta.8",
3
+ "version": "2.0.0-alpha.2",
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.9.0-beta.8",
10
- "@nocobase/utils": "1.9.0-beta.8",
9
+ "@nocobase/logger": "2.0.0-alpha.2",
10
+ "@nocobase/utils": "2.0.0-alpha.2",
11
11
  "async-mutex": "^0.3.2",
12
12
  "chalk": "^4.1.1",
13
13
  "cron-parser": "4.4.0",
@@ -18,6 +18,7 @@
18
18
  "flat": "^5.0.2",
19
19
  "glob": "^7.1.6",
20
20
  "graphlib": "^2.1.8",
21
+ "joi": "^17.13.3",
21
22
  "lodash": "^4.17.21",
22
23
  "mathjs": "^10.6.1",
23
24
  "nanoid": "^3.3.11",
@@ -38,5 +39,5 @@
38
39
  "url": "git+https://github.com/nocobase/nocobase.git",
39
40
  "directory": "packages/database"
40
41
  },
41
- "gitHead": "f3d4f3d1ba7adbf4d4c60e656c23da45565769c8"
42
+ "gitHead": "1322f486b248bef53ed8c8f42f0a39dfd02125fd"
42
43
  }