openxiangda-cli 2.0.0-alpha.61 → 2.0.0-alpha.66
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/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +1 -0
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/AGENTS.md +1 -1
- package/template/README.md +7 -7
- package/template/apps/server/Dockerfile +2 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -3
- package/template/apps/server/test/smoke.test.ts +6 -23
- package/template/apps/web/e2e/resources.spec.ts +20 -0
- package/template/apps/web/index.html +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +298 -41
- package/template/apps/web/src/Shell.tsx +39 -211
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +229 -16
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +142 -2
- package/template/apps/web/src/data-provider.ts +23 -102
- package/template/apps/web/src/main.tsx +35 -83
- package/template/apps/web/src/platform-client.ts +112 -424
- package/template/apps/web/src/runtime-meta.ts +1 -1
- package/template/apps/web/src/styles.css +285 -3
- package/template/apps/web/test/contracts.test.ts +43 -769
- package/template/openxiangda.config.ts +14 -81
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +7 -804
- package/template/apps/server/src/instrument-context.controller.ts +0 -25
- package/template/apps/web/e2e/instruments.spec.ts +0 -1338
- package/template/apps/web/src/CollegePage.tsx +0 -181
- package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
- package/template/apps/web/src/InstrumentForm.tsx +0 -380
- package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
- package/template/apps/web/src/InstrumentListPage.tsx +0 -457
- package/template/apps/web/src/fields.ts +0 -18
- package/template/apps/web/src/instrument.ts +0 -56
- package/template/platform/data/colleges.ts +0 -21
- package/template/platform/data/instrument-surface.ts +0 -125
- package/template/platform/data/instruments.ts +0 -83
|
@@ -1,57 +1,73 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SCHEMA_VERSIONS,
|
|
3
|
+
type DataAuditPage,
|
|
3
4
|
type DataFilePreview,
|
|
4
5
|
type DataFileRef,
|
|
5
6
|
type DataFileUploadPlan,
|
|
6
|
-
type DataAuditPage,
|
|
7
7
|
type DataPage,
|
|
8
8
|
type DataQuery,
|
|
9
9
|
type DataRecord,
|
|
10
|
+
type DataResourceSurface,
|
|
10
11
|
type DirectoryEntryPage,
|
|
11
12
|
type DirectoryResolveRequest,
|
|
12
|
-
type NativeScopeValuePage,
|
|
13
|
-
type NativeScopeValueResolveRequest,
|
|
14
|
-
type DataResourceSurface,
|
|
15
13
|
} from 'openxiangda-contracts/browser';
|
|
16
|
-
import type { InstrumentListQuery, InstrumentRecord } from './instrument';
|
|
17
|
-
import { instrumentSurface, isInstrumentResourceField } from './fields';
|
|
18
14
|
import { applicationCode, runtimeMount } from './runtime-meta';
|
|
19
|
-
|
|
20
|
-
const resourceCode = 'instruments';
|
|
21
|
-
const nativeBase = () =>
|
|
22
|
-
`/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
|
|
23
|
-
const dataBase = (code = resourceCode) => `${nativeBase()}/data/${code}`;
|
|
15
|
+
import { useSyncExternalStore } from 'react';
|
|
24
16
|
|
|
25
17
|
interface PlatformEnvelope<T> {
|
|
26
18
|
code: number;
|
|
27
19
|
message?: string;
|
|
28
20
|
errorCode?: string;
|
|
21
|
+
requestId?: string;
|
|
29
22
|
data: T;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
let globalRequestCount = 0;
|
|
26
|
+
const globalRequestListeners = new Set<() => void>();
|
|
27
|
+
|
|
28
|
+
function beginGlobalRequest() {
|
|
29
|
+
globalRequestCount += 1;
|
|
30
|
+
globalRequestListeners.forEach(listener => listener());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function endGlobalRequest() {
|
|
34
|
+
globalRequestCount = Math.max(0, globalRequestCount - 1);
|
|
35
|
+
globalRequestListeners.forEach(listener => listener());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useGlobalRequestLoading() {
|
|
39
|
+
return useSyncExternalStore(
|
|
40
|
+
listener => {
|
|
41
|
+
globalRequestListeners.add(listener);
|
|
42
|
+
return () => globalRequestListeners.delete(listener);
|
|
40
43
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
() => globalRequestCount > 0,
|
|
45
|
+
() => false,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
50
|
+
beginGlobalRequest();
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(path, {
|
|
53
|
+
credentials: 'include',
|
|
54
|
+
...init,
|
|
55
|
+
headers: {
|
|
56
|
+
accept: 'application/json',
|
|
57
|
+
...(init?.body ? { 'content-type': 'application/json' } : {}),
|
|
58
|
+
...init?.headers,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const payload = (await response.json().catch(() => null)) as PlatformEnvelope<T> | T | null;
|
|
62
|
+
const envelope = payload && typeof payload === 'object' && 'code' in payload ? payload as PlatformEnvelope<T> : null;
|
|
63
|
+
if (!response.ok || (envelope && envelope.code !== 200)) {
|
|
64
|
+
const requestId = envelope?.requestId ? ` (requestId: ${envelope.requestId})` : '';
|
|
65
|
+
throw new Error(`${envelope?.errorCode || `HTTP_${response.status}`}: ${envelope?.message || '平台请求失败'}${requestId}`);
|
|
66
|
+
}
|
|
67
|
+
return envelope ? envelope.data : payload as T;
|
|
68
|
+
} finally {
|
|
69
|
+
endGlobalRequest();
|
|
53
70
|
}
|
|
54
|
-
return envelope ? envelope.data : (payload as T);
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
export interface RuntimeIdentity {
|
|
@@ -72,33 +88,14 @@ export interface RuntimeIdentity {
|
|
|
72
88
|
|
|
73
89
|
interface ConnectedCurrent {
|
|
74
90
|
environment: RuntimeIdentity['environment'];
|
|
75
|
-
principal:
|
|
76
|
-
userId: string;
|
|
77
|
-
roleCodes: string[];
|
|
78
|
-
capabilityCodes: string[];
|
|
79
|
-
isAppSuperAdmin: boolean;
|
|
80
|
-
};
|
|
91
|
+
principal: Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
interface DeployedCurrent {
|
|
84
95
|
schemaVersion: 'openxiangda.current-user/v2';
|
|
85
96
|
appCode: string;
|
|
86
|
-
environment:
|
|
87
|
-
|
|
88
|
-
key: 'preproduction' | 'production';
|
|
89
|
-
activeAppVersionId: string;
|
|
90
|
-
headRevision: number;
|
|
91
|
-
authzRevisionId: string;
|
|
92
|
-
authzVersion: number;
|
|
93
|
-
scopeDataVersion: string;
|
|
94
|
-
};
|
|
95
|
-
principal: {
|
|
96
|
-
type: 'user';
|
|
97
|
-
userId: string;
|
|
98
|
-
roleCodes: string[];
|
|
99
|
-
capabilityCodes: string[];
|
|
100
|
-
isAppSuperAdmin: boolean;
|
|
101
|
-
};
|
|
97
|
+
environment: RuntimeIdentity['environment'];
|
|
98
|
+
principal: { type: 'user' } & Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
let activeIdentity: RuntimeIdentity | undefined;
|
|
@@ -106,35 +103,16 @@ let activeIdentity: RuntimeIdentity | undefined;
|
|
|
106
103
|
export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
|
|
107
104
|
const mount = runtimeMount();
|
|
108
105
|
if (mount) {
|
|
109
|
-
const current = await request<DeployedCurrent>(
|
|
110
|
-
|
|
111
|
-
mount.environmentKey
|
|
112
|
-
)}`
|
|
113
|
-
);
|
|
114
|
-
if (
|
|
115
|
-
current.schemaVersion !== 'openxiangda.current-user/v2' ||
|
|
116
|
-
current.appCode !== mount.appCode ||
|
|
117
|
-
current.environment.key !== mount.environmentKey ||
|
|
118
|
-
current.principal.type !== 'user' ||
|
|
119
|
-
!Array.isArray(current.principal.roleCodes) ||
|
|
120
|
-
!Array.isArray(current.principal.capabilityCodes)
|
|
121
|
-
) {
|
|
106
|
+
const current = await request<DeployedCurrent>(`${nativeBase()}/current-user?environmentKey=${encodeURIComponent(mount.environmentKey)}`);
|
|
107
|
+
if (current.schemaVersion !== 'openxiangda.current-user/v2' || current.appCode !== mount.appCode || current.environment.key !== mount.environmentKey || current.principal.type !== 'user') {
|
|
122
108
|
throw new Error('OPENXIANGDA_DEPLOYED_CURRENT_USER_INVALID');
|
|
123
109
|
}
|
|
124
110
|
activeIdentity = { ...current.principal, environment: current.environment };
|
|
125
111
|
return activeIdentity;
|
|
126
112
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const current = await request<ConnectedCurrent>(connectedPath);
|
|
131
|
-
activeIdentity = { ...current.principal, environment: current.environment };
|
|
132
|
-
return activeIdentity;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`OPENXIANGDA_CONNECTED_CURRENT_USER_FAILED: ${error instanceof Error ? error.message : String(error)}`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
113
|
+
const current = await request<ConnectedCurrent>(`/service/openxiangda-api/v2/applications/${applicationCode()}/dev-sessions/current`);
|
|
114
|
+
activeIdentity = { ...current.principal, environment: current.environment };
|
|
115
|
+
return activeIdentity;
|
|
138
116
|
}
|
|
139
117
|
|
|
140
118
|
function currentEnvironmentKey() {
|
|
@@ -145,244 +123,59 @@ function currentEnvironmentKey() {
|
|
|
145
123
|
|
|
146
124
|
export type DirectoryKind = 'user' | 'department';
|
|
147
125
|
|
|
148
|
-
export async function searchDirectory(
|
|
149
|
-
|
|
150
|
-
options: { keyword: string; cursor?: string }
|
|
151
|
-
) {
|
|
152
|
-
const params = new URLSearchParams({
|
|
153
|
-
environmentKey: currentEnvironmentKey(),
|
|
154
|
-
keyword: options.keyword.trim(),
|
|
155
|
-
limit: '20',
|
|
156
|
-
});
|
|
126
|
+
export async function searchDirectory(kind: DirectoryKind, options: { keyword: string; cursor?: string }) {
|
|
127
|
+
const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), keyword: options.keyword.trim(), limit: '20' });
|
|
157
128
|
if (options.cursor) params.set('cursor', options.cursor);
|
|
158
|
-
return request<DirectoryEntryPage>(
|
|
159
|
-
`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`
|
|
160
|
-
);
|
|
129
|
+
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`);
|
|
161
130
|
}
|
|
162
131
|
|
|
163
|
-
export async function browseDepartmentTree(options: {
|
|
164
|
-
|
|
165
|
-
cursor?: string;
|
|
166
|
-
}) {
|
|
167
|
-
const params = new URLSearchParams({
|
|
168
|
-
environmentKey: currentEnvironmentKey(),
|
|
169
|
-
limit: '100',
|
|
170
|
-
});
|
|
132
|
+
export async function browseDepartmentTree(options: { parentId?: string; cursor?: string }) {
|
|
133
|
+
const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), limit: '100' });
|
|
171
134
|
if (options.parentId) params.set('parentId', options.parentId);
|
|
172
135
|
if (options.cursor) params.set('offset', options.cursor);
|
|
173
|
-
return request<DirectoryEntryPage>(
|
|
174
|
-
`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`
|
|
175
|
-
);
|
|
136
|
+
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`);
|
|
176
137
|
}
|
|
177
138
|
|
|
178
|
-
export async function browseDepartmentUsers(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
) {
|
|
182
|
-
const params = new URLSearchParams({
|
|
183
|
-
environmentKey: currentEnvironmentKey(),
|
|
184
|
-
page: String(page),
|
|
185
|
-
limit: '20',
|
|
186
|
-
});
|
|
187
|
-
return request<DirectoryEntryPage>(
|
|
188
|
-
`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(
|
|
189
|
-
departmentId
|
|
190
|
-
)}/users?${params}`
|
|
191
|
-
);
|
|
139
|
+
export async function browseDepartmentUsers(departmentId: string, page = 1) {
|
|
140
|
+
const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), page: String(page), limit: '20' });
|
|
141
|
+
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(departmentId)}/users?${params}`);
|
|
192
142
|
}
|
|
193
143
|
|
|
194
|
-
export async function resolveDirectory(
|
|
195
|
-
kind: DirectoryKind,
|
|
196
|
-
ids: string[]
|
|
197
|
-
) {
|
|
144
|
+
export async function resolveDirectory(kind: DirectoryKind, ids: string[]) {
|
|
198
145
|
const uniqueIds = [...new Set(ids)];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
};
|
|
204
|
-
if (body.ids.length === 0) {
|
|
205
|
-
throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
|
|
206
|
-
}
|
|
207
|
-
if (body.ids.length > 50) {
|
|
208
|
-
throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
|
|
209
|
-
}
|
|
210
|
-
return request<DirectoryEntryPage>(
|
|
211
|
-
`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(
|
|
212
|
-
currentEnvironmentKey()
|
|
213
|
-
)}`,
|
|
214
|
-
{ method: 'POST', body: JSON.stringify(body) }
|
|
215
|
-
);
|
|
146
|
+
if (uniqueIds.length === 0) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
|
|
147
|
+
if (uniqueIds.length > 50) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
|
|
148
|
+
const body: DirectoryResolveRequest = { schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest, kind, ids: uniqueIds };
|
|
149
|
+
return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`, { method: 'POST', body: JSON.stringify(body) });
|
|
216
150
|
}
|
|
217
151
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (options.cursor) params.set('cursor', options.cursor);
|
|
233
|
-
return request<NativeScopeValuePage>(`${scopeValuesBase()}?${params}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export async function resolveCollegeScope(
|
|
237
|
-
operation: 'create' | 'update',
|
|
238
|
-
values: string[]
|
|
239
|
-
) {
|
|
240
|
-
const uniqueValues = [...new Set(values)];
|
|
241
|
-
const body: NativeScopeValueResolveRequest = {
|
|
242
|
-
schemaVersion: SCHEMA_VERSIONS.nativeScopeValueResolveRequest,
|
|
243
|
-
dimensionCode: 'college',
|
|
244
|
-
operation,
|
|
245
|
-
values: uniqueValues,
|
|
152
|
+
export async function searchResource(resourceCode: string, labelField: string, options: { keyword: string; cursor?: string }) {
|
|
153
|
+
const pageSize = 20;
|
|
154
|
+
const offset = options.cursor ? Number(options.cursor) : 0;
|
|
155
|
+
const query: DataQuery = {
|
|
156
|
+
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
157
|
+
filters: options.keyword.trim() ? [{ field: labelField, operator: 'ilike', value: `%${options.keyword.trim()}%` }] : [],
|
|
158
|
+
order: [{ field: labelField, direction: 'asc' }],
|
|
159
|
+
limit: pageSize,
|
|
160
|
+
offset: Number.isSafeInteger(offset) && offset >= 0 ? offset : 0,
|
|
161
|
+
};
|
|
162
|
+
const page = await request<DataPage<Record<string, unknown>>>(`${dataBase(resourceCode)}/query`, { method: 'POST', body: JSON.stringify({ ...query, environmentKey: currentEnvironmentKey() }) });
|
|
163
|
+
return {
|
|
164
|
+
items: page.items.map(item => ({ id: String(item.id), label: String(item[labelField] ?? item.id), selectable: true })),
|
|
165
|
+
nextCursor: page.items.length >= pageSize ? String(page.offset + page.items.length) : null,
|
|
246
166
|
};
|
|
247
|
-
if (body.values.length === 0) {
|
|
248
|
-
throw new Error('OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED');
|
|
249
|
-
}
|
|
250
|
-
if (body.values.length > 50) {
|
|
251
|
-
throw new Error('OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED');
|
|
252
|
-
}
|
|
253
|
-
return request<NativeScopeValuePage>(
|
|
254
|
-
`${scopeValuesBase()}/resolve?environmentKey=${encodeURIComponent(
|
|
255
|
-
currentEnvironmentKey()
|
|
256
|
-
)}`,
|
|
257
|
-
{ method: 'POST', body: JSON.stringify(body) }
|
|
258
|
-
);
|
|
259
167
|
}
|
|
260
168
|
|
|
261
|
-
export
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
169
|
+
export async function resolveResource(resourceCode: string, ids: string[], labelField: string) {
|
|
170
|
+
if (ids.length === 0) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_REQUIRED');
|
|
171
|
+
if (ids.length > 50) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_EXCEEDED');
|
|
172
|
+
const items = await Promise.all(ids.map(async id => {
|
|
173
|
+
const record = await request<DataRecord<Record<string, unknown>>>(`${dataBase(resourceCode)}/records/${encodeURIComponent(id)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
|
|
174
|
+
return { id, label: String(record.data[labelField] ?? id), selectable: true };
|
|
175
|
+
}));
|
|
176
|
+
return { items, nextCursor: null };
|
|
269
177
|
}
|
|
270
178
|
|
|
271
|
-
export const instrumentDataApi: InstrumentDataApiAdapter = {
|
|
272
|
-
async list(query) {
|
|
273
|
-
const filters: NonNullable<DataQuery['filters']> = [];
|
|
274
|
-
// The current DataQuery contract has AND filters but no grouped OR. Keep
|
|
275
|
-
// this honest single-field search until the platform adds a native OR AST.
|
|
276
|
-
if (query.keyword?.trim()) {
|
|
277
|
-
const searchableFields = instrumentSurface.list?.searchableFields || [];
|
|
278
|
-
const field = searchableFields.includes(query.searchField || '')
|
|
279
|
-
? query.searchField!
|
|
280
|
-
: searchableFields[0] || 'chineseName';
|
|
281
|
-
filters.push({ field, operator: 'ilike', value: `%${query.keyword.trim()}%` });
|
|
282
|
-
}
|
|
283
|
-
if (query.collegeId) filters.push({ field: 'collegeId', operator: 'eq', value: query.collegeId });
|
|
284
|
-
if (query.instrumentAdminId) filters.push({ field: 'instrumentAdminIds', operator: 'cs', value: [query.instrumentAdminId] });
|
|
285
|
-
if (query.instrumentStatus) filters.push({ field: 'instrumentStatus', operator: 'eq', value: query.instrumentStatus });
|
|
286
|
-
if (query.openToOutside !== undefined) filters.push({ field: 'openToOutside', operator: 'eq', value: query.openToOutside });
|
|
287
|
-
if (query.enabledDate) {
|
|
288
|
-
filters.push({ field: 'enabledDate', operator: 'gte', value: query.enabledDate[0] });
|
|
289
|
-
filters.push({ field: 'enabledDate', operator: 'lte', value: query.enabledDate[1] });
|
|
290
|
-
}
|
|
291
|
-
if (query.assetValue?.[0] !== undefined) filters.push({ field: 'assetValue', operator: 'gte', value: query.assetValue[0] });
|
|
292
|
-
if (query.assetValue?.[1] !== undefined) filters.push({ field: 'assetValue', operator: 'lte', value: query.assetValue[1] });
|
|
293
|
-
const body: DataQuery = {
|
|
294
|
-
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
295
|
-
filters,
|
|
296
|
-
order: [{ field: String(query.sort?.field || 'instrumentCode'), direction: query.sort?.order || 'asc' }],
|
|
297
|
-
limit: query.pageSize,
|
|
298
|
-
offset: (query.page - 1) * query.pageSize,
|
|
299
|
-
};
|
|
300
|
-
assertInstrumentQueryFields(body);
|
|
301
|
-
const page = await request<DataPage<InstrumentRecord>>(`${dataBase()}/query`, {
|
|
302
|
-
method: 'POST',
|
|
303
|
-
body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }),
|
|
304
|
-
});
|
|
305
|
-
return { rows: page.items, total: page.total };
|
|
306
|
-
},
|
|
307
|
-
async get(id) {
|
|
308
|
-
const record = await request<DataRecord<InstrumentRecord>>(
|
|
309
|
-
`${dataBase()}/records/${encodeURIComponent(
|
|
310
|
-
id
|
|
311
|
-
)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
312
|
-
);
|
|
313
|
-
return record.data;
|
|
314
|
-
},
|
|
315
|
-
async audit(id) {
|
|
316
|
-
return await request<DataAuditPage>(
|
|
317
|
-
`${dataBase()}/records/${encodeURIComponent(
|
|
318
|
-
id
|
|
319
|
-
)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(
|
|
320
|
-
currentEnvironmentKey()
|
|
321
|
-
)}`
|
|
322
|
-
);
|
|
323
|
-
},
|
|
324
|
-
async create(data) {
|
|
325
|
-
const record = await request<DataRecord<InstrumentRecord>>(`${dataBase()}/records`, {
|
|
326
|
-
method: 'POST',
|
|
327
|
-
body: JSON.stringify({ environmentKey: currentEnvironmentKey(), data }),
|
|
328
|
-
});
|
|
329
|
-
return record.data;
|
|
330
|
-
},
|
|
331
|
-
async update(id, expectedRevision, data) {
|
|
332
|
-
const record = await request<DataRecord<InstrumentRecord>>(
|
|
333
|
-
`${dataBase()}/records/${encodeURIComponent(id)}/update`,
|
|
334
|
-
{
|
|
335
|
-
method: 'POST',
|
|
336
|
-
body: JSON.stringify({
|
|
337
|
-
environmentKey: currentEnvironmentKey(),
|
|
338
|
-
expectedRevision,
|
|
339
|
-
data,
|
|
340
|
-
}),
|
|
341
|
-
}
|
|
342
|
-
);
|
|
343
|
-
return record.data;
|
|
344
|
-
},
|
|
345
|
-
async remove(id, expectedRevision) {
|
|
346
|
-
const record = await request<DataRecord<InstrumentRecord>>(
|
|
347
|
-
`${dataBase()}/records/${encodeURIComponent(id)}/delete`,
|
|
348
|
-
{
|
|
349
|
-
method: 'POST',
|
|
350
|
-
body: JSON.stringify({
|
|
351
|
-
environmentKey: currentEnvironmentKey(),
|
|
352
|
-
expectedRevision,
|
|
353
|
-
}),
|
|
354
|
-
}
|
|
355
|
-
);
|
|
356
|
-
return record.data;
|
|
357
|
-
},
|
|
358
|
-
async upload(fieldCode, file, recordId) {
|
|
359
|
-
const plan = await request<DataFileUploadPlan>(`${dataBase()}/files/uploads/initiate`, {
|
|
360
|
-
method: 'POST',
|
|
361
|
-
body: JSON.stringify({
|
|
362
|
-
environmentKey: currentEnvironmentKey(),
|
|
363
|
-
fieldCode,
|
|
364
|
-
fileName: file.name,
|
|
365
|
-
fileSize: file.size,
|
|
366
|
-
contentType: file.type || 'application/octet-stream',
|
|
367
|
-
...(recordId ? { recordId } : {}),
|
|
368
|
-
}),
|
|
369
|
-
});
|
|
370
|
-
const uploaded = await fetch(plan.uploadUrl, {
|
|
371
|
-
method: plan.uploadMethod,
|
|
372
|
-
headers: plan.headers,
|
|
373
|
-
body: file,
|
|
374
|
-
});
|
|
375
|
-
if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
|
|
376
|
-
return await request<DataFileRef>(
|
|
377
|
-
`${dataBase()}/files/${encodeURIComponent(plan.file.id)}/complete`,
|
|
378
|
-
{
|
|
379
|
-
method: 'POST',
|
|
380
|
-
body: JSON.stringify({ environmentKey: currentEnvironmentKey() }),
|
|
381
|
-
}
|
|
382
|
-
);
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
|
|
386
179
|
export interface GenericResourceQuery {
|
|
387
180
|
page: number;
|
|
388
181
|
pageSize: number;
|
|
@@ -392,17 +185,11 @@ export interface GenericResourceQuery {
|
|
|
392
185
|
sort?: { field: string; order: 'asc' | 'desc' };
|
|
393
186
|
}
|
|
394
187
|
|
|
395
|
-
|
|
396
|
-
export function createNativeResourceClient(
|
|
397
|
-
code: string,
|
|
398
|
-
surface: DataResourceSurface
|
|
399
|
-
) {
|
|
188
|
+
export function createNativeResourceClient(code: string, surface: DataResourceSurface) {
|
|
400
189
|
const base = dataBase(code);
|
|
401
190
|
const declaredFields = new Set(Object.keys(surface.fields));
|
|
402
191
|
const assertField = (field: string) => {
|
|
403
|
-
if (!declaredFields.has(field)) {
|
|
404
|
-
throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
|
|
405
|
-
}
|
|
192
|
+
if (!declaredFields.has(field)) throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
|
|
406
193
|
return field;
|
|
407
194
|
};
|
|
408
195
|
const filtersFor = (query: GenericResourceQuery) => {
|
|
@@ -418,7 +205,7 @@ export function createNativeResourceClient(
|
|
|
418
205
|
if (Array.isArray(value) && value.length === 2 && ['date', 'datetime', 'number', 'money', 'percent'].includes(field?.widget || '')) {
|
|
419
206
|
if (value[0] !== undefined && value[0] !== null && value[0] !== '') filters.push({ field: fieldCode, operator: 'gte', value: value[0] });
|
|
420
207
|
if (value[1] !== undefined && value[1] !== null && value[1] !== '') filters.push({ field: fieldCode, operator: 'lte', value: value[1] });
|
|
421
|
-
} else if (Array.isArray(value) && ['multi', 'directory-user', 'directory-department'].includes(field?.widget || '')) {
|
|
208
|
+
} else if (Array.isArray(value) && ['multi', 'directory-user', 'directory-department', 'resource'].includes(field?.widget || '')) {
|
|
422
209
|
filters.push({ field: fieldCode, operator: 'cs', value });
|
|
423
210
|
} else {
|
|
424
211
|
filters.push({ field: fieldCode, operator: 'eq', value });
|
|
@@ -429,17 +216,8 @@ export function createNativeResourceClient(
|
|
|
429
216
|
return {
|
|
430
217
|
async list(query: GenericResourceQuery) {
|
|
431
218
|
const sort = query.sort || surface.list?.defaultSort || { field: Object.keys(surface.fields)[0] || 'id', order: 'asc' as const };
|
|
432
|
-
const body: DataQuery = {
|
|
433
|
-
|
|
434
|
-
filters: filtersFor(query),
|
|
435
|
-
order: [{ field: assertField(sort.field), direction: sort.order || 'asc' }],
|
|
436
|
-
limit: query.pageSize,
|
|
437
|
-
offset: (query.page - 1) * query.pageSize,
|
|
438
|
-
};
|
|
439
|
-
const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, {
|
|
440
|
-
method: 'POST',
|
|
441
|
-
body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }),
|
|
442
|
-
});
|
|
219
|
+
const body: DataQuery = { schemaVersion: SCHEMA_VERSIONS.dataQuery, filters: filtersFor(query), order: [{ field: assertField(sort.field), direction: sort.order || 'asc' }], limit: query.pageSize, offset: (query.page - 1) * query.pageSize };
|
|
220
|
+
const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, { method: 'POST', body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }) });
|
|
443
221
|
return { rows: page.items, total: page.total };
|
|
444
222
|
},
|
|
445
223
|
async get(id: string) {
|
|
@@ -447,7 +225,7 @@ export function createNativeResourceClient(
|
|
|
447
225
|
return record.data;
|
|
448
226
|
},
|
|
449
227
|
async audit(id: string) {
|
|
450
|
-
return
|
|
228
|
+
return request<DataAuditPage>(`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
|
|
451
229
|
},
|
|
452
230
|
async create(data: Record<string, unknown>) {
|
|
453
231
|
const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey(), data }) });
|
|
@@ -466,120 +244,30 @@ export function createNativeResourceClient(
|
|
|
466
244
|
const plan = await request<DataFileUploadPlan>(`${base}/files/uploads/initiate`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey(), fieldCode, fileName: file.name, fileSize: file.size, contentType: file.type || 'application/octet-stream', ...(recordId ? { recordId } : {}) }) });
|
|
467
245
|
const uploaded = await fetch(plan.uploadUrl, { method: plan.uploadMethod, headers: plan.headers, body: file });
|
|
468
246
|
if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
|
|
469
|
-
return
|
|
247
|
+
return request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey() }) });
|
|
470
248
|
},
|
|
471
249
|
};
|
|
472
250
|
}
|
|
473
251
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
fileId: string,
|
|
477
|
-
disposition: 'attachment' | 'inline' = 'attachment'
|
|
478
|
-
) {
|
|
479
|
-
const query = new URLSearchParams({
|
|
480
|
-
environmentKey: currentEnvironmentKey(),
|
|
481
|
-
disposition,
|
|
482
|
-
});
|
|
483
|
-
return `${dataBase(resource)}/files/${encodeURIComponent(
|
|
484
|
-
fileId
|
|
485
|
-
)}/content?${query.toString()}`;
|
|
252
|
+
function nativeBase() {
|
|
253
|
+
return `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
|
|
486
254
|
}
|
|
487
255
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
fileId: string
|
|
491
|
-
) {
|
|
492
|
-
return await request<DataFilePreview>(
|
|
493
|
-
`${dataBase(resource)}/files/${encodeURIComponent(
|
|
494
|
-
fileId
|
|
495
|
-
)}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
|
|
496
|
-
);
|
|
256
|
+
function dataBase(code: string) {
|
|
257
|
+
return `${nativeBase()}/data/${encodeURIComponent(code)}`;
|
|
497
258
|
}
|
|
498
259
|
|
|
499
|
-
export
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
headers: { accept: 'application/octet-stream,*/*' },
|
|
503
|
-
});
|
|
504
|
-
if (!response.ok) {
|
|
505
|
-
const payload = (await response.json().catch(() => null)) as
|
|
506
|
-
| PlatformEnvelope<null>
|
|
507
|
-
| null;
|
|
508
|
-
const code = payload?.errorCode || `HTTP_${response.status}`;
|
|
509
|
-
throw new Error(`${code}: ${payload?.message || '文件内容读取失败'}`);
|
|
510
|
-
}
|
|
511
|
-
return await response.blob();
|
|
260
|
+
export function dataFileContentUrl(resource: string, fileId: string, disposition: 'attachment' | 'inline' = 'attachment') {
|
|
261
|
+
const query = new URLSearchParams({ environmentKey: currentEnvironmentKey(), disposition });
|
|
262
|
+
return `${dataBase(resource)}/files/${encodeURIComponent(fileId)}/content?${query}`;
|
|
512
263
|
}
|
|
513
264
|
|
|
514
|
-
export
|
|
515
|
-
|
|
516
|
-
revision: number;
|
|
517
|
-
name: string;
|
|
518
|
-
enabled: boolean;
|
|
265
|
+
export async function loadDataFilePreview(resource: string, fileId: string) {
|
|
266
|
+
return request<DataFilePreview>(`${dataBase(resource)}/files/${encodeURIComponent(fileId)}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
|
|
519
267
|
}
|
|
520
268
|
|
|
521
|
-
export
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
order: [{ field: 'name', direction: 'asc' }],
|
|
526
|
-
limit: pageSize,
|
|
527
|
-
offset: (page - 1) * pageSize,
|
|
528
|
-
};
|
|
529
|
-
const result = await request<DataPage<CollegeRecord>>(
|
|
530
|
-
`${dataBase('colleges')}/query`,
|
|
531
|
-
{
|
|
532
|
-
method: 'POST',
|
|
533
|
-
body: JSON.stringify({
|
|
534
|
-
...body,
|
|
535
|
-
environmentKey: currentEnvironmentKey(),
|
|
536
|
-
}),
|
|
537
|
-
}
|
|
538
|
-
);
|
|
539
|
-
return { rows: result.items, total: result.total };
|
|
540
|
-
},
|
|
541
|
-
async create(data: Pick<CollegeRecord, 'name' | 'enabled'>) {
|
|
542
|
-
const record = await request<DataRecord<CollegeRecord>>(
|
|
543
|
-
`${dataBase('colleges')}/records`,
|
|
544
|
-
{
|
|
545
|
-
method: 'POST',
|
|
546
|
-
body: JSON.stringify({
|
|
547
|
-
environmentKey: currentEnvironmentKey(),
|
|
548
|
-
data,
|
|
549
|
-
}),
|
|
550
|
-
}
|
|
551
|
-
);
|
|
552
|
-
return record.data;
|
|
553
|
-
},
|
|
554
|
-
async update(
|
|
555
|
-
id: string,
|
|
556
|
-
expectedRevision: number,
|
|
557
|
-
data: Pick<CollegeRecord, 'name' | 'enabled'>
|
|
558
|
-
) {
|
|
559
|
-
const record = await request<DataRecord<CollegeRecord>>(
|
|
560
|
-
`${dataBase('colleges')}/records/${encodeURIComponent(id)}/update`,
|
|
561
|
-
{
|
|
562
|
-
method: 'POST',
|
|
563
|
-
body: JSON.stringify({
|
|
564
|
-
environmentKey: currentEnvironmentKey(),
|
|
565
|
-
expectedRevision,
|
|
566
|
-
data,
|
|
567
|
-
}),
|
|
568
|
-
}
|
|
569
|
-
);
|
|
570
|
-
return record.data;
|
|
571
|
-
},
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
function assertInstrumentQueryFields(query: DataQuery) {
|
|
575
|
-
for (const field of [
|
|
576
|
-
...(query.filters || []).map(filter => filter.field),
|
|
577
|
-
...(query.order || []).map(order => order.field),
|
|
578
|
-
]) {
|
|
579
|
-
if (!isInstrumentResourceField(field)) {
|
|
580
|
-
throw new Error(
|
|
581
|
-
`OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:${field}`
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
269
|
+
export async function fetchDataFileBlob(resource: string, fileId: string) {
|
|
270
|
+
const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), { credentials: 'include', headers: { accept: 'application/octet-stream,*/*' } });
|
|
271
|
+
if (!response.ok) throw new Error(`FILE_CONTENT_READ_FAILED: HTTP_${response.status}`);
|
|
272
|
+
return response.blob();
|
|
585
273
|
}
|
|
@@ -31,7 +31,7 @@ export function runtimeMount() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function applicationCode() {
|
|
34
|
-
return runtimeMount()?.appCode || '
|
|
34
|
+
return runtimeMount()?.appCode || 'openxiangda-application';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function resolveApplicationBasename(mount: RuntimeMount | null) {
|