@nocobase/database 1.9.0-alpha.11 → 1.9.0-alpha.12

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.
@@ -88,6 +88,13 @@ export declare class Collection<TModelAttributes extends {} = any, TCreationAttr
88
88
  get db(): Database;
89
89
  get treeParentField(): BelongsToField | null;
90
90
  get treeChildrenField(): HasManyField | null;
91
+ validate(options: {
92
+ values: Record<string, any> | Record<string, any>[];
93
+ operation: 'create' | 'update';
94
+ context: {
95
+ t: Function;
96
+ };
97
+ }): void;
91
98
  isMultiFilterTargetKey(): boolean;
92
99
  tableName(): any;
93
100
  /**
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) {
@@ -152,6 +154,71 @@ const _Collection = class _Collection extends import_events.EventEmitter {
152
154
  }
153
155
  }
154
156
  }
157
+ validate(options) {
158
+ const { values: updateValues, context, operation } = options;
159
+ if (!updateValues) {
160
+ return;
161
+ }
162
+ const values = Array.isArray(updateValues) ? updateValues : [updateValues];
163
+ const { t } = context || { t: /* @__PURE__ */ __name((key, options2) => key, "t") };
164
+ const unwrapTplLabel = /* @__PURE__ */ __name((label) => {
165
+ if (typeof label !== "string") return label;
166
+ const m = label.match(/^[\s\t]*\{\{\s*t\(\s*(['"])(.*?)\1(?:\s*,[\s\S]*)?\)\s*\}\}[\s\t]*$/);
167
+ return m ? m[2] : label;
168
+ }, "unwrapTplLabel");
169
+ const helper = /* @__PURE__ */ __name((field, value) => {
170
+ var _a, _b, _c;
171
+ const val = value[field.name];
172
+ if (!field.options.validation) {
173
+ return;
174
+ }
175
+ const fieldLabel = unwrapTplLabel(((_a = field.options.uiSchema) == null ? void 0 : _a.title) || field.name);
176
+ if (field instanceof import_fields.RelationField) {
177
+ if ((_b = field.options) == null ? void 0 : _b.validation.rules) {
178
+ const isRequired = (_c = field.options) == null ? void 0 : _c.validation.rules.some((rule) => rule.name === "required");
179
+ if (isRequired && !val) {
180
+ throw new Error(
181
+ t("{{#label}} is required", {
182
+ ns: "client",
183
+ "#label": `${t("Collection", { ns: "client" })}: ${this.name}, ${t("Field", { ns: "client" })}: ${t(
184
+ fieldLabel,
185
+ { ns: "client" }
186
+ )}`
187
+ })
188
+ );
189
+ }
190
+ }
191
+ return;
192
+ }
193
+ const joiSchema = (0, import_field_validation.buildJoiSchema)(field.options.validation, {
194
+ label: `${t("Collection", { ns: "client" })}: ${this.name}, ${t("Field", { ns: "client" })}: ${t(fieldLabel, {
195
+ ns: "client"
196
+ })}`,
197
+ value: val
198
+ });
199
+ const { error } = joiSchema.validate(val, {
200
+ messages: (0, import_field_validation.getJoiErrorMessage)(t)
201
+ });
202
+ if (error) {
203
+ throw error;
204
+ }
205
+ }, "helper");
206
+ for (const value of values) {
207
+ if (operation === "create") {
208
+ for (const [, field] of this.fields) {
209
+ helper(field, value);
210
+ }
211
+ }
212
+ if (operation === "update") {
213
+ for (const key of Object.keys(value)) {
214
+ const field = this.getField(key);
215
+ if (field) {
216
+ helper(field, value);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
155
222
  isMultiFilterTargetKey() {
156
223
  return Array.isArray(this.filterTargetKey) && this.filterTargetKey.length > 1;
157
224
  }
@@ -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 {};
@@ -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 {
@@ -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;
package/lib/index.d.ts CHANGED
@@ -42,3 +42,4 @@ export { default as fieldTypeMap } from './view/field-type-map';
42
42
  export * from './view/view-inference';
43
43
  export * from './update-guard';
44
44
  export { default as operators } from './operators';
45
+ export { filterIncludes, mergeIncludes } from './utils/filter-include';
package/lib/index.js CHANGED
@@ -54,8 +54,10 @@ __export(src_exports, {
54
54
  ValidationErrorItem: () => import_sequelize.ValidationErrorItem,
55
55
  default: () => import_database.Database,
56
56
  fieldTypeMap: () => import_field_type_map.default,
57
+ filterIncludes: () => import_filter_include.filterIncludes,
57
58
  fn: () => import_sequelize.fn,
58
59
  literal: () => import_sequelize.literal,
60
+ mergeIncludes: () => import_filter_include.mergeIncludes,
59
61
  operators: () => import_operators.default,
60
62
  snakeCase: () => import_utils.snakeCase,
61
63
  sqlParser: () => import_sql_parser.default,
@@ -98,6 +100,7 @@ var import_field_type_map = __toESM(require("./view/field-type-map"));
98
100
  __reExport(src_exports, require("./view/view-inference"), module.exports);
99
101
  __reExport(src_exports, require("./update-guard"), module.exports);
100
102
  var import_operators = __toESM(require("./operators"));
103
+ var import_filter_include = require("./utils/filter-include");
101
104
  // Annotate the CommonJS export names for ESM import in node:
102
105
  0 && (module.exports = {
103
106
  BaseError,
@@ -116,8 +119,10 @@ var import_operators = __toESM(require("./operators"));
116
119
  ValidationError,
117
120
  ValidationErrorItem,
118
121
  fieldTypeMap,
122
+ filterIncludes,
119
123
  fn,
120
124
  literal,
125
+ mergeIncludes,
121
126
  operators,
122
127
  snakeCase,
123
128
  sqlParser,
@@ -111,7 +111,14 @@ const _PostgresQueryInterface = class _PostgresQueryInterface extends import_que
111
111
  return results[0]["exists"];
112
112
  }
113
113
  async listViews() {
114
- const sql = `
114
+ var _a;
115
+ const targetSchema = ((_a = this.db.options) == null ? void 0 : _a.schema) || "public";
116
+ const sql = targetSchema ? `
117
+ SELECT viewname as name, definition, schemaname as schema
118
+ FROM pg_views
119
+ WHERE schemaname = '${targetSchema}'
120
+ ORDER BY viewname;
121
+ ` : `
115
122
  SELECT viewname as name, definition, schemaname as schema
116
123
  FROM pg_views
117
124
  WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
@@ -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
@@ -433,6 +433,9 @@ const _Repository = class _Repository {
433
433
  }
434
434
  return this.create({ values, transaction: transaction2, context, ...rest });
435
435
  }
436
+ validate(options) {
437
+ this.collection.validate(options);
438
+ }
436
439
  async create(options) {
437
440
  if (Array.isArray(options.values)) {
438
441
  return this.createMany({
@@ -447,6 +450,7 @@ const _Repository = class _Repository {
447
450
  underscored: this.collection.options.underscored
448
451
  });
449
452
  const values = this.model.callSetters(guard.sanitize(options.values || {}), options);
453
+ this.validate({ values, context: options.context, operation: "create" });
450
454
  const instance = await this.model.create(values, {
451
455
  ...options,
452
456
  transaction: transaction2
@@ -491,6 +495,7 @@ const _Repository = class _Repository {
491
495
  const transaction2 = await this.getTransaction(options);
492
496
  const guard = import_update_guard.UpdateGuard.fromOptions(this.model, { ...options, underscored: this.collection.options.underscored });
493
497
  const values = this.model.callSetters(guard.sanitize(options.values || {}), options);
498
+ this.validate({ values, context: options.context, operation: "update" });
494
499
  if (options.individualHooks === false) {
495
500
  const { model: Model2 } = this.collection;
496
501
  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
+ });
@@ -0,0 +1,55 @@
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
+ * Merge a flat list of include descriptors by association (alias) key.
11
+ * - Nodes with the same association are merged into one.
12
+ * - `required` is OR-ed (true if any source is true).
13
+ * - Child includes are recursively merged with the same rules.
14
+ * - Empty `include` arrays are removed to keep the payload minimal.
15
+ *
16
+ * Notes:
17
+ * - Association keys are normalized via snake_case to ensure consistent merging.
18
+ * - This function is idempotent and order-insensitive for equivalent input sets.
19
+ *
20
+ * Usage example (input and output):
21
+ *
22
+ * const includesA = [
23
+ * { association: 'posts', required: true, include: [{ association: 'comments' }] },
24
+ * { association: 'profile' },
25
+ * ];
26
+ *
27
+ * const includesB = [
28
+ * { association: 'posts', include: [
29
+ * { association: 'comments', required: true },
30
+ * { association: 'tags' },
31
+ * ]
32
+ * },
33
+ * { association: 'roles', required: true },
34
+ * ];
35
+ *
36
+ * const merged = mergeIncludes([...includesA, ...includesB]);
37
+ *
38
+ * Result:
39
+ * [
40
+ * {
41
+ * association: 'posts',
42
+ * required: true,
43
+ * include: [
44
+ * { association: 'comments', required: true },
45
+ * { association: 'tags' },
46
+ * ],
47
+ * },
48
+ * { association: 'profile' },
49
+ * { association: 'roles', required: true },
50
+ * ]
51
+ */
52
+ export declare const mergeIncludes: (includes?: any[]) => any[];
53
+ export declare const filterIncludes: (where: any, includes: any, options: {
54
+ underscored: boolean;
55
+ }) => any[];
@@ -0,0 +1,118 @@
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 __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var filter_include_exports = {};
29
+ __export(filter_include_exports, {
30
+ filterIncludes: () => filterIncludes,
31
+ mergeIncludes: () => mergeIncludes
32
+ });
33
+ module.exports = __toCommonJS(filter_include_exports);
34
+ var import_utils = require("../utils");
35
+ const collectAssociationPathsFromWhere = /* @__PURE__ */ __name((where) => {
36
+ const aliasPaths = /* @__PURE__ */ new Set();
37
+ const traverse = /* @__PURE__ */ __name((value) => {
38
+ if (value == null) return;
39
+ if (Array.isArray(value)) {
40
+ for (const item of value) traverse(item);
41
+ return;
42
+ }
43
+ if (typeof value === "object") {
44
+ for (const [rawKey, child] of Object.entries(value)) {
45
+ if (typeof rawKey === "string" && rawKey.startsWith("$") && rawKey.endsWith("$")) {
46
+ const inner = rawKey.slice(1, -1);
47
+ const segments = inner.split(".");
48
+ if (segments.length > 1) {
49
+ aliasPaths.add(segments.slice(0, -1).join("."));
50
+ }
51
+ }
52
+ traverse(child);
53
+ }
54
+ for (const sym of Object.getOwnPropertySymbols(value)) {
55
+ traverse(value[sym]);
56
+ }
57
+ }
58
+ }, "traverse");
59
+ traverse(where);
60
+ return Array.from(aliasPaths);
61
+ }, "collectAssociationPathsFromWhere");
62
+ const pruneIncludeTreeByPaths = /* @__PURE__ */ __name((includes = [], requiredPathsRaw, options) => {
63
+ const normalizedPaths = options.underscored ? requiredPathsRaw.map(
64
+ (p) => p.split(".").map((s) => (0, import_utils.snakeCase)(s)).join(".")
65
+ ) : requiredPathsRaw;
66
+ if (!(includes == null ? void 0 : includes.length) || !(normalizedPaths == null ? void 0 : normalizedPaths.length)) return [];
67
+ const pruned = [];
68
+ for (const inc of includes) {
69
+ const association = inc == null ? void 0 : inc.association;
70
+ if (!association) continue;
71
+ const assocKey = options.underscored ? (0, import_utils.snakeCase)(String(association)) : String(association);
72
+ const matched = normalizedPaths.filter((p) => p === assocKey || p.startsWith(assocKey + "."));
73
+ if (!matched.length) continue;
74
+ const childRemainders = matched.map((p) => p === assocKey ? null : p.slice(assocKey.length + 1)).filter(Boolean);
75
+ const children = pruneIncludeTreeByPaths(inc.include ?? [], childRemainders, options);
76
+ const copy = { ...inc };
77
+ if (children.length) {
78
+ copy.include = children;
79
+ } else if ("include" in copy) {
80
+ delete copy.include;
81
+ }
82
+ pruned.push(copy);
83
+ }
84
+ return pruned;
85
+ }, "pruneIncludeTreeByPaths");
86
+ const mergeIncludes = /* @__PURE__ */ __name((includes = []) => {
87
+ const byAssociation = /* @__PURE__ */ new Map();
88
+ const mergeAll = /* @__PURE__ */ __name((list = []) => {
89
+ for (const inc of list) {
90
+ const association = inc == null ? void 0 : inc.association;
91
+ if (!association) continue;
92
+ const key = (0, import_utils.snakeCase)(String(association));
93
+ if (!byAssociation.has(key)) {
94
+ byAssociation.set(key, { ...inc, include: void 0 });
95
+ }
96
+ const target = byAssociation.get(key);
97
+ if (inc.required) target.required = true;
98
+ const mergedChildren = mergeIncludes([...target.include ?? [], ...inc.include ?? []]);
99
+ if (mergedChildren.length) {
100
+ target.include = mergedChildren;
101
+ } else if ("include" in target) {
102
+ delete target.include;
103
+ }
104
+ }
105
+ }, "mergeAll");
106
+ mergeAll(includes);
107
+ return Array.from(byAssociation.values());
108
+ }, "mergeIncludes");
109
+ const filterIncludes = /* @__PURE__ */ __name((where, includes, options) => {
110
+ const path = collectAssociationPathsFromWhere(where);
111
+ const result = pruneIncludeTreeByPaths(includes, path, options);
112
+ return result;
113
+ }, "filterIncludes");
114
+ // Annotate the CommonJS export names for ESM import in node:
115
+ 0 && (module.exports = {
116
+ filterIncludes,
117
+ mergeIncludes
118
+ });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@nocobase/database",
3
- "version": "1.9.0-alpha.11",
3
+ "version": "1.9.0-alpha.12",
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-alpha.11",
10
- "@nocobase/utils": "1.9.0-alpha.11",
9
+ "@nocobase/logger": "1.9.0-alpha.12",
10
+ "@nocobase/utils": "1.9.0-alpha.12",
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": "2dbef60d43720e92b483fc07d8ef1fd013083c88"
42
+ "gitHead": "f7840ec3f679262884ad17b4743ee047cb37b8fe"
42
43
  }