@ldsg/cli 0.3.38 → 0.3.41
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 +69 -0
- package/CHANGELOG.md +22 -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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { ResourceRecords } from "@ldsg/types";
|
|
4
|
+
import {
|
|
5
|
+
applyIdMapToRecords,
|
|
6
|
+
buildIdMap,
|
|
7
|
+
buildMutatePlan,
|
|
8
|
+
mutateRecordsDirect,
|
|
9
|
+
mutateResourceRecords,
|
|
10
|
+
mutateResources,
|
|
11
|
+
pullRemoteResourceRecords,
|
|
12
|
+
purifyResourceRecord,
|
|
13
|
+
} from "../commands/resource/mutate";
|
|
14
|
+
import { SourceData } from "../types/resource-record";
|
|
15
|
+
import { DEFAULT_RRM_GRAPHQL_ENDPOINT } from "../constants";
|
|
16
|
+
|
|
17
|
+
jest.mock("axios");
|
|
18
|
+
jest.mock("fs-extra");
|
|
19
|
+
|
|
20
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
21
|
+
const mockedFs = fs as jest.Mocked<typeof fs>;
|
|
22
|
+
|
|
23
|
+
const localRecords: ResourceRecords = [
|
|
24
|
+
{
|
|
25
|
+
id: "app-temp-1",
|
|
26
|
+
kind: "application",
|
|
27
|
+
parentId: null,
|
|
28
|
+
settings: { slug: "my-app", title: "My App", description: "" },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "form-temp-1",
|
|
32
|
+
kind: "form",
|
|
33
|
+
parentId: "app-temp-1",
|
|
34
|
+
settings: { slug: "login", title: "Login", description: "" },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "existing-app",
|
|
38
|
+
kind: "application",
|
|
39
|
+
parentId: null,
|
|
40
|
+
settings: { slug: "existing-application", title: "Existing App", description: "", extra: true },
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const remoteRecords: ResourceRecords = [
|
|
45
|
+
{
|
|
46
|
+
id: "existing-app",
|
|
47
|
+
kind: "application",
|
|
48
|
+
parentId: null,
|
|
49
|
+
settings: { slug: "existing-application", title: "Existing App", description: "" },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "remote-only-app",
|
|
53
|
+
kind: "application",
|
|
54
|
+
parentId: null,
|
|
55
|
+
settings: { slug: "remote-only-application", title: "Remote Only", description: "" },
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const sourceData: SourceData = {
|
|
60
|
+
resourceRecords: localRecords,
|
|
61
|
+
format: "array",
|
|
62
|
+
filePath: "/tmp/resource-records.json",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("mutate command", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
jest.resetAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("purifyResourceRecord", () => {
|
|
71
|
+
it("should keep only id, kind, parentId, settings", () => {
|
|
72
|
+
const result = purifyResourceRecord({
|
|
73
|
+
id: "x",
|
|
74
|
+
kind: "table",
|
|
75
|
+
parentId: "app1",
|
|
76
|
+
settings: { title: "T" },
|
|
77
|
+
extraField: "nope",
|
|
78
|
+
} as any);
|
|
79
|
+
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
id: "x",
|
|
82
|
+
kind: "table",
|
|
83
|
+
parentId: "app1",
|
|
84
|
+
settings: { title: "T" },
|
|
85
|
+
});
|
|
86
|
+
expect(result).not.toHaveProperty("extraField");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("buildMutatePlan", () => {
|
|
91
|
+
it("should split create/update/delete like console", () => {
|
|
92
|
+
const plan = buildMutatePlan(localRecords, remoteRecords);
|
|
93
|
+
|
|
94
|
+
expect(plan.createResourceRecords.map((r) => r.id)).toEqual(["app-temp-1", "form-temp-1"]);
|
|
95
|
+
expect(plan.updateResourceRecords.map((r) => r.id)).toEqual(["existing-app"]);
|
|
96
|
+
expect(plan.deleteResourceRecords.map((r) => r.id)).toEqual(["remote-only-app"]);
|
|
97
|
+
expect(plan.deleteResourceRecords[0].settings.isDeleted).toBe(true);
|
|
98
|
+
expect(plan.remoteCount).toBe(2);
|
|
99
|
+
expect(plan.localCount).toBe(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should skip soft-delete when deleteMissing=false", () => {
|
|
103
|
+
const plan = buildMutatePlan(localRecords, remoteRecords, { deleteMissing: false });
|
|
104
|
+
expect(plan.deleteResourceRecords).toHaveLength(0);
|
|
105
|
+
expect(plan.createResourceRecords).toHaveLength(2);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("replaceIdsDeep / applyIdMapToRecords", () => {
|
|
110
|
+
it("should replace id, parentId and settings references", () => {
|
|
111
|
+
const idMap = {
|
|
112
|
+
"app-temp-1": "6a76d34f09e2c7630c859301",
|
|
113
|
+
"form-temp-1": "6a76d34f09e2c7630c859302",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = applyIdMapToRecords(localRecords, idMap);
|
|
117
|
+
|
|
118
|
+
expect(result[0].id).toBe("6a76d34f09e2c7630c859301");
|
|
119
|
+
expect(result[1].parentId).toBe("6a76d34f09e2c7630c859301");
|
|
120
|
+
expect(result[1].id).toBe("6a76d34f09e2c7630c859302");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should replace exact-string values inside settings", () => {
|
|
124
|
+
const record: any = {
|
|
125
|
+
id: "obj-temp",
|
|
126
|
+
kind: "object",
|
|
127
|
+
parentId: "app-temp-1",
|
|
128
|
+
settings: {
|
|
129
|
+
databaseResourceId: "app-temp-1",
|
|
130
|
+
title: "keep app-temp-1 as literal substring?",
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const result = applyIdMapToRecords([record], { "app-temp-1": "SERVER-APP" }) as any;
|
|
134
|
+
|
|
135
|
+
expect(result[0].settings.databaseResourceId).toBe("SERVER-APP");
|
|
136
|
+
expect(result[0].settings.title).toBe("keep app-temp-1 as literal substring?");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("buildIdMap", () => {
|
|
141
|
+
it("should map by index and skip identity", () => {
|
|
142
|
+
const map = buildIdMap(
|
|
143
|
+
[
|
|
144
|
+
{ id: "a", kind: "k", parentId: null, settings: {} },
|
|
145
|
+
{ id: "b", kind: "k", parentId: null, settings: {} },
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
{ id: "server-a", kind: "k", parentId: null, settings: {} },
|
|
149
|
+
{ id: "b", kind: "k", parentId: null, settings: {} },
|
|
150
|
+
],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(map).toEqual({ a: "server-a" });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should return empty map on length mismatch", () => {
|
|
157
|
+
const map = buildIdMap([{ id: "a", kind: "k", parentId: null, settings: {} }], []);
|
|
158
|
+
expect(map).toEqual({});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("mutateResourceRecords", () => {
|
|
163
|
+
it("should POST the exact mutation payload", async () => {
|
|
164
|
+
mockedAxios.post.mockResolvedValue({
|
|
165
|
+
data: {
|
|
166
|
+
data: {
|
|
167
|
+
mutateResourceRecords: {
|
|
168
|
+
createdResourceRecords: [{ id: "s1" }],
|
|
169
|
+
updatedResourceRecords: [{ id: "u1" }],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const res = await mutateResourceRecords({
|
|
176
|
+
endpoint: DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
177
|
+
token: "TOKEN",
|
|
178
|
+
createResourceRecords: [{ id: "a", kind: "k", parentId: null, settings: {} }],
|
|
179
|
+
updateResourceRecords: [],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const [, body, config] = mockedAxios.post.mock.calls[0];
|
|
183
|
+
expect((body as any).variables.createResourceRecords).toEqual([
|
|
184
|
+
{ id: "a", kind: "k", parentId: null, settings: {} },
|
|
185
|
+
]);
|
|
186
|
+
expect((config as any).headers.authorization).toBe("Bearer TOKEN");
|
|
187
|
+
expect((body as any).query).toContain("mutateResourceRecords");
|
|
188
|
+
expect(res.createdResourceRecords[0].id).toBe("s1");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should throw on GraphQL errors", async () => {
|
|
192
|
+
mockedAxios.post.mockResolvedValue({
|
|
193
|
+
data: { errors: [{ message: "boom" }] },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await expect(
|
|
197
|
+
mutateResourceRecords({
|
|
198
|
+
endpoint: DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
199
|
+
createResourceRecords: [],
|
|
200
|
+
updateResourceRecords: [],
|
|
201
|
+
}),
|
|
202
|
+
).rejects.toThrow("boom");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("mutateResources (auto mode)", () => {
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
mockedAxios.post.mockImplementation(async (url, body: any) => {
|
|
209
|
+
const query = String(body.query ?? "");
|
|
210
|
+
if (query.includes("resourceRecordConnection")) {
|
|
211
|
+
return {
|
|
212
|
+
data: {
|
|
213
|
+
data: {
|
|
214
|
+
resourceRecordConnection: {
|
|
215
|
+
pageInfo: { hasNextPage: false },
|
|
216
|
+
edges: remoteRecords.map((r) => ({ node: r })),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (query.includes("mutateResourceRecords")) {
|
|
223
|
+
return {
|
|
224
|
+
data: {
|
|
225
|
+
data: {
|
|
226
|
+
mutateResourceRecords: {
|
|
227
|
+
createdResourceRecords: [
|
|
228
|
+
{ id: "SERVER-APP", kind: "application", parentId: null, settings: {} },
|
|
229
|
+
{ id: "SERVER-FORM", kind: "form", parentId: "SERVER-APP", settings: {} },
|
|
230
|
+
],
|
|
231
|
+
updatedResourceRecords: [],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { data: {} };
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should return plan only in dry-run mode without calling mutation", async () => {
|
|
242
|
+
const res = await mutateResources({ sourceData, dryRun: true });
|
|
243
|
+
|
|
244
|
+
expect(res.dryRun).toBe(true);
|
|
245
|
+
expect(res.plan.createResourceRecords).toHaveLength(2);
|
|
246
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1); // 只拉取了远端
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should throw when token missing and there are writes", async () => {
|
|
250
|
+
await expect(mutateResources({ sourceData })).rejects.toThrow(/token/i);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should throw requiring --yes when soft-deleting", async () => {
|
|
254
|
+
process.env.RRM_ACCESS_TOKEN = "ENV_TOKEN";
|
|
255
|
+
await expect(mutateResources({ sourceData })).rejects.toThrow(/--yes/);
|
|
256
|
+
delete process.env.RRM_ACCESS_TOKEN;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should mutate, write back ids and confirm deletion with --yes", async () => {
|
|
260
|
+
mockedFs.writeJson.mockResolvedValue(undefined as never);
|
|
261
|
+
|
|
262
|
+
const res = await mutateResources({ sourceData, token: "TOKEN", yes: true });
|
|
263
|
+
|
|
264
|
+
expect(res.idMap).toEqual({ "app-temp-1": "SERVER-APP", "form-temp-1": "SERVER-FORM" });
|
|
265
|
+
expect(res.writeBackPath).toBe("/tmp/resource-records.json");
|
|
266
|
+
expect(mockedFs.writeJson).toHaveBeenCalledWith(
|
|
267
|
+
"/tmp/resource-records.json",
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
id: "SERVER-APP",
|
|
271
|
+
kind: "application",
|
|
272
|
+
parentId: null,
|
|
273
|
+
settings: { slug: "my-app", title: "My App", description: "" },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "SERVER-FORM",
|
|
277
|
+
kind: "form",
|
|
278
|
+
parentId: "SERVER-APP",
|
|
279
|
+
settings: { slug: "login", title: "Login", description: "" },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "existing-app",
|
|
283
|
+
kind: "application",
|
|
284
|
+
parentId: null,
|
|
285
|
+
settings: {
|
|
286
|
+
slug: "existing-application",
|
|
287
|
+
title: "Existing App",
|
|
288
|
+
description: "",
|
|
289
|
+
extra: true,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
{ spaces: 2 },
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should skip write-back when --no-write-back", async () => {
|
|
298
|
+
const res = await mutateResources({
|
|
299
|
+
sourceData,
|
|
300
|
+
token: "TOKEN",
|
|
301
|
+
yes: true,
|
|
302
|
+
writeBack: false,
|
|
303
|
+
});
|
|
304
|
+
expect(res.writeBackPath).toBeUndefined();
|
|
305
|
+
expect(mockedFs.writeJson).not.toHaveBeenCalled();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("mutateRecordsDirect (explicit mode)", () => {
|
|
310
|
+
it("should POST exactly the given records without fetching remote", async () => {
|
|
311
|
+
mockedAxios.post.mockResolvedValue({
|
|
312
|
+
data: {
|
|
313
|
+
data: {
|
|
314
|
+
mutateResourceRecords: {
|
|
315
|
+
createdResourceRecords: [{ id: "server-1" }],
|
|
316
|
+
updatedResourceRecords: [],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const res = await mutateRecordsDirect({
|
|
323
|
+
endpoint: DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
324
|
+
token: "TOKEN",
|
|
325
|
+
createResourceRecords: [{ id: "temp-1", kind: "table", parentId: null, settings: {} }],
|
|
326
|
+
updateResourceRecords: [{ id: "u1", kind: "table", parentId: null, settings: {} }],
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
330
|
+
const [, body] = mockedAxios.post.mock.calls[0];
|
|
331
|
+
expect((body as any).variables.createResourceRecords).toEqual([
|
|
332
|
+
{ id: "temp-1", kind: "table", parentId: null, settings: {} },
|
|
333
|
+
]);
|
|
334
|
+
expect((body as any).variables.updateResourceRecords).toEqual([
|
|
335
|
+
{ id: "u1", kind: "table", parentId: null, settings: {} },
|
|
336
|
+
]);
|
|
337
|
+
expect(res.createCount).toBe(1);
|
|
338
|
+
expect(res.updateCount).toBe(1);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should not POST in dry-run mode", async () => {
|
|
342
|
+
const res = await mutateRecordsDirect({
|
|
343
|
+
endpoint: DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
344
|
+
token: "TOKEN",
|
|
345
|
+
createResourceRecords: [{ id: "a", kind: "k", parentId: null, settings: {} }],
|
|
346
|
+
updateResourceRecords: [],
|
|
347
|
+
dryRun: true,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(res.dryRun).toBe(true);
|
|
351
|
+
expect(mockedAxios.post).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should throw when token missing", async () => {
|
|
355
|
+
await expect(
|
|
356
|
+
mutateRecordsDirect({
|
|
357
|
+
endpoint: DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
358
|
+
createResourceRecords: [{ id: "a", kind: "k", parentId: null, settings: {} }],
|
|
359
|
+
updateResourceRecords: [],
|
|
360
|
+
}),
|
|
361
|
+
).rejects.toThrow(/token/i);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("pullRemoteResourceRecords", () => {
|
|
366
|
+
it("should fetch remote records and write to output file", async () => {
|
|
367
|
+
mockedAxios.post.mockResolvedValue({
|
|
368
|
+
data: {
|
|
369
|
+
data: {
|
|
370
|
+
resourceRecordConnection: {
|
|
371
|
+
pageInfo: { hasNextPage: false },
|
|
372
|
+
edges: remoteRecords.map((r) => ({ node: r })),
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
mockedFs.writeJson.mockResolvedValue(undefined as never);
|
|
378
|
+
|
|
379
|
+
const res = await pullRemoteResourceRecords({
|
|
380
|
+
endpoint: DEFAULT_RRM_GRAPHQL_ENDPOINT,
|
|
381
|
+
output: "/tmp/pulled.json",
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(res.count).toBe(2);
|
|
385
|
+
expect(res.outputPath).toBe("/tmp/pulled.json");
|
|
386
|
+
expect(mockedFs.writeJson).toHaveBeenCalledWith("/tmp/pulled.json", remoteRecords, {
|
|
387
|
+
spaces: 2,
|
|
388
|
+
});
|
|
389
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { convertApaasResourceRecordsToResourceRecords } from "@ldsg/apaas-to-runtime";
|
|
5
4
|
import { AppData } from "@ldsg/types";
|
|
6
5
|
import {
|
|
7
6
|
extendAppData,
|
|
@@ -28,8 +27,13 @@ import { updateResource } from "./commands/resource/update";
|
|
|
28
27
|
import { deleteResource } from "./commands/resource/delete";
|
|
29
28
|
import { listResources, getTree } from "./commands/resource/list";
|
|
30
29
|
import { getResource } from "./commands/resource/get";
|
|
31
|
-
import { convertResource } from "./commands/resource/convert";
|
|
32
30
|
import { diffResource } from "./commands/resource/diff";
|
|
31
|
+
import {
|
|
32
|
+
mutateRecordsDirect,
|
|
33
|
+
mutateResources,
|
|
34
|
+
pullRemoteResourceRecords,
|
|
35
|
+
purifyResourceRecord,
|
|
36
|
+
} from "./commands/resource/mutate";
|
|
33
37
|
|
|
34
38
|
const { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
35
39
|
|
|
@@ -42,12 +46,16 @@ program
|
|
|
42
46
|
.aliases(["g", "gen"])
|
|
43
47
|
.description("Generate standalone application from resource records")
|
|
44
48
|
.option("--app-data-uri <uri>", "runtime app data URI")
|
|
45
|
-
.option("--apaas-app-data-uri <uri>", "aPaaS app data URI (GraphQL)")
|
|
46
49
|
.option(
|
|
47
|
-
"--
|
|
48
|
-
"
|
|
50
|
+
"--app-data-expression <expression>",
|
|
51
|
+
"runtime app data expression (jsonata), used to convert source data to standard ldsg resource records",
|
|
49
52
|
)
|
|
50
53
|
.option("--using-workspace-ldsg-module", "use workspace ldsg module mode")
|
|
54
|
+
.option(
|
|
55
|
+
"--layout <layout>",
|
|
56
|
+
`app layout: nested or flat (default: flat for workspace mode, nested otherwise)`,
|
|
57
|
+
)
|
|
58
|
+
.option("--app-package-name <name>", "app package name (default: @ldsg/app)")
|
|
51
59
|
.option(
|
|
52
60
|
"-o, --output <path>",
|
|
53
61
|
`output directory for prepared app code (default: ${DEFAULT_OUTPUT_PATH})`,
|
|
@@ -57,27 +65,32 @@ program
|
|
|
57
65
|
|
|
58
66
|
const usingWorkspaceLdsgModule = options.usingWorkspaceLdsgModule ?? false;
|
|
59
67
|
|
|
60
|
-
const
|
|
68
|
+
const layout =
|
|
69
|
+
options.layout === "nested"
|
|
70
|
+
? ZipAppDataLayout.NESTED
|
|
71
|
+
: options.layout === "flat"
|
|
72
|
+
? ZipAppDataLayout.FLAT
|
|
73
|
+
: undefined;
|
|
74
|
+
if (options.layout && layout === undefined) {
|
|
75
|
+
throw new Error(`invalid layout: ${options.layout} (expected nested or flat)`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { resourceRecords } = await loadAppResourceRecords({
|
|
61
79
|
appDataUri: options.appDataUri,
|
|
62
|
-
|
|
63
|
-
apaasAppDataExpression: options.apaasAppDataExpression,
|
|
80
|
+
appDataExpression: options.appDataExpression,
|
|
64
81
|
});
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (sourceType === "apaas") {
|
|
69
|
-
const { targetResourceRecords } = convertApaasResourceRecordsToResourceRecords({
|
|
70
|
-
resourceRecords,
|
|
71
|
-
});
|
|
72
|
-
appData = { resourceRecords: targetResourceRecords };
|
|
73
|
-
} else {
|
|
74
|
-
appData = { resourceRecords };
|
|
75
|
-
}
|
|
83
|
+
const appData: AppData = { resourceRecords };
|
|
76
84
|
|
|
77
85
|
let extendAppDataParams: ExtendAppDataParams;
|
|
78
86
|
|
|
79
87
|
if (usingWorkspaceLdsgModule) {
|
|
80
|
-
extendAppDataParams = await getExtendAppDataParamsUsingWorkspaceLdsgModule({
|
|
88
|
+
extendAppDataParams = await getExtendAppDataParamsUsingWorkspaceLdsgModule({
|
|
89
|
+
appData,
|
|
90
|
+
...(layout ? { layout } : {}),
|
|
91
|
+
...(options.appPackageName ? { appPackageName: options.appPackageName } : {}),
|
|
92
|
+
outputPath,
|
|
93
|
+
});
|
|
81
94
|
} else {
|
|
82
95
|
extendAppDataParams = await getExtendAppDataParamsUsingNpmLdsgModule({ appData });
|
|
83
96
|
}
|
|
@@ -87,7 +100,7 @@ program
|
|
|
87
100
|
await prepare({
|
|
88
101
|
...extendAppDataRes,
|
|
89
102
|
outputPath,
|
|
90
|
-
...(usingWorkspaceLdsgModule ? { layout: ZipAppDataLayout.FLAT } : {}),
|
|
103
|
+
...(layout ? { layout } : usingWorkspaceLdsgModule ? { layout: ZipAppDataLayout.FLAT } : {}),
|
|
91
104
|
});
|
|
92
105
|
});
|
|
93
106
|
|
|
@@ -324,40 +337,6 @@ resource
|
|
|
324
337
|
}
|
|
325
338
|
});
|
|
326
339
|
|
|
327
|
-
resource
|
|
328
|
-
.command("convert")
|
|
329
|
-
.description("Convert resource records between aPaaS and Runtime formats")
|
|
330
|
-
.option("--from <format>", "source format (apaas or runtime)")
|
|
331
|
-
.option("--to <format>", "target format (apaas or runtime)")
|
|
332
|
-
.option("--input <path>", "input file path")
|
|
333
|
-
.option("--output <path>", "output file path (omit for stdout)")
|
|
334
|
-
.action(async (options) => {
|
|
335
|
-
try {
|
|
336
|
-
if (!options.from || !options.to || !options.input) {
|
|
337
|
-
console.error(
|
|
338
|
-
formatJson(formatError("MISSING_ARGS", "Required options: --from, --to, --input")),
|
|
339
|
-
);
|
|
340
|
-
process.exit(1);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const result = await convertResource({
|
|
344
|
-
from: options.from,
|
|
345
|
-
to: options.to,
|
|
346
|
-
input: options.input,
|
|
347
|
-
output: options.output,
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
if (result.outputPath) {
|
|
351
|
-
console.log(formatJson(formatSuccess({ outputPath: result.outputPath })));
|
|
352
|
-
} else {
|
|
353
|
-
console.log(formatJson(formatSuccess({ data: result.data })));
|
|
354
|
-
}
|
|
355
|
-
} catch (error) {
|
|
356
|
-
console.error(formatJson(formatError("CONVERT_ERROR", (error as Error).message)));
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
340
|
resource
|
|
362
341
|
.command("diff <id>")
|
|
363
342
|
.description("Diff a resource between two sources")
|
|
@@ -395,4 +374,150 @@ resource
|
|
|
395
374
|
}
|
|
396
375
|
});
|
|
397
376
|
|
|
377
|
+
resource
|
|
378
|
+
.command("pull")
|
|
379
|
+
.description("Pull resource records from RRM to a local file (read-only)")
|
|
380
|
+
.option("--endpoint <url>", "RRM GraphQL endpoint")
|
|
381
|
+
.option("--path <regex>", "scope remote records to a path regex (e.g. application id)")
|
|
382
|
+
.option("-o, --output <path>", `output file (default: ${DEFAULT_SOURCE_PATH})`)
|
|
383
|
+
.action(async (options) => {
|
|
384
|
+
try {
|
|
385
|
+
const result = await pullRemoteResourceRecords({
|
|
386
|
+
endpoint: options.endpoint,
|
|
387
|
+
path: options.path,
|
|
388
|
+
output: options.output,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
console.log(formatJson(formatSuccess(result)));
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error(formatJson(formatError("PULL_ERROR", (error as Error).message)));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
async function parseExplicitRecords(
|
|
399
|
+
value?: string,
|
|
400
|
+
file?: string,
|
|
401
|
+
): Promise<
|
|
402
|
+
| { id: string; kind: string; parentId: string | null; settings: Record<string, unknown> }[]
|
|
403
|
+
| undefined
|
|
404
|
+
> {
|
|
405
|
+
if (value && file) {
|
|
406
|
+
throw new Error("Cannot use both an inline value and a file for the same record list");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let data: unknown;
|
|
410
|
+
|
|
411
|
+
if (file) {
|
|
412
|
+
const fs = await import("fs-extra");
|
|
413
|
+
data = await fs.readJson(file);
|
|
414
|
+
} else if (value) {
|
|
415
|
+
data = JSON.parse(value);
|
|
416
|
+
} else {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const list = Array.isArray(data) ? data : [data];
|
|
421
|
+
return list.map((record) => purifyResourceRecord(record as never));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
resource
|
|
425
|
+
.command("mutate")
|
|
426
|
+
.description("Mutate local resource records on RRM via mutateResourceRecords")
|
|
427
|
+
.option("--source <path>", `data source (default: ${DEFAULT_SOURCE_PATH})`)
|
|
428
|
+
.option("--endpoint <url>", "RRM GraphQL endpoint")
|
|
429
|
+
.option("--token <token>", "RRM Bearer token (or RRM_ACCESS_TOKEN env)")
|
|
430
|
+
.option(
|
|
431
|
+
"--path <regex>",
|
|
432
|
+
"scope remote records to a path regex (e.g. application id) before splitting",
|
|
433
|
+
)
|
|
434
|
+
.option(
|
|
435
|
+
"--no-delete-missing",
|
|
436
|
+
"do not soft-delete remote-only records (default: soft-delete like console)",
|
|
437
|
+
)
|
|
438
|
+
.option("--no-write-back", "do not write server-assigned ids back to the local source file")
|
|
439
|
+
.option("--yes", "confirm soft-deletion without prompting")
|
|
440
|
+
.option("--dry-run", "preview the plan without sending")
|
|
441
|
+
.option("--create <json>", "explicit create records as JSON (skip auto split)")
|
|
442
|
+
.option("--update <json>", "explicit update records as JSON (skip auto split)")
|
|
443
|
+
.option("--create-file <path>", "explicit create records from JSON file (skip auto split)")
|
|
444
|
+
.option("--update-file <path>", "explicit update records from JSON file (skip auto split)")
|
|
445
|
+
.action(async (options) => {
|
|
446
|
+
try {
|
|
447
|
+
const explicitCreate = await parseExplicitRecords(options.create, options.createFile);
|
|
448
|
+
const explicitUpdate = await parseExplicitRecords(options.update, options.updateFile);
|
|
449
|
+
|
|
450
|
+
if (explicitCreate !== undefined || explicitUpdate !== undefined) {
|
|
451
|
+
const result = await mutateRecordsDirect({
|
|
452
|
+
endpoint: options.endpoint,
|
|
453
|
+
token: options.token,
|
|
454
|
+
createResourceRecords: explicitCreate ?? [],
|
|
455
|
+
updateResourceRecords: explicitUpdate ?? [],
|
|
456
|
+
dryRun: options.dryRun,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
console.log(
|
|
460
|
+
formatJson(
|
|
461
|
+
formatSuccess({
|
|
462
|
+
mode: "explicit",
|
|
463
|
+
dryRun: result.dryRun,
|
|
464
|
+
createCount: result.createCount,
|
|
465
|
+
updateCount: result.updateCount,
|
|
466
|
+
createdResourceRecords: result.createdResourceRecords,
|
|
467
|
+
updatedResourceRecords: result.updatedResourceRecords,
|
|
468
|
+
}),
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (isStdinSource(options.source)) {
|
|
475
|
+
console.error(
|
|
476
|
+
formatJson(
|
|
477
|
+
formatError(
|
|
478
|
+
"STDIN_NOT_SUPPORTED",
|
|
479
|
+
"stdin source is not supported for mutate; use a local JSON file",
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const sourceData = await resolveSource(options.source);
|
|
487
|
+
|
|
488
|
+
const result = await mutateResources({
|
|
489
|
+
sourceData,
|
|
490
|
+
endpoint: options.endpoint,
|
|
491
|
+
token: options.token,
|
|
492
|
+
path: options.path,
|
|
493
|
+
deleteMissing: options.deleteMissing,
|
|
494
|
+
writeBack: options.writeBack,
|
|
495
|
+
yes: options.yes,
|
|
496
|
+
dryRun: options.dryRun,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const { plan } = result;
|
|
500
|
+
|
|
501
|
+
console.log(
|
|
502
|
+
formatJson(
|
|
503
|
+
formatSuccess({
|
|
504
|
+
mode: "auto",
|
|
505
|
+
dryRun: result.dryRun,
|
|
506
|
+
source: getSourceDescription(options.source),
|
|
507
|
+
remoteCount: plan.remoteCount,
|
|
508
|
+
localCount: plan.localCount,
|
|
509
|
+
createCount: plan.createResourceRecords.length,
|
|
510
|
+
updateCount: plan.updateResourceRecords.length,
|
|
511
|
+
deleteCount: plan.deleteResourceRecords.length,
|
|
512
|
+
idMap: result.idMap,
|
|
513
|
+
writeBackPath: result.writeBackPath,
|
|
514
|
+
}),
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
console.error(formatJson(formatError("MUTATE_ERROR", (error as Error).message)));
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
398
523
|
program.parse(process.argv);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResourceRecord } from "@ldsg/types";
|
|
1
|
+
import { ResourceRecord, ResourceSettings } from "@ldsg/types";
|
|
2
2
|
import { SourceData } from "../../types/resource-record";
|
|
3
3
|
import { appendRecord, generateId } from "../../utils/json-file-mutator";
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ export async function createResource(options: CreateOptions): Promise<{ record:
|
|
|
26
26
|
id: partial.id || generateId(),
|
|
27
27
|
kind: partial.kind,
|
|
28
28
|
parentId: partial.parentId ?? sourceData.resourceRecords[0]?.parentId ?? null,
|
|
29
|
-
settings: (partial.settings || {}) as
|
|
29
|
+
settings: (partial.settings || {}) as ResourceSettings,
|
|
30
30
|
};
|
|
31
31
|
} else {
|
|
32
32
|
if (!options.kind) {
|
|
@@ -40,7 +40,7 @@ export async function createResource(options: CreateOptions): Promise<{ record:
|
|
|
40
40
|
id: options.id || generateId(),
|
|
41
41
|
kind: options.kind,
|
|
42
42
|
parentId: options.parentId ?? null,
|
|
43
|
-
settings: options.settings || {},
|
|
43
|
+
settings: (options.settings || {}) as ResourceSettings,
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|