cabloy 5.1.236 → 5.1.239

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.
Files changed (70) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/package.json +1 -1
  4. package/repo-docs/backend/broadcast-guide.md +2 -0
  5. package/repo-docs/backend/queue-guide.md +31 -0
  6. package/repo-docs/backend/schedule-guide.md +23 -0
  7. package/vona/packages-cli/cli/package.json +1 -1
  8. package/vona/packages-cli/cli-set-api/package.json +1 -1
  9. package/vona/packages-vona/vona/package.json +1 -1
  10. package/vona/packages-vona/vona-core/package.json +1 -1
  11. package/vona/packages-vona/vona-core/src/lib/bean/resource/locale/appLocale.ts +2 -2
  12. package/vona/packages-vona/vona-core/src/lib/bean/resource/locale/utils.ts +46 -1
  13. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +4 -0
  14. package/vona/packages-vona/vona-core/src/lib/module/locales.ts +41 -22
  15. package/vona/packages-vona/vona-core/src/lib/utils/zod-enhance.ts +1 -1
  16. package/vona/packages-vona/vona-core/src/types/config/config.ts +2 -0
  17. package/vona/packages-vona/vona-core/src/types/config/locale.ts +11 -4
  18. package/vona/packages-vona/vona-core/src/types/interface/module.ts +1 -0
  19. package/vona/packages-vona/vona-mock/package.json +1 -1
  20. package/vona/patches/{zova-core@5.1.94.patch → zova-core@5.1.95.patch} +2 -2
  21. package/vona/pnpm-lock.yaml +146 -21
  22. package/vona/pnpm-workspace.yaml +1 -1
  23. package/vona/src/backend/config/config/config.ts +9 -0
  24. package/vona/src/suite/a-home/modules/home-base/src/main.ts +1 -4
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  27. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  29. package/vona/src/suite-vendor/a-vona/modules/a-hmr/src/service/watch.ts +1 -0
  30. package/vona/src/suite-vendor/a-vona/modules/a-locale/package.json +1 -1
  31. package/vona/src/suite-vendor/a-vona/modules/a-locale/src/config/config.ts +2 -4
  32. package/vona/src/suite-vendor/a-vona/modules/a-locale/src/main.ts +12 -30
  33. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  34. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/bean/bean.openapi.ts +6 -2
  35. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/service/openapi.ts +107 -48
  36. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/contract.ts +25 -0
  37. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/index.ts +1 -0
  38. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  39. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/decorator.ts +1 -0
  40. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/decorator/index.ts +1 -0
  41. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/decorator/mcp.ts +28 -0
  42. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/types/api.ts +8 -0
  43. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  44. package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +1 -1
  45. package/vona/src/suite-vendor/a-vona/modules/a-queue/src/config/config.ts +2 -0
  46. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  47. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/service/rateLimit.ts +3 -1
  48. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/rateLimit.ts +4 -0
  49. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  50. package/zova/packages-utils/zova-jsx/package.json +2 -2
  51. package/zova/packages-zova/zova/package.json +3 -3
  52. package/zova/packages-zova/zova-core/package.json +1 -1
  53. package/zova/packages-zova/zova-core/src/core/component/locale.ts +13 -3
  54. package/zova/packages-zova/zova-core/src/core/sys/config.ts +28 -1
  55. package/zova/packages-zova/zova-core/src/core/sys/locale.ts +24 -0
  56. package/zova/packages-zova/zova-core/src/core/sys/module.ts +16 -4
  57. package/zova/packages-zova/zova-core/src/core/sys/sys.ts +7 -2
  58. package/zova/packages-zova/zova-core/src/types/interface/module.ts +1 -0
  59. package/zova/packages-zova/zova-core/src/utils/zod-enhance.ts +1 -1
  60. package/zova/pnpm-lock.yaml +7 -7
  61. package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
  62. package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
  63. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.locale.tsx +1 -1
  64. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  65. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +13 -1
  66. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrNavigation.ts +24 -0
  67. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +8 -1
  68. package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrNavigation.test.ts +54 -1
  69. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  70. package/zova/src/suite-vendor/a-zova/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import type { RouteConfig } from '@cabloy/zod-to-openapi';
1
2
  import type {
2
3
  OpenAPIObject as OpenAPIObject30,
3
4
  SchemaObject as SchemaObject30,
@@ -36,6 +37,11 @@ import {
36
37
  import { SymbolRequestMappingHandler } from 'vona-module-a-web';
37
38
  import { z } from 'zod';
38
39
 
40
+ import type {
41
+ IControllerActionContract,
42
+ IControllerActionRequest,
43
+ IGenerateJsonOfControllerActionOptions,
44
+ } from '../types/contract.ts';
39
45
  import type { RouteHandlerArgumentMetaDecorator } from '../types/decorator.ts';
40
46
 
41
47
  const __ArgumentTypes = ['param', 'query', 'body', 'headers', 'fields', 'field', 'files', 'file'];
@@ -180,6 +186,7 @@ export class ServiceOpenapi extends BeanBase {
180
186
  moduleName: string,
181
187
  controller: Constructable,
182
188
  actionKey?: string,
189
+ options: IGenerateJsonOfControllerActionOptions = {},
183
190
  ) {
184
191
  // info
185
192
  const info = ModuleInfo.parseInfo(moduleName)!;
@@ -193,14 +200,21 @@ export class ServiceOpenapi extends BeanBase {
193
200
  SymbolOpenApiOptions,
194
201
  controller,
195
202
  );
196
- if (controllerOpenApiOptions?.exclude) return;
203
+ if (controllerOpenApiOptions?.exclude && !options.includeExcluded) return;
197
204
  // descs
198
205
  const descs = Object.getOwnPropertyDescriptors(controller.prototype);
199
206
  const actionKeys = actionKey ? [actionKey] : Object.keys(descs);
200
207
  for (const actionKey of actionKeys) {
201
- const desc = descs[actionKey];
208
+ let desc = descs[actionKey];
209
+ if (actionKey && !desc) {
210
+ let prototype = Object.getPrototypeOf(controller.prototype);
211
+ while (prototype && !desc) {
212
+ desc = Object.getOwnPropertyDescriptor(prototype, actionKey)!;
213
+ prototype = Object.getPrototypeOf(prototype);
214
+ }
215
+ }
202
216
  if (['constructor'].includes(actionKey)) continue;
203
- if (!desc.value || typeof desc.value !== 'function') continue;
217
+ if (!desc?.value || typeof desc.value !== 'function') continue;
204
218
  this._registerControllerAction(
205
219
  registry,
206
220
  info,
@@ -211,6 +225,7 @@ export class ServiceOpenapi extends BeanBase {
211
225
  controllerOpenApiOptions,
212
226
  actionKey,
213
227
  desc,
228
+ options,
214
229
  );
215
230
  }
216
231
  }
@@ -225,6 +240,7 @@ export class ServiceOpenapi extends BeanBase {
225
240
  controllerOpenApiOptions: IOpenapiOptions | undefined,
226
241
  actionKey: string,
227
242
  _desc: PropertyDescriptor,
243
+ options: IGenerateJsonOfControllerActionOptions,
228
244
  ) {
229
245
  // app
230
246
  const app = this.app;
@@ -235,7 +251,7 @@ export class ServiceOpenapi extends BeanBase {
235
251
  controller.prototype,
236
252
  actionKey,
237
253
  );
238
- if (actionOpenApiOptions?.exclude) return;
254
+ if (actionOpenApiOptions?.exclude && !options.includeExcluded) return;
239
255
 
240
256
  // actionPath/actionMethod
241
257
  if (!appMetadata.hasMetadata(SymbolRequestMappingHandler, controller.prototype, actionKey))
@@ -302,12 +318,71 @@ export class ServiceOpenapi extends BeanBase {
302
318
  });
303
319
  }
304
320
 
321
+ /** Resolve validators in the current context; do not retain across locale changes. */
322
+ public getControllerActionContract(
323
+ controller: Constructable,
324
+ actionKey: string,
325
+ ): IControllerActionContract {
326
+ const beanOptions = appResource.getBean(controller);
327
+ if (!beanOptions) throw new Error('invalid controller');
328
+ const info = ModuleInfo.parseInfo(beanOptions.module)!;
329
+ const actionOpenApiOptions = appMetadata.getMetadata<IOpenapiOptions>(
330
+ SymbolOpenApiOptions,
331
+ controller.prototype,
332
+ actionKey,
333
+ );
334
+ const controllerOpenApiOptions = appMetadata.getMetadata<IOpenapiOptions>(
335
+ SymbolOpenApiOptions,
336
+ controller,
337
+ );
338
+ const { request } = this._groupRequestSchemas(
339
+ info,
340
+ controller,
341
+ actionKey,
342
+ actionOpenApiOptions,
343
+ controllerOpenApiOptions,
344
+ );
345
+ return {
346
+ request,
347
+ response: this._getBodySchema(controller, actionKey, actionOpenApiOptions),
348
+ contentType: actionOpenApiOptions?.contentType || 'application/json',
349
+ openapiOptions: actionOpenApiOptions,
350
+ argsMeta: this._getArgsMeta(controller, actionKey) ?? [],
351
+ };
352
+ }
353
+
305
354
  private _collectRequest(
306
355
  info: ModuleInfo.IModuleInfo,
307
356
  controller: Constructable,
308
357
  actionKey: string,
309
358
  actionOpenApiOptions: IOpenapiOptions | undefined,
310
359
  controllerOpenApiOptions: IOpenapiOptions | undefined,
360
+ ) {
361
+ const { request: schemas, isUpload } = this._groupRequestSchemas(
362
+ info,
363
+ controller,
364
+ actionKey,
365
+ actionOpenApiOptions,
366
+ controllerOpenApiOptions,
367
+ );
368
+ const { body, ...request } = schemas;
369
+ if (!body) return request as RouteConfig['request'];
370
+ const contentType = isUpload ? 'multipart/form-data' : 'application/json';
371
+ return {
372
+ ...request,
373
+ body: {
374
+ required: isUpload || !body.safeParse(undefined).success,
375
+ content: { [contentType]: { schema: body } },
376
+ },
377
+ } as RouteConfig['request'];
378
+ }
379
+
380
+ private _groupRequestSchemas(
381
+ info: ModuleInfo.IModuleInfo,
382
+ controller: Constructable,
383
+ actionKey: string,
384
+ actionOpenApiOptions: IOpenapiOptions | undefined,
385
+ controllerOpenApiOptions: IOpenapiOptions | undefined,
311
386
  ) {
312
387
  // meta
313
388
  const argsMeta = this._prepareArgsMeta(
@@ -316,12 +391,11 @@ export class ServiceOpenapi extends BeanBase {
316
391
  actionOpenApiOptions,
317
392
  controllerOpenApiOptions,
318
393
  );
319
- if (!argsMeta) return;
320
394
  // args
321
395
  const argsMapWithField: any = {};
322
396
  const argsMapIsolate: any = {};
323
397
  let isUpload;
324
- for (const argMeta of argsMeta) {
398
+ for (const argMeta of argsMeta ?? []) {
325
399
  if (!argMeta) continue;
326
400
  if (!__ArgumentTypes.includes(argMeta.type)) continue;
327
401
  if (['fields', 'field', 'files', 'file'].includes(argMeta.type)) {
@@ -339,7 +413,7 @@ export class ServiceOpenapi extends BeanBase {
339
413
  }
340
414
  }
341
415
  // request
342
- const request: any = {};
416
+ const request: IControllerActionRequest = {};
343
417
  if (isUpload) {
344
418
  const schemaObj: any = {};
345
419
  // not check argsMapIsolate.fields
@@ -348,16 +422,7 @@ export class ServiceOpenapi extends BeanBase {
348
422
  if (argsMapWithField.files) Object.assign(schemaObj, argsMapWithField.files);
349
423
  if (argsMapWithField.file) Object.assign(schemaObj, argsMapWithField.file);
350
424
  if (argsMapIsolate.files) schemaObj.blobs = argsMapIsolate.files;
351
- const schema = z.object(schemaObj);
352
- // body
353
- request.body = {
354
- required: true,
355
- content: {
356
- 'multipart/form-data': {
357
- schema,
358
- },
359
- },
360
- };
425
+ request.body = z.object(schemaObj);
361
426
  } else {
362
427
  for (const argumentType of __ArgumentTypes) {
363
428
  let schema: z.ZodType | undefined = argsMapIsolate[argumentType];
@@ -375,26 +440,12 @@ export class ServiceOpenapi extends BeanBase {
375
440
  `Invalid Openapi argument type: ${info.relativeName}:${controller.name}.${actionKey}#${argumentType}`,
376
441
  );
377
442
  }
378
- // record
379
- if (argumentType === 'body') {
380
- // body
381
- request.body = {
382
- required: !schema.safeParse(undefined).success,
383
- content: {
384
- 'application/json': {
385
- schema,
386
- },
387
- },
388
- };
389
- } else {
390
- // others
391
- const name = argumentType === 'param' ? 'params' : argumentType;
392
- request[name] = schema;
393
- }
443
+ const name = argumentType === 'param' ? 'params' : argumentType;
444
+ request[name] = schema;
394
445
  }
395
446
  }
396
447
 
397
- return request;
448
+ return { request, isUpload: !!isUpload };
398
449
  }
399
450
 
400
451
  private _collectResponses(
@@ -431,14 +482,7 @@ export class ServiceOpenapi extends BeanBase {
431
482
  actionOpenApiOptions: IOpenapiOptions | undefined,
432
483
  contentType: string,
433
484
  ) {
434
- // bodySchema
435
- let bodySchema: z.ZodType;
436
- if (actionOpenApiOptions?.bodySchema) {
437
- bodySchema = actionOpenApiOptions.bodySchema;
438
- } else {
439
- const metaType = appMetadata.getDesignReturntype(controller.prototype, actionKey);
440
- bodySchema = $schema(metaType as any);
441
- }
485
+ const bodySchema = this._getBodySchema(controller, actionKey, actionOpenApiOptions);
442
486
  // wrapper
443
487
  if (contentType !== 'application/json') return bodySchema;
444
488
  if (actionOpenApiOptions?.bodySchemaWrapper === false) return bodySchema;
@@ -446,18 +490,33 @@ export class ServiceOpenapi extends BeanBase {
446
490
  return wrapper(bodySchema);
447
491
  }
448
492
 
449
- private _prepareArgsMeta(
493
+ private _getBodySchema(
450
494
  controller: Constructable,
451
495
  actionKey: string,
452
496
  actionOpenApiOptions: IOpenapiOptions | undefined,
453
- controllerOpenApiOptions: IOpenapiOptions | undefined,
454
- ) {
455
- // meta
456
- let argsMeta = appMetadata.getMetadata<RouteHandlerArgumentMetaDecorator[]>(
497
+ ): z.ZodType {
498
+ if (actionOpenApiOptions?.bodySchema) return actionOpenApiOptions.bodySchema;
499
+ const metaType = appMetadata.getDesignReturntype(controller.prototype, actionKey);
500
+ return $schema(metaType as any);
501
+ }
502
+
503
+ private _getArgsMeta(controller: Constructable, actionKey: string) {
504
+ const argsMeta = appMetadata.getMetadata<RouteHandlerArgumentMetaDecorator[]>(
457
505
  SymbolRouteHandlersArgumentsMeta,
458
506
  controller.prototype,
459
507
  actionKey,
460
508
  );
509
+ return argsMeta?.map(item => item && { ...item, pipes: item.pipes?.slice() });
510
+ }
511
+
512
+ private _prepareArgsMeta(
513
+ controller: Constructable,
514
+ actionKey: string,
515
+ actionOpenApiOptions: IOpenapiOptions | undefined,
516
+ controllerOpenApiOptions: IOpenapiOptions | undefined,
517
+ ) {
518
+ // meta
519
+ let argsMeta = this._getArgsMeta(controller, actionKey);
461
520
  // headers
462
521
  const objHeaders = Object.assign(
463
522
  {} as any,
@@ -467,7 +526,7 @@ export class ServiceOpenapi extends BeanBase {
467
526
  if (isEmptyObject(objHeaders)) return argsMeta;
468
527
  // merge
469
528
  if (!argsMeta) argsMeta = [];
470
- let argHeaders = argsMeta.find(item => item.type === 'headers' && !item.field);
529
+ let argHeaders = argsMeta.find(item => item?.type === 'headers' && !item.field);
471
530
  if (!argHeaders) {
472
531
  argHeaders = { type: 'headers', field: undefined, schema: z.object(objHeaders) } as any;
473
532
  argsMeta.push(argHeaders!);
@@ -0,0 +1,25 @@
1
+ import type { IOpenapiOptions, TypeResponseContentType } from 'vona-module-a-openapiutils';
2
+ import type { z } from 'zod';
3
+
4
+ import type { RouteHandlerArgumentMetaDecorator } from './decorator.ts';
5
+
6
+ export interface IControllerActionRequest {
7
+ params?: z.ZodType;
8
+ query?: z.ZodType;
9
+ body?: z.ZodType;
10
+ headers?: z.ZodType;
11
+ }
12
+
13
+ export interface IControllerActionContract {
14
+ request: IControllerActionRequest;
15
+ /** The controller return validator, without the HTTP response wrapper. */
16
+ response: z.ZodType;
17
+ contentType: TypeResponseContentType;
18
+ openapiOptions?: IOpenapiOptions;
19
+ /** Copies of the original executor metadata, without synthetic documentation headers. */
20
+ argsMeta: RouteHandlerArgumentMetaDecorator[];
21
+ }
22
+
23
+ export interface IGenerateJsonOfControllerActionOptions {
24
+ includeExcluded?: boolean;
25
+ }
@@ -1,5 +1,6 @@
1
1
  import '@cabloy/zod-query';
2
2
 
3
+ export * from './contract.ts';
3
4
  export * from './decorator.ts';
4
5
  export * from './formMeta.ts';
5
6
  export * from './order.ts';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-openapiutils",
3
- "version": "5.1.22",
3
+ "version": "5.1.23",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -2,6 +2,7 @@ export const SymbolRouteHandlersArgumentsMeta = Symbol('SymbolRouteHandlersArgum
2
2
  export const SymbolRouteHandlersArgumentsValue = Symbol('SymbolRouteHandlersArgumentsValue');
3
3
 
4
4
  export const SymbolOpenApiOptions = Symbol('SymbolOpenApiOptions');
5
+ export const SymbolMcpOptions = Symbol('SymbolMcpOptions');
5
6
  export const SymbolControllerResource = Symbol('SymbolControllerResource');
6
7
  export const SymbolDecoratorRule = Symbol('SymbolDecoratorRule');
7
8
  export const SymbolDecoratorDtoOpenapi = Symbol('SymbolDecoratorDtoOpenapi');
@@ -1,2 +1,3 @@
1
1
  export * from './api.ts';
2
+ export * from './mcp.ts';
2
3
  export * from './resource.ts';
@@ -0,0 +1,28 @@
1
+ import type { MetadataKey } from 'vona';
2
+
3
+ import { appMetadata } from 'vona';
4
+
5
+ import type { IMcpToolOptions } from '../../types/api.ts';
6
+
7
+ import { SymbolMcpOptions } from '../const/decorator.ts';
8
+
9
+ function tool(options?: Omit<IMcpToolOptions, 'name'>): ClassDecorator & MethodDecorator;
10
+ function tool(options: IMcpToolOptions): MethodDecorator;
11
+ function tool(options: IMcpToolOptions = {}): ClassDecorator & MethodDecorator {
12
+ return function (target: object, prop?: MetadataKey, descriptor?: PropertyDescriptor) {
13
+ if (prop === undefined && options.name !== undefined) {
14
+ throw new Error('mcp.tool name is only valid on a controller action');
15
+ }
16
+ // An options object, including an empty one, is the explicit opt-in marker.
17
+ const metadata = appMetadata.getOwnMetadataMap(
18
+ true,
19
+ SymbolMcpOptions,
20
+ target,
21
+ prop,
22
+ ) as IMcpToolOptions;
23
+ Object.assign(metadata, options);
24
+ return descriptor;
25
+ } as ClassDecorator & MethodDecorator;
26
+ }
27
+
28
+ export const mcp = { tool };
@@ -34,6 +34,14 @@ export interface IOpenapiSchemaObject {
34
34
 
35
35
  export type TypeSecuritySchemes = Record<string, ((this: VonaApplication) => object) | object>;
36
36
 
37
+ export interface IMcpToolOptions {
38
+ /** A public tool name override, valid only on an action. */
39
+ name?: string;
40
+ description?: string | ILocaleMagic;
41
+ readOnly?: boolean;
42
+ output?: { pick: string[] };
43
+ }
44
+
37
45
  export interface IOpenapiOptions {
38
46
  public?: boolean;
39
47
  description?: string | ILocaleMagic;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.32",
3
+ "version": "5.1.33",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-queue",
3
- "version": "5.1.16",
3
+ "version": "5.1.17",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -4,6 +4,8 @@ import type { VonaApplication } from 'vona';
4
4
  export function config(_app: VonaApplication) {
5
5
  return {
6
6
  worker: {
7
+ removeOnComplete: { count: 0 },
8
+ removeOnFail: { count: 0 },
7
9
  lockDuration: 30 * 1000,
8
10
  maxStalledCount: 1000,
9
11
  stalledInterval: 10 * 1000,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-ratelimit",
3
- "version": "5.1.2",
3
+ "version": "5.1.3",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -34,7 +34,9 @@ export class ServiceRateLimit extends BeanBase {
34
34
  const identity = this._getIdentity(policy);
35
35
  const identityHash = createHash('sha256').update(identity).digest('base64url');
36
36
  const policyName = policy.name ?? `${route.controllerBeanFullName}:${String(route.action)}`;
37
- const routeName = `${route.routeMethod}:${route.routePathRaw}`;
37
+ const routeName = policy.route
38
+ ? `${policy.route.method}:${policy.route.path}`
39
+ : `${route.routeMethod}:${route.routePathRaw}`;
38
40
  const windowStart = Math.floor(now / policy.windowMs) * policy.windowMs;
39
41
  const instanceName = this.ctx.instanceName ?? 'default';
40
42
  return [
@@ -7,6 +7,10 @@ export interface IRateLimitPolicy {
7
7
  windowMs: number;
8
8
  key: RateLimitKey;
9
9
  name?: string;
10
+ route?: {
11
+ method: string;
12
+ path: string;
13
+ };
10
14
  headers: boolean;
11
15
  }
12
16
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.108",
3
+ "version": "5.1.109",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-jsx",
3
- "version": "1.1.94",
3
+ "version": "1.1.95",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "Zova JSX",
6
6
  "keywords": [
@@ -50,7 +50,7 @@
50
50
  "@cabloy/word-utils": "^2.1.14",
51
51
  "typestyle": "^2.4.0",
52
52
  "vue": "^3.5.38",
53
- "zova-core": "^5.1.94"
53
+ "zova-core": "^5.1.95"
54
54
  },
55
55
  "devDependencies": {
56
56
  "clean-package": "^2.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.176",
3
+ "version": "5.1.177",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -45,8 +45,8 @@
45
45
  "postpack": "clean-package restore"
46
46
  },
47
47
  "dependencies": {
48
- "zova-core": "^5.1.94",
49
- "zova-suite-a-zova": "^5.1.175"
48
+ "zova-core": "^5.1.95",
49
+ "zova-suite-a-zova": "^5.1.176"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-core",
3
- "version": "5.1.94",
3
+ "version": "5.1.95",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -12,6 +12,7 @@ import type {
12
12
  import { BeanSimple } from '../../bean/beanSimple.ts';
13
13
  import { LocaleModuleNameSeparator } from '../../bean/resource/locale/type.ts';
14
14
  import { cast } from '../../types/index.ts';
15
+ import { normalizeLocale } from '../sys/config.ts';
15
16
 
16
17
  const SymbolLocaleCurrent = Symbol('SymbolLocaleCurrent');
17
18
  const SymbolTzCurrent = Symbol('SymbolTzCurrent');
@@ -31,12 +32,11 @@ export class AppLocale extends BeanSimple {
31
32
  if (!locale) {
32
33
  locale = this.metaCookie.getItem(this.sys.config.locale.storeKey);
33
34
  }
34
- if (!locale) locale = this.sys.config.locale.default;
35
- return locale as keyof ILocaleRecord;
35
+ return this.normalize(locale);
36
36
  }
37
37
 
38
38
  set current(value: keyof ILocaleRecord | '' | undefined) {
39
- if (!value) value = this.sys.config.locale.default;
39
+ value = this.normalize(value);
40
40
  if (this[SymbolLocaleCurrent].value === value) return;
41
41
  this[SymbolLocaleCurrent].value = value;
42
42
  if (this._cookieEnabled()) {
@@ -44,6 +44,16 @@ export class AppLocale extends BeanSimple {
44
44
  }
45
45
  }
46
46
 
47
+ public normalize(value: unknown): keyof ILocaleRecord {
48
+ return normalizeLocale(value, this.sys.config.locale);
49
+ }
50
+
51
+ public getItems(items?: Partial<Record<keyof ILocaleRecord, string>>) {
52
+ const enabled = this.sys.config.locale.items;
53
+ if (!items) return enabled;
54
+ return Object.fromEntries(Object.entries(items).filter(([key]) => Object.hasOwn(enabled, key)));
55
+ }
56
+
47
57
  private _cookieEnabled() {
48
58
  return !process.env.SERVER || cast(this.ctx.meta).$ssr.profileOptions.useCookie;
49
59
  }
@@ -87,7 +87,7 @@ export interface ZovaConfig {
87
87
  locale: {
88
88
  default: keyof ILocaleRecord;
89
89
  storeKey: string;
90
- items: Record<keyof ILocaleRecord, string>;
90
+ items: Partial<Record<keyof ILocaleRecord, string>>;
91
91
  };
92
92
  tz: {
93
93
  storeKey: string;
@@ -105,3 +105,30 @@ export interface ZovaConfig {
105
105
  }
106
106
 
107
107
  export type ZovaConfigOptional = PowerPartial<ZovaConfig>;
108
+
109
+ export function validateLocaleConfig(locale: ZovaConfig['locale']) {
110
+ if (!locale?.items || Object.keys(locale.items).length === 0) {
111
+ throw new Error('config.locale.items must contain at least one enabled locale');
112
+ }
113
+ for (const [key, label] of Object.entries(locale.items)) {
114
+ if (
115
+ !key ||
116
+ key.trim().toLowerCase().replaceAll('_', '-') !== key ||
117
+ typeof label !== 'string'
118
+ ) {
119
+ throw new Error(`config.locale.items contains an invalid locale: ${key}`);
120
+ }
121
+ }
122
+ if (!Object.hasOwn(locale.items, locale.default)) {
123
+ throw new Error('config.locale.default must be an enabled own key of config.locale.items');
124
+ }
125
+ }
126
+
127
+ export function normalizeLocale(value: unknown, locale: ZovaConfig['locale']): keyof ILocaleRecord {
128
+ validateLocaleConfig(locale);
129
+ const canonical =
130
+ typeof value === 'string' ? value.trim().toLowerCase().replaceAll('_', '-') : '';
131
+ return Object.hasOwn(locale.items, canonical)
132
+ ? (canonical as keyof ILocaleRecord)
133
+ : locale.default;
134
+ }
@@ -36,6 +36,30 @@ export class SysLocale extends AppLocale {
36
36
  }
37
37
  }
38
38
 
39
+ /** @internal */
40
+ public _registerProviderLocales(
41
+ locales: TypeModuleResourceLocales,
42
+ modules: Record<string, unknown>,
43
+ ) {
44
+ if (!locales) return;
45
+ for (const [locale, resource] of Object.entries(locales)) {
46
+ const { modules: targets, ...globals } = resource as {
47
+ modules?: Record<string, Record<string, string>>;
48
+ [key: string]: unknown;
49
+ };
50
+ // Provider envelopes are data, never a message or a provider-owned scope.
51
+ const strings = Object.fromEntries(
52
+ Object.entries(globals).filter(([, value]) => typeof value === 'string'),
53
+ );
54
+ this.locales[locale] = Object.assign({}, strings, this.locales[locale]);
55
+ for (const [moduleName, dictionary] of Object.entries(targets ?? {})) {
56
+ // Membership, not installation: a translated target must remain lazy.
57
+ if (!Object.hasOwn(modules, moduleName)) continue;
58
+ this._registerLocale(moduleName, locale, dictionary);
59
+ }
60
+ }
61
+ }
62
+
39
63
  private _registerLocale(moduleName: string, locale: string, moduleLocales: object) {
40
64
  // locales
41
65
  this.locales[locale] = Object.assign({}, moduleLocales, this.locales[locale]);
@@ -29,6 +29,7 @@ export class SysModule extends BeanSimple {
29
29
  public async initialize(modulesMeta: PluginZovaModulesMeta) {
30
30
  this.modulesMeta = modulesMeta;
31
31
  await this._loadAllMonkeysAndSyncsAndPreloads();
32
+ await this._requireAllSpecifics('locale');
32
33
  await this._requireAllSpecifics('preload');
33
34
  await this._requireAllSpecifics('monkey');
34
35
  await this._requireAllSpecifics('sync');
@@ -123,7 +124,8 @@ export class SysModule extends BeanSimple {
123
124
  process.env.SERVER ||
124
125
  info.capabilities?.monkey ||
125
126
  info.capabilities?.sync ||
126
- info.capabilities?.preload;
127
+ info.capabilities?.preload ||
128
+ info.capabilities?.locale;
127
129
  if (shouldLoad) {
128
130
  const moduleResource = module.resource as any;
129
131
  if (typeof moduleResource === 'function') {
@@ -159,7 +161,7 @@ export class SysModule extends BeanSimple {
159
161
  }
160
162
  }
161
163
 
162
- private async _requireAllSpecifics(capabilityName: 'preload' | 'monkey' | 'sync') {
164
+ private async _requireAllSpecifics(capabilityName: 'locale' | 'preload' | 'monkey' | 'sync') {
163
165
  const modulesMeta = this._ensureModulesMeta();
164
166
  const moduleNames = modulesMeta.moduleNames.filter(moduleName => {
165
167
  const module = modulesMeta.modules[moduleName];
@@ -182,7 +184,10 @@ export class SysModule extends BeanSimple {
182
184
  const module = modulesMeta.modules[moduleName];
183
185
  const info = module.info;
184
186
  const shouldInstall =
185
- !info.capabilities?.monkey && !info.capabilities?.sync && !info.capabilities?.preload;
187
+ !info.capabilities?.locale &&
188
+ !info.capabilities?.monkey &&
189
+ !info.capabilities?.sync &&
190
+ !info.capabilities?.preload;
186
191
  if (shouldInstall) {
187
192
  await this._install(moduleName, module);
188
193
  }
@@ -257,7 +262,14 @@ export class SysModule extends BeanSimple {
257
262
  }
258
263
 
259
264
  private _registerLocales(module: IModule) {
260
- this.sys.meta.locale._registerLocales(module.info.relativeName, module.resource.locales);
265
+ if (module.info.capabilities?.locale) {
266
+ this.sys.meta.locale._registerProviderLocales(
267
+ module.resource.localesProvider,
268
+ this._ensureModulesMeta().modules,
269
+ );
270
+ } else {
271
+ this.sys.meta.locale._registerLocales(module.info.relativeName, module.resource.locales);
272
+ }
261
273
  }
262
274
 
263
275
  private _registerConstants(module: IModule) {