-
Notifications
You must be signed in to change notification settings - Fork 702
/
Copy pathRootResolver.ts
365 lines (335 loc) · 9.52 KB
/
RootResolver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import IaItemModel from "../../../data/IaItemModel";
import SkinModel from "../../../data/SkinModel";
import TweetModel from "../../../data/TweetModel";
import SkinResolver from "../resolvers/SkinResolver";
import TweetResolver from "../resolvers/TweetResolver";
import UserResolver from "../resolvers/UserResolver";
import SkinsConnection from "../SkinsConnection";
import TweetsConnection, { TweetsSortOption } from "../TweetsConnection";
import InternetArchiveItemResolver from "./InternetArchiveItemResolver";
import * as Skins from "../../../data/skins";
import { ID, Int } from "grats";
import algoliasearch from "algoliasearch";
import MutationResolver from "./MutationResolver";
import { knex } from "../../../db";
import ArchiveFileModel from "../../../data/ArchiveFileModel";
import ArchiveFileResolver from "./ArchiveFileResolver";
import DatabaseStatisticsResolver from "./DatabaseStatisticsResolver";
import { fromId, NodeResolver } from "./NodeResolver";
import ModernSkinsConnection from "../ModernSkinsConnection";
import ModernSkinResolver from "./ModernSkinResolver";
import { ISkin } from "./CommonSkinResolver";
// These keys are already in the web client, so they are not secret at all.
const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51");
const index = client.initIndex("Skins");
/** @gqlType Query */
class RootResolver extends MutationResolver {
/**
* Get a globally unique object by its ID.
*
* https://graphql.org/learn/global-object-identification/
* @gqlField
*/
async node({ id }: { id: ID }, { ctx }): Promise<NodeResolver | null> {
const { graphqlType, id: localId } = fromId(id);
// TODO Use typeResolver
switch (graphqlType) {
case "ClassicSkin":
case "ModernSkin": {
const skin = await SkinModel.fromMd5(ctx, localId);
if (skin == null) {
return null;
}
return SkinResolver.fromModel(skin);
}
}
return null;
}
/**
* Get a skin by its MD5 hash
* @gqlField
*/
async fetch_skin_by_md5(
{ md5 }: { md5: string },
{ ctx }
): Promise<ISkin | null> {
const skin = await SkinModel.fromMd5(ctx, md5);
if (skin == null) {
return null;
}
if (skin.getSkinType() === "MODERN") {
return new ModernSkinResolver(skin);
} else {
return SkinResolver.fromModel(skin);
}
}
/**
* Get a tweet by its URL
* @gqlField
*/
async fetch_tweet_by_url(
{ url }: { url: string },
{ ctx }
): Promise<TweetResolver | null> {
const tweet = await TweetModel.fromAnything(ctx, url);
if (tweet == null) {
return null;
}
return new TweetResolver(tweet);
}
/**
* Get an archive.org item by its identifier. You can find this in the URL:
*
* https://archive.org/details/<identifier>/
* @gqlField
*/
async fetch_internet_archive_item_by_identifier(
{ identifier }: { identifier: string },
{ ctx }
): Promise<InternetArchiveItemResolver | null> {
const iaItem = await IaItemModel.fromIdentifier(ctx, identifier);
if (iaItem == null) {
return null;
}
return new InternetArchiveItemResolver(iaItem);
}
/**
* Fetch archive file by it's MD5 hash
*
* Get information about a file found within a skin's wsz/wal/zip archive.
* @gqlField
*/
async fetch_archive_file_by_md5(
{ md5 }: { md5: string },
{ ctx }
): Promise<ArchiveFileResolver | null> {
const archiveFile = await ArchiveFileModel.fromFileMd5(ctx, md5);
if (archiveFile == null) {
return null;
}
return new ArchiveFileResolver(archiveFile);
}
/**
* Search the database using the Algolia search index used by the Museum.
*
* Useful for locating a particular skin.
* @gqlField
*/
async search_skins(
{
query,
first = 10,
offset = 0,
}: { query: string; first?: Int; offset?: Int },
{ ctx }
): Promise<Array<ISkin | null>> {
if (first > 1000) {
throw new Error("Can only query 1000 records via search.");
}
const results: { hits: { md5: string }[] } = await index.search(query, {
attributesToRetrieve: ["md5"],
length: first,
offset,
});
return Promise.all(
results.hits.map(async (hit) => {
const model = await SkinModel.fromMd5Assert(ctx, hit.md5);
return SkinResolver.fromModel(model);
})
);
}
/**
* All classic skins in the database
*
* **Note:** We don't currently support combining sorting and filtering.
* @gqlField */
skins({
first = 10,
offset = 0,
sort,
filter,
}: {
first?: Int;
offset?: Int;
sort?: SkinsSortOption;
filter?: SkinsFilterOption;
}): SkinsConnection {
if (first > 1000) {
throw new Error("Maximum limit is 1000");
}
return new SkinsConnection(first, offset, sort, filter);
}
/**
* All modern skins in the database
* @gqlField */
async modern_skins({
first = 10,
offset = 0,
}: {
first?: Int;
offset?: Int;
}): Promise<ModernSkinsConnection> {
if (first > 1000) {
throw new Error("Maximum limit is 1000");
}
return new ModernSkinsConnection(first, offset);
}
/**
* A random skin that needs to be reviewed
* @gqlField */
async skin_to_review(_args: never, { ctx }): Promise<ISkin | null> {
if (!ctx.authed()) {
return null;
}
const { md5 } = await Skins.getSkinToReview();
const model = await SkinModel.fromMd5Assert(ctx, md5);
return SkinResolver.fromModel(model);
}
/**
* Tweets tweeted by @winampskins
* @gqlField
*/
async tweets({
first = 10,
offset = 0,
sort,
}: {
first?: Int;
offset?: Int;
sort?: TweetsSortOption;
}): Promise<TweetsConnection> {
if (first > 1000) {
throw new Error("Maximum limit is 1000");
}
return new TweetsConnection(first, offset, sort);
}
/**
* The currently authenticated user, if any.
* @gqlField
*/
me(): UserResolver | null {
return new UserResolver();
}
/**
* Get the status of a batch of uploads by md5s
* @gqlField
* @deprecated Prefer `upload_statuses` instead, were we operate on ids.
*/
async upload_statuses_by_md5(
{ md5s }: { md5s: string[] },
{ ctx }
): Promise<Array<SkinUpload | null>> {
return this._upload_statuses({ keyName: "skin_md5", keys: md5s }, ctx);
}
/**
* Get the status of a batch of uploads by ids
* @gqlField */
async upload_statuses(
{ ids }: { ids: string[] },
{ ctx }
): Promise<Array<SkinUpload | null>> {
return this._upload_statuses({ keyName: "id", keys: ids }, ctx);
}
// Shared implementation for upload_statuses and upload_statuses_by_md5
async _upload_statuses({ keyName, keys }, ctx) {
const skins = await knex("skin_uploads")
.whereIn(keyName, keys)
.orderBy("id", "desc")
.select("id", "skin_md5", "status");
return Promise.all(
skins.map(async ({ id, skin_md5, status }) => {
// TODO: Could we avoid fetching the skin if it's not read?
const skinModel = await SkinModel.fromMd5(ctx, skin_md5);
const skin =
skinModel == null ? null : SkinResolver.fromModel(skinModel);
// Most of the time when a skin fails to process, it's due to some infa
// issue on our side, and we can recover. For now, we'll always tell the user
// That processing is just delayed.
status = status === "ERRORED" ? "DELAYED" : status;
return { id, skin, status, upload_md5: skin_md5 };
})
);
}
/**
* A namespace for statistics about the database
* @gqlField */
statistics(): DatabaseStatisticsResolver {
return new DatabaseStatisticsResolver();
}
}
/**
* Information about an attempt to upload a skin to the Museum.
* @gqlType
*/
type SkinUpload = {
/** @gqlField */
id: string;
/** @gqlField */
status: SkinUploadStatus;
/**
* Skin that was uploaded. **Note:** This is null if the skin has not yet been
* fully processed. (status == ARCHIVED)
* @gqlField
*/
skin: ISkin | null;
/**
* Md5 hash given when requesting the upload URL.
* @gqlField
*/
upload_md5: string;
};
/** @gqlEnum */
type SkinsSortOption =
/**
the Museum's (https://skins.webamp.org) special sorting rules.
Roughly speaking, it's:
1. The four classic default skins
2. Tweeted skins first (sorted by the number of likes/retweets)
3. Approved, but not tweeted yet, skins
4. Unreviwed skins
5. Rejected skins
6. NSFW skins
*/
"MUSEUM";
/** @gqlEnum */
type SkinsFilterOption =
/*
Only the skins that have been approved for tweeting
*/
| "APPROVED"
/*
Only the skins that have been rejected for tweeting
*/
| "REJECTED"
/*
Only the skins that have been marked NSFW
*/
| "NSFW"
/*
Only the skins that have been tweeted
*/
| "TWEETED";
/**
* The current status of a pending upload.
*
* **Note:** Expect more values here as we try to be more transparent about
* the status of a pending uploads.
* @gqlEnum
*/
type SkinUploadStatus =
/** The user has requested a URL, but the skin has not yet been processed. */
| "URL_REQUESTED"
/** The user has notified us that the skin has been uploaded, but we haven't yet
processed it. */
| "UPLOAD_REPORTED"
/** An error occured processing the skin. Usually this is a transient error, and
the skin will be retried at a later time. */
| "ERRORED"
/** An error occured processing the skin, but it was the fault of the server. It
will be processed at a later date. */
| "DELAYED"
/** The skin has been successfully added to the Museum.
* @deprecated
*/
| "ARCHIVED";
export default RootResolver;