@ldsg/cli 0.3.38 → 0.3.40
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/.rush/temp/package-deps_build.json +36 -33
- package/.rush/temp/shrinkwrap-deps.json +4 -1
- package/.tsbuildinfo +1 -1
- package/CHANGELOG.json +46 -0
- package/CHANGELOG.md +15 -1
- package/README.md +87 -4
- package/dist/__tests__/commands.test.js +68 -12
- package/dist/__tests__/commands.test.js.map +1 -1
- package/dist/__tests__/formatter.test.js +18 -3
- package/dist/__tests__/formatter.test.js.map +1 -1
- package/dist/__tests__/generate-app-index-ts.test.d.ts +1 -0
- package/dist/__tests__/generate-app-index-ts.test.js +109 -0
- package/dist/__tests__/generate-app-index-ts.test.js.map +1 -0
- package/dist/__tests__/get-extend-app-data-params-using-workspace-ldsg-module.test.d.ts +1 -0
- package/dist/__tests__/get-extend-app-data-params-using-workspace-ldsg-module.test.js +81 -0
- package/dist/__tests__/get-extend-app-data-params-using-workspace-ldsg-module.test.js.map +1 -0
- package/dist/__tests__/json-file-mutator.test.js +54 -9
- package/dist/__tests__/json-file-mutator.test.js.map +1 -1
- package/dist/__tests__/mutate.test.d.ts +1 -0
- package/dist/__tests__/mutate.test.js +334 -0
- package/dist/__tests__/mutate.test.js.map +1 -0
- package/dist/cli.js +131 -50
- package/dist/cli.js.map +1 -1
- package/dist/commands/resource/create.js +1 -1
- package/dist/commands/resource/create.js.map +1 -1
- package/dist/commands/resource/list.js +1 -1
- package/dist/commands/resource/list.js.map +1 -1
- package/dist/commands/resource/mutate.d.ts +80 -0
- package/dist/commands/resource/mutate.js +311 -0
- package/dist/commands/resource/mutate.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +4 -1
- package/dist/constants.js.map +1 -1
- package/dist/types/get-extend-app-data-params.d.ts +4 -1
- package/dist/utils/get-extend-app-data-params-using-workspace-ldsg-module/index.js +22 -6
- package/dist/utils/get-extend-app-data-params-using-workspace-ldsg-module/index.js.map +1 -1
- package/jest.config.js +3 -4
- package/package.json +4 -5
- package/src/__tests__/commands.test.ts +68 -12
- package/src/__tests__/formatter.test.ts +18 -3
- package/src/__tests__/generate-app-index-ts.test.ts +141 -0
- package/src/__tests__/get-extend-app-data-params-using-workspace-ldsg-module.test.ts +109 -0
- package/src/__tests__/json-file-mutator.test.ts +54 -9
- package/src/__tests__/mutate.test.ts +392 -0
- package/src/cli.ts +179 -54
- package/src/commands/resource/create.ts +3 -3
- package/src/commands/resource/list.ts +1 -1
- package/src/commands/resource/mutate.ts +542 -0
- package/src/constants.ts +4 -0
- package/src/types/get-extend-app-data-params.ts +4 -1
- package/src/utils/get-extend-app-data-params-using-workspace-ldsg-module/index.ts +43 -7
- package/.rush/temp/chunked-rush-logs/cli.format.chunks.jsonl +0 -2
- package/.rush/temp/package-deps_format.json +0 -38
- package/dist/commands/resource/convert.d.ts +0 -11
- package/dist/commands/resource/convert.js +0 -44
- package/dist/commands/resource/convert.js.map +0 -1
- package/rush-logs/cli.format.log +0 -2
- package/src/commands/resource/convert.ts +0 -54
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -137,7 +137,7 @@ export async function getTree(
|
|
|
137
137
|
id: "__virtual_root__",
|
|
138
138
|
kind: "virtual",
|
|
139
139
|
parentId: null,
|
|
140
|
-
settings: { title: "Virtual Root" },
|
|
140
|
+
settings: { slug: "virtual-root", title: "Virtual Root", description: "" },
|
|
141
141
|
};
|
|
142
142
|
const childMap = buildChildMap(records);
|
|
143
143
|
const children = roots.map((r) => buildTree(records, r.id)!).filter(Boolean);
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { ResourceRecord, ResourceRecords, ResourceSettings } from "@ldsg/types";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
6
|
+
DEFAULT_SOURCE_PATH,
|
|
7
|
+
RRM_FETCH_PAGE_SIZE,
|
|
8
|
+
} from "../../constants";
|
|
9
|
+
import { SourceData } from "../../types/resource-record";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 与 @ldsg/console 的 purifyResourceRecords 一致:
|
|
13
|
+
* 发送前只保留 { id, kind, parentId, settings } 四个字段。
|
|
14
|
+
*/
|
|
15
|
+
export type MutateRecord = {
|
|
16
|
+
id: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
parentId: string | null;
|
|
19
|
+
settings: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MutateRecordIdMap = Record<string, string>;
|
|
23
|
+
|
|
24
|
+
export interface MutateOptions {
|
|
25
|
+
sourceData: SourceData;
|
|
26
|
+
/** GraphQL 端点,默认 https://api.rrm.ldsgy.com/graphql */
|
|
27
|
+
endpoint?: string;
|
|
28
|
+
/** Bearer token;不传则读取环境变量 RRM_ACCESS_TOKEN */
|
|
29
|
+
token?: string;
|
|
30
|
+
/** 可选:限定远端记录的作用范围(path regex,如应用 id) */
|
|
31
|
+
path?: string;
|
|
32
|
+
/** 是否软删除远端有、本地没有的记录(默认 true,对齐 console 全量同步) */
|
|
33
|
+
deleteMissing?: boolean;
|
|
34
|
+
/** 是否把线上分配的新 id 写回本地源文件(默认 true) */
|
|
35
|
+
writeBack?: boolean;
|
|
36
|
+
/** 存在软删除时是否跳过确认(默认 false) */
|
|
37
|
+
yes?: boolean;
|
|
38
|
+
/** 只计算 plan 不真正提交 */
|
|
39
|
+
dryRun?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MutatePlan {
|
|
43
|
+
/** 需要新建的记录(id 不在远端) */
|
|
44
|
+
createResourceRecords: MutateRecord[];
|
|
45
|
+
/** 需要更新的记录(id 已存在于远端) */
|
|
46
|
+
updateResourceRecords: MutateRecord[];
|
|
47
|
+
/** 需要软删除的记录(远端有、本地没有,settings.isDeleted=true) */
|
|
48
|
+
deleteResourceRecords: MutateRecord[];
|
|
49
|
+
/** 本次拉取到的远端记录数 */
|
|
50
|
+
remoteCount: number;
|
|
51
|
+
/** 本地记录数 */
|
|
52
|
+
localCount: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MutateResult {
|
|
56
|
+
plan: MutatePlan;
|
|
57
|
+
dryRun: boolean;
|
|
58
|
+
/** 服务器实际返回的新建记录(含线上 id) */
|
|
59
|
+
createdResourceRecords: MutateRecord[];
|
|
60
|
+
/** 服务器实际返回的更新记录 */
|
|
61
|
+
updatedResourceRecords: MutateRecord[];
|
|
62
|
+
/** 本地临时 id -> 线上 id 映射(新建记录) */
|
|
63
|
+
idMap: MutateRecordIdMap;
|
|
64
|
+
/** 写回本地源文件的路径(仅本地文件源且非 dry-run 时存在) */
|
|
65
|
+
writeBackPath?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function purifyResourceRecord(record: ResourceRecord): MutateRecord {
|
|
69
|
+
return {
|
|
70
|
+
id: record.id,
|
|
71
|
+
kind: record.kind,
|
|
72
|
+
parentId: record.parentId ?? null,
|
|
73
|
+
settings: (record.settings ?? {}) as Record<string, unknown>,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 根据本地与远端记录拆分 create / update / delete(软删)。
|
|
79
|
+
* 对齐 console 的 mutateApplicationTreeResourceRecords 语义:
|
|
80
|
+
* - 本地有、远端无 -> create
|
|
81
|
+
* - 本地有、远端有 -> update
|
|
82
|
+
* - 远端有、本地无 -> 软删除(settings.isDeleted = true,走 update)
|
|
83
|
+
*/
|
|
84
|
+
export function buildMutatePlan(
|
|
85
|
+
localRecords: ResourceRecords,
|
|
86
|
+
remoteRecords: ResourceRecords,
|
|
87
|
+
options: { deleteMissing?: boolean } = {},
|
|
88
|
+
): MutatePlan {
|
|
89
|
+
const { deleteMissing = true } = options;
|
|
90
|
+
|
|
91
|
+
const remoteIdSet = new Set(remoteRecords.map((r) => r.id));
|
|
92
|
+
const localIdSet = new Set(localRecords.map((r) => r.id));
|
|
93
|
+
|
|
94
|
+
const createResourceRecords = localRecords
|
|
95
|
+
.filter((r) => !remoteIdSet.has(r.id))
|
|
96
|
+
.map(purifyResourceRecord);
|
|
97
|
+
|
|
98
|
+
const updateResourceRecords = localRecords
|
|
99
|
+
.filter((r) => remoteIdSet.has(r.id))
|
|
100
|
+
.map(purifyResourceRecord);
|
|
101
|
+
|
|
102
|
+
const deleteResourceRecords = deleteMissing
|
|
103
|
+
? remoteRecords
|
|
104
|
+
.filter((r) => !localIdSet.has(r.id))
|
|
105
|
+
.map((r) => {
|
|
106
|
+
const purified = purifyResourceRecord(r);
|
|
107
|
+
return {
|
|
108
|
+
...purified,
|
|
109
|
+
settings: { ...purified.settings, isDeleted: true },
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
createResourceRecords,
|
|
116
|
+
updateResourceRecords,
|
|
117
|
+
deleteResourceRecords,
|
|
118
|
+
remoteCount: remoteRecords.length,
|
|
119
|
+
localCount: localRecords.length,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 递归替换任意 string 值(精确匹配 idMap 的 key):
|
|
125
|
+
* 覆盖 id、parentId 以及 settings 中对其它临时 id 的引用
|
|
126
|
+
* (如 fieldTypeResourceId / workflowNodeTypeResourceId / objectResourceId 等)。
|
|
127
|
+
*/
|
|
128
|
+
export function replaceIdsDeep(value: unknown, idMap: MutateRecordIdMap): unknown {
|
|
129
|
+
if (typeof value === "string") {
|
|
130
|
+
return idMap[value] ?? value;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.map((item) => replaceIdsDeep(item, idMap));
|
|
134
|
+
}
|
|
135
|
+
if (value && typeof value === "object") {
|
|
136
|
+
const result: Record<string, unknown> = {};
|
|
137
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
138
|
+
result[key] = replaceIdsDeep(item, idMap);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 把本地记录里的临时 id 替换为线上 id(用于写回本地源文件)。
|
|
147
|
+
*/
|
|
148
|
+
export function applyIdMapToRecords(
|
|
149
|
+
records: ResourceRecords,
|
|
150
|
+
idMap: MutateRecordIdMap,
|
|
151
|
+
): ResourceRecords {
|
|
152
|
+
return records.map((record) => replaceIdsDeep(record, idMap) as ResourceRecord);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildRemoteFilter(path?: string): Record<string, unknown> {
|
|
156
|
+
const filter: Record<string, unknown> = {
|
|
157
|
+
settings: { isDeleted: { $ne: true } },
|
|
158
|
+
};
|
|
159
|
+
if (path) {
|
|
160
|
+
filter._operators = { path: { regex: path } };
|
|
161
|
+
}
|
|
162
|
+
return filter;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 从 RRM 分页拉取远端记录(匿名可读,无需 token)。
|
|
167
|
+
*/
|
|
168
|
+
export async function fetchRemoteResourceRecords(options: {
|
|
169
|
+
endpoint: string;
|
|
170
|
+
token?: string;
|
|
171
|
+
path?: string;
|
|
172
|
+
}): Promise<ResourceRecords> {
|
|
173
|
+
const { endpoint, token, path } = options;
|
|
174
|
+
|
|
175
|
+
const query = `
|
|
176
|
+
query ResourceRecordConnectionQuery(
|
|
177
|
+
$filter: FilterFindManyResourceRecordInput
|
|
178
|
+
$first: Int
|
|
179
|
+
$after: String
|
|
180
|
+
) {
|
|
181
|
+
resourceRecordConnection(filter: $filter, first: $first, after: $after) {
|
|
182
|
+
pageInfo {
|
|
183
|
+
hasNextPage
|
|
184
|
+
endCursor
|
|
185
|
+
}
|
|
186
|
+
edges {
|
|
187
|
+
node {
|
|
188
|
+
id: _id
|
|
189
|
+
kind
|
|
190
|
+
parentId
|
|
191
|
+
settings
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const records: ResourceRecords = [];
|
|
199
|
+
let after: string | undefined;
|
|
200
|
+
|
|
201
|
+
while (true) {
|
|
202
|
+
const res = await axios.post(
|
|
203
|
+
endpoint,
|
|
204
|
+
{
|
|
205
|
+
query,
|
|
206
|
+
variables: {
|
|
207
|
+
filter: buildRemoteFilter(path),
|
|
208
|
+
first: RRM_FETCH_PAGE_SIZE,
|
|
209
|
+
...(after ? { after } : {}),
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
216
|
+
},
|
|
217
|
+
timeout: 60000,
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (res.data?.errors?.length > 0) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`RRM query failed: ${res.data.errors.map((e: { message: string }) => e.message).join("; ")}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const page = res.data?.data?.resourceRecordConnection;
|
|
228
|
+
if (!page) {
|
|
229
|
+
throw new Error("RRM query returned no resourceRecordConnection");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const edge of page.edges ?? []) {
|
|
233
|
+
records.push({
|
|
234
|
+
id: edge.node.id,
|
|
235
|
+
kind: edge.node.kind,
|
|
236
|
+
parentId: edge.node.parentId ?? null,
|
|
237
|
+
settings: (edge.node.settings ?? {}) as ResourceSettings,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!page.pageInfo?.hasNextPage) break;
|
|
242
|
+
after = page.pageInfo.endCursor ?? undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return records;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 调用 RRM 的 mutateResourceRecords mutation(与 console 完全一致)。
|
|
250
|
+
*/
|
|
251
|
+
export async function mutateResourceRecords(options: {
|
|
252
|
+
endpoint: string;
|
|
253
|
+
token?: string;
|
|
254
|
+
createResourceRecords: MutateRecord[];
|
|
255
|
+
updateResourceRecords: MutateRecord[];
|
|
256
|
+
}): Promise<{
|
|
257
|
+
createdResourceRecords: MutateRecord[];
|
|
258
|
+
updatedResourceRecords: MutateRecord[];
|
|
259
|
+
}> {
|
|
260
|
+
const { endpoint, token, createResourceRecords, updateResourceRecords } = options;
|
|
261
|
+
|
|
262
|
+
const mutation = `
|
|
263
|
+
mutation MutateResourceRecordsMutation(
|
|
264
|
+
$createResourceRecords: JSON
|
|
265
|
+
$updateResourceRecords: JSON
|
|
266
|
+
) {
|
|
267
|
+
mutateResourceRecords(
|
|
268
|
+
createResourceRecords: $createResourceRecords
|
|
269
|
+
updateResourceRecords: $updateResourceRecords
|
|
270
|
+
) {
|
|
271
|
+
updatedResourceRecords
|
|
272
|
+
createdResourceRecords
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
const res = await axios.post(
|
|
278
|
+
endpoint,
|
|
279
|
+
{
|
|
280
|
+
query: mutation,
|
|
281
|
+
variables: {
|
|
282
|
+
createResourceRecords,
|
|
283
|
+
updateResourceRecords,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
headers: {
|
|
288
|
+
"Content-Type": "application/json",
|
|
289
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
290
|
+
},
|
|
291
|
+
timeout: 120000,
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (res.data?.errors?.length > 0) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`RRM mutateResourceRecords failed: ${res.data.errors
|
|
298
|
+
.map((e: { message: string }) => e.message)
|
|
299
|
+
.join("; ")}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const data = res.data?.data?.mutateResourceRecords ?? {};
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
createdResourceRecords: (data.createdResourceRecords ?? []) as MutateRecord[],
|
|
307
|
+
updatedResourceRecords: (data.updatedResourceRecords ?? []) as MutateRecord[],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 根据服务器返回的新建记录,构建 本地临时id -> 线上id 映射。
|
|
313
|
+
* createdResourceRecords 与 createResourceRecords 按数组下标对应;
|
|
314
|
+
* 若数量不一致则放弃映射(避免写坏本地文件)。
|
|
315
|
+
*/
|
|
316
|
+
export function buildIdMap(
|
|
317
|
+
localCreateRecords: MutateRecord[],
|
|
318
|
+
createdResourceRecords: MutateRecord[],
|
|
319
|
+
): MutateRecordIdMap {
|
|
320
|
+
if (createdResourceRecords.length !== localCreateRecords.length) {
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const idMap: MutateRecordIdMap = {};
|
|
325
|
+
for (let i = 0; i < localCreateRecords.length; i++) {
|
|
326
|
+
const localId = localCreateRecords[i].id;
|
|
327
|
+
const remoteId = createdResourceRecords[i].id;
|
|
328
|
+
if (localId !== remoteId) {
|
|
329
|
+
idMap[localId] = remoteId;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return idMap;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function writeBackLocalFile(
|
|
336
|
+
sourceData: SourceData,
|
|
337
|
+
idMap: MutateRecordIdMap,
|
|
338
|
+
): Promise<string | undefined> {
|
|
339
|
+
if (sourceData.filePath === "-stdin-") {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
if (Object.keys(idMap).length === 0) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const updatedRecords = applyIdMapToRecords(sourceData.resourceRecords, idMap);
|
|
347
|
+
const output =
|
|
348
|
+
sourceData.format === "array" ? updatedRecords : { resourceRecords: updatedRecords };
|
|
349
|
+
await fs.writeJson(sourceData.filePath, output, { spaces: 2 });
|
|
350
|
+
return sourceData.filePath;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* mutate 自动模式:同步本地资源到 RRM
|
|
355
|
+
* 1. 拉取远端记录(可 --path 限定作用范围)
|
|
356
|
+
* 2. 拆分为 create / update / delete(软删)
|
|
357
|
+
* 3. 调用 mutateResourceRecords(需 Bearer token)
|
|
358
|
+
* 4. 把线上分配的新 id 写回本地源文件
|
|
359
|
+
*/
|
|
360
|
+
export async function mutateResources(options: MutateOptions): Promise<MutateResult> {
|
|
361
|
+
const {
|
|
362
|
+
sourceData,
|
|
363
|
+
endpoint = DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
364
|
+
token,
|
|
365
|
+
path,
|
|
366
|
+
deleteMissing = true,
|
|
367
|
+
writeBack = true,
|
|
368
|
+
yes = false,
|
|
369
|
+
dryRun = false,
|
|
370
|
+
} = options;
|
|
371
|
+
|
|
372
|
+
const remoteRecords = await fetchRemoteResourceRecords({ endpoint, token, path });
|
|
373
|
+
|
|
374
|
+
const plan = buildMutatePlan(sourceData.resourceRecords, remoteRecords, { deleteMissing });
|
|
375
|
+
|
|
376
|
+
if (dryRun) {
|
|
377
|
+
return {
|
|
378
|
+
plan,
|
|
379
|
+
dryRun: true,
|
|
380
|
+
createdResourceRecords: [],
|
|
381
|
+
updatedResourceRecords: [],
|
|
382
|
+
idMap: {},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const hasWrites =
|
|
387
|
+
plan.createResourceRecords.length > 0 ||
|
|
388
|
+
plan.updateResourceRecords.length > 0 ||
|
|
389
|
+
plan.deleteResourceRecords.length > 0;
|
|
390
|
+
|
|
391
|
+
if (!hasWrites) {
|
|
392
|
+
return {
|
|
393
|
+
plan,
|
|
394
|
+
dryRun: false,
|
|
395
|
+
createdResourceRecords: [],
|
|
396
|
+
updatedResourceRecords: [],
|
|
397
|
+
idMap: {},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const effectiveToken = token || process.env.RRM_ACCESS_TOKEN;
|
|
402
|
+
|
|
403
|
+
if (!effectiveToken) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"RRM 写操作需要鉴权:请通过 --token 或环境变量 RRM_ACCESS_TOKEN 提供 Bearer token(console 登录后 localStorage 的 ACCESS_TOKEN)",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (plan.deleteResourceRecords.length > 0 && !yes) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`本次将软删除 ${plan.deleteResourceRecords.length} 条远端记录(settings.isDeleted=true)。` +
|
|
412
|
+
"请先用 --dry-run 查看,确认后加 --yes 执行;或加 --no-delete-missing 关闭软删除。",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const updateResourceRecords = [...plan.updateResourceRecords, ...plan.deleteResourceRecords];
|
|
417
|
+
|
|
418
|
+
const { createdResourceRecords, updatedResourceRecords: returnedUpdated } =
|
|
419
|
+
await mutateResourceRecords({
|
|
420
|
+
endpoint,
|
|
421
|
+
token: effectiveToken,
|
|
422
|
+
createResourceRecords: plan.createResourceRecords,
|
|
423
|
+
updateResourceRecords,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const idMap = buildIdMap(plan.createResourceRecords, createdResourceRecords);
|
|
427
|
+
|
|
428
|
+
let writeBackPath: string | undefined;
|
|
429
|
+
if (writeBack) {
|
|
430
|
+
writeBackPath = await writeBackLocalFile(sourceData, idMap);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
plan,
|
|
435
|
+
dryRun: false,
|
|
436
|
+
createdResourceRecords,
|
|
437
|
+
updatedResourceRecords: returnedUpdated,
|
|
438
|
+
idMap,
|
|
439
|
+
writeBackPath,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export interface MutateRecordsDirectOptions {
|
|
444
|
+
endpoint?: string;
|
|
445
|
+
token?: string;
|
|
446
|
+
createResourceRecords: MutateRecord[];
|
|
447
|
+
updateResourceRecords: MutateRecord[];
|
|
448
|
+
dryRun?: boolean;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* mutate 显式模式:把调用方明确给出的 create / update 记录直接提交给
|
|
453
|
+
* mutateResourceRecords(不拉远端、不自动拆分、不写回)。与 console 行为一致。
|
|
454
|
+
*/
|
|
455
|
+
export async function mutateRecordsDirect(options: MutateRecordsDirectOptions): Promise<{
|
|
456
|
+
dryRun: boolean;
|
|
457
|
+
createCount: number;
|
|
458
|
+
updateCount: number;
|
|
459
|
+
createdResourceRecords: MutateRecord[];
|
|
460
|
+
updatedResourceRecords: MutateRecord[];
|
|
461
|
+
}> {
|
|
462
|
+
const {
|
|
463
|
+
endpoint = DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
464
|
+
token,
|
|
465
|
+
createResourceRecords,
|
|
466
|
+
updateResourceRecords,
|
|
467
|
+
dryRun = false,
|
|
468
|
+
} = options;
|
|
469
|
+
|
|
470
|
+
const hasWrites = createResourceRecords.length > 0 || updateResourceRecords.length > 0;
|
|
471
|
+
|
|
472
|
+
if (!hasWrites) {
|
|
473
|
+
return {
|
|
474
|
+
dryRun,
|
|
475
|
+
createCount: 0,
|
|
476
|
+
updateCount: 0,
|
|
477
|
+
createdResourceRecords: [],
|
|
478
|
+
updatedResourceRecords: [],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (dryRun) {
|
|
483
|
+
return {
|
|
484
|
+
dryRun: true,
|
|
485
|
+
createCount: createResourceRecords.length,
|
|
486
|
+
updateCount: updateResourceRecords.length,
|
|
487
|
+
createdResourceRecords: [],
|
|
488
|
+
updatedResourceRecords: [],
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const effectiveToken = token || process.env.RRM_ACCESS_TOKEN;
|
|
493
|
+
|
|
494
|
+
if (!effectiveToken) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
"RRM 写操作需要鉴权:请通过 --token 或环境变量 RRM_ACCESS_TOKEN 提供 Bearer token(console 登录后 localStorage 的 ACCESS_TOKEN)",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { createdResourceRecords: created, updatedResourceRecords: updated } =
|
|
501
|
+
await mutateResourceRecords({
|
|
502
|
+
endpoint,
|
|
503
|
+
token: effectiveToken,
|
|
504
|
+
createResourceRecords,
|
|
505
|
+
updateResourceRecords,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
dryRun: false,
|
|
510
|
+
createCount: createResourceRecords.length,
|
|
511
|
+
updateCount: updateResourceRecords.length,
|
|
512
|
+
createdResourceRecords: created,
|
|
513
|
+
updatedResourceRecords: updated,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export interface PullRemoteResourceRecordsOptions {
|
|
518
|
+
endpoint?: string;
|
|
519
|
+
token?: string;
|
|
520
|
+
path?: string;
|
|
521
|
+
output?: string;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* pull:把远端记录拉到本地文件(只读,无需 token)。
|
|
526
|
+
*/
|
|
527
|
+
export async function pullRemoteResourceRecords(
|
|
528
|
+
options: PullRemoteResourceRecordsOptions,
|
|
529
|
+
): Promise<{ count: number; outputPath: string }> {
|
|
530
|
+
const {
|
|
531
|
+
endpoint = DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
532
|
+
token,
|
|
533
|
+
path,
|
|
534
|
+
output = DEFAULT_SOURCE_PATH,
|
|
535
|
+
} = options;
|
|
536
|
+
|
|
537
|
+
const records = await fetchRemoteResourceRecords({ endpoint, token, path });
|
|
538
|
+
|
|
539
|
+
await fs.writeJson(output, records, { spaces: 2 });
|
|
540
|
+
|
|
541
|
+
return { count: records.length, outputPath: output };
|
|
542
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -2,3 +2,7 @@ export const DEFAULT_OUTPUT_PATH = ".";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SOURCE_FILE = "resource-records.json";
|
|
4
4
|
export const DEFAULT_SOURCE_PATH = `./${DEFAULT_SOURCE_FILE}`;
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_RRM_GRAPHQL_ENDPOINT = "https://api.rrm.ldsgy.com/graphql";
|
|
7
|
+
export const RRM_ACCESS_TOKEN_ENV = "RRM_ACCESS_TOKEN";
|
|
8
|
+
export const RRM_FETCH_PAGE_SIZE = 100;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { AppData } from "@ldsg/types";
|
|
2
|
-
import { ExtendAppDataParams } from "@ldsg/utils";
|
|
2
|
+
import { ExtendAppDataParams, ZipAppDataLayout } from "@ldsg/utils";
|
|
3
3
|
|
|
4
4
|
interface GetExtendAppDataParamsParams {
|
|
5
5
|
appData: AppData;
|
|
6
|
+
layout?: ZipAppDataLayout;
|
|
7
|
+
appPackageName?: string;
|
|
8
|
+
outputPath?: string;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export type GetExtendAppDataParams = (
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { FILES_IN_APP_TEMPLATE_MODULE } from "@ldsg/constants";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ExtendAppDataParams,
|
|
4
|
+
RUSH_JSON_PATH,
|
|
5
|
+
syncRushProjects,
|
|
6
|
+
ZipAppDataLayout,
|
|
7
|
+
} from "@ldsg/utils";
|
|
3
8
|
import fs from "fs-extra";
|
|
4
9
|
import _ from "lodash";
|
|
10
|
+
import path from "path";
|
|
5
11
|
import { GetExtendAppDataParams } from "../../types/get-extend-app-data-params";
|
|
6
12
|
|
|
7
|
-
const
|
|
13
|
+
const DEFAULT_APP_PACKAGE_NAME = "@ldsg/app";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OUTPUT_PATH = "../../generated";
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* 主函数,用于准备调试应用环境
|
|
@@ -22,10 +30,17 @@ const DEBUG_APP_PACKAGE_NAME = "@ldsg/app";
|
|
|
22
30
|
export const getExtendAppDataParamsUsingWorkspaceLdsgModule: GetExtendAppDataParams = async (
|
|
23
31
|
params,
|
|
24
32
|
) => {
|
|
25
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
appData: paramsAppData,
|
|
35
|
+
layout = ZipAppDataLayout.FLAT,
|
|
36
|
+
appPackageName = DEFAULT_APP_PACKAGE_NAME,
|
|
37
|
+
outputPath = DEFAULT_OUTPUT_PATH,
|
|
38
|
+
} = params;
|
|
26
39
|
|
|
27
40
|
let appData = paramsAppData;
|
|
28
41
|
|
|
42
|
+
const handlerResourceIds = [];
|
|
43
|
+
|
|
29
44
|
/**
|
|
30
45
|
* 相对于普通应用准备,增加了如下操作
|
|
31
46
|
* 1. ldsg 模块使用 workspace 模式
|
|
@@ -34,13 +49,25 @@ export const getExtendAppDataParamsUsingWorkspaceLdsgModule: GetExtendAppDataPar
|
|
|
34
49
|
{
|
|
35
50
|
const { resourceRecords: appDataResourceRecords } = appData;
|
|
36
51
|
|
|
52
|
+
const repoRoot = path.resolve(process.cwd(), "../..");
|
|
53
|
+
const outputAbs = path.resolve(process.cwd(), outputPath);
|
|
54
|
+
const outputRel = path.relative(repoRoot, outputAbs);
|
|
55
|
+
|
|
56
|
+
const appProjectFolder =
|
|
57
|
+
layout === ZipAppDataLayout.NESTED ? outputRel : path.join(outputRel, "app");
|
|
58
|
+
|
|
59
|
+
const getHandlerProjectFolder = (id: string) =>
|
|
60
|
+
layout === ZipAppDataLayout.NESTED
|
|
61
|
+
? path.join(outputRel, "handlers", id)
|
|
62
|
+
: path.join(outputRel, id);
|
|
63
|
+
|
|
37
64
|
const subspaceProjects: {
|
|
38
65
|
packageName: string;
|
|
39
66
|
projectFolder: string;
|
|
40
67
|
}[] = [
|
|
41
68
|
{
|
|
42
|
-
packageName:
|
|
43
|
-
projectFolder:
|
|
69
|
+
packageName: appPackageName,
|
|
70
|
+
projectFolder: appProjectFolder,
|
|
44
71
|
},
|
|
45
72
|
];
|
|
46
73
|
|
|
@@ -51,9 +78,11 @@ export const getExtendAppDataParamsUsingWorkspaceLdsgModule: GetExtendAppDataPar
|
|
|
51
78
|
const { id, kind } = resourceRecord;
|
|
52
79
|
|
|
53
80
|
if (kind === "handler") {
|
|
81
|
+
handlerResourceIds.push(id);
|
|
82
|
+
|
|
54
83
|
subspaceProjects.push({
|
|
55
84
|
packageName: `${id}`,
|
|
56
|
-
projectFolder:
|
|
85
|
+
projectFolder: getHandlerProjectFolder(id),
|
|
57
86
|
});
|
|
58
87
|
|
|
59
88
|
let dependencies = resourceRecord.settings.dependencies.map((dependency) => {
|
|
@@ -110,7 +139,14 @@ export const getExtendAppDataParamsUsingWorkspaceLdsgModule: GetExtendAppDataPar
|
|
|
110
139
|
|
|
111
140
|
const packageJsonFile = FILES_IN_APP_TEMPLATE_MODULE.find((file) => file.path === "package.json");
|
|
112
141
|
|
|
113
|
-
_.set(packageJsonFile, "data.name",
|
|
142
|
+
_.set(packageJsonFile, "data.name", appPackageName);
|
|
143
|
+
|
|
144
|
+
const dependencies = {
|
|
145
|
+
..._.get(packageJsonFile, "data.dependencies"),
|
|
146
|
+
..._.mapValues(_.keyBy(handlerResourceIds), () => "workspace:*"),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
_.set(packageJsonFile, "data.dependencies", dependencies);
|
|
114
150
|
|
|
115
151
|
const res: ExtendAppDataParams = {
|
|
116
152
|
appData,
|