member-reader-before.mjs 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. import { indexBoardMembers, seedMemberGroups, changeMemberGroups, MEMBER_GROUP_SCHEMA } from '../../src/core/member-groups.js';
  2. /** Read exactly the existing public projection, including deployed employee identity overlays. */
  3. export async function readMemberGroupBoard({ url, appId, fetchImpl = fetch }) {
  4. const raw = { session: [], artifact: [], project: [], opportunity: [] };
  5. let cursor;
  6. for (let page = 0; page < 500; page++) {
  7. const response = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId },
  8. body: JSON.stringify({ path: '/ksfGetPublicBoard', params: { limit: 500, ...(cursor ? { cursor } : {}) } }), signal: AbortSignal.timeout(30000) });
  9. const result = (await response.json())?.result;
  10. if (!response.ok || !result?.ok || !result.data) throw Object.assign(new Error('成员读取失败,请刷新后重试'), { code: 'MEMBER_READ_FAILED' });
  11. for (const key of Object.keys(raw)) {
  12. if (!Array.isArray(result.data[key])) throw Object.assign(new Error('成员数据不完整'), { code: 'MEMBER_READ_FAILED' });
  13. raw[key].push(...result.data[key]);
  14. }
  15. cursor = result.data.nextCursor;
  16. if (!cursor) return raw;
  17. }
  18. throw Object.assign(new Error('成员分页超限'), { code: 'MEMBER_READ_FAILED' });
  19. }
  20. /** Shared presentation groups only. Never mutate workshop groups or access grants. */
  21. export function attachMemberGroupHandlers(app, { store, loadBoard }) {
  22. const { fail } = app, configKey = 'public-board-member-groups';
  23. const actor = { id: 'public-board', kind: 'anonymous', scopes: [], projectIds: [] };
  24. const current = (row, members) => {
  25. if (!row) return seedMemberGroups(members);
  26. const state = row.value;
  27. if (state?.schema !== MEMBER_GROUP_SCHEMA || !Number.isSafeInteger(state.revision) || state.revision < 1 || !Array.isArray(state.groups) ||
  28. !state.assignments || typeof state.assignments !== 'object' || Array.isArray(state.assignments)) fail('GROUP_CONFIG_INVALID', '分组数据异常,请联系维护人');
  29. return structuredClone(state);
  30. };
  31. const read = async (s, members, lock = false) => {
  32. const rows = await s.list('KsfRuntimeConfig', { where: { key: configKey }, limit: 2, forUpdate: lock });
  33. if (rows.length > 1) fail('GROUP_CONFIG_INVALID', '分组配置重复,请联系维护人');
  34. return { row: rows[0], state: current(rows[0], members) };
  35. };
  36. app.handlers.ksfGetPublicMemberGroups = async (p) => {
  37. if (Object.keys(p).some((key) => key !== 'requestId')) fail('VALIDATION_FAILED', '查询包含未允许字段');
  38. const members = indexBoardMembers(await loadBoard()).members;
  39. const { state } = await read(store, members);
  40. return { ...state, readOnly: true, memberCount: members.length };
  41. };
  42. app.handlers.ksfManagePublicMemberGroups = async (p) => {
  43. const fields = { create: ['name'], rename: ['groupId', 'name'], delete: ['groupId'], assign: ['groupId', 'memberIds'] };
  44. if (!Object.hasOwn(fields, p.action) || Object.keys(p).some((key) => !['requestId', 'expectedRevision', 'action', ...fields[p.action]].includes(key))) fail('VALIDATION_FAILED', '操作包含未允许字段');
  45. if (!Number.isSafeInteger(p.expectedRevision) || p.expectedRevision < 0) fail('VALIDATION_FAILED', '缺少有效分组版本,请刷新');
  46. const members = indexBoardMembers(await loadBoard()).members;
  47. const allowed = new Set(members.map((m) => m.id));
  48. return app.operation(actor, 'public-member-groups', p, async (s) => {
  49. await s.lock('ksf:public-member-groups');
  50. const { row, state } = await read(s, members, true);
  51. if (state.revision !== p.expectedRevision) fail('VERSION_CONFLICT', '分组已被其他人更新,请刷新后再操作');
  52. const next = changeMemberGroups(state, p, allowed, 'group:' + app.hash(p.requestId).slice(0,24));
  53. next.updatedAt = new Date().toISOString();
  54. if (row) await s.update('KsfRuntimeConfig', row.objectId, { value: next });
  55. else await s.create('KsfRuntimeConfig', { key: configKey, value: next });
  56. return { ...next, readOnly: false, groupingOnly: true, businessRecordsChanged: 0 };
  57. });
  58. };
  59. }