Skip to content

Commit d8687b5

Browse files
Merge pull request #2621 from github/robertbrignull/data-saving
Load existing modeled methods before saving, to avoid overwriting data
2 parents f2c7c41 + bb24614 commit d8687b5

5 files changed

Lines changed: 582 additions & 39 deletions

File tree

extensions/ql-vscode/src/data-extensions-editor/data-extensions-editor-view.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export class DataExtensionsEditorView extends AbstractWebview<
123123
msg.externalApiUsages,
124124
msg.modeledMethods,
125125
this.mode,
126+
this.cliServer,
126127
this.app.logger,
127128
);
128129
await Promise.all([this.setViewState(), this.loadExternalApiUsages()]);

extensions/ql-vscode/src/data-extensions-editor/modeled-method-fs.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@ import { ExternalApiUsage } from "./external-api-usage";
33
import { ModeledMethod } from "./modeled-method";
44
import { Mode } from "./shared/mode";
55
import { createDataExtensionYamls, loadDataExtensionYaml } from "./yaml";
6-
import { join } from "path";
6+
import { join, relative } from "path";
77
import { ExtensionPack } from "./shared/extension-pack";
8-
import {
9-
Logger,
10-
NotificationLogger,
11-
showAndLogErrorMessage,
12-
} from "../common/logging";
8+
import { NotificationLogger, showAndLogErrorMessage } from "../common/logging";
139
import { getOnDiskWorkspaceFolders } from "../common/vscode/workspace-folders";
1410
import { load as loadYaml } from "js-yaml";
1511
import { CodeQLCliServer } from "../codeql-cli/cli";
@@ -22,13 +18,21 @@ export async function saveModeledMethods(
2218
externalApiUsages: ExternalApiUsage[],
2319
modeledMethods: Record<string, ModeledMethod>,
2420
mode: Mode,
25-
logger: Logger,
21+
cliServer: CodeQLCliServer,
22+
logger: NotificationLogger,
2623
): Promise<void> {
24+
const existingModeledMethods = await loadModeledMethodFiles(
25+
extensionPack,
26+
cliServer,
27+
logger,
28+
);
29+
2730
const yamls = createDataExtensionYamls(
2831
databaseName,
2932
language,
3033
externalApiUsages,
3134
modeledMethods,
35+
existingModeledMethods,
3236
mode,
3337
);
3438

@@ -39,17 +43,20 @@ export async function saveModeledMethods(
3943
void logger.log(`Saved data extension YAML`);
4044
}
4145

42-
export async function loadModeledMethods(
46+
async function loadModeledMethodFiles(
4347
extensionPack: ExtensionPack,
4448
cliServer: CodeQLCliServer,
4549
logger: NotificationLogger,
46-
): Promise<Record<string, ModeledMethod>> {
50+
): Promise<Record<string, Record<string, ModeledMethod>>> {
4751
const modelFiles = await listModelFiles(extensionPack.path, cliServer);
4852

49-
const existingModeledMethods: Record<string, ModeledMethod> = {};
53+
const modeledMethodsByFile: Record<
54+
string,
55+
Record<string, ModeledMethod>
56+
> = {};
5057

5158
for (const modelFile of modelFiles) {
52-
const yaml = await readFile(modelFile, "utf8");
59+
const yaml = await readFile(join(extensionPack.path, modelFile), "utf8");
5360

5461
const data = loadYaml(yaml, {
5562
filename: modelFile,
@@ -63,7 +70,25 @@ export async function loadModeledMethods(
6370
);
6471
continue;
6572
}
73+
modeledMethodsByFile[modelFile] = modeledMethods;
74+
}
75+
76+
return modeledMethodsByFile;
77+
}
6678

79+
export async function loadModeledMethods(
80+
extensionPack: ExtensionPack,
81+
cliServer: CodeQLCliServer,
82+
logger: NotificationLogger,
83+
): Promise<Record<string, ModeledMethod>> {
84+
const existingModeledMethods: Record<string, ModeledMethod> = {};
85+
86+
const modeledMethodsByFile = await loadModeledMethodFiles(
87+
extensionPack,
88+
cliServer,
89+
logger,
90+
);
91+
for (const modeledMethods of Object.values(modeledMethodsByFile)) {
6792
for (const [key, value] of Object.entries(modeledMethods)) {
6893
existingModeledMethods[key] = value;
6994
}
@@ -85,7 +110,7 @@ export async function listModelFiles(
85110
for (const [path, extensions] of Object.entries(result.data)) {
86111
if (pathsEqual(path, extensionPackPath)) {
87112
for (const extension of extensions) {
88-
modelFiles.add(extension.file);
113+
modelFiles.add(relative(extensionPackPath, extension.file));
89114
}
90115
}
91116
}

extensions/ql-vscode/src/data-extensions-editor/yaml.ts

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,25 @@ export function createDataExtensionYamls(
7272
databaseName: string,
7373
language: string,
7474
externalApiUsages: ExternalApiUsage[],
75-
modeledMethods: Record<string, ModeledMethod>,
75+
newModeledMethods: Record<string, ModeledMethod>,
76+
existingModeledMethods: Record<string, Record<string, ModeledMethod>>,
7677
mode: Mode,
7778
) {
7879
switch (mode) {
7980
case Mode.Application:
8081
return createDataExtensionYamlsForApplicationMode(
8182
language,
8283
externalApiUsages,
83-
modeledMethods,
84+
newModeledMethods,
85+
existingModeledMethods,
8486
);
8587
case Mode.Framework:
8688
return createDataExtensionYamlsForFrameworkMode(
8789
databaseName,
8890
language,
8991
externalApiUsages,
90-
modeledMethods,
92+
newModeledMethods,
93+
existingModeledMethods,
9194
);
9295
default:
9396
assertNever(mode);
@@ -97,27 +100,51 @@ export function createDataExtensionYamls(
97100
export function createDataExtensionYamlsForApplicationMode(
98101
language: string,
99102
externalApiUsages: ExternalApiUsage[],
100-
modeledMethods: Record<string, ModeledMethod>,
103+
newModeledMethods: Record<string, ModeledMethod>,
104+
existingModeledMethods: Record<string, Record<string, ModeledMethod>>,
101105
): Record<string, string> {
102-
const methodsByLibraryFilename: Record<string, ModeledMethod[]> = {};
103-
106+
const methodsByLibraryFilename: Record<
107+
string,
108+
Record<string, ModeledMethod>
109+
> = {};
110+
111+
// We only want to generate a yaml file when it's a known external API usage
112+
// and there are new modeled methods for it. This avoids us overwriting other
113+
// files that may contain data we don't know about.
104114
for (const externalApiUsage of externalApiUsages) {
105-
const modeledMethod = modeledMethods[externalApiUsage.signature];
106-
if (!modeledMethod) {
107-
continue;
115+
if (externalApiUsage.signature in newModeledMethods) {
116+
methodsByLibraryFilename[
117+
createFilenameForLibrary(externalApiUsage.library)
118+
] = {};
108119
}
120+
}
109121

110-
const filename = createFilenameForLibrary(externalApiUsage.library);
122+
// First populate methodsByLibraryFilename with any existing modeled methods.
123+
for (const [filename, methods] of Object.entries(existingModeledMethods)) {
124+
if (filename in methodsByLibraryFilename) {
125+
for (const [signature, method] of Object.entries(methods)) {
126+
methodsByLibraryFilename[filename][signature] = method;
127+
}
128+
}
129+
}
111130

112-
methodsByLibraryFilename[filename] =
113-
methodsByLibraryFilename[filename] || [];
114-
methodsByLibraryFilename[filename].push(modeledMethod);
131+
// Add the new modeled methods, potentially overwriting existing modeled methods
132+
// but not removing existing modeled methods that are not in the new set.
133+
for (const externalApiUsage of externalApiUsages) {
134+
const method = newModeledMethods[externalApiUsage.signature];
135+
if (method) {
136+
const filename = createFilenameForLibrary(externalApiUsage.library);
137+
methodsByLibraryFilename[filename][method.signature] = method;
138+
}
115139
}
116140

117141
const result: Record<string, string> = {};
118142

119143
for (const [filename, methods] of Object.entries(methodsByLibraryFilename)) {
120-
result[filename] = createDataExtensionYaml(language, methods);
144+
result[filename] = createDataExtensionYaml(
145+
language,
146+
Object.values(methods),
147+
);
121148
}
122149

123150
return result;
@@ -127,7 +154,8 @@ export function createDataExtensionYamlsForFrameworkMode(
127154
databaseName: string,
128155
language: string,
129156
externalApiUsages: ExternalApiUsage[],
130-
modeledMethods: Record<string, ModeledMethod>,
157+
newModeledMethods: Record<string, ModeledMethod>,
158+
existingModeledMethods: Record<string, Record<string, ModeledMethod>>,
131159
prefix = "models/",
132160
suffix = ".model",
133161
): Record<string, string> {
@@ -136,16 +164,28 @@ export function createDataExtensionYamlsForFrameworkMode(
136164
.slice(1)
137165
.map((part) => sanitizeExtensionPackName(part))
138166
.join("-");
167+
const filename = `${prefix}${libraryName}${suffix}.yml`;
168+
169+
const methods: Record<string, ModeledMethod> = {};
170+
171+
// First populate methodsByLibraryFilename with any existing modeled methods.
172+
for (const [signature, method] of Object.entries(
173+
existingModeledMethods[filename] || {},
174+
)) {
175+
methods[signature] = method;
176+
}
139177

140-
const methods = externalApiUsages
141-
.map((externalApiUsage) => modeledMethods[externalApiUsage.signature])
142-
.filter((modeledMethod) => modeledMethod !== undefined);
178+
// Add the new modeled methods, potentially overwriting existing modeled methods
179+
// but not removing existing modeled methods that are not in the new set.
180+
for (const externalApiUsage of externalApiUsages) {
181+
const modeledMethod = newModeledMethods[externalApiUsage.signature];
182+
if (modeledMethod) {
183+
methods[modeledMethod.signature] = modeledMethod;
184+
}
185+
}
143186

144187
return {
145-
[`${prefix}${libraryName}${suffix}.yml`]: createDataExtensionYaml(
146-
language,
147-
methods,
148-
),
188+
[filename]: createDataExtensionYaml(language, Object.values(methods)),
149189
};
150190
}
151191

0 commit comments

Comments
 (0)