openxiangda-cli 2.0.0-alpha.86 → 2.0.0-alpha.90
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 +2 -2
- package/bin/run.js +1 -0
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +3 -3
- package/dist/create-workspace.js.map +1 -1
- package/dist/toolchain-capsule.d.ts.map +1 -1
- package/dist/toolchain-capsule.js +4 -8
- package/dist/toolchain-capsule.js.map +1 -1
- package/package.json +6 -7
- package/template/AGENTS.md +2 -1
- package/template/README.md +5 -5
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +14 -1
- package/template/apps/server/src/main.ts +1 -1
- package/template/apps/server/test/smoke.test.ts +3 -1
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
- package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
- package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
- package/template/apps/web/e2e/resources.spec.ts +20 -181
- package/template/apps/web/field-protocol.e2e.html +3 -0
- package/template/apps/web/package.json +2 -5
- package/template/apps/web/src/main.tsx +12 -60
- package/template/apps/web/test/contracts.test.ts +52 -104
- package/template/openxiangda.config.ts +2 -2
- package/template/package.json +1 -3
- package/template/packages/contracts/src/generated.ts +27 -0
- package/template/scripts/verify-template-budget.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
- package/template/apps/web/src/FilePreviewPage.tsx +0 -324
- package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
- package/template/apps/web/src/Shell.tsx +0 -445
- package/template/apps/web/src/appearance.tsx +0 -147
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
- package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
- package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
- package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
- package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
- package/template/apps/web/src/components/resource/resource-import.ts +0 -205
- package/template/apps/web/src/data-provider.ts +0 -72
- package/template/apps/web/src/platform-client.ts +0 -708
- package/template/apps/web/src/runtime-meta.ts +0 -75
- package/template/apps/web/src/runtime.tsx +0 -227
- package/template/apps/web/src/styles.css +0 -1941
- package/template/apps/web/test/address-field.test.ts +0 -66
- package/template/apps/web/test/cascade-value.test.ts +0 -68
- package/template/apps/web/test/directory-value.test.ts +0 -55
- package/template/apps/web/test/field-bounds.test.ts +0 -17
- package/template/apps/web/test/field-form-codec.test.ts +0 -85
- package/template/apps/web/test/location-field.test.ts +0 -83
- package/template/apps/web/test/resource-import.test.ts +0 -143
- package/template/apps/web/test/resource-query.test.ts +0 -144
- package/template/apps/web/test/rich-json-field.test.ts +0 -53
- package/template/apps/web/test/signature-serial-field.test.ts +0 -40
- package/template/apps/web/test/subtable-value.test.ts +0 -144
|
@@ -1,708 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SCHEMA_VERSIONS,
|
|
3
|
-
type DataAuditPage,
|
|
4
|
-
type DataFilePreview,
|
|
5
|
-
type DataFileRef,
|
|
6
|
-
type DataFileUploadPlan,
|
|
7
|
-
type DataFieldSourcePage,
|
|
8
|
-
type DataFieldSourceQuery,
|
|
9
|
-
type DataExportRequest,
|
|
10
|
-
type DataPage,
|
|
11
|
-
type DataQuery,
|
|
12
|
-
type DataWhere,
|
|
13
|
-
type DataRecord,
|
|
14
|
-
type DataResourceSurface,
|
|
15
|
-
type DataTransactionOperation,
|
|
16
|
-
type DataTransactionRequest,
|
|
17
|
-
type DataTransactionResult,
|
|
18
|
-
type DirectoryEntryPage,
|
|
19
|
-
type DirectoryResolveRequest,
|
|
20
|
-
type RoleSessionContext,
|
|
21
|
-
type RoleSubjectChoice,
|
|
22
|
-
type RoleSubjectChoicePage,
|
|
23
|
-
type ResourceReferenceValue,
|
|
24
|
-
} from 'openxiangda-contracts/browser';
|
|
25
|
-
import { useSyncExternalStore } from 'react';
|
|
26
|
-
import {
|
|
27
|
-
buildResourceWhere,
|
|
28
|
-
type GenericResourceQuery,
|
|
29
|
-
} from './components/platform-fields/resource-query';
|
|
30
|
-
export type { GenericResourceQuery } from './components/platform-fields/resource-query';
|
|
31
|
-
import {
|
|
32
|
-
applicationApiPath,
|
|
33
|
-
applicationCode,
|
|
34
|
-
runtimeMount,
|
|
35
|
-
} from './runtime-meta';
|
|
36
|
-
|
|
37
|
-
interface PlatformEnvelope<T> {
|
|
38
|
-
code: number | string;
|
|
39
|
-
message?: string;
|
|
40
|
-
errorCode?: string;
|
|
41
|
-
requestId?: string;
|
|
42
|
-
data: T;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let globalRequestCount = 0;
|
|
46
|
-
const globalRequestListeners = new Set<() => void>();
|
|
47
|
-
|
|
48
|
-
function beginGlobalRequest() {
|
|
49
|
-
globalRequestCount += 1;
|
|
50
|
-
globalRequestListeners.forEach((listener) => listener());
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function endGlobalRequest() {
|
|
54
|
-
globalRequestCount = Math.max(0, globalRequestCount - 1);
|
|
55
|
-
globalRequestListeners.forEach((listener) => listener());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function useGlobalRequestLoading() {
|
|
59
|
-
return useSyncExternalStore(
|
|
60
|
-
(listener) => {
|
|
61
|
-
globalRequestListeners.add(listener);
|
|
62
|
-
return () => globalRequestListeners.delete(listener);
|
|
63
|
-
},
|
|
64
|
-
() => globalRequestCount > 0,
|
|
65
|
-
() => false
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
70
|
-
beginGlobalRequest();
|
|
71
|
-
try {
|
|
72
|
-
const headers = new Headers(init?.headers);
|
|
73
|
-
if (!headers.has('accept')) headers.set('accept', 'application/json');
|
|
74
|
-
if (init?.body && !headers.has('content-type')) {
|
|
75
|
-
headers.set('content-type', 'application/json');
|
|
76
|
-
}
|
|
77
|
-
if (activeRoleSessionId && roleSessionBoundPath(path)) {
|
|
78
|
-
headers.set('x-openxiangda-role-session-id', activeRoleSessionId);
|
|
79
|
-
}
|
|
80
|
-
const response = await fetch(path, {
|
|
81
|
-
credentials: 'include',
|
|
82
|
-
...init,
|
|
83
|
-
headers,
|
|
84
|
-
});
|
|
85
|
-
const payload = (await response.json().catch(() => null)) as PlatformEnvelope<T> | T | null;
|
|
86
|
-
const envelope = payload && typeof payload === 'object' && 'code' in payload ? (payload as PlatformEnvelope<T>) : null;
|
|
87
|
-
if (!response.ok || (envelope && envelope.code !== 200)) {
|
|
88
|
-
const requestId = envelope?.requestId ? ` (requestId: ${envelope.requestId})` : '';
|
|
89
|
-
throw Object.assign(
|
|
90
|
-
new Error(`${envelope?.errorCode || `HTTP_${response.status}`}: ${envelope?.message || '平台请求失败'}${requestId}`),
|
|
91
|
-
{
|
|
92
|
-
code: envelope?.errorCode || `HTTP_${response.status}`,
|
|
93
|
-
status: response.status,
|
|
94
|
-
data: envelope?.data ?? null,
|
|
95
|
-
}
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return envelope ? envelope.data : (payload as T);
|
|
99
|
-
} finally {
|
|
100
|
-
endGlobalRequest();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function requestBlob(path: string, init?: RequestInit) {
|
|
105
|
-
beginGlobalRequest();
|
|
106
|
-
try {
|
|
107
|
-
const headers = new Headers(init?.headers);
|
|
108
|
-
headers.set('accept', 'text/csv');
|
|
109
|
-
if (init?.body && !headers.has('content-type')) {
|
|
110
|
-
headers.set('content-type', 'application/json');
|
|
111
|
-
}
|
|
112
|
-
if (activeRoleSessionId && roleSessionBoundPath(path)) {
|
|
113
|
-
headers.set('x-openxiangda-role-session-id', activeRoleSessionId);
|
|
114
|
-
}
|
|
115
|
-
const response = await fetch(path, {
|
|
116
|
-
credentials: 'include',
|
|
117
|
-
...init,
|
|
118
|
-
headers,
|
|
119
|
-
});
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
const payload = (await response.json().catch(() => null)) as PlatformEnvelope<unknown> | null;
|
|
122
|
-
const requestId = payload?.requestId ? ` (requestId: ${payload.requestId})` : '';
|
|
123
|
-
throw Object.assign(
|
|
124
|
-
new Error(`${payload?.errorCode || `HTTP_${response.status}`}: ${payload?.message || '平台导出失败'}${requestId}`),
|
|
125
|
-
{
|
|
126
|
-
code: payload?.errorCode || `HTTP_${response.status}`,
|
|
127
|
-
status: response.status,
|
|
128
|
-
data: payload?.data ?? null,
|
|
129
|
-
}
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const disposition = response.headers.get('content-disposition') || '';
|
|
133
|
-
const encodedName = /filename\*=UTF-8''([^;]+)/i.exec(disposition)?.[1];
|
|
134
|
-
return {
|
|
135
|
-
blob: await response.blob(),
|
|
136
|
-
fileName: encodedName ? decodeURIComponent(encodedName) : undefined,
|
|
137
|
-
rowLimit: Number(response.headers.get('x-openxiangda-export-row-limit') || 10_000),
|
|
138
|
-
};
|
|
139
|
-
} finally {
|
|
140
|
-
endGlobalRequest();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function roleSessionBoundPath(path: string) {
|
|
145
|
-
if (path.startsWith(applicationServiceBase())) return true;
|
|
146
|
-
const mount = runtimeMount();
|
|
147
|
-
return Boolean(
|
|
148
|
-
mount &&
|
|
149
|
-
path.startsWith(
|
|
150
|
-
`/service/openxiangda-app-api/v2/${encodeURIComponent(mount.appCode)}/${encodeURIComponent(mount.environmentKey)}/`
|
|
151
|
-
)
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function requestApplicationApi<T>(
|
|
156
|
-
path: string,
|
|
157
|
-
init?: RequestInit
|
|
158
|
-
) {
|
|
159
|
-
return await request<T>(applicationApiPath(path), init);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface RuntimeIdentity {
|
|
163
|
-
userId: string;
|
|
164
|
-
roleCodes: string[];
|
|
165
|
-
capabilityCodes: string[];
|
|
166
|
-
isAppSuperAdmin: boolean;
|
|
167
|
-
identityScope: string;
|
|
168
|
-
subjectProfile: RoleSessionContext['subjectProfile'] | null;
|
|
169
|
-
currentRoleSubject: RoleSubjectChoice | null;
|
|
170
|
-
roleSubjectPage: RoleSubjectChoicePage | null;
|
|
171
|
-
roleSwitchingAvailable: boolean;
|
|
172
|
-
environment: {
|
|
173
|
-
id: string;
|
|
174
|
-
key: 'preproduction' | 'production';
|
|
175
|
-
activeAppVersionId?: string;
|
|
176
|
-
headRevision?: number;
|
|
177
|
-
authzRevisionId?: string;
|
|
178
|
-
authzVersion?: number;
|
|
179
|
-
scopeDataVersion?: string;
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
interface ConnectedCurrent {
|
|
184
|
-
environment: RuntimeIdentity['environment'];
|
|
185
|
-
principal: Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export interface RuntimeAuthorization {
|
|
189
|
-
state: RoleSessionContext['state'];
|
|
190
|
-
identity: RuntimeIdentity | null;
|
|
191
|
-
roleContext: RoleSessionContext | null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let activeIdentity: RuntimeIdentity | undefined;
|
|
195
|
-
let activeRoleSessionId: string | undefined;
|
|
196
|
-
|
|
197
|
-
function applicationServiceBase() {
|
|
198
|
-
return `/service/openxiangda-api/v2/applications/${applicationCode()}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function validateRoleContext(context: RoleSessionContext): RuntimeAuthorization {
|
|
202
|
-
const mount = runtimeMount();
|
|
203
|
-
if (
|
|
204
|
-
!mount ||
|
|
205
|
-
context.schemaVersion !== SCHEMA_VERSIONS.roleSessionContext ||
|
|
206
|
-
context.environment.key !== mount.environmentKey
|
|
207
|
-
) {
|
|
208
|
-
throw new Error('OPENXIANGDA_ROLE_SESSION_CONTEXT_INVALID');
|
|
209
|
-
}
|
|
210
|
-
if (context.state !== 'active') {
|
|
211
|
-
if (context.roleSession || context.principal || context.currentRoleSubject) {
|
|
212
|
-
throw new Error('OPENXIANGDA_ROLE_SESSION_CONTEXT_INACTIVE_INVALID');
|
|
213
|
-
}
|
|
214
|
-
activeIdentity = undefined;
|
|
215
|
-
activeRoleSessionId = undefined;
|
|
216
|
-
return { state: context.state, identity: null, roleContext: context };
|
|
217
|
-
}
|
|
218
|
-
if (
|
|
219
|
-
!context.roleSession ||
|
|
220
|
-
!context.principal ||
|
|
221
|
-
!context.currentRoleSubject ||
|
|
222
|
-
!context.identityScope ||
|
|
223
|
-
!context.principal.activeRoleCode ||
|
|
224
|
-
context.principal.appCode !== mount.appCode ||
|
|
225
|
-
context.principal.roleSessionId !== context.roleSession.id ||
|
|
226
|
-
context.principal.activeRoleCode !== context.currentRoleSubject.role.code
|
|
227
|
-
) {
|
|
228
|
-
throw new Error('OPENXIANGDA_ROLE_SESSION_CONTEXT_ACTIVE_INVALID');
|
|
229
|
-
}
|
|
230
|
-
activeRoleSessionId = context.roleSession.id;
|
|
231
|
-
activeIdentity = {
|
|
232
|
-
userId: context.principal.userId,
|
|
233
|
-
roleCodes: [context.principal.activeRoleCode],
|
|
234
|
-
capabilityCodes: context.principal.capabilities,
|
|
235
|
-
isAppSuperAdmin: context.principal.isAppSuperAdmin,
|
|
236
|
-
identityScope: context.identityScope,
|
|
237
|
-
subjectProfile: context.subjectProfile,
|
|
238
|
-
currentRoleSubject: context.currentRoleSubject,
|
|
239
|
-
roleSubjectPage: context.roleSubjectPage,
|
|
240
|
-
roleSwitchingAvailable: true,
|
|
241
|
-
environment: {
|
|
242
|
-
id: context.environment.id,
|
|
243
|
-
key: context.environment.key,
|
|
244
|
-
activeAppVersionId: context.environment.activeAppVersionId,
|
|
245
|
-
headRevision: context.environment.headRevision,
|
|
246
|
-
authzRevisionId: context.principal.authzRevisionId,
|
|
247
|
-
authzVersion: context.principal.authzVersion,
|
|
248
|
-
scopeDataVersion: context.principal.scopeDataVersion,
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
return { state: context.state, identity: activeIdentity, roleContext: context };
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export async function loadRuntimeAuthorization(): Promise<RuntimeAuthorization> {
|
|
255
|
-
const mount = runtimeMount();
|
|
256
|
-
if (mount) {
|
|
257
|
-
const context = await request<RoleSessionContext>(
|
|
258
|
-
`${nativeBase()}/authz/role-session/context?environmentKey=${encodeURIComponent(mount.environmentKey)}`
|
|
259
|
-
);
|
|
260
|
-
return validateRoleContext(context);
|
|
261
|
-
}
|
|
262
|
-
const current = await request<ConnectedCurrent>(
|
|
263
|
-
`${applicationServiceBase()}/dev-sessions/current`
|
|
264
|
-
);
|
|
265
|
-
activeRoleSessionId = undefined;
|
|
266
|
-
activeIdentity = {
|
|
267
|
-
...current.principal,
|
|
268
|
-
identityScope: `connected-dev:${applicationCode()}:${current.environment.key}:${current.principal.userId}`,
|
|
269
|
-
subjectProfile: null,
|
|
270
|
-
currentRoleSubject: null,
|
|
271
|
-
roleSubjectPage: null,
|
|
272
|
-
roleSwitchingAvailable: false,
|
|
273
|
-
environment: current.environment,
|
|
274
|
-
};
|
|
275
|
-
return { state: 'active', identity: activeIdentity, roleContext: null };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export async function loadRoleSubjects(input: {
|
|
279
|
-
keyword?: string;
|
|
280
|
-
cursor?: string;
|
|
281
|
-
limit?: number;
|
|
282
|
-
} = {}) {
|
|
283
|
-
const query = new URLSearchParams({
|
|
284
|
-
environmentKey: currentEnvironmentKey(),
|
|
285
|
-
limit: String(Math.min(Math.max(input.limit || 20, 1), 50)),
|
|
286
|
-
});
|
|
287
|
-
if (input.keyword?.trim()) query.set('keyword', input.keyword.trim());
|
|
288
|
-
if (input.cursor) query.set('cursor', input.cursor);
|
|
289
|
-
return request<RoleSubjectChoicePage>(
|
|
290
|
-
`${nativeBase()}/authz/role-session/context/role-subjects?${query}`
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export async function switchRuntimeRole(
|
|
295
|
-
targetRoleSubjectKey: string,
|
|
296
|
-
expectedRoleSessionId: string | null
|
|
297
|
-
) {
|
|
298
|
-
const context = await request<RoleSessionContext>(
|
|
299
|
-
`${nativeBase()}/authz/role-session/context/switch`,
|
|
300
|
-
{
|
|
301
|
-
method: 'POST',
|
|
302
|
-
body: JSON.stringify({
|
|
303
|
-
targetRoleSubjectKey,
|
|
304
|
-
expectedRoleSessionId,
|
|
305
|
-
environmentKey: currentEnvironmentKey(),
|
|
306
|
-
}),
|
|
307
|
-
}
|
|
308
|
-
);
|
|
309
|
-
const authorization = validateRoleContext(context);
|
|
310
|
-
if (!authorization.identity) {
|
|
311
|
-
throw new Error('OPENXIANGDA_ROLE_SESSION_SWITCH_INACTIVE');
|
|
312
|
-
}
|
|
313
|
-
publishRoleSessionInvalidation();
|
|
314
|
-
return authorization;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const ROLE_SESSION_CHANNEL = 'openxiangda.role-session-context/v2';
|
|
318
|
-
const ROLE_SESSION_STORAGE_KEY =
|
|
319
|
-
'openxiangda.role-session-context-invalidated/v2';
|
|
320
|
-
|
|
321
|
-
type RoleSessionInvalidation = {
|
|
322
|
-
type: 'role-session-context-invalidated';
|
|
323
|
-
appCode: string;
|
|
324
|
-
environmentKey: 'preproduction' | 'production';
|
|
325
|
-
recordedAt: number;
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
function validRoleSessionInvalidation(
|
|
329
|
-
value: unknown
|
|
330
|
-
): value is RoleSessionInvalidation {
|
|
331
|
-
if (!value || typeof value !== 'object') return false;
|
|
332
|
-
const candidate = value as Partial<RoleSessionInvalidation>;
|
|
333
|
-
return (
|
|
334
|
-
candidate.type === 'role-session-context-invalidated' &&
|
|
335
|
-
typeof candidate.appCode === 'string' &&
|
|
336
|
-
(candidate.environmentKey === 'preproduction' ||
|
|
337
|
-
candidate.environmentKey === 'production') &&
|
|
338
|
-
typeof candidate.recordedAt === 'number'
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function publishRoleSessionInvalidation() {
|
|
343
|
-
const mount = runtimeMount();
|
|
344
|
-
if (!mount || typeof window === 'undefined') return;
|
|
345
|
-
const event: RoleSessionInvalidation = {
|
|
346
|
-
type: 'role-session-context-invalidated',
|
|
347
|
-
appCode: mount.appCode,
|
|
348
|
-
environmentKey: mount.environmentKey,
|
|
349
|
-
recordedAt: Date.now(),
|
|
350
|
-
};
|
|
351
|
-
try {
|
|
352
|
-
const channel = new BroadcastChannel(ROLE_SESSION_CHANNEL);
|
|
353
|
-
channel.postMessage(event);
|
|
354
|
-
channel.close();
|
|
355
|
-
} catch {
|
|
356
|
-
// Storage below is the cross-tab fallback.
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
window.localStorage.setItem(ROLE_SESSION_STORAGE_KEY, JSON.stringify(event));
|
|
360
|
-
} catch {
|
|
361
|
-
// The active tab has already updated from the server response.
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export function subscribeRoleSessionInvalidation(listener: () => void) {
|
|
366
|
-
const mount = runtimeMount();
|
|
367
|
-
if (!mount || typeof window === 'undefined') return () => undefined;
|
|
368
|
-
const receive = (value: unknown) => {
|
|
369
|
-
if (
|
|
370
|
-
validRoleSessionInvalidation(value) &&
|
|
371
|
-
value.appCode === mount.appCode &&
|
|
372
|
-
value.environmentKey === mount.environmentKey
|
|
373
|
-
) {
|
|
374
|
-
activeIdentity = undefined;
|
|
375
|
-
activeRoleSessionId = undefined;
|
|
376
|
-
listener();
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
let channel: BroadcastChannel | undefined;
|
|
380
|
-
try {
|
|
381
|
-
channel = new BroadcastChannel(ROLE_SESSION_CHANNEL);
|
|
382
|
-
channel.onmessage = event => receive(event.data);
|
|
383
|
-
} catch {
|
|
384
|
-
channel = undefined;
|
|
385
|
-
}
|
|
386
|
-
const onStorage = (event: StorageEvent) => {
|
|
387
|
-
if (event.key !== ROLE_SESSION_STORAGE_KEY || !event.newValue) return;
|
|
388
|
-
try {
|
|
389
|
-
receive(JSON.parse(event.newValue));
|
|
390
|
-
} catch {
|
|
391
|
-
// Invalid browser signals never alter the server-owned identity.
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
window.addEventListener('storage', onStorage);
|
|
395
|
-
return () => {
|
|
396
|
-
channel?.close();
|
|
397
|
-
window.removeEventListener('storage', onStorage);
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export async function logoutCurrentUser() {
|
|
402
|
-
await request<null>('/api/auth/logout', {
|
|
403
|
-
method: 'POST',
|
|
404
|
-
});
|
|
405
|
-
activeIdentity = undefined;
|
|
406
|
-
activeRoleSessionId = undefined;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function currentEnvironmentKey() {
|
|
410
|
-
const key = activeIdentity?.environment.key || runtimeMount()?.environmentKey;
|
|
411
|
-
if (!key) throw new Error('OPENXIANGDA_CURRENT_ENVIRONMENT_REQUIRED');
|
|
412
|
-
return key;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export type DirectoryKind = 'user' | 'department';
|
|
416
|
-
|
|
417
|
-
export interface ChinaDivisionListItem {
|
|
418
|
-
adcode: string;
|
|
419
|
-
citycode?: string;
|
|
420
|
-
name: string;
|
|
421
|
-
level?: string;
|
|
422
|
-
center?: string;
|
|
423
|
-
parentAdcode?: string;
|
|
424
|
-
hasChildren: boolean;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
export async function loadChinaDivisions(parentAdcode?: string) {
|
|
428
|
-
const params = new URLSearchParams();
|
|
429
|
-
if (parentAdcode) params.set('parentAdcode', parentAdcode);
|
|
430
|
-
const suffix = params.size ? `?${params}` : '';
|
|
431
|
-
return await request<ChinaDivisionListItem[]>(`/china-divisions/${suffix}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export async function searchDirectory(kind: DirectoryKind, options: { keyword: string; cursor?: string }) {
|
|
435
|
-
const params = new URLSearchParams({
|
|
436
|
-
environmentKey: currentEnvironmentKey(),
|
|
437
|
-
keyword: options.keyword.trim(),
|
|
438
|
-
limit: '20',
|
|
439
|
-
});
|
|
440
|
-
if (options.cursor) params.set('cursor', options.cursor);
|
|
441
|
-
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
export async function browseDepartmentTree(options: { parentId?: string; cursor?: string }) {
|
|
445
|
-
const params = new URLSearchParams({
|
|
446
|
-
environmentKey: currentEnvironmentKey(),
|
|
447
|
-
limit: '100',
|
|
448
|
-
});
|
|
449
|
-
if (options.parentId) params.set('parentId', options.parentId);
|
|
450
|
-
if (options.cursor) params.set('offset', options.cursor);
|
|
451
|
-
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export async function browseDepartmentUsers(departmentId: string, page = 1) {
|
|
455
|
-
const params = new URLSearchParams({
|
|
456
|
-
environmentKey: currentEnvironmentKey(),
|
|
457
|
-
page: String(page),
|
|
458
|
-
limit: '20',
|
|
459
|
-
});
|
|
460
|
-
return request<DirectoryEntryPage>(
|
|
461
|
-
`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(departmentId)}/users?${params}`
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export async function resolveDirectory(kind: DirectoryKind, ids: string[]) {
|
|
466
|
-
const uniqueIds = [...new Set(ids)];
|
|
467
|
-
if (uniqueIds.length === 0) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
|
|
468
|
-
if (uniqueIds.length > 50) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
|
|
469
|
-
const body: DirectoryResolveRequest = {
|
|
470
|
-
schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest,
|
|
471
|
-
kind,
|
|
472
|
-
ids: uniqueIds,
|
|
473
|
-
};
|
|
474
|
-
return request<DirectoryEntryPage>(
|
|
475
|
-
`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`,
|
|
476
|
-
{ method: 'POST', body: JSON.stringify(body) }
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
export async function searchResource(
|
|
481
|
-
resourceCode: string,
|
|
482
|
-
fieldCode: string,
|
|
483
|
-
options: {
|
|
484
|
-
operation: 'create' | 'update';
|
|
485
|
-
keyword: string;
|
|
486
|
-
cursor?: string;
|
|
487
|
-
bindings?: Record<string, unknown>;
|
|
488
|
-
}
|
|
489
|
-
) {
|
|
490
|
-
const query: DataFieldSourceQuery = {
|
|
491
|
-
schemaVersion: SCHEMA_VERSIONS.dataFieldSourceQuery,
|
|
492
|
-
operation: options.operation,
|
|
493
|
-
...(options.keyword.trim() ? { keyword: options.keyword.trim() } : {}),
|
|
494
|
-
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
495
|
-
...(options.bindings && Object.keys(options.bindings).length
|
|
496
|
-
? { bindings: options.bindings }
|
|
497
|
-
: {}),
|
|
498
|
-
};
|
|
499
|
-
return await request<DataFieldSourcePage>(
|
|
500
|
-
`${nativeBase()}/data-resources/${encodeURIComponent(
|
|
501
|
-
resourceCode
|
|
502
|
-
)}/fields/${encodeURIComponent(fieldCode)}/source/query`,
|
|
503
|
-
{
|
|
504
|
-
method: 'POST',
|
|
505
|
-
body: JSON.stringify({
|
|
506
|
-
...query,
|
|
507
|
-
environmentKey: currentEnvironmentKey(),
|
|
508
|
-
}),
|
|
509
|
-
}
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export function createNativeResourceClient(code: string, surface: DataResourceSurface) {
|
|
514
|
-
const base = dataBase(code);
|
|
515
|
-
const declaredFields = new Set(Object.keys(surface.fields));
|
|
516
|
-
const assertField = (field: string) => {
|
|
517
|
-
if (!declaredFields.has(field)) throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
|
|
518
|
-
return field;
|
|
519
|
-
};
|
|
520
|
-
return {
|
|
521
|
-
async list(query: GenericResourceQuery) {
|
|
522
|
-
const fallbackSort = surface.list?.defaultSort || {
|
|
523
|
-
field: Object.keys(surface.fields)[0] || 'id',
|
|
524
|
-
order: 'asc' as const,
|
|
525
|
-
};
|
|
526
|
-
const requestedSort = query.sort || fallbackSort;
|
|
527
|
-
const sort = declaredFields.has(requestedSort.field) ? requestedSort : fallbackSort;
|
|
528
|
-
const where = buildResourceWhere(code, surface, query);
|
|
529
|
-
const body: DataQuery = {
|
|
530
|
-
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
531
|
-
...(where ? { where } : {}),
|
|
532
|
-
order: [{ field: sort.field, direction: sort.order || 'asc' }],
|
|
533
|
-
limit: query.pageSize,
|
|
534
|
-
offset: (query.page - 1) * query.pageSize,
|
|
535
|
-
};
|
|
536
|
-
const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, {
|
|
537
|
-
method: 'POST',
|
|
538
|
-
body: JSON.stringify({
|
|
539
|
-
...body,
|
|
540
|
-
environmentKey: currentEnvironmentKey(),
|
|
541
|
-
}),
|
|
542
|
-
});
|
|
543
|
-
return { rows: page.items, total: page.total };
|
|
544
|
-
},
|
|
545
|
-
async exportCsv(query: GenericResourceQuery, select: string[]) {
|
|
546
|
-
const fallbackSort = surface.list?.defaultSort || {
|
|
547
|
-
field: Object.keys(surface.fields)[0] || 'id',
|
|
548
|
-
order: 'asc' as const,
|
|
549
|
-
};
|
|
550
|
-
const requestedSort = query.sort || fallbackSort;
|
|
551
|
-
const sort = declaredFields.has(requestedSort.field) ? requestedSort : fallbackSort;
|
|
552
|
-
const where = buildResourceWhere(code, surface, query);
|
|
553
|
-
const body: DataExportRequest = {
|
|
554
|
-
schemaVersion: SCHEMA_VERSIONS.dataExportRequest,
|
|
555
|
-
select: select.map(assertField),
|
|
556
|
-
...(where ? { where } : {}),
|
|
557
|
-
order: [{ field: sort.field, direction: sort.order || 'asc' }],
|
|
558
|
-
};
|
|
559
|
-
return await requestBlob(`${base}/export`, {
|
|
560
|
-
method: 'POST',
|
|
561
|
-
body: JSON.stringify({
|
|
562
|
-
...body,
|
|
563
|
-
environmentKey: currentEnvironmentKey(),
|
|
564
|
-
}),
|
|
565
|
-
});
|
|
566
|
-
},
|
|
567
|
-
async get(id: string) {
|
|
568
|
-
const record = await request<DataRecord<Record<string, unknown>>>(
|
|
569
|
-
`${base}/records/${encodeURIComponent(id)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
570
|
-
);
|
|
571
|
-
return record.data;
|
|
572
|
-
},
|
|
573
|
-
async audit(id: string) {
|
|
574
|
-
return request<DataAuditPage>(
|
|
575
|
-
`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
576
|
-
);
|
|
577
|
-
},
|
|
578
|
-
async create(data: Record<string, unknown>) {
|
|
579
|
-
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records`, {
|
|
580
|
-
method: 'POST',
|
|
581
|
-
body: JSON.stringify({
|
|
582
|
-
environmentKey: currentEnvironmentKey(),
|
|
583
|
-
data,
|
|
584
|
-
}),
|
|
585
|
-
});
|
|
586
|
-
return record.data;
|
|
587
|
-
},
|
|
588
|
-
async update(id: string, expectedRevision: number, data: Record<string, unknown>) {
|
|
589
|
-
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records/${encodeURIComponent(id)}/update`, {
|
|
590
|
-
method: 'POST',
|
|
591
|
-
body: JSON.stringify({
|
|
592
|
-
environmentKey: currentEnvironmentKey(),
|
|
593
|
-
expectedRevision,
|
|
594
|
-
data,
|
|
595
|
-
}),
|
|
596
|
-
});
|
|
597
|
-
return record.data;
|
|
598
|
-
},
|
|
599
|
-
async remove(id: string, expectedRevision: number) {
|
|
600
|
-
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records/${encodeURIComponent(id)}/delete`, {
|
|
601
|
-
method: 'POST',
|
|
602
|
-
body: JSON.stringify({
|
|
603
|
-
environmentKey: currentEnvironmentKey(),
|
|
604
|
-
expectedRevision,
|
|
605
|
-
}),
|
|
606
|
-
});
|
|
607
|
-
return record.data;
|
|
608
|
-
},
|
|
609
|
-
async upload(fieldCode: string, file: File, recordId?: string) {
|
|
610
|
-
assertField(fieldCode);
|
|
611
|
-
const plan = await request<DataFileUploadPlan>(`${base}/files/uploads/initiate`, {
|
|
612
|
-
method: 'POST',
|
|
613
|
-
body: JSON.stringify({
|
|
614
|
-
environmentKey: currentEnvironmentKey(),
|
|
615
|
-
fieldCode,
|
|
616
|
-
fileName: file.name,
|
|
617
|
-
fileSize: file.size,
|
|
618
|
-
contentType: file.type || 'application/octet-stream',
|
|
619
|
-
...(recordId ? { recordId } : {}),
|
|
620
|
-
}),
|
|
621
|
-
});
|
|
622
|
-
const uploaded = await fetch(plan.uploadUrl, {
|
|
623
|
-
method: plan.uploadMethod,
|
|
624
|
-
headers: plan.headers,
|
|
625
|
-
body: file,
|
|
626
|
-
});
|
|
627
|
-
if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
|
|
628
|
-
return request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, {
|
|
629
|
-
method: 'POST',
|
|
630
|
-
body: JSON.stringify({ environmentKey: currentEnvironmentKey() }),
|
|
631
|
-
});
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
export async function transactNativeData(operations: DataTransactionOperation[], idempotencyKey: string = crypto.randomUUID()) {
|
|
637
|
-
const transaction: DataTransactionRequest = {
|
|
638
|
-
schemaVersion: SCHEMA_VERSIONS.dataTransactionRequest,
|
|
639
|
-
idempotencyKey,
|
|
640
|
-
operations,
|
|
641
|
-
};
|
|
642
|
-
return request<DataTransactionResult>(`${nativeBase()}/data/transactions`, {
|
|
643
|
-
method: 'POST',
|
|
644
|
-
body: JSON.stringify({
|
|
645
|
-
...transaction,
|
|
646
|
-
environmentKey: currentEnvironmentKey(),
|
|
647
|
-
}),
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function nativeBase() {
|
|
652
|
-
return `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function dataBase(code: string) {
|
|
656
|
-
return `${nativeBase()}/data/${encodeURIComponent(code)}`;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export function dataFileContentUrl(resource: string, fileId: string, disposition: 'attachment' | 'inline' = 'attachment') {
|
|
660
|
-
const query = new URLSearchParams({
|
|
661
|
-
environmentKey: currentEnvironmentKey(),
|
|
662
|
-
disposition,
|
|
663
|
-
});
|
|
664
|
-
return `${dataBase(resource)}/files/${encodeURIComponent(fileId)}/content?${query}`;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
export function dataRichTextImageSource(resource: string, fileId: string) {
|
|
668
|
-
return `${dataBase(resource)}/files/${encodeURIComponent(fileId)}/content?disposition=inline`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
export async function loadDataFilePreview(resource: string, fileId: string) {
|
|
672
|
-
return request<DataFilePreview>(
|
|
673
|
-
`${dataBase(resource)}/files/${encodeURIComponent(fileId)}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
export async function fetchDataFileBlob(resource: string, fileId: string) {
|
|
678
|
-
const headers = new Headers({ accept: 'application/octet-stream,*/*' });
|
|
679
|
-
if (activeRoleSessionId) {
|
|
680
|
-
headers.set('x-openxiangda-role-session-id', activeRoleSessionId);
|
|
681
|
-
}
|
|
682
|
-
const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), {
|
|
683
|
-
credentials: 'include',
|
|
684
|
-
headers,
|
|
685
|
-
});
|
|
686
|
-
if (!response.ok) throw new Error(`FILE_CONTENT_READ_FAILED: HTTP_${response.status}`);
|
|
687
|
-
return response.blob();
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
export async function downloadDataFile(
|
|
691
|
-
resource: string,
|
|
692
|
-
fileId: string,
|
|
693
|
-
fileName: string
|
|
694
|
-
) {
|
|
695
|
-
const blob = await fetchDataFileBlob(resource, fileId);
|
|
696
|
-
const url = URL.createObjectURL(blob);
|
|
697
|
-
try {
|
|
698
|
-
const anchor = document.createElement('a');
|
|
699
|
-
anchor.href = url;
|
|
700
|
-
anchor.download = fileName;
|
|
701
|
-
anchor.rel = 'noopener';
|
|
702
|
-
document.body.append(anchor);
|
|
703
|
-
anchor.click();
|
|
704
|
-
anchor.remove();
|
|
705
|
-
} finally {
|
|
706
|
-
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
707
|
-
}
|
|
708
|
-
}
|