openxiangda-devkit-core 2.0.0-alpha.107 → 2.0.0-alpha.109

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 (53) hide show
  1. package/dist/application-services.d.ts +181 -1
  2. package/dist/application-services.d.ts.map +1 -1
  3. package/dist/application-services.js +33 -0
  4. package/dist/application-services.js.map +1 -1
  5. package/dist/application-ui-contract.d.ts +5 -0
  6. package/dist/application-ui-contract.d.ts.map +1 -0
  7. package/dist/application-ui-contract.js +149 -0
  8. package/dist/application-ui-contract.js.map +1 -0
  9. package/dist/command-registry.d.ts +2 -2
  10. package/dist/command-registry.js +1 -1
  11. package/dist/command-registry.js.map +1 -1
  12. package/dist/compiler/application-model.d.ts +60 -0
  13. package/dist/compiler/application-model.d.ts.map +1 -0
  14. package/dist/compiler/application-model.js +141 -0
  15. package/dist/compiler/application-model.js.map +1 -0
  16. package/dist/compiler/bundle.d.ts.map +1 -1
  17. package/dist/compiler/bundle.js +19 -9
  18. package/dist/compiler/bundle.js.map +1 -1
  19. package/dist/compiler/config.d.ts +19 -5
  20. package/dist/compiler/config.d.ts.map +1 -1
  21. package/dist/compiler/config.js +137 -60
  22. package/dist/compiler/config.js.map +1 -1
  23. package/dist/compiler/field-surface.js +1 -1
  24. package/dist/compiler/field-surface.js.map +1 -1
  25. package/dist/compiler/permission-review.d.ts +139 -0
  26. package/dist/compiler/permission-review.d.ts.map +1 -0
  27. package/dist/compiler/permission-review.js +131 -0
  28. package/dist/compiler/permission-review.js.map +1 -0
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +4 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/optional-backend.d.ts +14 -0
  34. package/dist/optional-backend.d.ts.map +1 -0
  35. package/dist/optional-backend.js +145 -0
  36. package/dist/optional-backend.js.map +1 -0
  37. package/dist/workspace-loader.d.ts.map +1 -1
  38. package/dist/workspace-loader.js +16 -1
  39. package/dist/workspace-loader.js.map +1 -1
  40. package/package.json +3 -2
  41. package/templates/backend/.turbo/turbo-build.log +4 -0
  42. package/templates/backend/.turbo/turbo-check.log +4 -0
  43. package/templates/backend/.turbo/turbo-test.log +13 -0
  44. package/templates/backend/Dockerfile +20 -0
  45. package/templates/backend/node_modules/.bin/openxiangda +21 -0
  46. package/templates/backend/node_modules/.bin/tsc +21 -0
  47. package/templates/backend/node_modules/.bin/tsserver +21 -0
  48. package/templates/backend/node_modules/.bin/tsx +21 -0
  49. package/templates/backend/package.json +27 -0
  50. package/templates/backend/src/app.module.ts +23 -0
  51. package/templates/backend/src/main.ts +5 -0
  52. package/templates/backend/test/smoke.test.ts +17 -0
  53. package/templates/backend/tsconfig.json +9 -0
@@ -1,3 +1,4 @@
1
+ import { materializeApplicationModules } from './application-model.js';
1
2
  import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, WORKFLOW_SUMMARY_MAX_FIELDS, WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES, isWorkflowSummaryFieldType, } from 'openxiangda-contracts';
2
3
  import { Ajv2020 } from 'ajv/dist/2020.js';
3
4
  import { CronExpressionParser } from 'cron-parser';
@@ -33,8 +34,9 @@ export function defineAdminNavigation(groups) {
33
34
  export function adminNavigationGroup(code, label, items, options = {}) {
34
35
  return Object.freeze({ code, label, items: Object.freeze([...items]), ...options });
35
36
  }
36
- export function adminResourcePage(resourceCode, options) {
37
- return adminNavigationItem({ kind: 'resource', resourceCode }, options);
37
+ export function adminResourcePage(resourceCode, options = {}) {
38
+ const { viewCode, ...display } = options;
39
+ return adminNavigationItem({ kind: 'resource', resourceCode, ...(viewCode ? { viewCode } : {}) }, display);
38
40
  }
39
41
  export function adminOperationPage(routeCode, options) {
40
42
  return adminNavigationItem({ kind: 'operation', routeCode }, options);
@@ -92,10 +94,18 @@ function normalizeLegacyUserSurfaceAuthoring(declaration) {
92
94
  export function suggestAdminNavigation(config) {
93
95
  const groups = [];
94
96
  const resources = [...(config.data?.resources || [])]
95
- .filter(resource => resource.surface?.generated?.list !== false)
96
97
  .sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
97
- if (resources.length > 0) {
98
- groups.push(adminNavigationGroup('data-management', '数据管理', resources.map(resource => adminResourcePage(resource.code, { label: resource.name })), { icon: 'database', order: 100 }));
98
+ const resourceItems = resources.flatMap(resource => [
99
+ ...(resource.surface?.generated?.list !== false
100
+ ? [adminResourcePage(resource.code, { label: resource.name })]
101
+ : []),
102
+ ...[...(resource.surface?.views || [])]
103
+ .filter(view => view.generated.list !== false)
104
+ .sort((left, right) => left.code.localeCompare(right.code, 'en'))
105
+ .map(view => adminResourcePage(resource.code, { viewCode: view.code, label: view.name })),
106
+ ]);
107
+ if (resourceItems.length > 0) {
108
+ groups.push(adminNavigationGroup('data-management', '数据管理', resourceItems, { icon: 'database', order: 100 }));
99
109
  }
100
110
  const routes = [...(config.frontend.routes || [])]
101
111
  .filter(route => route.surface === 'admin' && !/[:*]/.test(route.path))
@@ -107,6 +117,9 @@ export function suggestAdminNavigation(config) {
107
117
  }
108
118
  function renderAdminNavigationOptions(options) {
109
119
  const entries = [
120
+ ...(options.viewCode !== undefined
121
+ ? [`viewCode: ${JSON.stringify(options.viewCode)}`]
122
+ : []),
110
123
  ...(options.label !== undefined
111
124
  ? [`label: ${JSON.stringify(options.label)}`]
112
125
  : []),
@@ -127,7 +140,9 @@ export function renderAdminNavigationSuggestion(config) {
127
140
  const groups = navigation.map(group => {
128
141
  imports.add('adminNavigationGroup');
129
142
  const items = group.items.map(item => {
130
- const options = renderAdminNavigationOptions(item);
143
+ const options = renderAdminNavigationOptions({ ...item,
144
+ ...(item.page.kind === 'resource' && item.page.viewCode ? { viewCode: item.page.viewCode } : {}),
145
+ });
131
146
  let expression;
132
147
  switch (item.page.kind) {
133
148
  case 'resource':
@@ -605,7 +620,6 @@ export function backendRuntimeRequired(config) {
605
620
  config.events?.subscriptions?.length ||
606
621
  config.events?.timers?.length ||
607
622
  config.events?.dateTriggers?.length ||
608
- config.workflows?.activations?.length ||
609
623
  config.workflows?.providers?.length)));
610
624
  }
611
625
  function object(value) {
@@ -870,10 +884,8 @@ export function validateAppConfig(value) {
870
884
  Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
871
885
  Array.isArray(eventConfig.dateTriggers) &&
872
886
  eventConfig.dateTriggers.length > 0 ||
873
- Array.isArray(workflowConfig.activations) &&
874
- workflowConfig.activations.length > 0 ||
875
887
  Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
876
- diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件或工作流运行时', 'backend.enabled'));
888
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件消费者或工作流人员提供器', 'backend.enabled'));
877
889
  }
878
890
  const secretNames = new Set();
879
891
  const secretEnvs = new Set();
@@ -2637,26 +2649,33 @@ function generatedPlatformRouteClaims(config) {
2637
2649
  const code = string(resource.code);
2638
2650
  if (!code)
2639
2651
  continue;
2640
- const surface = object(resource.surface);
2641
- const generated = object(surface.generated);
2642
- const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2643
- const operations = {
2644
- list: generated.list === undefined ? true : generated.list === true,
2645
- detail: generated.detail === undefined ? true : generated.detail === true,
2646
- create: generated.create === undefined ? native : generated.create === true,
2647
- update: generated.update === undefined ? native : generated.update === true,
2648
- };
2649
- const base = `/admin/resources/${code}`;
2650
- const candidates = [
2651
- ['list', base],
2652
- ['detail', `${base}/:id`],
2653
- ['create', `${base}/new`],
2654
- ['update', `${base}/:id/edit`],
2655
- ];
2656
- for (const [operation, path] of candidates) {
2657
- if (!operations[operation])
2658
- continue;
2659
- claims.push({ owner: `resource:${code}:${operation}`, path }, { owner: `resource:${code}:${operation}:mobile`, path: `/m${path}` });
2652
+ const resourceSurface = object(resource.surface);
2653
+ for (const selected of [resourceSurface, ...(Array.isArray(resourceSurface.views) ? resourceSurface.views.map(object) : [])]) {
2654
+ const surface = { ...resourceSurface, ...selected };
2655
+ const viewCode = string(selected.code);
2656
+ const generated = object(surface.generated);
2657
+ const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2658
+ const operations = {
2659
+ list: generated.list === undefined ? true : generated.list === true,
2660
+ detail: generated.detail === undefined ? true : generated.detail === true,
2661
+ create: generated.create === undefined ? native : generated.create === true,
2662
+ update: generated.update === undefined ? native : generated.update === true,
2663
+ };
2664
+ const base = `/admin/resources/${code}${viewCode ? `/views/${viewCode}` : ""}`;
2665
+ const candidates = [
2666
+ ['list', base],
2667
+ ['detail', `${base}/:id`],
2668
+ ['create', `${base}/new`],
2669
+ ['update', `${base}/:id/edit`],
2670
+ ];
2671
+ for (const [operation, path] of candidates) {
2672
+ if (!operations[operation])
2673
+ continue;
2674
+ const owner = `resource:${code}${viewCode ? `:view:${viewCode}` : ''}:${operation}`;
2675
+ claims.push({ owner, path });
2676
+ if (object(surface.mobile).enabled !== false)
2677
+ claims.push({ owner: `${owner}:mobile`, path: `/m${path}` });
2678
+ }
2660
2679
  }
2661
2680
  }
2662
2681
  const frontend = object(config.frontend);
@@ -3291,10 +3310,12 @@ function validateAdminNavigation(config, diagnostics) {
3291
3310
  if (kind === 'resource') {
3292
3311
  const resourceCode = string(page.resourceCode);
3293
3312
  const resource = resources.get(resourceCode);
3294
- pageKey = `resource:${resourceCode}:list`;
3295
- defaultLabel = resource?.name || '';
3313
+ const viewCode = string(page.viewCode);
3314
+ const view = resource?.surface?.views?.find((item) => item.code === viewCode);
3315
+ pageKey = `resource:${resourceCode}${viewCode ? `:view:${viewCode}` : ""}:list`;
3316
+ defaultLabel = (viewCode ? view?.name : resource?.name) || '';
3296
3317
  internalCode = resourceCode;
3297
- exists = Boolean(resource?.surface?.generated?.list);
3318
+ exists = Boolean(viewCode ? view?.generated?.list : resource?.surface?.generated?.list);
3298
3319
  }
3299
3320
  else if (kind === 'operation') {
3300
3321
  const routeCode = string(page.routeCode);
@@ -3500,24 +3521,21 @@ export function resourceCapabilityCodes(appCode, resourceCode) {
3500
3521
  delete: `${prefix}:delete`,
3501
3522
  };
3502
3523
  }
3503
- function materializeRoleCapabilities(config, role) {
3524
+ function materializeRoleCapabilities(role) {
3504
3525
  const capabilities = new Set(role.capabilities);
3505
- for (const resource of config.data?.resources || []) {
3506
- if (resource.surface?.mutationOwner !== 'native' ||
3507
- !capabilities.has(resource.capabilities.read)) {
3508
- continue;
3509
- }
3510
- for (const operation of ['create', 'update', 'delete']) {
3511
- if (resource.surface?.generated?.[operation] === true) {
3512
- capabilities.add(resource.capabilities[operation]);
3513
- }
3514
- }
3515
- }
3516
3526
  for (const denied of role.deniedCapabilities || []) {
3517
3527
  capabilities.delete(denied);
3518
3528
  }
3519
3529
  return [...capabilities].sort();
3520
3530
  }
3531
+ /** Explicit business permission preset; adding a page never changes grants. */
3532
+ export function resourceRoleCapabilities(appCode, resourceCode, access) {
3533
+ const codes = resourceCapabilityCodes(appCode, resourceCode);
3534
+ const operations = access === 'manage'
3535
+ ? ['read', 'create', 'update', 'delete']
3536
+ : access === 'read' ? ['read'] : access;
3537
+ return [...new Set(operations.map(operation => codes[operation]))].sort();
3538
+ }
3521
3539
  export function currentUserDataPolicy(input) {
3522
3540
  return {
3523
3541
  code: input.code,
@@ -3609,9 +3627,12 @@ export function materializeDataResource(appCode, declaration) {
3609
3627
  physicalPlanForField(definition, field.searchable === true);
3610
3628
  return definition;
3611
3629
  });
3612
- const visible = declaration.fields
3613
- .filter(field => field.list !== false)
3614
- .slice(0, 8);
3630
+ const listCandidates = declaration.fields
3631
+ .filter(field => field.list !== false && (field.hidden ?? field.system) !== true);
3632
+ const visible = declaration.list?.fields
3633
+ ? declaration.list.fields.flatMap(code => listCandidates.filter(field => field.code === code))
3634
+ : listCandidates.slice(0, 8);
3635
+ const defaultSortField = visible.find(field => supportsSort(field.type));
3615
3636
  const searchableFields = declaration.fields
3616
3637
  .filter(field => field.searchable === true)
3617
3638
  .map(field => field.code);
@@ -3634,6 +3655,7 @@ export function materializeDataResource(appCode, declaration) {
3634
3655
  return [capabilities[operation]];
3635
3656
  };
3636
3657
  const surface = {
3658
+ ...(declaration.views?.length ? { views: declaration.views } : {}),
3637
3659
  mutationOwner,
3638
3660
  generated,
3639
3661
  fields: Object.fromEntries(declaration.fields.map(field => [
@@ -3645,6 +3667,9 @@ export function materializeDataResource(appCode, declaration) {
3645
3667
  ...(field.section ? { section: field.section } : {}),
3646
3668
  ...(field.required === true ? { requiredHint: true } : {}),
3647
3669
  ...(field.system !== undefined ? { system: field.system } : {}),
3670
+ ...(field.hidden !== undefined || field.system
3671
+ ? { hidden: field.hidden ?? true }
3672
+ : {}),
3648
3673
  readCapabilities: access(field, 'read'),
3649
3674
  createCapabilities: access(field, 'create'),
3650
3675
  updateCapabilities: access(field, 'update'),
@@ -3678,22 +3703,23 @@ export function materializeDataResource(appCode, declaration) {
3678
3703
  },
3679
3704
  ])),
3680
3705
  list: {
3706
+ fieldOrder: visible.map(field => field.code),
3681
3707
  defaultPageSize: declaration.list?.defaultPageSize || 20,
3682
3708
  ...(searchableFields.length ? { searchableFields } : {}),
3683
3709
  ...(filterFields.length ? { filterFields } : {}),
3684
3710
  ...(declaration.list?.defaultSort
3685
3711
  ? { defaultSort: declaration.list.defaultSort }
3686
- : visible[0]
3687
- ? { defaultSort: { field: visible[0].code, order: 'asc' } }
3712
+ : defaultSortField
3713
+ ? { defaultSort: { field: defaultSortField.code, order: 'asc' } }
3688
3714
  : {}),
3689
3715
  },
3690
3716
  form: {
3691
3717
  layout: declaration.form?.layout || 'flat',
3692
- fieldOrder: declaration.fields.map(field => field.code),
3718
+ fieldOrder: declaration.form?.fields || declaration.fields.map(field => field.code),
3693
3719
  },
3694
3720
  detail: {
3695
3721
  layout: declaration.detail?.layout || 'flat',
3696
- fieldOrder: declaration.fields.map(field => field.code),
3722
+ fieldOrder: declaration.detail?.fields || declaration.fields.map(field => field.code),
3697
3723
  },
3698
3724
  mobile: declaration.mobile || { enabled: true },
3699
3725
  };
@@ -3730,6 +3756,7 @@ export function validateAppDeclaration(value) {
3730
3756
  const resources = Array.isArray(data.resources) ? data.resources : [];
3731
3757
  const diagnostics = [];
3732
3758
  const resourceKeys = new Set([
3759
+ 'views',
3733
3760
  'code',
3734
3761
  'name',
3735
3762
  'mutationOwner',
@@ -3764,6 +3791,7 @@ export function validateAppDeclaration(value) {
3764
3791
  'widget',
3765
3792
  'section',
3766
3793
  'system',
3794
+ 'hidden',
3767
3795
  'list',
3768
3796
  'filter',
3769
3797
  'searchable',
@@ -3843,12 +3871,6 @@ export function validateAppDeclaration(value) {
3843
3871
  else if (!rangeType && field.rangeBoundary !== undefined) {
3844
3872
  diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_RANGE_BOUNDARY_TYPE_INVALID', 'rangeBoundary 只能用于 date-range 或 datetime-range', `${fieldPath}.rangeBoundary`));
3845
3873
  }
3846
- if (field.indexed === false &&
3847
- (field.filter === true ||
3848
- field.searchable === true ||
3849
- field.sortable === true)) {
3850
- diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_INDEX_REQUIRED', '启用筛选、搜索或排序时不能显式关闭索引', `${fieldPath}.indexed`));
3851
- }
3852
3874
  if (['radio', 'checkbox'].includes(widget) &&
3853
3875
  object(field.source).loadMode !== 'all' &&
3854
3876
  field.source !== undefined) {
@@ -3862,6 +3884,7 @@ export function validateAppDeclaration(value) {
3862
3884
  'required',
3863
3885
  'indexed',
3864
3886
  'system',
3887
+ 'hidden',
3865
3888
  'list',
3866
3889
  'filter',
3867
3890
  'searchable',
@@ -3890,6 +3913,27 @@ export function validateAppDeclaration(value) {
3890
3913
  if (code)
3891
3914
  declaredCodes.add(code);
3892
3915
  });
3916
+ for (const view of ['list', 'form', 'detail']) {
3917
+ const selected = object(resource[view]).fields;
3918
+ if (selected === undefined)
3919
+ continue;
3920
+ if (!Array.isArray(selected) || new Set(selected).size !== selected.length ||
3921
+ selected.some(code => typeof code !== 'string' || !declaredCodes.has(code))) {
3922
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_FIELDS_INVALID', '列表/表单/详情 fields 必须是当前资源中不重复的字段列表', `${resourcePath}.${view}.fields`));
3923
+ materializable = false;
3924
+ }
3925
+ }
3926
+ const selectedFormFields = object(resource.form).fields ?? fields.map(raw => object(raw).code);
3927
+ if (Array.isArray(selectedFormFields) && (generated.create ?? mutationOwner === 'native')) {
3928
+ for (const rawField of fields) {
3929
+ const field = object(rawField);
3930
+ if (field.required === true && field.system !== true &&
3931
+ supportsGeneratedMutation({ type: field.type, system: false }) &&
3932
+ object(field.access).create !== false && (field.hidden === true || !selectedFormFields.includes(field.code))) {
3933
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FORM_REQUIRED_FIELD_MISSING', `标准新增表单缺少必填字段 ${field.label || field.code};请加入表单或关闭标准新增`, `${resourcePath}.form.fields`));
3934
+ }
3935
+ }
3936
+ }
3893
3937
  for (const operation of ['create', 'update']) {
3894
3938
  const enabled = generated[operation] ?? mutationOwner === 'native';
3895
3939
  const writable = fields.some(rawField => {
@@ -3911,6 +3955,19 @@ export function validateAppDeclaration(value) {
3911
3955
  }
3912
3956
  if (appCode && fields.length && materializable) {
3913
3957
  const canonical = materializeDataResource(appCode, rawResource);
3958
+ for (const [viewIndex, view] of (canonical.surface?.views || []).entries()) {
3959
+ for (const [property, values, supports] of [
3960
+ ['searchableFields', view.list?.searchableFields || [], supportsSearch],
3961
+ ['filterFields', view.list?.filterFields || [], supportsFilter],
3962
+ ['defaultSort', view.list?.defaultSort ? [view.list.defaultSort.field] : [], supportsSort],
3963
+ ]) {
3964
+ for (const code of values) {
3965
+ const field = canonical.schema.fields.find(field => field.code === code);
3966
+ if (!field || !supports(field.type))
3967
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_QUERY_FIELD_INVALID', '视图查询字段不存在或不支持此查询类型', `${resourcePath}.views[${viewIndex}].list.${property}`));
3968
+ }
3969
+ }
3970
+ }
3914
3971
  diagnostics.push(...validateDataResource(canonical).map(item => ({
3915
3972
  ...item,
3916
3973
  path: `${resourcePath}.${item.path}`,
@@ -3978,7 +4035,27 @@ export function validateAppDeclaration(value) {
3978
4035
  return diagnostics;
3979
4036
  }
3980
4037
  export function defineOpenXiangdaApp(declaration) {
3981
- const normalizedDeclaration = normalizeLegacyUserSurfaceAuthoring(declaration);
4038
+ const { modules, ...input } = declaration;
4039
+ const projected = materializeApplicationModules(modules || []);
4040
+ if (projected.diagnostics.length)
4041
+ throw new AppConfigValidationError(projected.diagnostics);
4042
+ const normalizedDeclaration = normalizeLegacyUserSurfaceAuthoring({
4043
+ ...input,
4044
+ schemaVersion: declaration.schemaVersion ?? 3,
4045
+ frontend: { root: 'apps/web', ...declaration.frontend },
4046
+ backend: {
4047
+ root: 'apps/server', runtime: 'node', framework: 'nestjs',
4048
+ enabled: Boolean(declaration.backend?.operations?.length || declaration.backend?.secrets?.length ||
4049
+ declaration.events?.subscriptions?.length || declaration.events?.timers?.length ||
4050
+ declaration.events?.dateTriggers?.length ||
4051
+ declaration.workflows?.providers?.length),
4052
+ ...declaration.backend,
4053
+ },
4054
+ platform: { root: 'platform', ...declaration.platform },
4055
+ ...(modules || declaration.data ? {
4056
+ data: { resources: [...(declaration.data?.resources || []), ...projected.resources] },
4057
+ } : {}),
4058
+ });
3982
4059
  const declarationDiagnostics = validateAppDeclaration(normalizedDeclaration);
3983
4060
  if (declarationDiagnostics.length > 0) {
3984
4061
  throw new AppConfigValidationError(declarationDiagnostics);
@@ -4001,7 +4078,7 @@ export function defineOpenXiangdaApp(declaration) {
4001
4078
  ...materialized.authz,
4002
4079
  roles: materialized.authz.roles.map(role => ({
4003
4080
  ...role,
4004
- capabilities: materializeRoleCapabilities(materialized, role),
4081
+ capabilities: materializeRoleCapabilities(role),
4005
4082
  })),
4006
4083
  },
4007
4084
  }