@nocobase/flow-engine 2.1.11 → 2.2.0-alpha.2
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/lib/JSRunner.d.ts +1 -0
- package/lib/JSRunner.js +110 -20
- package/lib/components/FlowContextSelector.js +24 -3
- package/lib/components/variables/VariableHybridInput.d.ts +8 -0
- package/lib/components/variables/VariableHybridInput.js +128 -12
- package/lib/components/variables/types.d.ts +8 -0
- package/lib/flowContext.d.ts +1 -1
- package/lib/flowContext.js +33 -7
- package/lib/flowI18n.js +3 -3
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/resources/apiResource.js +2 -1
- package/lib/resources/baseRecordResource.js +6 -17
- package/lib/resources/multiRecordResource.js +13 -3
- package/lib/resources/singleRecordResource.js +7 -2
- package/lib/runjs-context/helpers.js +12 -5
- package/lib/utils/dataSourceDirty.d.ts +20 -0
- package/lib/utils/dataSourceDirty.js +139 -0
- package/lib/utils/dirtyAwareApiClient.d.ts +11 -0
- package/lib/utils/dirtyAwareApiClient.js +378 -0
- package/lib/utils/index.d.ts +1 -1
- package/lib/utils/index.js +11 -11
- package/lib/utils/openViewRouteState.d.ts +28 -0
- package/lib/utils/openViewRouteState.js +125 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +3 -0
- package/lib/utils/parsePathnameToViewParams.js +18 -1
- package/lib/utils/resolveRunJSObjectValues.js +3 -2
- package/lib/utils/runjsModuleLoader.js +0 -30
- package/lib/views/ViewNavigation.js +5 -0
- package/package.json +4 -4
- package/src/JSRunner.ts +112 -25
- package/src/__tests__/JSRunner.test.ts +4 -5
- package/src/__tests__/flowContext.test.ts +88 -0
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +51 -0
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +15 -2
- package/src/components/FlowContextSelector.tsx +33 -2
- package/src/components/variables/VariableHybridInput.tsx +166 -9
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +178 -0
- package/src/components/variables/types.ts +8 -0
- package/src/flowContext.ts +43 -8
- package/src/flowI18n.ts +8 -3
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/resources/apiResource.ts +2 -1
- package/src/resources/baseRecordResource.ts +6 -23
- package/src/resources/multiRecordResource.ts +13 -3
- package/src/resources/singleRecordResource.ts +6 -1
- package/src/runjs-context/helpers.ts +12 -6
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +392 -0
- package/src/utils/__tests__/openViewRouteState.test.ts +40 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +36 -0
- package/src/utils/dataSourceDirty.ts +126 -0
- package/src/utils/dirtyAwareApiClient.ts +430 -0
- package/src/utils/index.ts +10 -9
- package/src/utils/openViewRouteState.ts +107 -0
- package/src/utils/parsePathnameToViewParams.ts +23 -1
- package/src/utils/resolveRunJSObjectValues.ts +5 -2
- package/src/utils/runjsModuleLoader.ts +0 -32
- package/src/views/ViewNavigation.ts +6 -1
- package/src/views/__tests__/ViewNavigation.test.ts +15 -0
- package/lib/utils/safeGlobals.d.ts +0 -28
- package/lib/utils/safeGlobals.js +0 -367
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +0 -38
- package/src/utils/__tests__/safeGlobals.test.ts +0 -106
- package/src/utils/safeGlobals.ts +0 -406
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FlowEngine } from '../flowEngine';
|
|
11
|
+
import { DATA_SOURCE_DIRTY_EVENT } from '../views/viewEvents';
|
|
12
|
+
|
|
13
|
+
type MarkDataSourceDirtyOptions = {
|
|
14
|
+
engine?: FlowEngine;
|
|
15
|
+
dataSourceKey?: unknown;
|
|
16
|
+
resourceName?: unknown;
|
|
17
|
+
includePreviousEngines?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getHeaderValue(headers: unknown, name: string): unknown {
|
|
21
|
+
if (!headers || typeof headers !== 'object') {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const maybeHeaders = headers as { get?: (key: string) => unknown };
|
|
26
|
+
if (typeof maybeHeaders.get === 'function') {
|
|
27
|
+
const value = maybeHeaders.get(name);
|
|
28
|
+
if (value != null && value !== '') {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const lowerName = name.toLowerCase();
|
|
34
|
+
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
35
|
+
if (key.toLowerCase() === lowerName) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getDataSourceKeyFromHeaders(headers: unknown): string {
|
|
44
|
+
const value = getHeaderValue(headers, 'x-data-source');
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return String(value[0] || 'main');
|
|
47
|
+
}
|
|
48
|
+
return value == null || value === '' ? 'main' : String(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getAffectedResourceNames(resourceName: unknown): string[] {
|
|
52
|
+
const name = String(resourceName || '').trim();
|
|
53
|
+
if (!name) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const names = new Set<string>([name]);
|
|
58
|
+
if (name.includes('.')) {
|
|
59
|
+
names.add(name.split('.')[0]);
|
|
60
|
+
}
|
|
61
|
+
return Array.from(names);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getDirtyTargetEngines(engine: FlowEngine, includePreviousEngines?: boolean): FlowEngine[] {
|
|
65
|
+
if (!includePreviousEngines) {
|
|
66
|
+
return [engine];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const engines: FlowEngine[] = [];
|
|
70
|
+
const seen = new Set<FlowEngine>();
|
|
71
|
+
let current: FlowEngine | undefined = engine;
|
|
72
|
+
let guard = 0;
|
|
73
|
+
|
|
74
|
+
while (current && guard++ < 50) {
|
|
75
|
+
if (!seen.has(current)) {
|
|
76
|
+
engines.push(current);
|
|
77
|
+
seen.add(current);
|
|
78
|
+
}
|
|
79
|
+
current = current.previousEngine;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return engines;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function markDataSourceDirty(options: MarkDataSourceDirtyOptions): string[] {
|
|
86
|
+
const { engine, resourceName, includePreviousEngines } = options;
|
|
87
|
+
if (!engine?.markDataSourceDirty) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resourceNames = getAffectedResourceNames(resourceName);
|
|
92
|
+
if (!resourceNames.length) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const dataSourceKey = String(options.dataSourceKey || 'main');
|
|
97
|
+
const targetEngines = getDirtyTargetEngines(engine, includePreviousEngines);
|
|
98
|
+
const beforeVersions = new Map<FlowEngine, Map<string, number>>();
|
|
99
|
+
|
|
100
|
+
for (const targetEngine of targetEngines) {
|
|
101
|
+
const versions = new Map<string, number>();
|
|
102
|
+
beforeVersions.set(targetEngine, versions);
|
|
103
|
+
for (const name of resourceNames) {
|
|
104
|
+
versions.set(name, targetEngine.getDataSourceDirtyVersion?.(dataSourceKey, name) || 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const targetEngine of targetEngines) {
|
|
109
|
+
const versions = beforeVersions.get(targetEngine);
|
|
110
|
+
for (const name of resourceNames) {
|
|
111
|
+
const before = versions?.get(name) || 0;
|
|
112
|
+
const current = targetEngine.getDataSourceDirtyVersion?.(dataSourceKey, name) || 0;
|
|
113
|
+
if (current !== before) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
targetEngine.markDataSourceDirty(dataSourceKey, name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
engine.emitter?.emit?.(DATA_SOURCE_DIRTY_EVENT, {
|
|
121
|
+
dataSourceKey,
|
|
122
|
+
resourceNames,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return resourceNames;
|
|
126
|
+
}
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ActionParams, APIClient, IResource, RequestOptions } from '@nocobase/sdk';
|
|
11
|
+
import type { FlowContext } from '../flowContext';
|
|
12
|
+
import { getDataSourceKeyFromHeaders, markDataSourceDirty } from './dataSourceDirty';
|
|
13
|
+
|
|
14
|
+
type ResourceActionFn = (params?: ActionParams, opts?: unknown) => Promise<unknown>;
|
|
15
|
+
|
|
16
|
+
export const SKIP_DATA_SOURCE_DIRTY = '__nocobaseSkipDataSourceDirty';
|
|
17
|
+
|
|
18
|
+
type ResourceRequestOptions = RequestOptions & {
|
|
19
|
+
resource?: unknown;
|
|
20
|
+
resourceOf?: unknown;
|
|
21
|
+
action?: unknown;
|
|
22
|
+
headers?: unknown;
|
|
23
|
+
[SKIP_DATA_SOURCE_DIRTY]?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type DirtyResourceAction = {
|
|
27
|
+
dataSourceKey?: string;
|
|
28
|
+
resourceName: string;
|
|
29
|
+
actionName: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ApiUrlProvider = {
|
|
33
|
+
getApiUrl?: (pathname?: string) => string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type DirtyAwareAPIClient = APIClient & {
|
|
37
|
+
resource: APIClient['resource'];
|
|
38
|
+
request: APIClient['request'];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type APIClientRequestConfig = Parameters<APIClient['request']>[0];
|
|
42
|
+
|
|
43
|
+
const dirtyAwareApiClientCache = new WeakMap<object, WeakMap<object, APIClient>>();
|
|
44
|
+
const dirtyAwareApiClientProxies = new WeakSet<object>();
|
|
45
|
+
|
|
46
|
+
const MUTATING_RESOURCE_ACTIONS = [
|
|
47
|
+
'add',
|
|
48
|
+
'bulkdestroy',
|
|
49
|
+
'bulkupdate',
|
|
50
|
+
'create',
|
|
51
|
+
'delete',
|
|
52
|
+
'destroy',
|
|
53
|
+
'execute',
|
|
54
|
+
'firstorcreate',
|
|
55
|
+
'import',
|
|
56
|
+
'move',
|
|
57
|
+
'remove',
|
|
58
|
+
'save',
|
|
59
|
+
'saveastemplate',
|
|
60
|
+
'set',
|
|
61
|
+
'setfields',
|
|
62
|
+
'submit',
|
|
63
|
+
'update',
|
|
64
|
+
'updateorcreate',
|
|
65
|
+
'upsert',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function isApiClientLike(value: unknown): value is DirtyAwareAPIClient {
|
|
69
|
+
if (!value || typeof value !== 'object') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const candidate = value as { resource?: unknown; request?: unknown };
|
|
73
|
+
return typeof candidate.resource === 'function' && typeof candidate.request === 'function';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isMutatingResourceAction(actionName: string): boolean {
|
|
77
|
+
const normalized = String(actionName || '').trim();
|
|
78
|
+
if (!normalized) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const baseActionName = normalized.split('/')[0];
|
|
82
|
+
const lowerBaseActionName = baseActionName.toLowerCase();
|
|
83
|
+
return MUTATING_RESOURCE_ACTIONS.some((action) => {
|
|
84
|
+
if (lowerBaseActionName === action) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (!lowerBaseActionName.startsWith(action) || baseActionName.length <= action.length) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nextChar = baseActionName[action.length];
|
|
92
|
+
return nextChar === '-' || nextChar === '_' || (nextChar >= 'A' && nextChar <= 'Z');
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getCurrentOrigin(): string | undefined {
|
|
97
|
+
return typeof window === 'undefined' ? undefined : window.location?.origin;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseUrl(value: string, base?: string): URL | undefined {
|
|
101
|
+
try {
|
|
102
|
+
return new URL(value, base);
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stripSearchAndHash(path: string): string {
|
|
109
|
+
const index = path.search(/[?#]/);
|
|
110
|
+
return index === -1 ? path : path.slice(0, index);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stripKnownApiPrefix(path: string): string | undefined {
|
|
114
|
+
const cleanPath = stripSearchAndHash(path).trim();
|
|
115
|
+
if (!cleanPath) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedPath = cleanPath.replace(/^\/+/, '');
|
|
120
|
+
if (!normalizedPath || normalizedPath === 'api') {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if (normalizedPath.startsWith('api/')) {
|
|
124
|
+
return normalizedPath.slice('api/'.length);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return normalizedPath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizePathname(pathname: string) {
|
|
131
|
+
return pathname.endsWith('/') ? pathname : `${pathname}/`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getAppApiUrl(app?: ApiUrlProvider): URL | undefined {
|
|
135
|
+
if (!app?.getApiUrl) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
return parseUrl(app.getApiUrl(), getCurrentOrigin());
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stripConfiguredApiPrefix(path: string, apiPathname: string): string | undefined {
|
|
147
|
+
const cleanPath = stripSearchAndHash(path).trim();
|
|
148
|
+
if (!cleanPath.startsWith('/')) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const apiPath = normalizePathname(apiPathname);
|
|
153
|
+
const requestPath = normalizePathname(cleanPath);
|
|
154
|
+
if (!requestPath.startsWith(apiPath)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const apiPathWithoutTrailingSlash = apiPath.replace(/\/$/, '');
|
|
159
|
+
return cleanPath.slice(apiPathWithoutTrailingSlash.length).replace(/^\/+/, '') || undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getDirtyResourcePathFromAbsoluteUrl(url: URL, app?: ApiUrlProvider): string | undefined {
|
|
163
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (app?.getApiUrl) {
|
|
168
|
+
const apiUrl = getAppApiUrl(app);
|
|
169
|
+
if (!apiUrl || url.origin !== apiUrl.origin) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return stripConfiguredApiPrefix(url.pathname, apiUrl.pathname);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const currentOrigin = getCurrentOrigin();
|
|
177
|
+
if (!currentOrigin || url.origin !== currentOrigin) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return stripKnownApiPrefix(url.pathname);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getDirtyResourcePathFromUrl(url: unknown, context: FlowContext): string | undefined {
|
|
185
|
+
if (typeof url !== 'string') {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const trimmedUrl = url.trim();
|
|
190
|
+
if (!trimmedUrl || trimmedUrl.startsWith('//')) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (/^https?:\/\//i.test(trimmedUrl)) {
|
|
195
|
+
const parsedUrl = parseUrl(trimmedUrl);
|
|
196
|
+
if (!parsedUrl) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
return getDirtyResourcePathFromAbsoluteUrl(parsedUrl, context.app as ApiUrlProvider | undefined);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const appApiUrl = getAppApiUrl(context.app as ApiUrlProvider | undefined);
|
|
203
|
+
const configuredResourcePath = appApiUrl ? stripConfiguredApiPrefix(trimmedUrl, appApiUrl.pathname) : undefined;
|
|
204
|
+
if (configuredResourcePath) {
|
|
205
|
+
return configuredResourcePath;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return stripKnownApiPrefix(trimmedUrl);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function decodeResourcePathSegment(segment: string): string {
|
|
212
|
+
try {
|
|
213
|
+
return decodeURIComponent(segment);
|
|
214
|
+
} catch {
|
|
215
|
+
return segment;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getDataSourceKeyFromResourceOf(resourceOf: unknown): string | undefined {
|
|
220
|
+
const dataSourceKey = String(resourceOf ?? '').trim();
|
|
221
|
+
return dataSourceKey || undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseResourceActionFromSegments(segments: string[]): DirtyResourceAction | undefined {
|
|
225
|
+
const resourceSegments: string[] = [];
|
|
226
|
+
let actionName: string | undefined;
|
|
227
|
+
let actionSegmentIndex = -1;
|
|
228
|
+
|
|
229
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
230
|
+
const segment = segments[index];
|
|
231
|
+
const actionDelimiterIndex = segment.lastIndexOf(':');
|
|
232
|
+
const resourceSegment = actionDelimiterIndex === -1 ? segment : segment.slice(0, actionDelimiterIndex);
|
|
233
|
+
if (!resourceSegment) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
resourceSegments.push(decodeResourcePathSegment(resourceSegment));
|
|
238
|
+
if (actionDelimiterIndex !== -1) {
|
|
239
|
+
actionName = decodeResourcePathSegment(segment.slice(actionDelimiterIndex + 1)).trim();
|
|
240
|
+
actionSegmentIndex = index;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!actionName || !resourceSegments.length) {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (segments.length > actionSegmentIndex + 2) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
resourceName: resourceSegments.join('.'),
|
|
255
|
+
actionName,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function parseDirtyResourceActionFromUrl(url: unknown, context: FlowContext): DirtyResourceAction | undefined {
|
|
260
|
+
const resourcePath = getDirtyResourcePathFromUrl(url, context);
|
|
261
|
+
if (!resourcePath) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const segments = stripSearchAndHash(resourcePath).split('/').filter(Boolean);
|
|
266
|
+
const firstSegment = decodeResourcePathSegment(segments[0] || '');
|
|
267
|
+
if (firstSegment === 'dataSources' && segments.length >= 3) {
|
|
268
|
+
const dataSourceKey = getDataSourceKeyFromResourceOf(decodeResourcePathSegment(segments[1]));
|
|
269
|
+
const parsed = parseResourceActionFromSegments(segments.slice(2));
|
|
270
|
+
if (dataSourceKey && parsed) {
|
|
271
|
+
return {
|
|
272
|
+
...parsed,
|
|
273
|
+
dataSourceKey,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return parseResourceActionFromSegments(segments);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveDirtyResourceActionFromResource(
|
|
282
|
+
resourceName: string,
|
|
283
|
+
resourceOf: unknown,
|
|
284
|
+
actionName: string,
|
|
285
|
+
context: FlowContext,
|
|
286
|
+
): DirtyResourceAction | undefined {
|
|
287
|
+
const normalizedResourceName = resourceName.trim();
|
|
288
|
+
const normalizedActionName = actionName.trim();
|
|
289
|
+
if (!normalizedResourceName || !normalizedActionName) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (normalizedResourceName.includes('/')) {
|
|
294
|
+
const parsed = parseDirtyResourceActionFromUrl(`${normalizedResourceName}:${normalizedActionName}`, context);
|
|
295
|
+
if (parsed) {
|
|
296
|
+
return parsed;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const dataSourcesPrefix = 'dataSources.';
|
|
301
|
+
if (normalizedResourceName.startsWith(dataSourcesPrefix)) {
|
|
302
|
+
const dataSourceKey = getDataSourceKeyFromResourceOf(resourceOf);
|
|
303
|
+
const nestedResourceName = normalizedResourceName.slice(dataSourcesPrefix.length).trim();
|
|
304
|
+
if (dataSourceKey && nestedResourceName) {
|
|
305
|
+
return {
|
|
306
|
+
dataSourceKey,
|
|
307
|
+
resourceName: nestedResourceName,
|
|
308
|
+
actionName: normalizedActionName,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
resourceName: normalizedResourceName,
|
|
315
|
+
actionName: normalizedActionName,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function resolveDirtyResourceAction(
|
|
320
|
+
options: ResourceRequestOptions,
|
|
321
|
+
context: FlowContext,
|
|
322
|
+
): DirtyResourceAction | undefined {
|
|
323
|
+
const resourceName = typeof options?.resource === 'string' ? options.resource : undefined;
|
|
324
|
+
const actionName = typeof options?.action === 'string' ? options.action : undefined;
|
|
325
|
+
if (resourceName && actionName) {
|
|
326
|
+
return resolveDirtyResourceActionFromResource(resourceName, options.resourceOf, actionName, context);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return parseDirtyResourceActionFromUrl(options?.url, context);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function markResourceActionDataSourceDirty(
|
|
333
|
+
context: FlowContext,
|
|
334
|
+
dirtyResourceAction: DirtyResourceAction,
|
|
335
|
+
headers: unknown,
|
|
336
|
+
) {
|
|
337
|
+
markDataSourceDirty({
|
|
338
|
+
engine: context.engine,
|
|
339
|
+
dataSourceKey: dirtyResourceAction.dataSourceKey || getDataSourceKeyFromHeaders(headers),
|
|
340
|
+
resourceName: dirtyResourceAction.resourceName,
|
|
341
|
+
includePreviousEngines: true,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function createDirtyAwareResource(
|
|
346
|
+
context: FlowContext,
|
|
347
|
+
resource: IResource,
|
|
348
|
+
resourceName: string,
|
|
349
|
+
resourceOf: unknown,
|
|
350
|
+
headers: unknown,
|
|
351
|
+
): IResource {
|
|
352
|
+
return new Proxy(resource, {
|
|
353
|
+
get(target, prop, receiver) {
|
|
354
|
+
const original = Reflect.get(target, prop, receiver);
|
|
355
|
+
if (typeof prop !== 'string' || typeof original !== 'function' || !isMutatingResourceAction(prop)) {
|
|
356
|
+
return original;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const action = original as ResourceActionFn;
|
|
360
|
+
return async (...args: Parameters<ResourceActionFn>) => {
|
|
361
|
+
const result = await action(...args);
|
|
362
|
+
const dirtyResourceAction = resolveDirtyResourceActionFromResource(resourceName, resourceOf, prop, context);
|
|
363
|
+
if (dirtyResourceAction) {
|
|
364
|
+
markResourceActionDataSourceDirty(context, dirtyResourceAction, headers);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
};
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function createDirtyAwareApiClient(api: DirtyAwareAPIClient, context: FlowContext): APIClient {
|
|
373
|
+
const resource: APIClient['resource'] = (name, of, headers, cancel) => {
|
|
374
|
+
const targetResource = api.resource(name, of, headers, cancel);
|
|
375
|
+
return createDirtyAwareResource(context, targetResource, name, of, headers);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const request = (<T, R, D>(config: APIClientRequestConfig): Promise<R> => {
|
|
379
|
+
const options = config as ResourceRequestOptions;
|
|
380
|
+
const skipDataSourceDirty = options?.[SKIP_DATA_SOURCE_DIRTY];
|
|
381
|
+
const dirtyResourceAction = skipDataSourceDirty ? undefined : resolveDirtyResourceAction(options, context);
|
|
382
|
+
const { [SKIP_DATA_SOURCE_DIRTY]: _skipDataSourceDirty, ...cleanConfig } = options;
|
|
383
|
+
return api.request<T, R, D>(cleanConfig as typeof config).then((result) => {
|
|
384
|
+
if (dirtyResourceAction && isMutatingResourceAction(dirtyResourceAction.actionName)) {
|
|
385
|
+
markResourceActionDataSourceDirty(context, dirtyResourceAction, options.headers);
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
});
|
|
389
|
+
}) as APIClient['request'];
|
|
390
|
+
|
|
391
|
+
const proxy = new Proxy(api, {
|
|
392
|
+
get(target, prop, receiver) {
|
|
393
|
+
if (prop === 'resource') {
|
|
394
|
+
return resource;
|
|
395
|
+
}
|
|
396
|
+
if (prop === 'request') {
|
|
397
|
+
return request;
|
|
398
|
+
}
|
|
399
|
+
return Reflect.get(target, prop, receiver);
|
|
400
|
+
},
|
|
401
|
+
}) as APIClient;
|
|
402
|
+
dirtyAwareApiClientProxies.add(proxy);
|
|
403
|
+
return proxy;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function getDirtyAwareApiClient(value: unknown, context: FlowContext): unknown {
|
|
407
|
+
if (!isApiClientLike(value)) {
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (dirtyAwareApiClientProxies.has(value)) {
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const api = value as unknown as APIClient;
|
|
416
|
+
let contextCache = dirtyAwareApiClientCache.get(api);
|
|
417
|
+
if (!contextCache) {
|
|
418
|
+
contextCache = new WeakMap<object, APIClient>();
|
|
419
|
+
dirtyAwareApiClientCache.set(api, contextCache);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const cached = contextCache.get(context);
|
|
423
|
+
if (cached) {
|
|
424
|
+
return cached;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const wrapped = createDirtyAwareApiClient(value, context);
|
|
428
|
+
contextCache.set(context, wrapped);
|
|
429
|
+
return wrapped;
|
|
430
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -66,6 +66,16 @@ export { extractPropertyPath, formatPathToVariable, isVariableExpression } from
|
|
|
66
66
|
|
|
67
67
|
export { clearAutoFlowError, getAutoFlowError, setAutoFlowError, type AutoFlowError } from './autoFlowError';
|
|
68
68
|
export { parsePathnameToViewParams, type ViewParam } from './parsePathnameToViewParams';
|
|
69
|
+
export {
|
|
70
|
+
createOpenViewRouteState,
|
|
71
|
+
decodeOpenViewRouteState,
|
|
72
|
+
encodeOpenViewRouteState,
|
|
73
|
+
isOpenViewRouteStateToken,
|
|
74
|
+
RUNJS_OPEN_VIEW_ROUTE_STATE,
|
|
75
|
+
type OpenViewRouteMode,
|
|
76
|
+
type OpenViewRouteSize,
|
|
77
|
+
type OpenViewRouteState,
|
|
78
|
+
} from './openViewRouteState';
|
|
69
79
|
export {
|
|
70
80
|
decodeBase64Url,
|
|
71
81
|
encodeBase64Url,
|
|
@@ -77,15 +87,6 @@ export {
|
|
|
77
87
|
serializeCtxDateValue,
|
|
78
88
|
} from './dateVariable';
|
|
79
89
|
|
|
80
|
-
// 安全全局对象(window/document)
|
|
81
|
-
export {
|
|
82
|
-
createSafeDocument,
|
|
83
|
-
createSafeWindow,
|
|
84
|
-
createSafeNavigator,
|
|
85
|
-
createSafeRunJSGlobals,
|
|
86
|
-
runjsWithSafeGlobals,
|
|
87
|
-
} from './safeGlobals';
|
|
88
|
-
|
|
89
90
|
// RunJS value helpers
|
|
90
91
|
export { isRunJSValue, normalizeRunJSValue, extractUsedVariablePathsFromRunJS, type RunJSValue } from './runjsValue';
|
|
91
92
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const OPEN_VIEW_ROUTE_MODES = ['drawer', 'dialog', 'embed'] as const;
|
|
11
|
+
const OPEN_VIEW_ROUTE_SIZES = ['small', 'medium', 'large'] as const;
|
|
12
|
+
const OPEN_VIEW_ROUTE_STATE_TOKEN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
13
|
+
const OPEN_VIEW_ROUTE_STATE_TOKEN_LENGTH = 8;
|
|
14
|
+
|
|
15
|
+
export type OpenViewRouteMode = (typeof OPEN_VIEW_ROUTE_MODES)[number];
|
|
16
|
+
export type OpenViewRouteSize = (typeof OPEN_VIEW_ROUTE_SIZES)[number];
|
|
17
|
+
|
|
18
|
+
export type OpenViewRouteState = {
|
|
19
|
+
mode?: OpenViewRouteMode;
|
|
20
|
+
size?: OpenViewRouteSize;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const RUNJS_OPEN_VIEW_ROUTE_STATE = Symbol.for('nocobase.runjs.openViewRouteState');
|
|
24
|
+
|
|
25
|
+
function isOpenViewRouteMode(value: unknown): value is OpenViewRouteMode {
|
|
26
|
+
return typeof value === 'string' && (OPEN_VIEW_ROUTE_MODES as readonly string[]).includes(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isOpenViewRouteSize(value: unknown): value is OpenViewRouteSize {
|
|
30
|
+
return typeof value === 'string' && (OPEN_VIEW_ROUTE_SIZES as readonly string[]).includes(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createOpenViewRouteState(input?: { mode?: unknown; size?: unknown }): OpenViewRouteState | undefined {
|
|
34
|
+
const state: OpenViewRouteState = {};
|
|
35
|
+
|
|
36
|
+
if (isOpenViewRouteMode(input?.mode)) {
|
|
37
|
+
state.mode = input.mode;
|
|
38
|
+
}
|
|
39
|
+
if (isOpenViewRouteSize(input?.size)) {
|
|
40
|
+
state.size = input.size;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return state.mode || state.size ? state : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hashString(value: string) {
|
|
47
|
+
let hash = 2166136261;
|
|
48
|
+
for (let i = 0; i < value.length; i++) {
|
|
49
|
+
hash ^= value.charCodeAt(i);
|
|
50
|
+
hash = Math.imul(hash, 16777619);
|
|
51
|
+
}
|
|
52
|
+
return hash >>> 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stateToCode(state: OpenViewRouteState) {
|
|
56
|
+
const modeIndex = state.mode ? OPEN_VIEW_ROUTE_MODES.indexOf(state.mode) + 1 : 0;
|
|
57
|
+
const sizeIndex = state.size ? OPEN_VIEW_ROUTE_SIZES.indexOf(state.size) + 1 : 0;
|
|
58
|
+
const code = modeIndex * 4 + sizeIndex;
|
|
59
|
+
return code > 0 ? code : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function codeToState(code: number): OpenViewRouteState | undefined {
|
|
63
|
+
const modeIndex = Math.floor(code / 4);
|
|
64
|
+
const sizeIndex = code % 4;
|
|
65
|
+
return createOpenViewRouteState({
|
|
66
|
+
mode: modeIndex ? OPEN_VIEW_ROUTE_MODES[modeIndex - 1] : undefined,
|
|
67
|
+
size: sizeIndex ? OPEN_VIEW_ROUTE_SIZES[sizeIndex - 1] : undefined,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tokenForCode(viewUid: string, code: number) {
|
|
72
|
+
let seed = hashString(`${viewUid}:${code}`);
|
|
73
|
+
let token = '';
|
|
74
|
+
for (let i = 0; i < OPEN_VIEW_ROUTE_STATE_TOKEN_LENGTH; i++) {
|
|
75
|
+
seed = Math.imul(seed ^ (code + i * 17), 16777619) >>> 0;
|
|
76
|
+
token += OPEN_VIEW_ROUTE_STATE_TOKEN_ALPHABET[seed % OPEN_VIEW_ROUTE_STATE_TOKEN_ALPHABET.length];
|
|
77
|
+
}
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isOpenViewRouteStateToken(value: unknown): value is string {
|
|
82
|
+
return (
|
|
83
|
+
typeof value === 'string' &&
|
|
84
|
+
value.length === OPEN_VIEW_ROUTE_STATE_TOKEN_LENGTH &&
|
|
85
|
+
[...value].every((char) => OPEN_VIEW_ROUTE_STATE_TOKEN_ALPHABET.includes(char))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function encodeOpenViewRouteState(viewUid: string, input?: { mode?: unknown; size?: unknown }) {
|
|
90
|
+
const state = createOpenViewRouteState(input);
|
|
91
|
+
const code = state ? stateToCode(state) : undefined;
|
|
92
|
+
return code ? tokenForCode(viewUid, code) : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function decodeOpenViewRouteState(viewUid: string, token: unknown) {
|
|
96
|
+
if (!isOpenViewRouteStateToken(token)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let code = 1; code < 16; code++) {
|
|
101
|
+
if (tokenForCode(viewUid, code) === token) {
|
|
102
|
+
return codeToState(code);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|