Skip to content

Commit 05e99b3

Browse files
[catalog-server] Handle invalid and not found packages better. (#1404)
1 parent 29a9b6f commit 05e99b3

File tree

17 files changed

+260
-85
lines changed

17 files changed

+260
-85
lines changed

packages/catalog-api/src/lib/schema.graphql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ type DistTag {
7272
"""
7373
The status of a package that has been successfully imported.
7474
75-
A package may also has a status from the UnreadablePackageStatus enum.
75+
A package may also have a status from the UnreadablePackageStatus enum.
7676
"""
7777
enum ReadablePackageStatus {
7878
"""
@@ -90,7 +90,7 @@ enum ReadablePackageStatus {
9090
"""
9191
The status of a package that has not been successfully imported.
9292
93-
A package may also has a status from the ReadablePackageStatus enum.
93+
A package may also have a status from the ReadablePackageStatus enum.
9494
"""
9595
enum UnreadablePackageStatus {
9696
"""
@@ -104,7 +104,7 @@ enum UnreadablePackageStatus {
104104
NOT_FOUND
105105

106106
"""
107-
An other recoverable error occured, such as network failure.
107+
A recoverable error occured, such as network failure.
108108
"""
109109
ERROR
110110
}

packages/catalog-server/src/lib/catalog.ts

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {distTagMapToList, getDistTagsForVersion} from './npm.js';
88
import {validatePackage} from '@webcomponents/custom-elements-manifest-tools/lib/validate.js';
99
import {getCustomElements} from '@webcomponents/custom-elements-manifest-tools';
1010
import {
11+
HttpError,
1112
Package,
1213
PackageFiles,
1314
} from '@webcomponents/custom-elements-manifest-tools/lib/npm.js';
@@ -19,6 +20,8 @@ import {
1920
PackageInfo,
2021
ReadablePackageInfo,
2122
CustomElement,
23+
UnreadablePackageStatus,
24+
VersionStatus,
2225
} from '@webcomponents/catalog-api/lib/schema.js';
2326
import {
2427
Temporal,
@@ -100,20 +103,29 @@ export class Catalog {
100103

101104
// Fetch package metadata from npm:
102105
console.log('Fetching package metadata...');
103-
let newPackage: Package | undefined;
106+
let newPackage: Package;
104107
try {
105108
newPackage = await this.#files.getPackageMetadata(packageName);
106109
} catch (e) {
107-
await this.#repository.endPackageImportWithError(packageName);
108-
return {};
110+
console.error('Error fetching package metadata');
111+
console.error(e);
112+
let status: UnreadablePackageStatus = UnreadablePackageStatus.ERROR;
113+
if ((e as HttpError).statusCode === 404) {
114+
status = UnreadablePackageStatus.NOT_FOUND;
115+
}
116+
const packageInfo = await this.#repository.endPackageImportWithError(
117+
packageName,
118+
status
119+
);
120+
return {packageInfo};
121+
} finally {
122+
console.log(' done');
109123
}
110-
console.log(' done');
111124

112125
if (newPackage === undefined) {
113-
await this.#repository.endPackageImportWithNotFound(packageName);
114126
// TODO (justinfagnani): a crazy edge case would be a package that was
115127
// previously found, but is not found now. Update package versions?
116-
return {};
128+
throw new Error(`Internal error importing package ${packageName}`);
117129
}
118130

119131
const newDistTags = newPackage['dist-tags'];
@@ -176,7 +188,7 @@ export class Catalog {
176188

177189
if (versionToImport !== undefined) {
178190
importResult = await this.importPackageVersion(
179-
packageName,
191+
newPackage,
180192
versionToImport
181193
);
182194
}
@@ -200,22 +212,44 @@ export class Catalog {
200212
};
201213
}
202214

203-
async importPackageVersion(packageName: string, version: string) {
215+
async importPackageVersion(packageOrName: string | Package, version: string) {
216+
const packageName =
217+
typeof packageOrName === 'string' ? packageOrName : packageOrName.name;
218+
204219
console.log('Marking package version as importing...');
205220
await this.#repository.startPackageVersionImport(packageName, version);
206221
console.log(' done');
207222

223+
let packageMetadata: Package;
224+
225+
if (typeof packageOrName === 'string') {
226+
try {
227+
packageMetadata = await this.#files.getPackageMetadata(packageName);
228+
} catch (e) {
229+
console.error(`Error fetching packageMetadata`);
230+
console.error(e);
231+
console.log('Marking package version as errored...');
232+
// We mark this as a recoverable error, since another attempt might
233+
// be able to fetch the package and therefore this package version.
234+
const packageVersion =
235+
await this.#repository.endPackageVersionImportWithError(
236+
packageName,
237+
version,
238+
VersionStatus.ERROR
239+
);
240+
console.log(' done');
241+
return {packageVersion};
242+
}
243+
} else {
244+
packageMetadata = packageOrName;
245+
}
246+
208247
const {manifestData, manifestSource, problems} = await validatePackage({
209248
packageName,
210249
version,
211250
files: this.#files,
212251
});
213252

214-
// TODO (justinfagnani): If we're calling this from importPackage(), we'll
215-
// already have package metadata, so either use that rather making another
216-
// call, or cache the manifests in PackageFiles.
217-
const packageMetadataPromise = this.#files.getPackageMetadata(packageName);
218-
219253
if (problems.length > 0) {
220254
console.log('Writing problems...');
221255
await this.#repository.writeProblems(packageName, version, problems);
@@ -224,13 +258,15 @@ export class Catalog {
224258

225259
if (manifestData === undefined) {
226260
console.error(`manifestData not found`);
227-
console.log('Marking package version as errored...');
228-
await this.#repository.endPackageVersionImportWithError(
229-
packageName,
230-
version
231-
);
261+
console.log('Marking package version as INVALID...');
262+
const packageVersion =
263+
await this.#repository.endPackageVersionImportWithError(
264+
packageName,
265+
version,
266+
VersionStatus.INVALID
267+
);
232268
console.log(' done');
233-
return {problems};
269+
return {packageVersion, problems};
234270
}
235271

236272
const customElements = getCustomElements(
@@ -241,27 +277,15 @@ export class Catalog {
241277

242278
if (customElements.length === 0) {
243279
console.error(`No customElements found`);
244-
console.log(manifestSource);
245-
console.log('Marking package version as errored...');
246-
await this.#repository.endPackageVersionImportWithError(
247-
packageName,
248-
version
249-
);
250-
console.log(' done');
251-
return {problems};
252-
}
253-
254-
const packageMetadata = await packageMetadataPromise;
255-
256-
if (packageMetadata === undefined) {
257-
console.error(`packageMetadata not found`);
258-
console.log('Marking package version as errored...');
259-
await this.#repository.endPackageVersionImportWithError(
260-
packageName,
261-
version
262-
);
280+
console.log('Marking package version as INVALID...');
281+
const packageVersion =
282+
await this.#repository.endPackageVersionImportWithError(
283+
packageName,
284+
version,
285+
VersionStatus.INVALID
286+
);
263287
console.log(' done');
264-
return {problems};
288+
return {packageVersion, problems};
265289
}
266290

267291
// eslint-disable-next-line

packages/catalog-server/src/lib/firestore/firestore-repository.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Query,
1010
CollectionReference,
1111
CollectionGroup,
12+
UpdateData,
1213
} from '@google-cloud/firestore';
1314
import {Firestore} from '@google-cloud/firestore';
1415
import firebase from 'firebase-admin';
@@ -29,6 +30,7 @@ import {
2930
ValidationProblem,
3031
ReadablePackageVersion,
3132
ReadablePackageInfo,
33+
UnreadablePackageStatus,
3234
} from '@webcomponents/catalog-api/lib/schema.js';
3335
import {
3436
Package,
@@ -105,7 +107,10 @@ export class FirestoreRepository implements Repository {
105107
});
106108
}
107109

108-
async endPackageImportWithNotFound(packageName: string): Promise<void> {
110+
async endPackageImportWithError(
111+
packageName: string,
112+
status: UnreadablePackageStatus
113+
): Promise<PackageInfo> {
109114
const packageRef = this.getPackageRef(packageName);
110115
await db.runTransaction(async (t) => {
111116
const packageDoc = await t.get(packageRef);
@@ -116,31 +121,20 @@ export class FirestoreRepository implements Repository {
116121
if (packageData.status !== PackageStatus.INITIALIZING) {
117122
throw new Error(`Unexpected package status: ${packageData.status}`);
118123
}
119-
await t.set(packageRef, {
124+
t.set(packageRef, {
120125
name: packageName,
121-
status: PackageStatus.NOT_FOUND,
126+
status,
122127
lastUpdate: FieldValue.serverTimestamp(),
123128
});
124129
});
125-
}
126-
127-
async endPackageImportWithError(packageName: string): Promise<void> {
128-
const packageRef = this.getPackageRef(packageName);
129-
await db.runTransaction(async (t) => {
130-
const packageDoc = await t.get(packageRef);
131-
const packageData = packageDoc.data();
132-
if (packageData === undefined) {
133-
throw new Error(`Package not found: ${packageName}`);
134-
}
135-
if (packageData.status !== PackageStatus.INITIALIZING) {
136-
throw new Error(`Unexpected package status: ${packageData.status}`);
137-
}
138-
await t.set(packageRef, {
139-
name: packageName,
140-
status: PackageStatus.ERROR,
141-
lastUpdate: FieldValue.serverTimestamp(),
142-
});
130+
const packageInfo = await db.runTransaction(async (t) => {
131+
// There doesn't seem to be a way to get a WriteResult and therefore
132+
// a writeTime inside a transaction, so we read from the database to
133+
// get the server timestamp.
134+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
135+
return (await t.get(packageRef)).data()!;
143136
});
137+
return packageInfo;
144138
}
145139

146140
/**
@@ -266,10 +260,11 @@ export class FirestoreRepository implements Repository {
266260

267261
async endPackageVersionImportWithError(
268262
packageName: string,
269-
version: string
270-
): Promise<void> {
263+
version: string,
264+
status: VersionStatus
265+
): Promise<PackageVersion> {
266+
const versionRef = this.getPackageVersionRef(packageName, version);
271267
await db.runTransaction(async (t) => {
272-
const versionRef = this.getPackageVersionRef(packageName, version);
273268
const packageVersionDoc = await t.get(
274269
versionRef.withConverter(packageVersionConverter)
275270
);
@@ -282,11 +277,21 @@ export class FirestoreRepository implements Repository {
282277
`Unexpected package version status: ${packageVersionData.status}`
283278
);
284279
}
285-
await t.update(versionRef, {
286-
status: VersionStatus.ERROR,
280+
// TODO (justinfagnani): figure out why we need the cast, since
281+
// UpdateData<T> should allow a Partial<T>
282+
t.update(versionRef, {
283+
status,
287284
lastUpdate: FieldValue.serverTimestamp(),
288-
});
285+
} as UpdateData<PackageVersion> as PackageVersion);
289286
});
287+
const packageVersion = await db.runTransaction(async (t) => {
288+
// There doesn't seem to be a way to get a WriteResult and therefore
289+
// a writeTime inside a transaction, so we read from the database to
290+
// get the server timestamp.
291+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292+
return (await t.get(versionRef)).data()!;
293+
});
294+
return packageVersion;
290295
}
291296

292297
async writeCustomElements(
@@ -477,7 +482,10 @@ export class FirestoreRepository implements Repository {
477482
packageName,
478483
versionOrTag
479484
);
480-
version = packageVersion!.version;
485+
if (packageVersion === undefined) {
486+
return [];
487+
}
488+
version = packageVersion.version;
481489
} else {
482490
version = versionOrTag;
483491
}

packages/catalog-server/src/lib/repository.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import type {
1010
PackageVersion,
1111
ReadablePackageInfo,
1212
ReadablePackageVersion,
13+
UnreadablePackageStatus,
1314
ValidationProblem,
15+
VersionStatus,
1416
} from '@webcomponents/catalog-api/lib/schema';
1517
import type {CustomElementInfo} from '@webcomponents/custom-elements-manifest-tools';
1618
import type {
@@ -48,9 +50,10 @@ export interface Repository {
4850

4951
startPackageUpdate(packageName: string): Promise<void>;
5052

51-
endPackageImportWithNotFound(packageName: string): Promise<void>;
52-
53-
endPackageImportWithError(packageName: string): Promise<void>;
53+
endPackageImportWithError(
54+
packageName: string,
55+
status: UnreadablePackageStatus
56+
): Promise<PackageInfo>;
5457

5558
endPackageImportWithReady(
5659
packageName: string,
@@ -85,13 +88,14 @@ export interface Repository {
8588
): Promise<ReadablePackageVersion>;
8689

8790
/**
88-
* Updates a PackageVersion to status: ERROR. Verifies that the PackageVersion
89-
* was in INITIALIZING status before the update.
91+
* Updates a PackageVersion to the given status, which should be ERROR or INVALID.
92+
* Verifies that the PackageVersion was in INITIALIZING status before the update.
9093
*/
9194
endPackageVersionImportWithError(
9295
packageName: string,
93-
version: string
94-
): Promise<void>;
96+
version: string,
97+
status: VersionStatus
98+
): Promise<PackageVersion>;
9599

96100
writeCustomElements(
97101
packageVersionMetadata: Version,

packages/catalog-server/src/lib/server/routes/bootstrap-packages.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const makeBootstrapPackagesRoute =
2727
async (
2828
packageName
2929
): Promise<
30-
| {error: unknown; packageName: string}
30+
| {error: Error; packageName: string}
3131
| {
3232
packageName: string;
3333
elements: Array<CustomElement>;
@@ -51,7 +51,7 @@ export const makeBootstrapPackagesRoute =
5151
} catch (error) {
5252
return {
5353
packageName,
54-
error,
54+
error: error as Error,
5555
};
5656
}
5757
}
@@ -67,7 +67,7 @@ export const makeBootstrapPackagesRoute =
6767
if ('error' in result) {
6868
return `
6969
<h3>${packageName}</h3>
70-
<code><pre>${result.error}</pre></code>
70+
<code><pre>${result.error}\n${result.error.stack ?? ''}</pre></code>
7171
`;
7272
} else {
7373
const {elements} = result;

0 commit comments

Comments
 (0)