@zhin.js/schema 1.1.4 → 1.1.6

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/README.md CHANGED
@@ -207,8 +207,8 @@ plugins:
207
207
  | `.default(value)` | 设置默认值 |
208
208
  | `.option(label, value)` | 添加可选项 |
209
209
  | `.multiple()` | 允许多选 |
210
- | `.min(value)` | 设置最小值 |
211
- | `.max(value)` | 设置最大值 |
210
+ | `.min(value)` | 设置最小值(数字下限 / 字符串最小长度 / 列表最少项);写入导出的 JSON Schema,由 Runtime 配置校验执行,`schema(value)` 与 `safeParse` 只做结构校验 |
211
+ | `.max(value)` | 设置最大值,规则同 `.min` |
212
212
  | `.step(value)` | 设置步长 |
213
213
  | `.component(name)` | 指定渲染组件 |
214
214
  | `.toJSON()` | 业务 Schema 的内部序列化 |
@@ -12,6 +12,15 @@ export interface ConfigFieldMetadata {
12
12
  readonly component?: string;
13
13
  readonly options?: readonly Schema.Option[];
14
14
  readonly effect?: 'reload';
15
+ /** Property-level narrowing of the Plugin's feature levels (ADR 0050 §2). */
16
+ readonly levels?: readonly ('global' | 'adapter' | 'endpoint' | 'conversation')[];
17
+ }
18
+ /** Root `x-zhin-config` of a Plugin schema: layered levels (ADR 0050 / 0055). */
19
+ export interface ConfigFeatureLevels {
20
+ readonly levels: readonly ('global' | 'adapter' | 'endpoint' | 'conversation')[];
21
+ readonly conversationFields?: readonly string[];
22
+ readonly selfServiceFields?: readonly string[];
23
+ readonly chatAdminFields?: readonly string[];
15
24
  }
16
25
  export type ConfigJsonSchema = Readonly<Record<string, unknown>>;
17
26
  /** Export serializable configuration declarations; non-JSON business types are explicit errors. */
@@ -100,6 +100,8 @@ export function toJsonSchema(schema) {
100
100
  ...(meta.component !== undefined ? { component: meta.component } : {}),
101
101
  ...(meta.options !== undefined ? { options: structuredClone(meta.options) } : {}),
102
102
  ...(meta.effect !== undefined ? { effect: meta.effect } : {}),
103
+ ...(meta.feature !== undefined ? structuredClone(meta.feature) : {}),
104
+ ...(meta.fieldLevels !== undefined ? { levels: [...meta.fieldLevels] } : {}),
103
105
  };
104
106
  if (Object.keys(annotations).length)
105
107
  result['x-zhin-config'] = annotations;
package/lib/schema.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { safeParseSchema, type SchemaSafeParseResult } from './validate.js';
2
- import { type ConfigFieldMetadata, type ConfigFieldI18n } from './json-schema.js';
2
+ import { type ConfigFeatureLevels, type ConfigFieldMetadata, type ConfigFieldI18n } from './json-schema.js';
3
3
  export { SchemaValidationError, type SchemaSafeParseResult, type SchemaValidationIssue } from './validate.js';
4
4
  export { safeParseSchema };
5
5
  declare const configPresent: unique symbol;
@@ -31,13 +31,31 @@ export declare class Schema<S = any, T = S> {
31
31
  secret(): this;
32
32
  /** Declare the effect shown in configuration forms. */
33
33
  effect(effect: ConfigFieldMetadata['effect']): this;
34
+ /**
35
+ * Plugin root only: layered feature levels (ADR 0050 / 0055) and the fields editable at the
36
+ * conversation level, in chat by group admins, and in chat above the conversation level.
37
+ */
38
+ featureLevels(levels: ConfigFeatureLevels['levels'], fields?: Omit<ConfigFeatureLevels, 'levels'>): this;
39
+ /**
40
+ * Plugin property only: narrow the feature levels that may override this field (ADR 0050 §2),
41
+ * e.g. `.fieldLevels(['global'])` for secrets and process-wide resources. It can never widen
42
+ * the root `featureLevels` declaration; the Runtime rejects a wider list at startup.
43
+ */
44
+ fieldLevels(levels: ConfigFeatureLevels['levels']): this;
34
45
  /** Export the standard JSON Schema consumed by Runtime and Console. */
35
46
  toJsonSchema(): Readonly<Record<string, unknown>>;
36
47
  /** 设置选项列表 */
37
48
  option(list: (T | Schema.Option<T>)[]): this;
38
49
  /** 设置是否允许多选 */
39
50
  multiple(): this;
40
- /** 设置最小值 */
51
+ /**
52
+ * 设置最小值。
53
+ * `min` / `max` / `step` are range constraints of the exported JSON Schema
54
+ * (minimum/minLength/minItems, maximum/…, multipleOf) and are enforced where that
55
+ * schema is compiled (Runtime config validation). The callable schema and
56
+ * `safeParse` are structural only; `toJsonSchema` relies on that when it matches
57
+ * union branches (see assertPublicDefault).
58
+ */
41
59
  min(value: number): this;
42
60
  /** 设置最大值 */
43
61
  max(value: number): this;
@@ -57,7 +75,9 @@ export declare class Schema<S = any, T = S> {
57
75
  static date(key?: string): Schema<number | Date, Date>;
58
76
  /** 声明一个字典类型 */
59
77
  static dict<X extends Schema>(input: X, key?: string): Schema<Record<string, Schema.Input<X>>, Record<string, Schema.Output<X>>>;
60
- static object<X extends Record<string, Schema<any, any>>>(input: X, key?: string): Schema<Schema.InputRecord<X>, Schema.RecordTypes<X>>;
78
+ static object<X extends Record<string, Schema<any, any>>>(input: X, key?: string): Schema<Schema.InputRecord<X>, Schema.RecordTypes<X>> & {
79
+ readonly output?: Schema.RecordTypes<X>;
80
+ };
61
81
  /** 声明一个列表类型 */
62
82
  static list<X extends Schema>(inner: X, key?: string): Schema<Schema.Input<X>[], Schema.Output<X>[]>;
63
83
  /** 声明一个元组类型 */
@@ -123,6 +143,8 @@ export declare namespace Schema {
123
143
  step?: number;
124
144
  secret?: boolean;
125
145
  effect?: ConfigFieldMetadata['effect'];
146
+ feature?: ConfigFeatureLevels;
147
+ fieldLevels?: ConfigFeatureLevels['levels'];
126
148
  }
127
149
  interface Options {
128
150
  object?: Record<string, Schema>;
@@ -130,11 +152,11 @@ export declare namespace Schema {
130
152
  list?: readonly Schema[];
131
153
  }
132
154
  type Types<T> = Output<T>;
133
- type RecordTypes<T> = T extends Record<string, Schema> ? {
155
+ type RecordTypes<T extends Record<string, any>> = {
134
156
  [K in keyof T as T[K] extends Present ? K : never]-?: Output<T[K]>;
135
157
  } & {
136
158
  [K in keyof T as T[K] extends Present ? never : K]?: Output<T[K]>;
137
- } : unknown;
159
+ };
138
160
  type Union<T extends readonly any[]> = T extends readonly [infer L, ...infer R] ? Types<L> | Union<R> : never;
139
161
  type Intersect<T extends readonly any[]> = T extends readonly [infer L, ...infer R] ? Types<L> & Intersect<R> : unknown;
140
162
  type Tuple<T extends readonly any[]> = T extends readonly [infer L, ...infer R] ? [Types<L>, ...Tuple<R>] : [];
package/lib/schema.js CHANGED
@@ -24,6 +24,9 @@ export class Schema {
24
24
  set(target, p, value, receiver) {
25
25
  return Reflect.set(_this, p, value, receiver);
26
26
  },
27
+ has(target, p) {
28
+ return Reflect.has(_this, p) || Reflect.has(target, p);
29
+ },
27
30
  });
28
31
  }
29
32
  static fromJSON(json) {
@@ -99,6 +102,27 @@ export class Schema {
99
102
  this.meta.effect = effect;
100
103
  return this;
101
104
  }
105
+ /**
106
+ * Plugin root only: layered feature levels (ADR 0050 / 0055) and the fields editable at the
107
+ * conversation level, in chat by group admins, and in chat above the conversation level.
108
+ */
109
+ featureLevels(levels, fields = {}) {
110
+ if (this.meta.type !== 'object')
111
+ throw new TypeError('featureLevels only supports the plugin root object');
112
+ this.meta.feature = structuredClone({ levels, ...fields });
113
+ return this;
114
+ }
115
+ /**
116
+ * Plugin property only: narrow the feature levels that may override this field (ADR 0050 §2),
117
+ * e.g. `.fieldLevels(['global'])` for secrets and process-wide resources. It can never widen
118
+ * the root `featureLevels` declaration; the Runtime rejects a wider list at startup.
119
+ */
120
+ fieldLevels(levels) {
121
+ if (!levels.length || !levels.includes('global'))
122
+ throw new TypeError('fieldLevels must include global');
123
+ this.meta.fieldLevels = [...levels];
124
+ return this;
125
+ }
102
126
  /** Export the standard JSON Schema consumed by Runtime and Console. */
103
127
  toJsonSchema() {
104
128
  return toJsonSchema(this);
@@ -115,7 +139,14 @@ export class Schema {
115
139
  this.meta.multiple = true;
116
140
  return this;
117
141
  }
118
- /** 设置最小值 */
142
+ /**
143
+ * 设置最小值。
144
+ * `min` / `max` / `step` are range constraints of the exported JSON Schema
145
+ * (minimum/minLength/minItems, maximum/…, multipleOf) and are enforced where that
146
+ * schema is compiled (Runtime config validation). The callable schema and
147
+ * `safeParse` are structural only; `toJsonSchema` relies on that when it matches
148
+ * union branches (see assertPublicDefault).
149
+ */
119
150
  min(value) {
120
151
  this.meta.min = value;
121
152
  return this;
@@ -221,7 +252,7 @@ export class Schema {
221
252
  throw new TypeError(`${key} is not a string`);
222
253
  break;
223
254
  case 'number':
224
- if (!['number', 'undefined'].includes(typeof value))
255
+ if (!['number', 'undefined'].includes(typeof value) || (typeof value === 'number' && Number.isNaN(value)))
225
256
  throw new TypeError(`${key} is not a number`);
226
257
  break;
227
258
  case 'boolean':
@@ -239,11 +270,11 @@ export class Schema {
239
270
  throw new TypeError(`${key} is not a valid Date`);
240
271
  break;
241
272
  case 'dict':
242
- if (!['object', 'undefined', 'null'].includes(typeof value))
273
+ if (!['object', 'undefined'].includes(typeof value) || value === null || Array.isArray(value))
243
274
  throw new TypeError(`${key} is not a object`);
244
275
  break;
245
276
  case 'object':
246
- if (!['object', 'undefined', 'null'].includes(typeof value))
277
+ if (!['object', 'undefined'].includes(typeof value) || value === null || Array.isArray(value))
247
278
  throw new TypeError(`${key} is not a object`);
248
279
  break;
249
280
  case 'list':
@@ -310,19 +341,27 @@ Schema.extend('dict', function (key, value) {
310
341
  Schema.extend('object', function (key, value) {
311
342
  const getDefault = (schema) => {
312
343
  const result = Object.create(null);
313
- for (const key in schema.options.object) {
314
- const propSchema = schema.options.object[key];
315
- result[key] = propSchema.default !== undefined ? propSchema.default : undefined;
344
+ for (const propKey in schema.options.object) {
345
+ const propSchema = schema.options.object[propKey];
346
+ result[propKey] = propSchema.meta.default !== undefined ? propSchema.meta.default : undefined;
316
347
  }
317
348
  return result;
318
349
  };
319
350
  value = Schema.checkDefault(this, key, value, getDefault(this));
351
+ if (value === undefined)
352
+ return value;
320
353
  const result = Object.create(null);
321
- for (const [k, val] of Object.entries(value)) {
322
- const propSchema = this.options.object[k];
354
+ const shape = this.options.object || {};
355
+ const allKeys = new Set([...Object.keys(shape), ...Object.keys(value)]);
356
+ for (const k of allKeys) {
357
+ const val = value[k];
358
+ const propSchema = shape[k];
323
359
  if (propSchema) {
324
360
  // propSchema是一个可调用的Schema实例
325
- result[k] = propSchema(val, `${key}.${k}`);
361
+ const evaluated = propSchema(val, `${key}.${k}`);
362
+ if (evaluated !== undefined || k in value) {
363
+ result[k] = evaluated;
364
+ }
326
365
  }
327
366
  else {
328
367
  result[k] = val;
@@ -355,8 +394,12 @@ Schema.extend('const', function (key, value) {
355
394
  return value;
356
395
  });
357
396
  Schema.extend('tuple', function (key, value) {
358
- value = Schema.checkDefault(this, key, value, []);
359
- if (value.length > 0 && value.length !== this.options.list.length)
397
+ // No `[]` fallback: an empty array is not a valid tuple (JSON Schema exports
398
+ // minItems = maxItems = length, and safeParse rejects it), so absent stays absent.
399
+ value = Schema.checkDefault(this, key, value);
400
+ if (value === undefined)
401
+ return value;
402
+ if (value.length !== this.options.list.length)
360
403
  throw new TypeError(`${key} tuple length mismatch`);
361
404
  return value.map((item, index) => this.options.list[index](item, `${key}[${index}]`));
362
405
  });
package/lib/validate.js CHANGED
@@ -29,7 +29,7 @@ function validateOnly(schema, path, value) {
29
29
  }
30
30
  return value;
31
31
  case 'number':
32
- if (value !== undefined && typeof value !== 'number') {
32
+ if (value !== undefined && (typeof value !== 'number' || Number.isNaN(value))) {
33
33
  throw issue(path, `${path} is not a number`);
34
34
  }
35
35
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/schema",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Configuration validation and schema system for Zhin.js",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -14,6 +14,10 @@
14
14
  "files": [
15
15
  "lib"
16
16
  ],
17
+ "engines": {
18
+ "node": ">=22.19.0"
19
+ },
20
+ "license": "MIT",
17
21
  "devDependencies": {
18
22
  "@types/node": "^26.1.2",
19
23
  "typescript": "^6.0.3"