openxiangda-cli 2.0.0-alpha.56 → 2.0.0-alpha.58

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 (36) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +13 -5
  3. package/dist/commands/create.js.map +1 -1
  4. package/dist/create-workspace.d.ts +6 -1
  5. package/dist/create-workspace.d.ts.map +1 -1
  6. package/dist/create-workspace.js +12 -9
  7. package/dist/create-workspace.js.map +1 -1
  8. package/package.json +4 -4
  9. package/template/.dockerignore +11 -0
  10. package/template/README.md +3 -1
  11. package/template/apps/server/package.json +1 -1
  12. package/template/apps/web/e2e/instruments.spec.ts +912 -9
  13. package/template/apps/web/package.json +1 -1
  14. package/template/apps/web/scripts/check.mjs +2 -2
  15. package/template/apps/web/src/AuthoritativeSelector.tsx +371 -0
  16. package/template/apps/web/src/CollegePage.tsx +181 -0
  17. package/template/apps/web/src/InstrumentDetailPage.tsx +94 -20
  18. package/template/apps/web/src/InstrumentForm.tsx +450 -88
  19. package/template/apps/web/src/InstrumentFormPage.tsx +49 -9
  20. package/template/apps/web/src/InstrumentListPage.tsx +30 -35
  21. package/template/apps/web/src/Shell.tsx +229 -41
  22. package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +698 -0
  23. package/template/apps/web/src/data-provider.ts +1 -1
  24. package/template/apps/web/src/fields.ts +17 -6
  25. package/template/apps/web/src/instrument.ts +1 -1
  26. package/template/apps/web/src/main.tsx +16 -1
  27. package/template/apps/web/src/platform-client.ts +213 -2
  28. package/template/apps/web/src/runtime.tsx +4 -2
  29. package/template/apps/web/src/styles.css +869 -29
  30. package/template/apps/web/test/contracts.test.ts +369 -10
  31. package/template/openxiangda.config.ts +24 -5
  32. package/template/package.json +3 -3
  33. package/template/packages/contracts/src/generated.ts +22 -0
  34. package/template/platform/data/colleges.ts +21 -0
  35. package/template/platform/data/instruments.ts +1 -1
  36. package/template/scripts/verify-template-budget.mjs +1 -1
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
2
3
  import test from 'node:test';
3
4
  import {
4
5
  instrumentFieldAllowed,
@@ -6,8 +7,14 @@ import {
6
7
  } from '../src/InstrumentForm';
7
8
  import { instrumentFields } from '../src/fields';
8
9
  import {
10
+ browseDepartmentTree,
11
+ browseDepartmentUsers,
9
12
  instrumentDataApi,
10
13
  loadCurrentIdentity,
14
+ resolveCollegeScope,
15
+ resolveDirectory,
16
+ searchCollegeScope,
17
+ searchDirectory,
11
18
  } from '../src/platform-client';
12
19
  import {
13
20
  applicationApiPath,
@@ -16,6 +23,9 @@ import {
16
23
  resolveApplicationBasename,
17
24
  } from '../src/runtime-meta';
18
25
 
26
+ const collegeId = '11111111-1111-4111-8111-111111111111';
27
+ const userId = '22222222-2222-4222-8222-222222222222';
28
+
19
29
  test('the golden module exposes exactly 30 fields and five restricted fields', () => {
20
30
  assert.equal(instrumentFields.length, 30);
21
31
  assert.deepEqual(
@@ -36,6 +46,87 @@ test('the golden module exposes exactly 30 fields and five restricted fields', (
36
46
  );
37
47
  });
38
48
 
49
+ test('application shell and instrument surfaces match the approved navigation model', () => {
50
+ const shell = readFileSync(new URL('../src/Shell.tsx', import.meta.url), 'utf8');
51
+ const list = readFileSync(
52
+ new URL('../src/InstrumentListPage.tsx', import.meta.url),
53
+ 'utf8'
54
+ );
55
+ const routes = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
56
+ const form = readFileSync(
57
+ new URL('../src/InstrumentForm.tsx', import.meta.url),
58
+ 'utf8'
59
+ );
60
+ const colleges = readFileSync(
61
+ new URL('../src/CollegePage.tsx', import.meta.url),
62
+ 'utf8'
63
+ );
64
+ const directory = readFileSync(
65
+ new URL('../src/components/platform-fields/PlatformDirectoryPicker.tsx', import.meta.url),
66
+ 'utf8'
67
+ );
68
+ for (const marker of [
69
+ '仪器资源管理',
70
+ '工作台',
71
+ '仪器管理',
72
+ '仪器资源',
73
+ '开放记录',
74
+ '基础数据',
75
+ '学院字典',
76
+ '系统设置',
77
+ '应用管理员',
78
+ '收起侧边栏',
79
+ ]) {
80
+ assert.match(shell, new RegExp(marker));
81
+ }
82
+ assert.doesNotMatch(shell, /<span>\{identity\.userId\}<\/span>/);
83
+ assert.doesNotMatch(shell, /验证本地 Nest|roleCodes\.join|environment\.key/);
84
+ assert.doesNotMatch(list, /<Drawer|surface="drawer"/);
85
+ assert.match(routes, /path="\/instruments\/new"[\s\S]*<InstrumentFormPage mode="create"/);
86
+ assert.match(colleges, /<Drawer/);
87
+ assert.match(colleges, /size=\{520\}/);
88
+ assert.match(directory, /width=\{kind === 'user' \? 980 : 760\}/);
89
+ assert.match(directory, /browseDepartmentTree|browseDepartmentUsers/);
90
+ assert.match(directory, /选择成员|选择部门|组织架构|已选/);
91
+ assert.match(form, /oxa-form-full/);
92
+ assert.match(form, /PlatformDirectoryPicker/);
93
+ assert.match(form, /oxa-file-dropzone/);
94
+ assert.match(form, /oxa-detail-layout/);
95
+ assert.doesNotMatch(
96
+ `${form}\n${directory}`,
97
+ /FormContext|tools\/openxiangda|RoleSession/
98
+ );
99
+ });
100
+
101
+ test('college, user, and department selectors have authoritative owners only', () => {
102
+ const appConfig = readFileSync(
103
+ new URL('../../../openxiangda.config.ts', import.meta.url),
104
+ 'utf8'
105
+ );
106
+ const colleges = readFileSync(
107
+ new URL('../../../platform/data/colleges.ts', import.meta.url),
108
+ 'utf8'
109
+ );
110
+ const fields = readFileSync(new URL('../src/fields.ts', import.meta.url), 'utf8');
111
+ assert.match(appConfig, /valueType:\s*'uuid'/);
112
+ assert.match(appConfig, /kind:\s*'native_resource'/);
113
+ assert.match(appConfig, /resourceCode:\s*'colleges'/);
114
+ assert.match(appConfig, /data:\s*\{\s*resources:\s*\[colleges, instruments\]/);
115
+ assert.match(colleges, /code:\s*'name'.*type:\s*'string'/s);
116
+ assert.match(colleges, /code:\s*'enabled'.*type:\s*'boolean'/s);
117
+ assert.match(fields, /kind:\s*'scope'/);
118
+ assert.match(fields, /kind:\s*'directory-user'/);
119
+ assert.match(fields, /kind:\s*'directory-department'/);
120
+ for (const marker of [
121
+ ['college', 'am'].join('-'),
122
+ ['user', 'wang'].join('-'),
123
+ ['dept', 'am-test'].join('-'),
124
+ ]) {
125
+ assert.equal(appConfig.includes(marker), false);
126
+ assert.equal(fields.includes(marker), false);
127
+ }
128
+ });
129
+
39
130
  test('college create restores five fields while update omits college reassignment', () => {
40
131
  const protectedFields = instrumentFields.filter(
41
132
  field => field.createCapability && field.updateCapability
@@ -53,8 +144,8 @@ test('college create restores five fields while update omits college reassignmen
53
144
  chineseName: '电子显微镜',
54
145
  assetCode: 'ASSET-001',
55
146
  assetValue: 100,
56
- collegeId: 'college-am',
57
- instrumentAdminIds: ['user-wang'],
147
+ collegeId,
148
+ instrumentAdminIds: [userId],
58
149
  revision: 7,
59
150
  };
60
151
  assert.equal(
@@ -78,15 +169,15 @@ test('college create restores five fields while update omits college reassignmen
78
169
  chineseName: '电子显微镜',
79
170
  assetCode: 'ASSET-001',
80
171
  assetValue: 100,
81
- collegeId: 'college-am',
82
- instrumentAdminIds: ['user-wang'],
172
+ collegeId,
173
+ instrumentAdminIds: [userId],
83
174
  });
84
175
  assert.deepEqual(sanitizeInstrumentValues(values, 'edit', hasCapability), {
85
176
  instrumentCode: 'INS-001',
86
177
  chineseName: '电子显微镜',
87
178
  assetCode: 'ASSET-001',
88
179
  assetValue: 100,
89
- instrumentAdminIds: ['user-wang'],
180
+ instrumentAdminIds: [userId],
90
181
  });
91
182
  });
92
183
 
@@ -206,29 +297,297 @@ test('list uses the native Data API with server paging, filters, and sorting', a
206
297
  await instrumentDataApi.list({
207
298
  page: 2,
208
299
  pageSize: 20,
209
- collegeId: 'college-am',
210
- instrumentAdminId: 'user-wang',
300
+ collegeId,
301
+ instrumentAdminId: userId,
211
302
  sort: { field: 'assetValue', order: 'asc' },
212
303
  });
304
+ await instrumentDataApi.list({ page: 1, pageSize: 20 });
213
305
  } finally {
214
306
  globalThis.fetch = originalFetch;
215
307
  }
216
- assert.equal(calls.length, 2);
308
+ assert.equal(calls.length, 3);
217
309
  assert.match(
218
310
  calls[1]!.path,
219
311
  /\/native\/data\/instruments\/query$/
220
312
  );
221
313
  const body = JSON.parse(String(calls[1]!.init?.body));
314
+ const resourceSource = readFileSync(
315
+ new URL('../../../platform/data/instruments.ts', import.meta.url),
316
+ 'utf8'
317
+ );
318
+ const declaredFields = new Set(
319
+ [...resourceSource.matchAll(/\{\s*code:\s*'([^']+)'/g)].map(
320
+ match => match[1]!
321
+ )
322
+ );
323
+ assert.deepEqual(
324
+ [...declaredFields],
325
+ instrumentFields.filter(field => !field.system).map(field => field.key)
326
+ );
222
327
  assert.equal(body.environmentKey, 'preproduction');
223
328
  assert.equal(body.limit, 20);
224
329
  assert.equal(body.offset, 20);
225
330
  assert.deepEqual(body.order, [{ field: 'assetValue', direction: 'asc' }]);
331
+ assert.equal(
332
+ [...body.filters, ...body.order].every(item =>
333
+ declaredFields.has(item.field)
334
+ ),
335
+ true
336
+ );
226
337
  assert.deepEqual(body.filters, [
227
- { field: 'collegeId', operator: 'eq', value: 'college-am' },
338
+ { field: 'collegeId', operator: 'eq', value: collegeId },
228
339
  {
229
340
  field: 'instrumentAdminIds',
230
341
  operator: 'cs',
231
- value: ['user-wang'],
342
+ value: [userId],
232
343
  },
233
344
  ]);
345
+ const defaultBody = JSON.parse(String(calls[2]!.init?.body));
346
+ assert.deepEqual(defaultBody.order, [
347
+ { field: 'instrumentCode', direction: 'asc' },
348
+ ]);
349
+ assert.equal(
350
+ [...defaultBody.filters, ...defaultBody.order].every(item =>
351
+ declaredFields.has(item.field)
352
+ ),
353
+ true
354
+ );
355
+ });
356
+
357
+ test('list rejects an undeclared sorter before issuing a Data API request', async () => {
358
+ let requests = 0;
359
+ const originalFetch = globalThis.fetch;
360
+ globalThis.fetch = async () => {
361
+ requests += 1;
362
+ throw new Error('unexpected fetch');
363
+ };
364
+ try {
365
+ await assert.rejects(
366
+ instrumentDataApi.list({
367
+ page: 1,
368
+ pageSize: 20,
369
+ sort: { field: 'updatedAt', order: 'desc' },
370
+ }),
371
+ /OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:updatedAt/
372
+ );
373
+ } finally {
374
+ globalThis.fetch = originalFetch;
375
+ }
376
+ assert.equal(requests, 0);
377
+ });
378
+
379
+ test('authoritative selectors page, resolve exact IDs, and never send RoleSession', async () => {
380
+ const calls: Array<{ path: string; init?: RequestInit }> = [];
381
+ const originalFetch = globalThis.fetch;
382
+ globalThis.fetch = async (input, init) => {
383
+ const path = String(input);
384
+ calls.push({ path, init });
385
+ if (path.endsWith('/dev-sessions/current')) {
386
+ return Response.json({
387
+ code: 200,
388
+ data: {
389
+ environment: {
390
+ id: 'test-id',
391
+ key: 'preproduction',
392
+ activeAppVersionId: 'version-1',
393
+ headRevision: 2,
394
+ authzRevisionId: 'authz-1',
395
+ authzVersion: 3,
396
+ scopeDataVersion: 'scope-1',
397
+ },
398
+ principal: {
399
+ userId,
400
+ roleCodes: ['school_admin'],
401
+ capabilityCodes: [],
402
+ isAppSuperAdmin: false,
403
+ },
404
+ },
405
+ });
406
+ }
407
+ if (path.includes('/directory/users?')) {
408
+ return Response.json({
409
+ code: 200,
410
+ data: {
411
+ schemaVersion: 'openxiangda.directory-entry-page/v2',
412
+ kind: 'user',
413
+ items: [
414
+ { kind: 'user', id: userId, label: '王老师', selectable: true },
415
+ ],
416
+ nextCursor: 'next-user-page',
417
+ },
418
+ });
419
+ }
420
+ if (path.includes('/directory/departments/tree?')) {
421
+ return Response.json({
422
+ code: 200,
423
+ data: {
424
+ schemaVersion: 'openxiangda.directory-entry-page/v2',
425
+ kind: 'department',
426
+ items: [
427
+ {
428
+ kind: 'department',
429
+ id: 'department-root',
430
+ label: '材料学院',
431
+ hasChildren: true,
432
+ selectable: true,
433
+ },
434
+ ],
435
+ nextCursor: null,
436
+ },
437
+ });
438
+ }
439
+ if (path.includes('/directory/departments/department-root/users?')) {
440
+ return Response.json({
441
+ code: 200,
442
+ data: {
443
+ schemaVersion: 'openxiangda.directory-entry-page/v2',
444
+ kind: 'user',
445
+ items: [
446
+ { kind: 'user', id: userId, label: '王老师', selectable: true },
447
+ ],
448
+ nextCursor: null,
449
+ },
450
+ });
451
+ }
452
+ if (path.includes('/directory/resolve?')) {
453
+ const body = JSON.parse(String(init?.body));
454
+ return Response.json({
455
+ code: 200,
456
+ data: {
457
+ schemaVersion: 'openxiangda.directory-entry-page/v2',
458
+ kind: body.kind,
459
+ items: body.ids.map((id: string) => ({
460
+ kind: body.kind,
461
+ id,
462
+ label: '精确解析',
463
+ selectable: false,
464
+ })),
465
+ nextCursor: null,
466
+ },
467
+ });
468
+ }
469
+ if (path.includes('/scope-values/resolve?')) {
470
+ const body = JSON.parse(String(init?.body));
471
+ return Response.json({
472
+ code: 200,
473
+ data: scopePage(
474
+ body.operation,
475
+ body.values.map((value: string) => ({
476
+ value,
477
+ label: '先进材料学院',
478
+ selectable: false,
479
+ }))
480
+ ),
481
+ });
482
+ }
483
+ if (path.includes('/scope-values?')) {
484
+ return Response.json({
485
+ code: 200,
486
+ data: scopePage('create', [
487
+ { value: collegeId, label: '先进材料学院', selectable: true },
488
+ ]),
489
+ });
490
+ }
491
+ return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
492
+ };
493
+ try {
494
+ await loadCurrentIdentity();
495
+ const users = await searchDirectory('user', {
496
+ keyword: '王老',
497
+ cursor: 'cursor-1',
498
+ });
499
+ assert.equal(users.nextCursor, 'next-user-page');
500
+ assert.equal((await resolveDirectory('department', [userId])).items[0]?.selectable, false);
501
+ assert.equal(
502
+ (await browseDepartmentTree({ parentId: 'department-parent' })).items[0]
503
+ ?.id,
504
+ 'department-root'
505
+ );
506
+ assert.equal(
507
+ (await browseDepartmentUsers('department-root', 2)).items[0]?.id,
508
+ userId
509
+ );
510
+ assert.equal(
511
+ (await searchCollegeScope('create', { keyword: '', cursor: 'scope-1' }))
512
+ .items[0]?.value,
513
+ collegeId
514
+ );
515
+ assert.equal(
516
+ (await resolveCollegeScope('update', [collegeId])).items[0]?.selectable,
517
+ false
518
+ );
519
+ await assert.rejects(
520
+ resolveDirectory('user', []),
521
+ /OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED/
522
+ );
523
+ await assert.rejects(
524
+ resolveCollegeScope('update', []),
525
+ /OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED/
526
+ );
527
+ await assert.rejects(
528
+ resolveDirectory(
529
+ 'user',
530
+ Array.from({ length: 51 }, (_, index) => `user-${index}`)
531
+ ),
532
+ /OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED/
533
+ );
534
+ await assert.rejects(
535
+ resolveCollegeScope(
536
+ 'update',
537
+ Array.from({ length: 51 }, (_, index) =>
538
+ `${String(index).padStart(8, '0')}-0000-4000-8000-000000000000`
539
+ )
540
+ ),
541
+ /OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED/
542
+ );
543
+ } finally {
544
+ globalThis.fetch = originalFetch;
545
+ }
546
+ const userSearch = calls.find(call => call.path.includes('/directory/users?'))!;
547
+ const treeBrowse = calls.find(call => call.path.includes('/directory/departments/tree?'))!;
548
+ const memberBrowse = calls.find(call => call.path.includes('/directory/departments/department-root/users?'))!;
549
+ const scopeSearch = calls.find(call => call.path.includes('/scope-values?'))!;
550
+ assert.match(userSearch.path, /keyword=%E7%8E%8B%E8%80%81/);
551
+ assert.match(userSearch.path, /cursor=cursor-1/);
552
+ assert.match(treeBrowse.path, /parentId=department-parent/);
553
+ assert.match(memberBrowse.path, /page=2/);
554
+ assert.match(scopeSearch.path, /environmentKey=preproduction/);
555
+ assert.match(
556
+ scopeSearch.path,
557
+ /\/native\/data-resources\/instruments\/fields\/collegeId\/scope-values\?/
558
+ );
559
+ assert.match(scopeSearch.path, /operation=create/);
560
+ assert.match(scopeSearch.path, /cursor=scope-1/);
561
+ assert.equal(
562
+ calls.some(call =>
563
+ Object.keys(call.init?.headers || {}).some(header =>
564
+ header.toLowerCase().includes('role-session')
565
+ )
566
+ ),
567
+ false
568
+ );
234
569
  });
570
+
571
+ function scopePage(
572
+ operation: 'create' | 'update',
573
+ items: Array<{ value: string; label: string; selectable: boolean }>
574
+ ) {
575
+ return {
576
+ schemaVersion: 'openxiangda.native-scope-value-page/v2',
577
+ environment: {
578
+ id: 'test-id',
579
+ key: 'preproduction',
580
+ activeAppVersionId: 'version-1',
581
+ headRevision: 2,
582
+ authzRevisionId: 'authz-1',
583
+ authzVersion: 3,
584
+ scopeDataVersion: 'scope-1',
585
+ },
586
+ resourceCode: 'instruments',
587
+ fieldCode: 'collegeId',
588
+ dimensionCode: 'college',
589
+ operation,
590
+ items,
591
+ nextCursor: null,
592
+ };
593
+ }
@@ -3,12 +3,17 @@ import {
3
3
  instruments,
4
4
  protectedFieldCapabilities,
5
5
  } from './platform/data/instruments.js';
6
+ import { colleges } from './platform/data/colleges.js';
6
7
 
7
8
  const read = 'app:instrument-center:data:instruments:read';
8
9
  const create = 'app:instrument-center:data:instruments:create';
9
10
  const update = 'app:instrument-center:data:instruments:update';
10
11
  const remove = 'app:instrument-center:data:instruments:delete';
11
12
  const inspectContext = 'app:instrument-center:instrument:context:read';
13
+ const directoryRead = 'app:instrument-center:directory:read';
14
+ const collegeRead = 'app:instrument-center:data:colleges:read';
15
+ const collegeCreate = 'app:instrument-center:data:colleges:create';
16
+ const collegeUpdate = 'app:instrument-center:data:colleges:update';
12
17
  const allProtectedFieldCapabilities = Object.values(
13
18
  protectedFieldCapabilities
14
19
  ).flatMap(capabilities => [capabilities.create, capabilities.update]);
@@ -26,6 +31,7 @@ export default defineOpenXiangdaApp({
26
31
  root: 'apps/web',
27
32
  routes: [
28
33
  { code: 'instruments', path: '/instruments', label: '仪器资源', surface: 'admin', navigation: 'menu', capability: read },
34
+ { code: 'colleges', path: '/colleges', label: '学院字典', surface: 'admin', navigation: 'menu', capability: collegeRead },
29
35
  { code: 'instrument-create', path: '/instruments/new', label: '新增仪器', surface: 'admin', navigation: 'hidden', parentCode: 'instruments', capability: create },
30
36
  { code: 'instrument-edit', path: '/instruments/:id/edit', label: '编辑仪器', surface: 'admin', navigation: 'hidden', parentCode: 'instruments', capability: update },
31
37
  { code: 'instrument-detail', path: '/instruments/:id', label: '仪器详情', surface: 'admin', navigation: 'hidden', parentCode: 'instruments', capability: read },
@@ -54,11 +60,24 @@ export default defineOpenXiangdaApp({
54
60
  { code: inspectContext, kind: 'backend', name: '查看当前应用身份摘要' },
55
61
  ],
56
62
  roles: [
57
- { code: 'school_admin', name: '学校管理员', capabilities: [read, create, update, remove, ...allProtectedFieldCapabilities, inspectContext] },
58
- { code: 'college_admin', name: '学院管理员', capabilities: [read, create, update, remove, ...collegeCreateCapabilities, ...collegeUpdateCapabilities, inspectContext] },
59
- { code: 'instrument_admin', name: '仪器管理员', capabilities: [read, update, inspectContext] },
63
+ { code: 'school_admin', name: '学校管理员', capabilities: [read, create, update, remove, collegeRead, collegeCreate, collegeUpdate, directoryRead, ...allProtectedFieldCapabilities, inspectContext] },
64
+ { code: 'college_admin', name: '学院管理员', capabilities: [read, create, update, remove, directoryRead, ...collegeCreateCapabilities, ...collegeUpdateCapabilities, inspectContext] },
65
+ { code: 'instrument_admin', name: '仪器管理员', capabilities: [read, update, directoryRead, inspectContext] },
66
+ ],
67
+ scopeDimensions: [
68
+ {
69
+ code: 'college',
70
+ name: '学院',
71
+ valueType: 'uuid',
72
+ hierarchyMode: 'flat',
73
+ valueSource: {
74
+ kind: 'native_resource',
75
+ resourceCode: 'colleges',
76
+ labelField: 'name',
77
+ enabledField: 'enabled',
78
+ },
79
+ },
60
80
  ],
61
- scopeDimensions: [{ code: 'college', name: '学院', valueType: 'string', hierarchyMode: 'flat' }],
62
81
  dataPolicies: [
63
82
  {
64
83
  code: 'instrument-access',
@@ -73,5 +92,5 @@ export default defineOpenXiangdaApp({
73
92
  },
74
93
  ],
75
94
  },
76
- data: { resources: [instruments] },
95
+ data: { resources: [colleges, instruments] },
77
96
  });
@@ -21,9 +21,9 @@
21
21
  "build": "pnpm --recursive build"
22
22
  },
23
23
  "devDependencies": {
24
- "openxiangda-cli": "2.0.0-alpha.56",
25
- "openxiangda-contracts": "2.0.0-alpha.26",
26
- "openxiangda-devkit-core": "2.0.0-alpha.33",
24
+ "openxiangda-cli": "2.0.0-alpha.58",
25
+ "openxiangda-contracts": "2.0.0-alpha.27",
26
+ "openxiangda-devkit-core": "2.0.0-alpha.34",
27
27
  "typescript": "5.9.3"
28
28
  }
29
29
  }
@@ -2,9 +2,14 @@
2
2
  export const compilerContractVersion = "native-2" as const;
3
3
  export const appCode = "instrument-center" as const;
4
4
  export const resourceCodes = [
5
+ "colleges",
5
6
  "instruments"
6
7
  ] as const;
7
8
  export const capabilities = [
9
+ "app:instrument-center:data:colleges:create",
10
+ "app:instrument-center:data:colleges:delete",
11
+ "app:instrument-center:data:colleges:read",
12
+ "app:instrument-center:data:colleges:update",
8
13
  "app:instrument-center:data:instruments:create",
9
14
  "app:instrument-center:data:instruments:delete",
10
15
  "app:instrument-center:data:instruments:read",
@@ -39,6 +44,16 @@ export const appOperations = {
39
44
  }
40
45
  } as const;
41
46
  export const appRoutes = {
47
+ colleges: {
48
+ "code": "colleges",
49
+ "path": "/colleges",
50
+ "label": "学院字典",
51
+ "surface": "admin",
52
+ "navigation": "menu",
53
+ "capability": "app:instrument-center:data:colleges:read",
54
+ "tabPersistence": "session",
55
+ "keepAlive": "none"
56
+ },
42
57
  instrumentCreate: {
43
58
  "code": "instrument-create",
44
59
  "path": "/instruments/new",
@@ -95,6 +110,13 @@ export type EventType = (typeof eventTypes)[number];
95
110
  export type EventSubscriptionCode = (typeof eventSubscriptionCodes)[number];
96
111
  export type WorkflowCode = (typeof workflowCodes)[number];
97
112
 
113
+ export interface CollegesRecord {
114
+ id: string;
115
+ revision: number;
116
+ name: string;
117
+ enabled: boolean;
118
+ }
119
+
98
120
  export interface InstrumentsRecord {
99
121
  id: string;
100
122
  revision: number;
@@ -0,0 +1,21 @@
1
+ import { SCHEMA_VERSIONS, type DataResource } from 'openxiangda-contracts';
2
+
3
+ export const colleges: DataResource = {
4
+ schemaVersion: SCHEMA_VERSIONS.dataResource,
5
+ appCode: 'instrument-center',
6
+ code: 'colleges',
7
+ name: '学院',
8
+ schema: {
9
+ fields: [
10
+ { code: 'name', type: 'string', nullable: false, indexed: true },
11
+ { code: 'enabled', type: 'boolean', nullable: false, indexed: true },
12
+ ],
13
+ },
14
+ capabilities: {
15
+ read: 'app:instrument-center:data:colleges:read',
16
+ create: 'app:instrument-center:data:colleges:create',
17
+ update: 'app:instrument-center:data:colleges:update',
18
+ delete: 'app:instrument-center:data:colleges:delete',
19
+ },
20
+ fieldPolicies: {},
21
+ };
@@ -49,7 +49,7 @@ export const instruments: DataResource = {
49
49
  { code: 'openToOutside', type: 'boolean', nullable: false },
50
50
  { code: 'subjectAreas', type: 'json' },
51
51
  { code: 'platformProperty', type: 'string' },
52
- { code: 'collegeId', type: 'string', nullable: false, indexed: true },
52
+ { code: 'collegeId', type: 'uuid', nullable: false, indexed: true },
53
53
  { code: 'manageDepartmentId', type: 'string', nullable: false },
54
54
  { code: 'useDepartmentId', type: 'string', nullable: false },
55
55
  { code: 'instrumentAdminIds', type: 'json', nullable: false, indexed: true },
@@ -31,7 +31,7 @@ const productionFiles = files.filter(file => {
31
31
  );
32
32
  });
33
33
  const source = productionFiles.map(file => readFileSync(file, 'utf8')).join('\n');
34
- for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'departmentScopeKey', 'instrumentScopeKey', 'adminUserIds']) {
34
+ for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'departmentScopeKey', 'instrumentScopeKey', 'adminUserIds', 'college-' + 'am', 'user-' + 'wang', 'dept-' + 'am-test']) {
35
35
  if (source.includes(marker)) throw new Error(`TEMPLATE_FORBIDDEN_MARKER:${marker}`);
36
36
  }
37
37
  process.stdout.write(`Verified compact template: ${files.length} source files, ${bytes} bytes.\n`);