From 3e5bd5748f7aabcf858d3e6ebe4c342df2d63d39 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 3 Nov 2023 12:18:28 -0700 Subject: [PATCH] Initial version of Internet Archive example --- examples/internet-archive/ArchiveApi.ts | 88 +++++++ examples/internet-archive/README.md | 26 ++ examples/internet-archive/package.json | 24 ++ examples/internet-archive/schema.graphql | 111 +++++++++ .../internet-archive/schema/Collection.ts | 50 ++++ examples/internet-archive/schema/Item.ts | 109 ++++++++ .../schema/ItemsConnection.ts | 43 ++++ examples/internet-archive/schema/Query.ts | 52 ++++ examples/internet-archive/server.ts | 59 +++++ examples/internet-archive/tsconfig.json | 22 ++ pnpm-lock.yaml | 233 +++++++++++++++++- 11 files changed, 810 insertions(+), 7 deletions(-) create mode 100644 examples/internet-archive/ArchiveApi.ts create mode 100644 examples/internet-archive/README.md create mode 100644 examples/internet-archive/package.json create mode 100644 examples/internet-archive/schema.graphql create mode 100644 examples/internet-archive/schema/Collection.ts create mode 100644 examples/internet-archive/schema/Item.ts create mode 100644 examples/internet-archive/schema/ItemsConnection.ts create mode 100644 examples/internet-archive/schema/Query.ts create mode 100644 examples/internet-archive/server.ts create mode 100644 examples/internet-archive/tsconfig.json diff --git a/examples/internet-archive/ArchiveApi.ts b/examples/internet-archive/ArchiveApi.ts new file mode 100644 index 00000000..fcf021c4 --- /dev/null +++ b/examples/internet-archive/ArchiveApi.ts @@ -0,0 +1,88 @@ +// Note: These match the fields requested in the scrape API. +export type ItemApiResponse = { + identifier: string; + title: string; + mediatype: string; + stars: number; +}; + +const ITEM_FIELDS = ["identifier", "title", "mediatype", "stars"].join(","); + +export type ScapeApiResponse = { + items: ItemApiResponse[]; +}; + +// https://archive.org/services/swagger/?url=%2Fservices%2Fsearch%2Fbeta%2Fswagger.yaml#!/search/get_scrape_php +export async function scrapeApi( + query: string, + count: number +): Promise { + if (count > 10000) { + throw new Error("The maximum value for `count` is 10,000"); + } + if (count < 100) { + throw new Error("The minimum value for `count` is 100"); + } + // We use the scrape API because it supports cursor-based pagination. + const searchUrl = new URL("https://archive.org/services/search/v1/scrape"); + searchUrl.searchParams.set("q", query); + searchUrl.searchParams.set("count", count.toString()); + searchUrl.searchParams.set("fields", ITEM_FIELDS); + // TODO: If only `count` is being read, we could use the `totals_only` param as an optimization. + const response = await fetch(searchUrl); + if (!response.ok) { + throw new Error(`Failed to search for ${query}: ${response.statusText}`); + } + + return response.json(); +} + +type MetadataApiFileResponse = { + name: string; + source: "original" | "derivative"; + format: string; + md5: string; + size?: string; + mtime?: string; + crc32?: string; + sha1?: string; + + // Lots of these almost seem free-form + /* + rotation?: string; + original?: string; + pdf_module_version?: string; + ocr_module_version?: string; + ocr_converted?: string; + */ +}; + +export type MetadataApiResponse = { + files: MetadataApiFileResponse[]; + files_count: number; + item_last_updated: number; + item_size: number; + metadata: { + title: string; + creator: string; + uploader: string; + subject: string[]; + description: string; + date: string; + collection: string[] | string; + }; +}; + +// https://blog.archive.org/2013/07/04/metadata-api/ +export async function metadataApi( + identifier: string +): Promise { + // FIXME: Is there a safer way to do this that prevents injection attacks? + const url = new URL(`http://archive.org/metadata/${identifier}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to search for ${url}: ${response.statusText}`); + } + + return response.json(); +} diff --git a/examples/internet-archive/README.md b/examples/internet-archive/README.md new file mode 100644 index 00000000..6eb8c5c4 --- /dev/null +++ b/examples/internet-archive/README.md @@ -0,0 +1,26 @@ +# Grats Internet Archive + +An in-progress implementation of a GraphQL facade over the [Internet Archive's](https://archive.org) REST API. + +Some ideas I'd like to explore in this example: + +- [ ] Isomorphism. Can Grats run in the browser and on the server? +- [ ] Grafast. Can we use Grafast's query planing approach with Grats? Can it reduce waterfalls? + +## TODO + +- [ ] Items should really be an interface that could be a concrete item (is there a name for this?) or a collection. +- [ ] Collections should expose their item properties +- [ ] Top level query fields to get item/collection +- [ ] Type for files +- [ ] Type for user? + +## Internet Archive API Documentation + +The Internet Archive API is a bit haphazard in how it's documented. Here are some links include relevant information: + +- https://archive.org/developers/index.html +- https://blog.archive.org/2013/07/04/metadata-api/#read +- + +Below diff --git a/examples/internet-archive/package.json b/examples/internet-archive/package.json new file mode 100644 index 00000000..998f54cf --- /dev/null +++ b/examples/internet-archive/package.json @@ -0,0 +1,24 @@ +{ + "name": "express-graphql-grats-example", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node --esm server.ts", + "build": "tsc", + "dev": "tsc && ts-node --esm server.ts" + }, + "dependencies": { + "graphql": "^16.6.0", + "graphql-yoga": "^5.0.0", + "grats": "workspace:*", + "typescript": "^4.9.5" + }, + "devDependencies": { + "@types/node": "^18.14.6", + "ts-node": "^10.9.1" + }, + "keywords": [], + "author": "", + "prettier": {} +} diff --git a/examples/internet-archive/schema.graphql b/examples/internet-archive/schema.graphql new file mode 100644 index 00000000..af4245ab --- /dev/null +++ b/examples/internet-archive/schema.graphql @@ -0,0 +1,111 @@ +schema { + query: Query +} + +directive @exported( + filename: String! + functionName: String! +) on FIELD_DEFINITION + +directive @methodName(name: String!) on FIELD_DEFINITION + +""" +Items can be placed in collections. For example, a collection called European +Libraries can contain several items, one of which can be Euclid’s Geometry. +An item can belong to more than one collection. See [Internet Archive +Items](https://archive.org/developers/items.html). +""" +type Collection { + """ + Unique identifier for this collection. + """ + identifier: String + items( + """ + Max 10,000 + """ + first: Int = 100 + ): ItemsConnection + url: String +} + +""" +Archive.org is made up of “items”. An item is a logical “thing” that we +represent on one web page on archive.org. An item can be considered as a +group of files that deserve their own metadata. If the files in an item have +separate metadata, the files should probably be in different items. An item +can be a book, a song, an album, a dataset, a movie, an image or set of +images, etc. Every item has an identifier that is unique across archive.org. + +https://archive.org/developers/items.html +""" +type Item { + collections: [Collection!] + creator_name: String + """ + HTML string of the item's description. + """ + description: String + """ + The Internet Archive's unique identifier for this item. + """ + identifier: String + mediaType: String + stars: Float + title: String + uploader_name: String + url: String +} + +""" +A connection to a list of items. +""" +type ItemsConnection { + """ + A list of edges. + """ + edges: [ItemsEdge!] + nodes: [Item!] +} + +""" +An edge in a connection of Search Items. +""" +type ItemsEdge { + """ + The item at the end of the edge + """ + node: Item +} + +""" +This API is a GraphQL facade on top of the Internet Archive's existing REST API. + +Its goal is to improve the developer experience of using the Internet Archive's +API by: + +- Providing a single endpoint for all queries. +- Providing a well defined schema that can be used to explore the API and reason about the data it returns. + +In the future it might also: + +- Provide an abstraction that can be used client-side in the browser or server-side in Node.js. +- Provide a more efficient way to fetch data by leveraging query planing to batch requests or make other optimizations. +- Provide a proof of concept to motivate the Internet Archive to build a GraphQL API. +""" +type Query { + """ + Search the Internet Archive for books, movies, and more. + """ + searchItems( + """ + Max 10,000 + """ + first: Int = 100 + query: String! + ): ItemsConnection + @exported( + filename: "../../examples/internet-archive/dist/schema/Query.js" + functionName: "searchItems" + ) +} diff --git a/examples/internet-archive/schema/Collection.ts b/examples/internet-archive/schema/Collection.ts new file mode 100644 index 00000000..9fb277ba --- /dev/null +++ b/examples/internet-archive/schema/Collection.ts @@ -0,0 +1,50 @@ +import { Int } from "../../../dist/src"; +import { scrapeApi } from "../ArchiveApi"; +import ItemsConnection from "./ItemsConnection"; + +/** + * Items can be placed in collections. For example, a collection called European + * Libraries can contain several items, one of which can be Euclid’s Geometry. + * An item can belong to more than one collection. See [Internet Archive + * Items](https://archive.org/developers/items.html). + * @gqlType */ +export default class Collection { + /** + * Unique identifier for this collection. + * @gqlField */ + identifier: string; + + constructor(identifier: string) { + this.identifier = identifier; + } + + /** @gqlField */ + url(): string { + return "https://archive.org/details/" + this.identifier; + } + + /** @gqlField */ + async items({ + first = 100, + }: { + /** Max 10,000 */ + first?: Int; + }): Promise { + if (first > 10000) { + throw new Error("The maximum value for `first` is 10,000."); + } + + let response = await scrapeApi( + `collection:${this.identifier}`, + Math.max(first, 100) + ); + + if (first < 100) { + response = { + ...response, + items: response.items.slice(0, first), + }; + } + return ItemsConnection.fromScapeApiResponse(response); + } +} diff --git a/examples/internet-archive/schema/Item.ts b/examples/internet-archive/schema/Item.ts new file mode 100644 index 00000000..980efc8e --- /dev/null +++ b/examples/internet-archive/schema/Item.ts @@ -0,0 +1,109 @@ +import { Float } from "grats"; +import { + ItemApiResponse, + MetadataApiResponse, + metadataApi, +} from "../ArchiveApi"; +import Collection from "./Collection"; + +/** + * Archive.org is made up of “items”. An item is a logical “thing” that we + * represent on one web page on archive.org. An item can be considered as a + * group of files that deserve their own metadata. If the files in an item have + * separate metadata, the files should probably be in different items. An item + * can be a book, a song, an album, a dataset, a movie, an image or set of + * images, etc. Every item has an identifier that is unique across archive.org. + * + * https://archive.org/developers/items.html + * + * @gqlType */ +export default class Item { + apiResponse: ItemApiResponse; + + _metadata: Promise | null = null; + + constructor(apiResponse: ItemApiResponse) { + this.apiResponse = apiResponse; + this._metadata; + } + + async metadata(): Promise { + if (this._metadata == null) { + this._metadata = metadataApi(this.identifier()); + } + return this._metadata; + } + + /** + * The Internet Archive's unique identifier for this item. + * @gqlField */ + identifier(): string { + return this.apiResponse.identifier; + } + + /** @gqlField */ + title(): string { + return this.apiResponse.title; + } + + /** @gqlField */ + url(): string { + return "https://archive.org/details/" + this.identifier(); + } + + /** + * HTML string of the item's description. + * @gqlField */ + async description(): Promise { + const metadata = await this.metadata(); + return metadata.metadata.description; + } + + /** @gqlField */ + async uploader_name(): Promise { + const metadata = await this.metadata(); + return metadata.metadata.uploader; + } + + /** @gqlField */ + async creator_name(): Promise { + const metadata = await this.metadata(); + return metadata.metadata.creator; + } + + /** @gqlField */ + stars(): Float | null { + return this.apiResponse.stars; + } + + /** @gqlField */ + // TODO: Should this be an enum? + mediaType(): string { + return this.apiResponse.mediatype; + } + + /** @gqlField */ + async collections(): Promise { + const metadata = await this.metadata(); + let collections = metadata.metadata.collection; + if (typeof collections === "string") { + collections = [collections]; + } + return collections.map((identifier) => { + return new Collection(identifier); + }); + } +} + +/* +export function getItemByIdentifier(identifier: string): Promise { + return metadataApi(identifier).then((metadata) => { + return new Item({ + identifier: metadata.metadata.identifier, + title: metadata.metadata.title, + stars: null, + mediatype: metadata.metadata.collection, + }); + }); +} +*/ diff --git a/examples/internet-archive/schema/ItemsConnection.ts b/examples/internet-archive/schema/ItemsConnection.ts new file mode 100644 index 00000000..1950a71f --- /dev/null +++ b/examples/internet-archive/schema/ItemsConnection.ts @@ -0,0 +1,43 @@ +import { ScapeApiResponse } from "../ArchiveApi"; +import Item from "./Item"; + +/** + * A connection modeling a list of items. + * + * In the future this could be extended to support cursor-based pagination. + * @gqlType */ +export default class ItemsConnection { + static fromScapeApiResponse(json: ScapeApiResponse): ItemsConnection { + const edges = json.items.map((item) => { + const node = new Item(item); + return new ItemsEdge(node); + }); + return new ItemsConnection(edges); + } + + constructor(edges: ItemsEdge[]) { + this.edges = edges; + } + + /** + * A list of edges. + * @gqlField */ + edges: ItemsEdge[]; + + /** + * Convenience field for getting the nodes of the edges. + * @gqlField */ + nodes(): Item[] { + return this.edges.map((edge) => edge.node); + } +} + +/** @gqlType */ +class ItemsEdge { + constructor(item: Item) { + this.node = item; + } + + /** @gqlField */ + node: Item; +} diff --git a/examples/internet-archive/schema/Query.ts b/examples/internet-archive/schema/Query.ts new file mode 100644 index 00000000..1d9ab8c7 --- /dev/null +++ b/examples/internet-archive/schema/Query.ts @@ -0,0 +1,52 @@ +import { Int } from "grats"; +import { scrapeApi } from "../ArchiveApi"; +import ItemsConnection from "./ItemsConnection"; + +// TODO: Ideally this comment would be added to the schema, not the query. +/** + * This API is a GraphQL facade on top of the Internet Archive's existing REST API. + * + * Its goal is to improve the developer experience of using the Internet Archive's + * API by: + * + * - Providing a single endpoint for all queries. + * - Providing a well defined schema that can be used to explore the API and reason about the data it returns. + * + * In the future it might also: + * + * - Provide an abstraction that can be used client-side in the browser or server-side in Node.js. + * - Provide a more efficient way to fetch data by leveraging query planing to batch requests or make other optimizations. + * - Provide a proof of concept to motivate the Internet Archive to build a GraphQL API. + * @gqlType */ +export class Query {} // TODO: Allow grats to support (and enforce!) Query be type undefined. + +/** + * Search the Internet Archive for books, movies, and more. + * + + * @gqlField */ +export async function searchItems( + _: Query, + { + query, + first = 100, + }: { + query: string; + /** Max 10,000 */ + first?: Int; + } +): Promise { + if (first > 10000) { + throw new Error("The maximum value for `first` is 10,000."); + } + + let response = await scrapeApi(query, Math.max(first, 100)); + + if (first < 100) { + response = { + ...response, + items: response.items.slice(0, first), + }; + } + return ItemsConnection.fromScapeApiResponse(response); +} diff --git a/examples/internet-archive/server.ts b/examples/internet-archive/server.ts new file mode 100644 index 00000000..115b1c34 --- /dev/null +++ b/examples/internet-archive/server.ts @@ -0,0 +1,59 @@ +import { extractGratsSchemaAtRuntime, buildSchemaFromSDL } from "grats"; +import { readFileSync } from "fs"; +import { createServer } from "node:http"; +import { createYoga } from "graphql-yoga"; + +const DEFAULT_QUERY = ` +# Welcome to the Internet Archive GraphQL API! +query { + searchItems(query: "winamp", first: 10) { + nodes { + title + identifier + url + collections { + identifier + url + } + } + } +} +`; + +async function main() { + // FIXME: This is relative to the current working directory, not the file, or + // something more sensible. + + const schema = getSchema(); + // Create a Yoga instance with a GraphQL schema. + const yoga = createYoga({ + logging: true, + graphiql: { + defaultQuery: DEFAULT_QUERY, + title: "Internet Archive GraphQL API (unofficial)", + }, + schema, + }); + + // Pass it into a server to hook into request handlers. + const server = createServer(yoga); + + // Start the server and you're done! + server.listen(4000, () => { + console.info("Server is running on http://localhost:4000/graphql"); + }); +} + +function getSchema() { + if (process.env.FROM_SDL) { + console.log("Building schema from SDL..."); + const sdl = readFileSync("./schema.graphql", "utf8"); + return buildSchemaFromSDL(sdl); + } + console.log("Building schema from source..."); + return extractGratsSchemaAtRuntime({ + emitSchemaFile: "./schema.graphql", + }); +} + +main(); diff --git a/examples/internet-archive/tsconfig.json b/examples/internet-archive/tsconfig.json new file mode 100644 index 00000000..a6ab0e15 --- /dev/null +++ b/examples/internet-archive/tsconfig.json @@ -0,0 +1,22 @@ +{ + // Most ts-node options can be specified here using their programmatic names. + "ts-node": { + // It is faster to skip typechecking. + // Remove if you want ts-node to do typechecking. + "files": true, + "transpileOnly": true + }, + "grats": { + "nullableByDefault": true + }, + "compilerOptions": { + "skipLibCheck": true, + "lib": ["es2017"], + // typescript options here + "outDir": "dist", + "downlevelIteration": true, + "strictNullChecks": true, + "declaration": true + }, + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b9c650c..aa93101b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,28 @@ importers: specifier: ^10.9.1 version: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) + examples/internet-archive: + dependencies: + graphql: + specifier: ^16.6.0 + version: 16.6.0 + graphql-yoga: + specifier: ^5.0.0 + version: 5.0.0(graphql@16.6.0) + grats: + specifier: workspace:* + version: link:../.. + typescript: + specifier: ^4.9.5 + version: 4.9.5 + devDependencies: + '@types/node': + specifier: ^18.14.6 + version: 18.15.0 + ts-node: + specifier: ^10.9.1 + version: 10.9.1(@types/node@18.15.0)(typescript@4.9.5) + website: dependencies: '@algolia/client-search': @@ -2656,6 +2678,21 @@ packages: - webpack-cli dev: false + /@envelop/core@5.0.0: + resolution: {integrity: sha512-aJdnH/ptv+cvwfvciCBe7TSvccBwo9g0S5f6u35TBVzRVqIGkK03lFlIL+x1cnfZgN9EfR2b1PH2galrT1CdCQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@envelop/types': 5.0.0 + tslib: 2.6.2 + dev: false + + /@envelop/types@5.0.0: + resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + /@esbuild/android-arm64@0.19.4: resolution: {integrity: sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg==} engines: {node: '>=12'} @@ -3134,6 +3171,57 @@ packages: readable-stream: 4.3.0 dev: false + /@graphql-tools/executor@1.2.0(graphql@16.6.0): + resolution: {integrity: sha512-SKlIcMA71Dha5JnEWlw4XxcaJ+YupuXg0QCZgl2TOLFz4SkGCwU/geAsJvUJFwK2RbVLpQv/UMq67lOaBuwDtg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.6.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.6.0) + '@repeaterjs/repeater': 3.0.4 + graphql: 16.6.0 + tslib: 2.6.2 + value-or-promise: 1.0.12 + dev: false + + /@graphql-tools/merge@9.0.0(graphql@16.6.0): + resolution: {integrity: sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-tools/utils': 10.0.8(graphql@16.6.0) + graphql: 16.6.0 + tslib: 2.6.2 + dev: false + + /@graphql-tools/schema@10.0.0(graphql@16.6.0): + resolution: {integrity: sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-tools/merge': 9.0.0(graphql@16.6.0) + '@graphql-tools/utils': 10.0.8(graphql@16.6.0) + graphql: 16.6.0 + tslib: 2.6.2 + value-or-promise: 1.0.12 + dev: false + + /@graphql-tools/utils@10.0.8(graphql@16.6.0): + resolution: {integrity: sha512-yjyA8ycSa1WRlJqyX/aLqXeE5DvF/H02+zXMUFnCzIDrj0UvLMUrxhmVFnMK0Q2n3bh4uuTeY3621m5za9ovXw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.6.0) + cross-inspect: 1.0.0 + dset: 3.1.3 + graphql: 16.6.0 + tslib: 2.6.2 + dev: false + /@graphql-tools/utils@9.2.1(graphql@16.6.0): resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} peerDependencies: @@ -3152,6 +3240,39 @@ packages: graphql: 16.6.0 dev: false + /@graphql-typed-document-node/core@3.2.0(graphql@16.6.0): + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + dependencies: + graphql: 16.6.0 + dev: false + + /@graphql-yoga/logger@2.0.0: + resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} + engines: {node: '>=18.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@graphql-yoga/subscription@5.0.0: + resolution: {integrity: sha512-Ri7sK8hmxd/kwaEa0YT8uqQUb2wOLsmBMxI90QDyf96lzOMJRgBuNYoEkU1pSgsgmW2glceZ96sRYfaXqwVxUw==} + engines: {node: '>=18.0.0'} + dependencies: + '@graphql-yoga/typed-event-target': 3.0.0 + '@repeaterjs/repeater': 3.0.4 + '@whatwg-node/events': 0.1.1 + tslib: 2.6.2 + dev: false + + /@graphql-yoga/typed-event-target@3.0.0: + resolution: {integrity: sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==} + engines: {node: '>=18.0.0'} + dependencies: + '@repeaterjs/repeater': 3.0.4 + tslib: 2.6.2 + dev: false + /@grpc/grpc-js@1.9.9: resolution: {integrity: sha512-vQ1qwi/Kiyprt+uhb1+rHMpyk4CVRMTGNUGGPRGS7pLNfWkdCHrGEnT6T3/JyC2VZgoOX/X1KwdoU0WYQAeYcQ==} engines: {node: ^8.13.0 || >=10.10.0} @@ -4619,6 +4740,10 @@ packages: resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} dev: false + /@repeaterjs/repeater@3.0.4: + resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==} + dev: false + /@rollup/pluginutils@4.2.1: resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -5464,6 +5589,38 @@ packages: '@webassemblyjs/ast': 1.11.1 '@xtuc/long': 4.2.2 + /@whatwg-node/events@0.1.1: + resolution: {integrity: sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==} + engines: {node: '>=16.0.0'} + dev: false + + /@whatwg-node/fetch@0.9.14: + resolution: {integrity: sha512-wurZC82zzZwXRDSW0OS9l141DynaJQh7Yt0FD1xZ8niX7/Et/7RoiLiltbVU1fSF1RR9z6ndEaTUQBAmddTm1w==} + engines: {node: '>=16.0.0'} + dependencies: + '@whatwg-node/node-fetch': 0.5.0 + urlpattern-polyfill: 9.0.0 + dev: false + + /@whatwg-node/node-fetch@0.5.0: + resolution: {integrity: sha512-q76lDAafvHNGWedNAVHrz/EyYTS8qwRLcwne8SJQdRN5P3HydxU6XROFvJfTML6KZXQX2FDdGY4/SnaNyd7M0Q==} + engines: {node: '>=16.0.0'} + dependencies: + '@whatwg-node/events': 0.1.1 + busboy: 1.6.0 + fast-querystring: 1.1.1 + fast-url-parser: 1.1.3 + tslib: 2.6.2 + dev: false + + /@whatwg-node/server@0.9.16: + resolution: {integrity: sha512-gktQkRyONEw2EGpx7UZaC6zNlUm21CGlqAHQXU3QC6W0zlLM5ZQNDCeD66q/nsPHDV08X2NTHlABsuAEk5rh/w==} + engines: {node: '>=16.0.0'} + dependencies: + '@whatwg-node/fetch': 0.9.14 + tslib: 2.6.2 + dev: false + /@xhmikosr/archive-type@6.0.1: resolution: {integrity: sha512-PB3NeJL8xARZt52yDBupK0dNPn8uIVQDe15qNehUpoeeLWCZyAOam4vGXnoZGz2N9D1VXtjievJuCsXam2TmbQ==} engines: {node: ^14.14.0 || >=16.0.0} @@ -6365,6 +6522,13 @@ packages: semver: 7.5.4 dev: false + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + /byline@5.0.0: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} @@ -6450,7 +6614,7 @@ packages: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: pascal-case: 3.1.2 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /camelcase-css@2.0.1: @@ -7188,6 +7352,13 @@ packages: - encoding dev: false + /cross-inspect@1.0.0: + resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} + engines: {node: '>=16.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -7767,7 +7938,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /dot-prop@5.3.0: @@ -7801,6 +7972,11 @@ packages: engines: {node: '>=12'} dev: false + /dset@3.1.3: + resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} + engines: {node: '>=4'} + dev: false + /duplexer3@0.1.5: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} dev: false @@ -9192,6 +9368,26 @@ packages: vscode-languageserver-types: 3.17.3 dev: false + /graphql-yoga@5.0.0(graphql@16.6.0): + resolution: {integrity: sha512-ZvZlO8MHMDWuLRoDhvJQnXg8SOJD0iDaCA+M/zWuD26AlhEugOEbpnhw/645oqXTYtvHsM91WyxtV7p5XJWYMg==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + dependencies: + '@envelop/core': 5.0.0 + '@graphql-tools/executor': 1.2.0(graphql@16.6.0) + '@graphql-tools/schema': 10.0.0(graphql@16.6.0) + '@graphql-tools/utils': 10.0.8(graphql@16.6.0) + '@graphql-yoga/logger': 2.0.0 + '@graphql-yoga/subscription': 5.0.0 + '@whatwg-node/fetch': 0.9.14 + '@whatwg-node/server': 0.9.16 + dset: 3.1.3 + graphql: 16.6.0 + lru-cache: 10.0.1 + tslib: 2.6.2 + dev: false + /graphql@16.6.0: resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -10710,7 +10906,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.5.0 + tslib: 2.6.2 dev: false /lowercase-keys@1.0.1: @@ -10728,6 +10924,11 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false + /lru-cache@10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + dev: false + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -11362,7 +11563,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /node-domexception@1.0.0: @@ -11923,7 +12124,7 @@ packages: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: dot-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /parent-module@1.0.1: @@ -11999,7 +12200,7 @@ packages: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: no-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.2 dev: false /pascalcase@0.1.1: @@ -13563,7 +13764,7 @@ packages: /rxjs@7.8.0: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} dependencies: - tslib: 2.5.0 + tslib: 2.6.2 dev: false /safe-buffer@5.1.2: @@ -14134,6 +14335,11 @@ packages: bl: 5.1.0 dev: false + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + /streamx@2.15.2: resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} dependencies: @@ -14792,6 +14998,10 @@ packages: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} dev: false + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsutils@3.21.0(typescript@4.9.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -15171,6 +15381,10 @@ packages: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} dev: false + /urlpattern-polyfill@9.0.0: + resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} + dev: false + /use-composed-ref@1.3.0(react@17.0.2): resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} peerDependencies: @@ -15265,6 +15479,11 @@ packages: resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} dev: false + /value-or-promise@1.0.12: + resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} + engines: {node: '>=12'} + dev: false + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'}