diff --git a/.github/workflows/cdn-ci.yaml b/.github/workflows/cdn-ci.yaml new file mode 100644 index 0000000000..4dd5f2e4fd --- /dev/null +++ b/.github/workflows/cdn-ci.yaml @@ -0,0 +1,36 @@ +name: CDN CI +on: + pull_request: + paths: + - cdn-server/**/* + - .github/workflows/cdn-ci.yaml + +concurrency: + group: ${{github.workflow}}-${{github.head_ref}} + cancel-in-progress: true + +env: + CI: true + +jobs: + build_test: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: ./.github/actions/node + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run --filter ./cdn-server --filter ./cdn-server/cdn build + + - name: Lint + run: pnpm run --filter ./cdn-server --filter ./cdn-server/cdn lint + + - name: Test + run: pnpm run --filter ./cdn-server/cdn test diff --git a/cdn-server/.eslintrc b/cdn-server/.eslintrc new file mode 100644 index 0000000000..980e97ae56 --- /dev/null +++ b/cdn-server/.eslintrc @@ -0,0 +1,28 @@ +{ + "extends": ["eslint-config-unjs"], + "ignorePatterns": ["dist"], + "rules": { + "space-before-function-paren": 0, + "arrow-parens": 0, + "comma-dangle": 0, + "semi": 0, + "unicorn/prevent-abbreviations": 0, + "quotes": 0, + "keyword-spacing": 0, + "no-undef": 0, + "indent": 0, + "import/named": 0, + "unicorn/catch-error-name": 0, + "unicorn/no-null": 0, + "unicorn/no-useless-undefined": 0, + "unicorn/no-await-expression-member": 0, + "unicorn/no-array-push-push": 0, + "unicorn/filename-case": 0, + "@typescript-eslint/no-unused-vars": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "unicorn/expiring-todo-comments": 0, + "no-unexpected-multiline": 0, + "no-useless-constructor": 0, + "unicorn/prefer-ternary": 0 + } +} diff --git a/cdn-server/.gitignore b/cdn-server/.gitignore new file mode 100644 index 0000000000..e458aab612 --- /dev/null +++ b/cdn-server/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/dist +/.eslintcache diff --git a/cdn-server/cdn/.eslintrc b/cdn-server/cdn/.eslintrc new file mode 100644 index 0000000000..980e97ae56 --- /dev/null +++ b/cdn-server/cdn/.eslintrc @@ -0,0 +1,28 @@ +{ + "extends": ["eslint-config-unjs"], + "ignorePatterns": ["dist"], + "rules": { + "space-before-function-paren": 0, + "arrow-parens": 0, + "comma-dangle": 0, + "semi": 0, + "unicorn/prevent-abbreviations": 0, + "quotes": 0, + "keyword-spacing": 0, + "no-undef": 0, + "indent": 0, + "import/named": 0, + "unicorn/catch-error-name": 0, + "unicorn/no-null": 0, + "unicorn/no-useless-undefined": 0, + "unicorn/no-await-expression-member": 0, + "unicorn/no-array-push-push": 0, + "unicorn/filename-case": 0, + "@typescript-eslint/no-unused-vars": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "unicorn/expiring-todo-comments": 0, + "no-unexpected-multiline": 0, + "no-useless-constructor": 0, + "unicorn/prefer-ternary": 0 + } +} diff --git a/cdn-server/cdn/.gitignore b/cdn-server/cdn/.gitignore new file mode 100644 index 0000000000..e458aab612 --- /dev/null +++ b/cdn-server/cdn/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/dist +/.eslintcache diff --git a/cdn-server/cdn/package.json b/cdn-server/cdn/package.json new file mode 100644 index 0000000000..14f3a8abc9 --- /dev/null +++ b/cdn-server/cdn/package.json @@ -0,0 +1,31 @@ +{ + "name": "@wundergraph/cosmo-cdn", + "version": "1.0.0", + "author": { + "name": "WunderGraph Maintainers", + "email": "info@wundergraph.com" + }, + "license": "Apache-2.0", + "scripts": { + "dev": "tsc --watch", + "build": "del dist && tsc", + "test:watch": "vitest test", + "test": "vitest run", + "lint": "eslint --cache && prettier -c src test", + "format:fix": "prettier --write -c src test" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "hono": "^3.10.0", + "jose": "^4.15.2" + }, + "devDependencies": { + "eslint": "^8.53.0", + "eslint-config-unjs": "^0.2.1", + "vitest": "^0.34.6" + } +} diff --git a/cdn-server/cdn/src/index.ts b/cdn-server/cdn/src/index.ts new file mode 100644 index 0000000000..77a176a628 --- /dev/null +++ b/cdn-server/cdn/src/index.ts @@ -0,0 +1,100 @@ +import { JWTVerifyResult, jwtVerify } from 'jose'; +import { Context, Env, Hono, Next, Schema } from 'hono'; + +export interface BlobStorage { + getObject(context: Context, key: string): Promise; +} + +export class BlobNotFoundError extends Error { + constructor(message: string, cause?: Error) { + super(message, cause); + Object.setPrototypeOf(this, BlobNotFoundError.prototype); + } +} + +interface CdnOptions { + authJwtSecret: string | ((c: Context) => string); + blobStorage: BlobStorage; +} + +declare module 'hono' { + interface ContextVariableMap { + authenticatedOrganizationId: string; + authenticatedFederatedGraphId: string; + } +} + +const jwtMiddleware = (secret: string | ((c: Context) => string)) => { + return async (c: Context, next: Next) => { + const authHeader = c.req.header('Authorization'); + if (!authHeader) { + return c.text('Unauthorized', 401); + } + const [type, token] = authHeader.split(' '); + if (type !== 'Bearer' || !token) { + return c.text('Unauthorized', 401); + } + let result: JWTVerifyResult; + const secretKey = new TextEncoder().encode(typeof secret === 'function' ? secret(c) : secret); + try { + result = await jwtVerify(token, secretKey); + } catch (e: any) { + if (e instanceof Error && (e.name === 'JWSSignatureVerificationFailed' || e.name === 'JWSInvalid')) { + return c.text('Forbidden', 403); + } + throw e; + } + const organizationId = result.payload.organization_id; + const federatedGraphId = result.payload.federated_graph_id; + if (!organizationId || !federatedGraphId) { + return c.text('Forbidden', 403); + } + c.set('authenticatedOrganizationId', organizationId); + c.set('authenticatedFederatedGraphId', federatedGraphId); + await next(); + }; +}; + +const persistedOperation = (storage: BlobStorage) => { + return async (c: Context) => { + const organizationId = c.req.param('organization_id'); + const federatedGraphId = c.req.param('federated_graph_id'); + // Check authentication + if ( + organizationId !== c.get('authenticatedOrganizationId') || + federatedGraphId !== c.get('authenticatedFederatedGraphId') + ) { + return c.text('Forbidden', 403); + } + const clientId = c.req.param('client_id'); + const operation = c.req.param('operation'); + if (!operation.endsWith('.json')) { + return c.notFound(); + } + const key = `${organizationId}/${federatedGraphId}/operations/${clientId}/${operation}`; + let operationStream: ReadableStream; + try { + operationStream = await storage.getObject(c, key); + } catch (e: any) { + if (e instanceof Error && e.constructor.name === 'BlobNotFoundError') { + return c.notFound(); + } + throw e; + } + return c.stream(async (stream) => { + await stream.pipe(operationStream); + await stream.close(); + }); + }; +}; + +// eslint-disable-next-line @typescript-eslint/ban-types +export const cdn = ( + hono: Hono, + opts: CdnOptions, +) => { + const operations = '/:organization_id/:federated_graph_id/operations/:client_id/:operation{.+\\.json$}'; + hono.use(operations, jwtMiddleware(opts.authJwtSecret)); + + hono.get(operations, persistedOperation(opts.blobStorage)); +}; diff --git a/cdn-server/cdn/test/cdn.test.ts b/cdn-server/cdn/test/cdn.test.ts new file mode 100644 index 0000000000..13b882f33f --- /dev/null +++ b/cdn-server/cdn/test/cdn.test.ts @@ -0,0 +1,122 @@ +import { SignJWT } from 'jose'; +import { describe, test, expect } from 'vitest'; +import { Context, Hono } from 'hono'; +import { BlobStorage, BlobNotFoundError, cdn } from '../dist'; + +const secretKey = 'hunter2'; + +const generateToken = async (organizationId: string, federatedGraphId: string, secret: string) => { + const secretKey = new TextEncoder().encode(secret); + return await new SignJWT({ organization_id: organizationId, federated_graph_id: federatedGraphId }) + .setProtectedHeader({ alg: 'HS256' }) + .sign(secretKey); +}; + +class InMemoryBlobStorage implements BlobStorage { + objects: Map = new Map(); + getObject(context: Context, key: string): Promise { + const obj = this.objects.get(key); + if (!obj) { + return Promise.reject(new BlobNotFoundError(`Object with key ${key} not found`)); + } + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(obj); + controller.close(); + }, + }); + return Promise.resolve(stream); + } +} + +describe('Test JWT authentication', async () => { + const federatedGraphId = 'federatedGraphId'; + const organizationId = 'organizationId'; + const token = await generateToken(organizationId, federatedGraphId, secretKey); + const blobStorage = new InMemoryBlobStorage(); + + const requestPath = `/${organizationId}/${federatedGraphId}/operations/clientName/operation.json`; + + const app = new Hono(); + + cdn(app, { + authJwtSecret: secretKey, + blobStorage, + }); + + test('it returns a 401 if no Authorization header is provided', async () => { + const res = await app.request(requestPath, { + method: 'GET', + }); + expect(res.status).toBe(401); + }); + + test('it returns a 403 if an invalid Authorization header is provided', async () => { + const res = await app.request(requestPath, { + method: 'GET', + headers: { + Authorization: `Bearer ${token.slice(0, -1)}}`, + }, + }); + expect(res.status).toBe(403); + }); + + test('it authenticates the request when a valid Authorization header is provided', async () => { + const res = await app.request(requestPath, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + expect(res.status).toBe(404); + }); +}); + +describe('Test persisted operations handler', async () => { + const federatedGraphId = 'federatedGraphId'; + const organizationId = 'organizationId'; + const token = await generateToken(organizationId, federatedGraphId, secretKey); + const blobStorage = new InMemoryBlobStorage(); + const clientName = 'clientName'; + const operationHash = 'operationHash'; + const operationContents = JSON.stringify({ version: 1, body: 'query { hello }' }); + + blobStorage.objects.set( + `${organizationId}/${federatedGraphId}/operations/${clientName}/${operationHash}.json`, + Buffer.from(operationContents), + ); + + const app = new Hono(); + + cdn(app, { + authJwtSecret: secretKey, + blobStorage, + }); + + test('it returns a persisted operation', async () => { + const res = await app.request( + `/${organizationId}/${federatedGraphId}/operations/${clientName}/${operationHash}.json`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + expect(res.status).toBe(200); + expect(await res.text()).toBe(operationContents); + }); + + test('it returns a 404 if the persisted operation does not exist', async () => { + const res = await app.request( + `/${organizationId}/${federatedGraphId}/operations/${clientName}/does_not_exist.json`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + expect(res.status).toBe(404); + }); +}); diff --git a/cdn-server/cdn/tsconfig.json b/cdn-server/cdn/tsconfig.json new file mode 100644 index 0000000000..6913da8b6d --- /dev/null +++ b/cdn-server/cdn/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "module": "commonjs", + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] + } + \ No newline at end of file diff --git a/cdn-server/package.json b/cdn-server/package.json new file mode 100644 index 0000000000..9f226a41d0 --- /dev/null +++ b/cdn-server/package.json @@ -0,0 +1,29 @@ +{ + "name": "@wundergraph/cdn-server", + "version": "1.0.0", + "author": { + "name": "WunderGraph Maintainers", + "email": "info@wundergraph.com" + }, + "license": "Apache-2.0", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "del dist && tsc", + "start": "tsx src/index.ts", + "lint": "eslint --cache && prettier -c src", + "format:fix": "prettier --write -c src" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.445.0", + "@hono/node-server": "^1.2.2", + "@wundergraph/cosmo-cdn": "workspace:*", + "dotenv": "^16.3.1", + "hono": "^3.10.0" + }, + "devDependencies": { + "@types/node": "^20.9.0", + "eslint": "^8.53.0", + "eslint-config-unjs": "^0.2.1", + "tsx": "^3.12.2" + } +} diff --git a/cdn-server/src/index.ts b/cdn-server/src/index.ts new file mode 100644 index 0000000000..f4996407bf --- /dev/null +++ b/cdn-server/src/index.ts @@ -0,0 +1,22 @@ +import dotenv from 'dotenv'; +import { serve } from '@hono/node-server'; +import { Hono } from 'hono'; +import { cdn } from '../cdn/src/index'; +import { createS3BlobStorage } from './s3'; + +dotenv.config(); + +if (!process.env.S3_STORAGE_URL) { + throw new Error('S3_STORAGE_URL is required'); +} + +const blobStorage = createS3BlobStorage(process.env.S3_STORAGE_URL); + +const app = new Hono(); +cdn(app, { + authJwtSecret: process.env.AUTH_JWT_SECRET!, + blobStorage, +}); + +const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8787; +serve({ fetch: app.fetch, port }); diff --git a/cdn-server/src/s3.ts b/cdn-server/src/s3.ts new file mode 100644 index 0000000000..2a731723b0 --- /dev/null +++ b/cdn-server/src/s3.ts @@ -0,0 +1,48 @@ +import { GetObjectCommand, NoSuchKey, S3Client } from '@aws-sdk/client-s3'; +import { BlobNotFoundError, BlobStorage } from '@wundergraph/cosmo-cdn'; +import { Context } from 'hono'; + +/** + * Retrieves objects from S3 given an S3Client and a bucket name + */ +class S3BlobStorage implements BlobStorage { + constructor( + private s3Client: S3Client, + private bucketName: string, + ) {} + + async getObject(_c: Context, key: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + try { + const resp = await this.s3Client.send(command); + if (resp.$metadata.httpStatusCode !== 200) { + throw new BlobNotFoundError(`Failed to retrieve object from S3: ${resp}`); + } + return resp.Body!.transformToWebStream(); + } catch (e: any) { + if (e instanceof NoSuchKey) { + throw new BlobNotFoundError(`Failed to retrieve object from S3: ${e}`); + } + throw e; + } + } +} + +export const createS3BlobStorage = (storageUrl: string): BlobStorage => { + const url = new URL(storageUrl); + const region = url.searchParams.get('region') ?? 'default'; + const s3Client = new S3Client({ + region, + endpoint: url.origin, + credentials: { + accessKeyId: url.username ?? '', + secretAccessKey: url.password ?? '', + }, + forcePathStyle: true, + }); + const bucketName = url.pathname.slice(1); + return new S3BlobStorage(s3Client, bucketName); +}; diff --git a/cdn-server/tsconfig.json b/cdn-server/tsconfig.json new file mode 100644 index 0000000000..03ead7afad --- /dev/null +++ b/cdn-server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "module": "commonjs" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/cli/package.json b/cli/package.json index fc459ce57c..46396a3754 100644 --- a/cli/package.json +++ b/cli/package.json @@ -39,6 +39,7 @@ "cli-table3": "^0.6.3", "commander": "^11.1.0", "date-fns": "^2.30.0", + "dotenv": "^16.3.1", "env-paths": "^3.0.0", "graphql": "^16.7.1", "inquirer": "^9.2.7", diff --git a/cli/src/commands/index.ts b/cli/src/commands/index.ts index 763d2c73bd..4ff2104e95 100644 --- a/cli/src/commands/index.ts +++ b/cli/src/commands/index.ts @@ -3,6 +3,7 @@ import { CreateClient } from '../core/client/client.js'; import { config } from '../core/config.js'; import AuthCommands from './auth/index.js'; import FederatedGraphCommands from './federated-graph/index.js'; +import OperationCommands from './operations/index.js'; import RouterCommands from './router/index.js'; import SchemaCommands from './subgraph/index.js'; @@ -33,6 +34,11 @@ program.addCommand( client, }), ); +program.addCommand( + OperationCommands({ + client, + }), +); program.addCommand( RouterCommands({ client, diff --git a/cli/src/commands/operations/commands/push.ts b/cli/src/commands/operations/commands/push.ts new file mode 100644 index 0000000000..126db55f83 --- /dev/null +++ b/cli/src/commands/operations/commands/push.ts @@ -0,0 +1,109 @@ +import { readFile } from 'node:fs/promises'; + +import { Command } from 'commander'; +import pc from 'picocolors'; + +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; +import { PublishedOperationStatus } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; + +import { BaseCommandOptions } from '../../../core/types/types'; +import { baseHeaders } from '../../../core/config.js'; + +const collect = (value: string, previous: string[]): string[] => { + return [...previous, value]; +}; + +interface ApolloPersistedQueryManifest { + format: 'apollo-persisted-query-manifest'; + version?: number; + operations?: [ + { + id?: string; + name?: string; + type?: string; + body?: string; + }, + ]; +} + +const parseApolloPersistedQueryManifest = (data: ApolloPersistedQueryManifest): string[] => { + if (data.version !== 1) { + throw new Error(`unknown Apollo persisted query manifest version ${data.version}`); + } + return data.operations?.map((op) => op.body).filter((x): x is string => !!x) ?? []; +}; + +const parseOperationsJson = (data: any): string[] => { + if (data.format === 'apollo-persisted-query-manifest') { + return parseApolloPersistedQueryManifest(data); + } + // Check if all elements are 2-element arrays of strings. In that case + // the data is a relay query map + if ( + Array.isArray(data) && + data.length === + data.filter((x) => Array.isArray(x) && x.length === 2 && typeof x[0] === 'string' && typeof x[1] === 'string') + .length + ) { + return data.map((x) => x[1]).filter((x): x is string => !!x); + } + throw new Error(`unknown data format`); +}; + +export const parseOperations = (contents: string): string[] => { + let data: any; + try { + data = JSON.parse(contents); + } catch { + // Assume it's plain graphql + return [contents]; + } + return parseOperationsJson(data); +}; + +export default (opts: BaseCommandOptions) => { + const command = new Command('push'); + command.description('Pushes new operations to the registry'); + command.argument('', 'The name of the federated graph on which the check operations are stored.'); + command.requiredOption('-c, --client ', 'The client identifier to register the operations to'); + command.requiredOption( + '-f, --file ', + 'The file with the operations to push - supports .graphql, .gql and .json manifests from Apollo and Relay', + collect, + [], + ); + command.action(async (name, options) => { + if (options.file.length === 0) { + command.error(pc.red('No files provided')); + } + const operations: string[] = []; + for (const file of options.file) { + const contents = await readFile(file, 'utf8'); + try { + operations.push(...parseOperations(contents)); + } catch (e: any) { + command.error(pc.red(`Failed to parse ${file}: ${e.message}`)); + } + } + + const result = await opts.client.platform.publishPersistedOperations( + { + fedGraphName: name, + clientName: options.client, + operations, + }, + { headers: baseHeaders }, + ); + if (result.response?.code === EnumStatusCode.OK) { + const upToDate = (result.operations?.filter((op) => op.status === PublishedOperationStatus.UP_TO_DATE) ?? []) + .length; + const created = (result.operations?.filter((op) => op.status === PublishedOperationStatus.CREATED) ?? []).length; + console.log( + pc.green(`pushed ${result.operations?.length ?? 0} operations: ${created} created, ${upToDate} up to date`), + ); + } else { + command.error(pc.red(`could not push operations: ${result.response?.details ?? 'unknown error'}`)); + } + }); + return command; +}; diff --git a/cli/src/commands/operations/index.ts b/cli/src/commands/operations/index.ts new file mode 100644 index 0000000000..c2d9d8a811 --- /dev/null +++ b/cli/src/commands/operations/index.ts @@ -0,0 +1,17 @@ +import { Command } from 'commander'; +import { checkAPIKey } from '../../utils'; +import { BaseCommandOptions } from '../../core/types/types'; + +import PushOperationsCommand from './commands/push'; + +export default (opts: BaseCommandOptions) => { + const command = new Command('operations'); + command.description('Provides commands manipulating registered operations'); + command.addCommand(PushOperationsCommand(opts)); + + command.hook('preAction', (thisCmd) => { + checkAPIKey(); + }); + + return command; +}; diff --git a/cli/src/index.ts b/cli/src/index.ts index 1c3cada8d6..65346bc505 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,5 +1,8 @@ #!/usr/bin/env node +import * as dotenv from 'dotenv'; import program from './commands/index.js'; +dotenv.config(); + await program.parseAsync(process.argv); diff --git a/cli/test/parse-operations.test.ts b/cli/test/parse-operations.test.ts new file mode 100644 index 0000000000..9f5069f776 --- /dev/null +++ b/cli/test/parse-operations.test.ts @@ -0,0 +1,36 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { describe, test, expect } from 'vitest'; + +import { parseOperations } from '../src/commands/operations/commands/push'; + + +describe('parse operations from different formats', () => { + test('parse operations from graphql', () => { + const operations = parseOperations(` + query { + hello + } + `); + expect(operations).toEqual([` + query { + hello + } + `]); + }); + test('parse operations from Apollo', async() => { + const persistedQueries = await fs.readFile(path.join('test', 'testdata', 'persisted-query-manifest.json'), 'utf8'); + const operations = parseOperations(persistedQueries); + expect(operations).toEqual([ + "query Employees {\n employees {\n id\n }\n}" + ]); + }); + test('parse query map', async() => { + const queryMap = await fs.readFile(path.join('test', 'testdata', 'query-map.json'), 'utf8'); + const operations = parseOperations(queryMap); + expect(operations).toEqual([ + "subscription {\n currentTime {\n unixTime \n }\n}", + "query { employee(id:1) { id } }" + ]); + }); +}) diff --git a/cli/test/testdata/persisted-query-manifest.json b/cli/test/testdata/persisted-query-manifest.json new file mode 100644 index 0000000000..4f83aa9f6c --- /dev/null +++ b/cli/test/testdata/persisted-query-manifest.json @@ -0,0 +1,12 @@ +{ + "format": "apollo-persisted-query-manifest", + "version": 1, + "operations": [ + { + "id": "2d9df67f96ce804da7a9107d33373132a53bf56aec29ef4b4e06569a43a16935", + "name": "Employees", + "type": "query", + "body": "query Employees {\n employees {\n id\n }\n}" + } + ] +} diff --git a/cli/test/testdata/query-map.json b/cli/test/testdata/query-map.json new file mode 100644 index 0000000000..c605177118 --- /dev/null +++ b/cli/test/testdata/query-map.json @@ -0,0 +1,4 @@ +[ + ["1","subscription {\n currentTime {\n unixTime \n }\n}"], + ["2","query { employee(id:1) { id } }"] +] diff --git a/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts b/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts index 847a81e2b0..3942a0f228 100644 --- a/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts +++ b/connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts @@ -5,7 +5,7 @@ /* eslint-disable */ // @ts-nocheck -import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateOIDCProviderRequest, CreateOIDCProviderResponse, CreateOrganizationWebhookConfigRequest, CreateOrganizationWebhookConfigResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteOIDCProviderRequest, DeleteOIDCProviderResponse, DeleteOrganizationRequest, DeleteOrganizationResponse, DeleteOrganizationWebhookConfigRequest, DeleteOrganizationWebhookConfigResponse, DeleteRouterTokenRequest, DeleteRouterTokenResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, ForceCheckSuccessRequest, ForceCheckSuccessResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetCheckOperationsRequest, GetCheckOperationsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetCheckSummaryRequest, GetCheckSummaryResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFieldUsageRequest, GetFieldUsageResponse, GetGraphMetricsRequest, GetGraphMetricsResponse, GetLatestValidSubgraphSDLByNameRequest, GetLatestValidSubgraphSDLByNameResponse, GetMetricsErrorRateRequest, GetMetricsErrorRateResponse, GetOIDCProviderRequest, GetOIDCProviderResponse, GetOperationContentRequest, GetOperationContentResponse, GetOrganizationIntegrationsRequest, GetOrganizationIntegrationsResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetOrganizationWebhookConfigsRequest, GetOrganizationWebhookConfigsResponse, GetOrganizationWebhookMetaRequest, GetOrganizationWebhookMetaResponse, GetRouterTokensRequest, GetRouterTokensResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, IsGitHubAppInstalledRequest, IsGitHubAppInstalledResponse, LeaveOrganizationRequest, LeaveOrganizationResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateIntegrationConfigRequest, UpdateIntegrationConfigResponse, UpdateOrganizationDetailsRequest, UpdateOrganizationDetailsResponse, UpdateOrganizationWebhookConfigRequest, UpdateOrganizationWebhookConfigResponse, UpdateOrgMemberRoleRequest, UpdateOrgMemberRoleResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js"; +import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateOIDCProviderRequest, CreateOIDCProviderResponse, CreateOrganizationWebhookConfigRequest, CreateOrganizationWebhookConfigResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteOIDCProviderRequest, DeleteOIDCProviderResponse, DeleteOrganizationRequest, DeleteOrganizationResponse, DeleteOrganizationWebhookConfigRequest, DeleteOrganizationWebhookConfigResponse, DeleteRouterTokenRequest, DeleteRouterTokenResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, ForceCheckSuccessRequest, ForceCheckSuccessResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetCheckOperationsRequest, GetCheckOperationsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetCheckSummaryRequest, GetCheckSummaryResponse, GetClientsRequest, GetClientsResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFieldUsageRequest, GetFieldUsageResponse, GetGraphMetricsRequest, GetGraphMetricsResponse, GetLatestValidSubgraphSDLByNameRequest, GetLatestValidSubgraphSDLByNameResponse, GetMetricsErrorRateRequest, GetMetricsErrorRateResponse, GetOIDCProviderRequest, GetOIDCProviderResponse, GetOperationContentRequest, GetOperationContentResponse, GetOrganizationIntegrationsRequest, GetOrganizationIntegrationsResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetOrganizationWebhookConfigsRequest, GetOrganizationWebhookConfigsResponse, GetOrganizationWebhookMetaRequest, GetOrganizationWebhookMetaResponse, GetRouterTokensRequest, GetRouterTokensResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, IsGitHubAppInstalledRequest, IsGitHubAppInstalledResponse, LeaveOrganizationRequest, LeaveOrganizationResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, PublishPersistedOperationsRequest, PublishPersistedOperationsResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateIntegrationConfigRequest, UpdateIntegrationConfigResponse, UpdateOrganizationDetailsRequest, UpdateOrganizationDetailsResponse, UpdateOrganizationWebhookConfigRequest, UpdateOrganizationWebhookConfigResponse, UpdateOrgMemberRoleRequest, UpdateOrgMemberRoleResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js"; import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; import { GetConfigRequest, GetConfigResponse } from "../../node/v1/node_pb.js"; import { createQueryService, createUnaryHooks, UnaryFunctionsWithHooks } from "@connectrpc/connect-query"; @@ -159,6 +159,17 @@ export const PlatformService = { O: DeleteRouterTokenResponse, kind: MethodKind.Unary, }, + /** + * Add persisted operations + * + * @generated from rpc wg.cosmo.platform.v1.PlatformService.PublishPersistedOperations + */ + publishPersistedOperations: { + name: "PublishPersistedOperations", + I: PublishPersistedOperationsRequest, + O: PublishPersistedOperationsResponse, + kind: MethodKind.Unary, + }, /** * GetFederatedGraphs returns the list of federated graphs. * @@ -588,6 +599,17 @@ export const PlatformService = { O: DeleteOIDCProviderResponse, kind: MethodKind.Unary, }, + /** + * GetClients returns all the clients of the organization + * + * @generated from rpc wg.cosmo.platform.v1.PlatformService.GetClients + */ + getClients: { + name: "GetClients", + I: GetClientsRequest, + O: GetClientsResponse, + kind: MethodKind.Unary, + }, /** * @generated from rpc wg.cosmo.platform.v1.PlatformService.GetAnalyticsView */ @@ -741,6 +763,13 @@ export const getRouterTokens: UnaryFunctionsWithHooks = { ...$queryService.deleteRouterToken, ...createUnaryHooks($queryService.deleteRouterToken)}; +/** + * Add persisted operations + * + * @generated from rpc wg.cosmo.platform.v1.PlatformService.PublishPersistedOperations + */ +export const publishPersistedOperations: UnaryFunctionsWithHooks = { ...$queryService.publishPersistedOperations, ...createUnaryHooks($queryService.publishPersistedOperations)}; + /** * GetFederatedGraphs returns the list of federated graphs. * @@ -1014,6 +1043,13 @@ export const getOIDCProvider: UnaryFunctionsWithHooks = { ...$queryService.deleteOIDCProvider, ...createUnaryHooks($queryService.deleteOIDCProvider)}; +/** + * GetClients returns all the clients of the organization + * + * @generated from rpc wg.cosmo.platform.v1.PlatformService.GetClients + */ +export const getClients: UnaryFunctionsWithHooks = { ...$queryService.getClients, ...createUnaryHooks($queryService.getClients)}; + /** * @generated from rpc wg.cosmo.platform.v1.PlatformService.GetAnalyticsView */ diff --git a/connect/src/wg/cosmo/platform/v1/platform_connect.ts b/connect/src/wg/cosmo/platform/v1/platform_connect.ts index 98a72c37d1..e0b1c9c001 100644 --- a/connect/src/wg/cosmo/platform/v1/platform_connect.ts +++ b/connect/src/wg/cosmo/platform/v1/platform_connect.ts @@ -5,7 +5,7 @@ /* eslint-disable */ // @ts-nocheck -import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateOIDCProviderRequest, CreateOIDCProviderResponse, CreateOrganizationWebhookConfigRequest, CreateOrganizationWebhookConfigResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteOIDCProviderRequest, DeleteOIDCProviderResponse, DeleteOrganizationRequest, DeleteOrganizationResponse, DeleteOrganizationWebhookConfigRequest, DeleteOrganizationWebhookConfigResponse, DeleteRouterTokenRequest, DeleteRouterTokenResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, ForceCheckSuccessRequest, ForceCheckSuccessResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetCheckOperationsRequest, GetCheckOperationsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetCheckSummaryRequest, GetCheckSummaryResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFieldUsageRequest, GetFieldUsageResponse, GetGraphMetricsRequest, GetGraphMetricsResponse, GetLatestValidSubgraphSDLByNameRequest, GetLatestValidSubgraphSDLByNameResponse, GetMetricsErrorRateRequest, GetMetricsErrorRateResponse, GetOIDCProviderRequest, GetOIDCProviderResponse, GetOperationContentRequest, GetOperationContentResponse, GetOrganizationIntegrationsRequest, GetOrganizationIntegrationsResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetOrganizationWebhookConfigsRequest, GetOrganizationWebhookConfigsResponse, GetOrganizationWebhookMetaRequest, GetOrganizationWebhookMetaResponse, GetRouterTokensRequest, GetRouterTokensResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, IsGitHubAppInstalledRequest, IsGitHubAppInstalledResponse, LeaveOrganizationRequest, LeaveOrganizationResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateIntegrationConfigRequest, UpdateIntegrationConfigResponse, UpdateOrganizationDetailsRequest, UpdateOrganizationDetailsResponse, UpdateOrganizationWebhookConfigRequest, UpdateOrganizationWebhookConfigResponse, UpdateOrgMemberRoleRequest, UpdateOrgMemberRoleResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js"; +import { CheckFederatedGraphRequest, CheckFederatedGraphResponse, CheckSubgraphSchemaRequest, CheckSubgraphSchemaResponse, CreateAPIKeyRequest, CreateAPIKeyResponse, CreateFederatedGraphRequest, CreateFederatedGraphResponse, CreateFederatedGraphTokenRequest, CreateFederatedGraphTokenResponse, CreateFederatedSubgraphRequest, CreateFederatedSubgraphResponse, CreateIntegrationRequest, CreateIntegrationResponse, CreateOIDCProviderRequest, CreateOIDCProviderResponse, CreateOrganizationWebhookConfigRequest, CreateOrganizationWebhookConfigResponse, DeleteAPIKeyRequest, DeleteAPIKeyResponse, DeleteFederatedGraphRequest, DeleteFederatedGraphResponse, DeleteFederatedSubgraphRequest, DeleteFederatedSubgraphResponse, DeleteIntegrationRequest, DeleteIntegrationResponse, DeleteOIDCProviderRequest, DeleteOIDCProviderResponse, DeleteOrganizationRequest, DeleteOrganizationResponse, DeleteOrganizationWebhookConfigRequest, DeleteOrganizationWebhookConfigResponse, DeleteRouterTokenRequest, DeleteRouterTokenResponse, FixSubgraphSchemaRequest, FixSubgraphSchemaResponse, ForceCheckSuccessRequest, ForceCheckSuccessResponse, GetAnalyticsViewRequest, GetAnalyticsViewResponse, GetAPIKeysRequest, GetAPIKeysResponse, GetCheckDetailsRequest, GetCheckDetailsResponse, GetCheckOperationsRequest, GetCheckOperationsResponse, GetChecksByFederatedGraphNameRequest, GetChecksByFederatedGraphNameResponse, GetCheckSummaryRequest, GetCheckSummaryResponse, GetClientsRequest, GetClientsResponse, GetDashboardAnalyticsViewRequest, GetDashboardAnalyticsViewResponse, GetFederatedGraphByNameRequest, GetFederatedGraphByNameResponse, GetFederatedGraphChangelogRequest, GetFederatedGraphChangelogResponse, GetFederatedGraphSDLByNameRequest, GetFederatedGraphSDLByNameResponse, GetFederatedGraphsRequest, GetFederatedGraphsResponse, GetFieldUsageRequest, GetFieldUsageResponse, GetGraphMetricsRequest, GetGraphMetricsResponse, GetLatestValidSubgraphSDLByNameRequest, GetLatestValidSubgraphSDLByNameResponse, GetMetricsErrorRateRequest, GetMetricsErrorRateResponse, GetOIDCProviderRequest, GetOIDCProviderResponse, GetOperationContentRequest, GetOperationContentResponse, GetOrganizationIntegrationsRequest, GetOrganizationIntegrationsResponse, GetOrganizationMembersRequest, GetOrganizationMembersResponse, GetOrganizationWebhookConfigsRequest, GetOrganizationWebhookConfigsResponse, GetOrganizationWebhookMetaRequest, GetOrganizationWebhookMetaResponse, GetRouterTokensRequest, GetRouterTokensResponse, GetSubgraphByNameRequest, GetSubgraphByNameResponse, GetSubgraphsRequest, GetSubgraphsResponse, GetTraceRequest, GetTraceResponse, InviteUserRequest, InviteUserResponse, IsGitHubAppInstalledRequest, IsGitHubAppInstalledResponse, LeaveOrganizationRequest, LeaveOrganizationResponse, MigrateFromApolloRequest, MigrateFromApolloResponse, PublishFederatedSubgraphRequest, PublishFederatedSubgraphResponse, PublishPersistedOperationsRequest, PublishPersistedOperationsResponse, RemoveInvitationRequest, RemoveInvitationResponse, UpdateFederatedGraphRequest, UpdateFederatedGraphResponse, UpdateIntegrationConfigRequest, UpdateIntegrationConfigResponse, UpdateOrganizationDetailsRequest, UpdateOrganizationDetailsResponse, UpdateOrganizationWebhookConfigRequest, UpdateOrganizationWebhookConfigResponse, UpdateOrgMemberRoleRequest, UpdateOrgMemberRoleResponse, UpdateSubgraphRequest, UpdateSubgraphResponse, WhoAmIRequest, WhoAmIResponse } from "./platform_pb.js"; import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; import { GetConfigRequest, GetConfigResponse } from "../../node/v1/node_pb.js"; @@ -156,6 +156,17 @@ export const PlatformService = { O: DeleteRouterTokenResponse, kind: MethodKind.Unary, }, + /** + * Add persisted operations + * + * @generated from rpc wg.cosmo.platform.v1.PlatformService.PublishPersistedOperations + */ + publishPersistedOperations: { + name: "PublishPersistedOperations", + I: PublishPersistedOperationsRequest, + O: PublishPersistedOperationsResponse, + kind: MethodKind.Unary, + }, /** * GetFederatedGraphs returns the list of federated graphs. * @@ -585,6 +596,17 @@ export const PlatformService = { O: DeleteOIDCProviderResponse, kind: MethodKind.Unary, }, + /** + * GetClients returns all the clients of the organization + * + * @generated from rpc wg.cosmo.platform.v1.PlatformService.GetClients + */ + getClients: { + name: "GetClients", + I: GetClientsRequest, + O: GetClientsResponse, + kind: MethodKind.Unary, + }, /** * @generated from rpc wg.cosmo.platform.v1.PlatformService.GetAnalyticsView */ diff --git a/connect/src/wg/cosmo/platform/v1/platform_pb.ts b/connect/src/wg/cosmo/platform/v1/platform_pb.ts index 4c71e7fee5..de34269b85 100644 --- a/connect/src/wg/cosmo/platform/v1/platform_pb.ts +++ b/connect/src/wg/cosmo/platform/v1/platform_pb.ts @@ -216,6 +216,26 @@ proto3.util.setEnumType(ExpiresAt, "wg.cosmo.platform.v1.ExpiresAt", [ { no: 3, name: "ONE_YEAR" }, ]); +/** + * @generated from enum wg.cosmo.platform.v1.PublishedOperationStatus + */ +export enum PublishedOperationStatus { + /** + * @generated from enum value: UP_TO_DATE = 0; + */ + UP_TO_DATE = 0, + + /** + * @generated from enum value: CREATED = 1; + */ + CREATED = 1, +} +// Retrieve enum metadata with: proto3.getEnumType(PublishedOperationStatus) +proto3.util.setEnumType(PublishedOperationStatus, "wg.cosmo.platform.v1.PublishedOperationStatus", [ + { no: 0, name: "UP_TO_DATE" }, + { no: 1, name: "CREATED" }, +]); + /** * @generated from enum wg.cosmo.platform.v1.IntegrationType */ @@ -5425,6 +5445,141 @@ export class DeleteRouterTokenResponse extends Message { + /** + * @generated from field: string fedGraphName = 1; + */ + fedGraphName = ""; + + /** + * @generated from field: string clientName = 2; + */ + clientName = ""; + + /** + * @generated from field: repeated string operations = 3; + */ + operations: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "wg.cosmo.platform.v1.PublishPersistedOperationsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "fedGraphName", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "clientName", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "operations", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PublishPersistedOperationsRequest { + return new PublishPersistedOperationsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PublishPersistedOperationsRequest { + return new PublishPersistedOperationsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PublishPersistedOperationsRequest { + return new PublishPersistedOperationsRequest().fromJsonString(jsonString, options); + } + + static equals(a: PublishPersistedOperationsRequest | PlainMessage | undefined, b: PublishPersistedOperationsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(PublishPersistedOperationsRequest, a, b); + } +} + +/** + * @generated from message wg.cosmo.platform.v1.PublishedOperation + */ +export class PublishedOperation extends Message { + /** + * @generated from field: string hash = 1; + */ + hash = ""; + + /** + * @generated from field: wg.cosmo.platform.v1.PublishedOperationStatus status = 2; + */ + status = PublishedOperationStatus.UP_TO_DATE; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "wg.cosmo.platform.v1.PublishedOperation"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "hash", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "status", kind: "enum", T: proto3.getEnumType(PublishedOperationStatus) }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PublishedOperation { + return new PublishedOperation().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PublishedOperation { + return new PublishedOperation().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PublishedOperation { + return new PublishedOperation().fromJsonString(jsonString, options); + } + + static equals(a: PublishedOperation | PlainMessage | undefined, b: PublishedOperation | PlainMessage | undefined): boolean { + return proto3.util.equals(PublishedOperation, a, b); + } +} + +/** + * @generated from message wg.cosmo.platform.v1.PublishPersistedOperationsResponse + */ +export class PublishPersistedOperationsResponse extends Message { + /** + * @generated from field: wg.cosmo.platform.v1.Response response = 1; + */ + response?: Response; + + /** + * @generated from field: repeated wg.cosmo.platform.v1.PublishedOperation operations = 2; + */ + operations: PublishedOperation[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "wg.cosmo.platform.v1.PublishPersistedOperationsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "response", kind: "message", T: Response }, + { no: 2, name: "operations", kind: "message", T: PublishedOperation, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PublishPersistedOperationsResponse { + return new PublishPersistedOperationsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PublishPersistedOperationsResponse { + return new PublishPersistedOperationsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PublishPersistedOperationsResponse { + return new PublishPersistedOperationsResponse().fromJsonString(jsonString, options); + } + + static equals(a: PublishPersistedOperationsResponse | PlainMessage | undefined, b: PublishPersistedOperationsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(PublishPersistedOperationsResponse, a, b); + } +} + /** * @generated from message wg.cosmo.platform.v1.Header */ @@ -7735,6 +7890,153 @@ export class DeleteOIDCProviderResponse extends Message { + /** + * @generated from field: string name = 1; + */ + name = ""; + + /** + * @generated from field: string id = 2; + */ + id = ""; + + /** + * @generated from field: string createdAt = 3; + */ + createdAt = ""; + + /** + * @generated from field: string lastUpdatedAt = 4; + */ + lastUpdatedAt = ""; + + /** + * @generated from field: string createdBy = 5; + */ + createdBy = ""; + + /** + * @generated from field: string lastUpdatedBy = 6; + */ + lastUpdatedBy = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "wg.cosmo.platform.v1.ClientInfo"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "id", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "createdAt", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "lastUpdatedAt", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 5, name: "createdBy", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 6, name: "lastUpdatedBy", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ClientInfo { + return new ClientInfo().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ClientInfo { + return new ClientInfo().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): ClientInfo { + return new ClientInfo().fromJsonString(jsonString, options); + } + + static equals(a: ClientInfo | PlainMessage | undefined, b: ClientInfo | PlainMessage | undefined): boolean { + return proto3.util.equals(ClientInfo, a, b); + } +} + +/** + * @generated from message wg.cosmo.platform.v1.GetClientsRequest + */ +export class GetClientsRequest extends Message { + /** + * @generated from field: string fedGraphName = 1; + */ + fedGraphName = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "wg.cosmo.platform.v1.GetClientsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "fedGraphName", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetClientsRequest { + return new GetClientsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetClientsRequest { + return new GetClientsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetClientsRequest { + return new GetClientsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetClientsRequest | PlainMessage | undefined, b: GetClientsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetClientsRequest, a, b); + } +} + +/** + * @generated from message wg.cosmo.platform.v1.GetClientsResponse + */ +export class GetClientsResponse extends Message { + /** + * @generated from field: wg.cosmo.platform.v1.Response response = 1; + */ + response?: Response; + + /** + * @generated from field: repeated wg.cosmo.platform.v1.ClientInfo clients = 2; + */ + clients: ClientInfo[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "wg.cosmo.platform.v1.GetClientsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "response", kind: "message", T: Response }, + { no: 2, name: "clients", kind: "message", T: ClientInfo, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetClientsResponse { + return new GetClientsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetClientsResponse { + return new GetClientsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetClientsResponse { + return new GetClientsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetClientsResponse | PlainMessage | undefined, b: GetClientsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetClientsResponse, a, b); + } +} + /** * @generated from message wg.cosmo.platform.v1.GetFieldUsageRequest */ diff --git a/controlplane/.env.example b/controlplane/.env.example index 51ef1c37ad..87e6e5c6bc 100644 --- a/controlplane/.env.example +++ b/controlplane/.env.example @@ -17,6 +17,7 @@ KC_API_URL="http://localhost:8080" KC_FRONTEND_URL="http://localhost:8080" WEBHOOK_URL= WEBHOOK_SECRET= +S3_STORAGE_URL="http://minio:changeme@localhost:10000/cosmo" # Optional for GitHub Integration GITHUB_APP_CLIENT_ID= @@ -27,4 +28,4 @@ GITHUB_APP_WEBHOOK_SECRET= # Optional for Slack Integration SLACK_APP_CLIENT_ID= -SLACK_APP_CLIENT_SECRET= \ No newline at end of file +SLACK_APP_CLIENT_SECRET= diff --git a/controlplane/migrations/0057_late_golden_guardian.sql b/controlplane/migrations/0057_late_golden_guardian.sql new file mode 100644 index 0000000000..b1c59c4d41 --- /dev/null +++ b/controlplane/migrations/0057_late_golden_guardian.sql @@ -0,0 +1,66 @@ +CREATE TABLE IF NOT EXISTS "federated_graph_clients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "federated_graph_id" uuid NOT NULL, + "name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone, + "created_by_id" uuid NOT NULL, + "updated_by_id" uuid, + CONSTRAINT "federated_graph_client_name" UNIQUE("federated_graph_id","name") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "federated_graph_persisted_operations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "federated_graph_id" uuid NOT NULL, + "client_id" uuid NOT NULL, + "hash" text NOT NULL, + "file_path" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone, + "created_by_id" uuid NOT NULL, + "updated_by_id" uuid, + CONSTRAINT "federated_graph_operation_hash" UNIQUE("federated_graph_id","hash"), + CONSTRAINT "federated_graph_operation_file_hash" UNIQUE("federated_graph_id","file_path") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_clients" ADD CONSTRAINT "federated_graph_clients_federated_graph_id_federated_graphs_id_fk" FOREIGN KEY ("federated_graph_id") REFERENCES "federated_graphs"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_clients" ADD CONSTRAINT "federated_graph_clients_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_clients" ADD CONSTRAINT "federated_graph_clients_updated_by_id_users_id_fk" FOREIGN KEY ("updated_by_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_persisted_operations" ADD CONSTRAINT "federated_graph_persisted_operations_federated_graph_id_federated_graphs_id_fk" FOREIGN KEY ("federated_graph_id") REFERENCES "federated_graphs"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_persisted_operations" ADD CONSTRAINT "federated_graph_persisted_operations_client_id_federated_graph_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "federated_graph_clients"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_persisted_operations" ADD CONSTRAINT "federated_graph_persisted_operations_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "federated_graph_persisted_operations" ADD CONSTRAINT "federated_graph_persisted_operations_updated_by_id_users_id_fk" FOREIGN KEY ("updated_by_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/controlplane/migrations/meta/0057_snapshot.json b/controlplane/migrations/meta/0057_snapshot.json new file mode 100644 index 0000000000..11f3b6b74f --- /dev/null +++ b/controlplane/migrations/meta/0057_snapshot.json @@ -0,0 +1,2359 @@ +{ + "version": "5", + "dialect": "pg", + "id": "a1d88a6a-1976-4acb-9430-6f44b7d87dd1", + "prevId": "cb19aae4-6a6d-4124-845d-64c80aed06a2", + "tables": { + "api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_name_idx": { + "name": "apikey_name_idx", + "columns": [ + "name", + "organization_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "api_keys_organization_id_organizations_id_fk": { + "name": "api_keys_organization_id_organizations_id_fk", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_unique": { + "name": "api_keys_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + } + }, + "federated_graph_clients": { + "name": "federated_graph_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "federated_graph_clients_federated_graph_id_federated_graphs_id_fk": { + "name": "federated_graph_clients_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "federated_graph_clients", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_graph_clients_created_by_id_users_id_fk": { + "name": "federated_graph_clients_created_by_id_users_id_fk", + "tableFrom": "federated_graph_clients", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_graph_clients_updated_by_id_users_id_fk": { + "name": "federated_graph_clients_updated_by_id_users_id_fk", + "tableFrom": "federated_graph_clients", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federated_graph_client_name": { + "name": "federated_graph_client_name", + "nullsNotDistinct": false, + "columns": [ + "federated_graph_id", + "name" + ] + } + } + }, + "federated_graph_configs": { + "name": "federated_graph_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "traffic_check_days": { + "name": "traffic_check_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 7 + } + }, + "indexes": {}, + "foreignKeys": { + "federated_graph_configs_federated_graph_id_federated_graphs_id_fk": { + "name": "federated_graph_configs_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "federated_graph_configs", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_graph_persisted_operations": { + "name": "federated_graph_persisted_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_by_id": { + "name": "updated_by_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "federated_graph_persisted_operations_federated_graph_id_federated_graphs_id_fk": { + "name": "federated_graph_persisted_operations_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "federated_graph_persisted_operations", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_graph_persisted_operations_client_id_federated_graph_clients_id_fk": { + "name": "federated_graph_persisted_operations_client_id_federated_graph_clients_id_fk", + "tableFrom": "federated_graph_persisted_operations", + "tableTo": "federated_graph_clients", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_graph_persisted_operations_created_by_id_users_id_fk": { + "name": "federated_graph_persisted_operations_created_by_id_users_id_fk", + "tableFrom": "federated_graph_persisted_operations", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_graph_persisted_operations_updated_by_id_users_id_fk": { + "name": "federated_graph_persisted_operations_updated_by_id_users_id_fk", + "tableFrom": "federated_graph_persisted_operations", + "tableTo": "users", + "columnsFrom": [ + "updated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federated_graph_operation_hash": { + "name": "federated_graph_operation_hash", + "nullsNotDistinct": false, + "columns": [ + "federated_graph_id", + "hash" + ] + }, + "federated_graph_operation_file_hash": { + "name": "federated_graph_operation_file_hash", + "nullsNotDistinct": false, + "columns": [ + "federated_graph_id", + "file_path" + ] + } + } + }, + "federated_graphs": { + "name": "federated_graphs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "routing_url": { + "name": "routing_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "composed_schema_version_id": { + "name": "composed_schema_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "federated_graphs_target_id_targets_id_fk": { + "name": "federated_graphs_target_id_targets_id_fk", + "tableFrom": "federated_graphs", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_graphs_composed_schema_version_id_schema_versions_id_fk": { + "name": "federated_graphs_composed_schema_version_id_schema_versions_id_fk", + "tableFrom": "federated_graphs", + "tableTo": "schema_versions", + "columnsFrom": [ + "composed_schema_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "git_installations": { + "name": "git_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "git_installation_type", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "provider_installation_id": { + "name": "provider_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "provider_name": { + "name": "provider_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "graph_api_tokens": { + "name": "graph_api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "graphApiToken_name_idx": { + "name": "graphApiToken_name_idx", + "columns": [ + "name", + "federated_graph_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "graph_api_tokens_organization_id_organizations_id_fk": { + "name": "graph_api_tokens_organization_id_organizations_id_fk", + "tableFrom": "graph_api_tokens", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "graph_api_tokens_federated_graph_id_federated_graphs_id_fk": { + "name": "graph_api_tokens_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "graph_api_tokens", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "graph_api_tokens_token_unique": { + "name": "graph_api_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "graph_composition_subgraphs": { + "name": "graph_composition_subgraphs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "graph_composition_id": { + "name": "graph_composition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "schema_version_id": { + "name": "schema_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "graph_composition_subgraphs_graph_composition_id_graph_compositions_id_fk": { + "name": "graph_composition_subgraphs_graph_composition_id_graph_compositions_id_fk", + "tableFrom": "graph_composition_subgraphs", + "tableTo": "graph_compositions", + "columnsFrom": [ + "graph_composition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "graph_composition_subgraphs_schema_version_id_schema_versions_id_fk": { + "name": "graph_composition_subgraphs_schema_version_id_schema_versions_id_fk", + "tableFrom": "graph_composition_subgraphs", + "tableTo": "schema_versions", + "columnsFrom": [ + "schema_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "graph_compositions": { + "name": "graph_compositions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_version_id": { + "name": "schema_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_composable": { + "name": "is_composable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "composition_errors": { + "name": "composition_errors", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "router_config": { + "name": "router_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "graph_compositions_schema_version_id_schema_versions_id_fk": { + "name": "graph_compositions_schema_version_id_schema_versions_id_fk", + "tableFrom": "graph_compositions", + "tableTo": "schema_versions", + "columnsFrom": [ + "schema_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "graph_compositions_created_by_users_id_fk": { + "name": "graph_compositions_created_by_users_id_fk", + "tableFrom": "graph_compositions", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "oidc_providers": { + "name": "oidc_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oidc_providers_organization_id_organizations_id_fk": { + "name": "oidc_providers_organization_id_organizations_id_fk", + "tableFrom": "oidc_providers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oidc_providers_alias_unique": { + "name": "oidc_providers_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "alias" + ] + } + } + }, + "organization_integrations": { + "name": "organization_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "integration_type", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_integration_idx": { + "name": "organization_integration_idx", + "columns": [ + "organization_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "organization_integrations_organization_id_organizations_id_fk": { + "name": "organization_integrations_organization_id_organizations_id_fk", + "tableFrom": "organization_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "organization_member_roles": { + "name": "organization_member_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_member_id": { + "name": "organization_member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "member_role", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "organization_member_role_idx": { + "name": "organization_member_role_idx", + "columns": [ + "organization_member_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "organization_member_roles_organization_member_id_organization_members_id_fk": { + "name": "organization_member_roles_organization_member_id_organization_members_id_fk", + "tableFrom": "organization_member_roles", + "tableTo": "organization_members", + "columnsFrom": [ + "organization_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "organization_webhook_configs": { + "name": "organization_webhook_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_webhook_configs_organization_id_organizations_id_fk": { + "name": "organization_webhook_configs_organization_id_organizations_id_fk", + "tableFrom": "organization_webhook_configs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invite_code": { + "name": "invite_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_personal": { + "name": "is_personal", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_free_trial": { + "name": "is_free_trial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "organizations_user_id_users_id_fk": { + "name": "organizations_user_id_users_id_fk", + "tableFrom": "organizations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + } + }, + "organization_members": { + "name": "organization_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_invite": { + "name": "accepted_invite", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_member_idx": { + "name": "organization_member_idx", + "columns": [ + "id" + ], + "isUnique": true + }, + "unique_organization_member_idx": { + "name": "unique_organization_member_idx", + "columns": [ + "user_id", + "organization_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_check_change_action": { + "name": "schema_check_change_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_check_id": { + "name": "schema_check_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "change_type": { + "name": "change_type", + "type": "schema_change_type", + "primaryKey": false, + "notNull": false + }, + "change_message": { + "name": "change_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_breaking": { + "name": "is_breaking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "schema_check_change_action_schema_check_id_schema_checks_id_fk": { + "name": "schema_check_change_action_schema_check_id_schema_checks_id_fk", + "tableFrom": "schema_check_change_action", + "tableTo": "schema_checks", + "columnsFrom": [ + "schema_check_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_check_change_operation_usage": { + "name": "schema_check_change_operation_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_check_change_action_id": { + "name": "schema_check_change_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schema_check_change_operation_usage_schema_check_change_action_id_schema_check_change_action_id_fk": { + "name": "schema_check_change_operation_usage_schema_check_change_action_id_schema_check_change_action_id_fk", + "tableFrom": "schema_check_change_operation_usage", + "tableTo": "schema_check_change_action", + "columnsFrom": [ + "schema_check_change_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_check_composition": { + "name": "schema_check_composition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_check_id": { + "name": "schema_check_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "composition_errors": { + "name": "composition_errors", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composed_schema_sdl": { + "name": "composed_schema_sdl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "schema_check_composition_schema_check_id_schema_checks_id_fk": { + "name": "schema_check_composition_schema_check_id_schema_checks_id_fk", + "tableFrom": "schema_check_composition", + "tableTo": "schema_checks", + "columnsFrom": [ + "schema_check_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schema_check_composition_target_id_targets_id_fk": { + "name": "schema_check_composition_target_id_targets_id_fk", + "tableFrom": "schema_check_composition", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_check_federated_graphs": { + "name": "schema_check_federated_graphs", + "schema": "", + "columns": { + "check_id": { + "name": "check_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "traffic_check_days": { + "name": "traffic_check_days", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schema_check_federated_graphs_check_id_schema_checks_id_fk": { + "name": "schema_check_federated_graphs_check_id_schema_checks_id_fk", + "tableFrom": "schema_check_federated_graphs", + "tableTo": "schema_checks", + "columnsFrom": [ + "check_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schema_check_federated_graphs_federated_graph_id_federated_graphs_id_fk": { + "name": "schema_check_federated_graphs_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "schema_check_federated_graphs", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_checks": { + "name": "schema_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_composable": { + "name": "is_composable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_deleted": { + "name": "is_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "has_breaking_changes": { + "name": "has_breaking_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "has_client_traffic": { + "name": "has_client_traffic", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "proposed_subgraph_schema_sdl": { + "name": "proposed_subgraph_schema_sdl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "gh_details": { + "name": "gh_details", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "forced_success": { + "name": "forced_success", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "schema_checks_target_id_targets_id_fk": { + "name": "schema_checks_target_id_targets_id_fk", + "tableFrom": "schema_checks", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_versions": { + "name": "schema_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schema_sdl": { + "name": "schema_sdl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "schema_versions_target_id_targets_id_fk": { + "name": "schema_versions_target_id_targets_id_fk", + "tableFrom": "schema_versions", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "schema_version_change_action": { + "name": "schema_version_change_action", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_version_id": { + "name": "schema_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "change_type": { + "name": "change_type", + "type": "schema_change_type", + "primaryKey": false, + "notNull": true + }, + "change_message": { + "name": "change_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "schema_version_change_action_schema_version_id_schema_versions_id_fk": { + "name": "schema_version_change_action_schema_version_id_schema_versions_id_fk", + "tableFrom": "schema_version_change_action", + "tableTo": "schema_versions", + "columnsFrom": [ + "schema_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_user_id_unique": { + "name": "sessions_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + } + }, + "slack_installations": { + "name": "slack_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slack_organization_id": { + "name": "slack_organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_organization_name": { + "name": "slack_organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_channel_name": { + "name": "slack_channel_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "slack_installations_idx": { + "name": "slack_installations_idx", + "columns": [ + "organization_id", + "slack_organization_id", + "slack_channel_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "slack_installations_organization_id_organizations_id_fk": { + "name": "slack_installations_organization_id_organizations_id_fk", + "tableFrom": "slack_installations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "slack_integration_configs": { + "name": "slack_integration_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "slack_integration_configs_integration_id_organization_integrations_id_fk": { + "name": "slack_integration_configs_integration_id_organization_integrations_id_fk", + "tableFrom": "slack_integration_configs", + "tableTo": "organization_integrations", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "slack_schema_update_event_configs": { + "name": "slack_schema_update_event_configs", + "schema": "", + "columns": { + "slack_integration_config_id": { + "name": "slack_integration_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "slack_schema_update_event_configs_slack_integration_config_id_slack_integration_configs_id_fk": { + "name": "slack_schema_update_event_configs_slack_integration_config_id_slack_integration_configs_id_fk", + "tableFrom": "slack_schema_update_event_configs", + "tableTo": "slack_integration_configs", + "columnsFrom": [ + "slack_integration_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_schema_update_event_configs_federated_graph_id_federated_graphs_id_fk": { + "name": "slack_schema_update_event_configs_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "slack_schema_update_event_configs", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "subgraphs": { + "name": "subgraphs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "routing_url": { + "name": "routing_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_url": { + "name": "subscription_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_protocol": { + "name": "subscription_protocol", + "type": "subscription_protocol", + "primaryKey": false, + "notNull": true, + "default": "'ws'" + }, + "schema_version_id": { + "name": "schema_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "subgraphs_schema_version_id_schema_versions_id_fk": { + "name": "subgraphs_schema_version_id_schema_versions_id_fk", + "tableFrom": "subgraphs", + "tableTo": "schema_versions", + "columnsFrom": [ + "schema_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "subgraphs_target_id_targets_id_fk": { + "name": "subgraphs_target_id_targets_id_fk", + "tableFrom": "subgraphs", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "federated_subgraphs": { + "name": "federated_subgraphs", + "schema": "", + "columns": { + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subgraph_id": { + "name": "subgraph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "federated_subgraphs_federated_graph_id_federated_graphs_id_fk": { + "name": "federated_subgraphs_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "federated_subgraphs", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federated_subgraphs_subgraph_id_subgraphs_id_fk": { + "name": "federated_subgraphs_subgraph_id_subgraphs_id_fk", + "tableFrom": "federated_subgraphs", + "tableTo": "subgraphs", + "columnsFrom": [ + "subgraph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "federated_subgraphs_federated_graph_id_subgraph_id": { + "name": "federated_subgraphs_federated_graph_id_subgraph_id", + "columns": [ + "federated_graph_id", + "subgraph_id" + ] + } + }, + "uniqueConstraints": {} + }, + "target_label_matchers": { + "name": "target_label_matchers", + "schema": "", + "columns": { + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_matcher": { + "name": "label_matcher", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "target_label_matchers_target_id_targets_id_fk": { + "name": "target_label_matchers_target_id_targets_id_fk", + "tableFrom": "target_label_matchers", + "tableTo": "targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "targets": { + "name": "targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "target_type", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "labels": { + "name": "labels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "organization_name_idx": { + "name": "organization_name_idx", + "columns": [ + "organization_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "targets_organization_id_organizations_id_fk": { + "name": "targets_organization_id_organizations_id_fk", + "tableFrom": "targets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "webhook_graph_schema_update": { + "name": "webhook_graph_schema_update", + "schema": "", + "columns": { + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "federated_graph_id": { + "name": "federated_graph_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "webhook_graph_schema_update_webhook_id_organization_webhook_configs_id_fk": { + "name": "webhook_graph_schema_update_webhook_id_organization_webhook_configs_id_fk", + "tableFrom": "webhook_graph_schema_update", + "tableTo": "organization_webhook_configs", + "columnsFrom": [ + "webhook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_graph_schema_update_federated_graph_id_federated_graphs_id_fk": { + "name": "webhook_graph_schema_update_federated_graph_id_federated_graphs_id_fk", + "tableFrom": "webhook_graph_schema_update", + "tableTo": "federated_graphs", + "columnsFrom": [ + "federated_graph_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "webhook_graph_schema_update_webhook_id_federated_graph_id": { + "name": "webhook_graph_schema_update_webhook_id_federated_graph_id", + "columns": [ + "webhook_id", + "federated_graph_id" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": { + "git_installation_type": { + "name": "git_installation_type", + "values": { + "PERSONAL": "PERSONAL", + "ORGANIZATION": "ORGANIZATION" + } + }, + "integration_type": { + "name": "integration_type", + "values": { + "slack": "slack" + } + }, + "member_role": { + "name": "member_role", + "values": { + "admin": "admin", + "developer": "developer", + "viewer": "viewer" + } + }, + "schema_change_type": { + "name": "schema_change_type", + "values": { + "FIELD_ARGUMENT_DESCRIPTION_CHANGED": "FIELD_ARGUMENT_DESCRIPTION_CHANGED", + "FIELD_ARGUMENT_DEFAULT_CHANGED": "FIELD_ARGUMENT_DEFAULT_CHANGED", + "FIELD_ARGUMENT_TYPE_CHANGED": "FIELD_ARGUMENT_TYPE_CHANGED", + "DIRECTIVE_REMOVED": "DIRECTIVE_REMOVED", + "DIRECTIVE_ADDED": "DIRECTIVE_ADDED", + "DIRECTIVE_DESCRIPTION_CHANGED": "DIRECTIVE_DESCRIPTION_CHANGED", + "DIRECTIVE_LOCATION_ADDED": "DIRECTIVE_LOCATION_ADDED", + "DIRECTIVE_LOCATION_REMOVED": "DIRECTIVE_LOCATION_REMOVED", + "DIRECTIVE_ARGUMENT_ADDED": "DIRECTIVE_ARGUMENT_ADDED", + "DIRECTIVE_ARGUMENT_REMOVED": "DIRECTIVE_ARGUMENT_REMOVED", + "DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED": "DIRECTIVE_ARGUMENT_DESCRIPTION_CHANGED", + "DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED": "DIRECTIVE_ARGUMENT_DEFAULT_VALUE_CHANGED", + "DIRECTIVE_ARGUMENT_TYPE_CHANGED": "DIRECTIVE_ARGUMENT_TYPE_CHANGED", + "ENUM_VALUE_REMOVED": "ENUM_VALUE_REMOVED", + "ENUM_VALUE_ADDED": "ENUM_VALUE_ADDED", + "ENUM_VALUE_DESCRIPTION_CHANGED": "ENUM_VALUE_DESCRIPTION_CHANGED", + "ENUM_VALUE_DEPRECATION_REASON_CHANGED": "ENUM_VALUE_DEPRECATION_REASON_CHANGED", + "ENUM_VALUE_DEPRECATION_REASON_ADDED": "ENUM_VALUE_DEPRECATION_REASON_ADDED", + "ENUM_VALUE_DEPRECATION_REASON_REMOVED": "ENUM_VALUE_DEPRECATION_REASON_REMOVED", + "FIELD_REMOVED": "FIELD_REMOVED", + "FIELD_ADDED": "FIELD_ADDED", + "FIELD_DESCRIPTION_CHANGED": "FIELD_DESCRIPTION_CHANGED", + "FIELD_DESCRIPTION_ADDED": "FIELD_DESCRIPTION_ADDED", + "FIELD_DESCRIPTION_REMOVED": "FIELD_DESCRIPTION_REMOVED", + "FIELD_DEPRECATION_ADDED": "FIELD_DEPRECATION_ADDED", + "FIELD_DEPRECATION_REMOVED": "FIELD_DEPRECATION_REMOVED", + "FIELD_DEPRECATION_REASON_CHANGED": "FIELD_DEPRECATION_REASON_CHANGED", + "FIELD_DEPRECATION_REASON_ADDED": "FIELD_DEPRECATION_REASON_ADDED", + "FIELD_DEPRECATION_REASON_REMOVED": "FIELD_DEPRECATION_REASON_REMOVED", + "FIELD_TYPE_CHANGED": "FIELD_TYPE_CHANGED", + "FIELD_ARGUMENT_ADDED": "FIELD_ARGUMENT_ADDED", + "FIELD_ARGUMENT_REMOVED": "FIELD_ARGUMENT_REMOVED", + "INPUT_FIELD_REMOVED": "INPUT_FIELD_REMOVED", + "INPUT_FIELD_ADDED": "INPUT_FIELD_ADDED", + "INPUT_FIELD_DESCRIPTION_ADDED": "INPUT_FIELD_DESCRIPTION_ADDED", + "INPUT_FIELD_DESCRIPTION_REMOVED": "INPUT_FIELD_DESCRIPTION_REMOVED", + "INPUT_FIELD_DESCRIPTION_CHANGED": "INPUT_FIELD_DESCRIPTION_CHANGED", + "INPUT_FIELD_DEFAULT_VALUE_CHANGED": "INPUT_FIELD_DEFAULT_VALUE_CHANGED", + "INPUT_FIELD_TYPE_CHANGED": "INPUT_FIELD_TYPE_CHANGED", + "OBJECT_TYPE_INTERFACE_ADDED": "OBJECT_TYPE_INTERFACE_ADDED", + "OBJECT_TYPE_INTERFACE_REMOVED": "OBJECT_TYPE_INTERFACE_REMOVED", + "SCHEMA_QUERY_TYPE_CHANGED": "SCHEMA_QUERY_TYPE_CHANGED", + "SCHEMA_MUTATION_TYPE_CHANGED": "SCHEMA_MUTATION_TYPE_CHANGED", + "SCHEMA_SUBSCRIPTION_TYPE_CHANGED": "SCHEMA_SUBSCRIPTION_TYPE_CHANGED", + "TYPE_REMOVED": "TYPE_REMOVED", + "TYPE_ADDED": "TYPE_ADDED", + "TYPE_KIND_CHANGED": "TYPE_KIND_CHANGED", + "TYPE_DESCRIPTION_CHANGED": "TYPE_DESCRIPTION_CHANGED", + "TYPE_DESCRIPTION_REMOVED": "TYPE_DESCRIPTION_REMOVED", + "TYPE_DESCRIPTION_ADDED": "TYPE_DESCRIPTION_ADDED", + "UNION_MEMBER_REMOVED": "UNION_MEMBER_REMOVED", + "UNION_MEMBER_ADDED": "UNION_MEMBER_ADDED" + } + }, + "subscription_protocol": { + "name": "subscription_protocol", + "values": { + "ws": "ws", + "sse": "sse", + "sse_post": "sse_post" + } + }, + "target_type": { + "name": "target_type", + "values": { + "federated": "federated", + "subgraph": "subgraph", + "graph": "graph" + } + } + }, + "schemas": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/controlplane/migrations/meta/_journal.json b/controlplane/migrations/meta/_journal.json index be3e914063..451e90b5fd 100644 --- a/controlplane/migrations/meta/_journal.json +++ b/controlplane/migrations/meta/_journal.json @@ -400,6 +400,13 @@ "when": 1700255510749, "tag": "0056_kind_wallop", "breakpoints": true + }, + { + "idx": 57, + "version": "5", + "when": 1700597273329, + "tag": "0057_late_golden_guardian", + "breakpoints": true } ] } \ No newline at end of file diff --git a/controlplane/package.json b/controlplane/package.json index da3c34a296..70127a1ffc 100644 --- a/controlplane/package.json +++ b/controlplane/package.json @@ -36,6 +36,7 @@ ], "license": "Apache-2.0", "dependencies": { + "@aws-sdk/client-s3": "^3.445.0", "@bufbuild/buf": "^1.27.2", "@connectrpc/connect": "^1.1.3", "@connectrpc/connect-fastify": "^1.1.3", diff --git a/controlplane/src/core/blobstorage/index.ts b/controlplane/src/core/blobstorage/index.ts new file mode 100644 index 0000000000..17c59bb1c3 --- /dev/null +++ b/controlplane/src/core/blobstorage/index.ts @@ -0,0 +1,34 @@ +export { S3BlobStorage } from './s3.js'; + +export class BlobNotFoundError extends Error { + constructor(message: string, cause?: Error) { + super(message, cause); + Object.setPrototypeOf(this, BlobNotFoundError.prototype); + } +} + +/** + * Describes the interface for a blob storage service + */ +export interface BlobStorage { + /** + * Stores an object in the blob storage under the given key, throwing an error if the operation fails + * @param key Key to store the object under + * @param body Data to store into the object + */ + putObject(key: string, body: Buffer): Promise; + /** + * Retrieves an object from the blob storage using the given key. If the blob doesn't exist, it throws + * BlobNotFoundError. + * @param key Key to retrieve the object from + */ + getObject(key: string): Promise; + + /** + * Remove a directory recursively, erasing all entries under the given key + * + * @param key Path to the directory + * @returns Number of deleted objects + */ + removeDirectory(key: string): Promise; +} diff --git a/controlplane/src/core/blobstorage/s3.ts b/controlplane/src/core/blobstorage/s3.ts new file mode 100644 index 0000000000..8a63bc111b --- /dev/null +++ b/controlplane/src/core/blobstorage/s3.ts @@ -0,0 +1,73 @@ +import { + DeleteObjectsCommand, + GetObjectCommand, + ListObjectsV2Command, + NoSuchKey, + PutObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; +import { BlobNotFoundError, type BlobStorage } from './index.js'; + +/** + * Stores objects in S3 given an S3Client and a bucket name + */ +export class S3BlobStorage implements BlobStorage { + constructor( + private s3Client: S3Client, + private bucketName: string, + ) {} + + async putObject(key: string, body: Buffer): Promise { + const command = new PutObjectCommand({ + Bucket: this.bucketName, + Key: key, + Body: body, + }); + const resp = await this.s3Client.send(command); + if (resp.$metadata.httpStatusCode !== 200) { + throw new Error(`Failed to put object to S3: ${resp}`); + } + } + + async getObject(key: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + try { + const resp = await this.s3Client.send(command); + if (resp.$metadata.httpStatusCode !== 200) { + throw new BlobNotFoundError(`Failed to retrieve object from S3: ${resp}`); + } + return resp.Body!.transformToWebStream(); + } catch (e: any) { + if (e instanceof NoSuchKey) { + throw new BlobNotFoundError(`Failed to retrieve object from S3: ${e}`); + } + throw e; + } + } + + async removeDirectory(key: string): Promise { + const listCommand = new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: key, + }); + const entries = await this.s3Client.send(listCommand); + const objectsToDelete = entries.Contents?.map((item) => ({ Key: item.Key })); + if (objectsToDelete && objectsToDelete.length > 0) { + const deleteCommand = new DeleteObjectsCommand({ + Bucket: this.bucketName, + Delete: { + Objects: objectsToDelete, + Quiet: false, + }, + }); + const deleted = await this.s3Client.send(deleteCommand); + if (deleted.Errors) { + throw new Error(`could not delete files: ${deleted.Errors}`); + } + } + return objectsToDelete?.length ?? 0; + } +} diff --git a/controlplane/src/core/bufservices/PlatformService.ts b/controlplane/src/core/bufservices/PlatformService.ts index 4f04155b59..657774eb20 100644 --- a/controlplane/src/core/bufservices/PlatformService.ts +++ b/controlplane/src/core/bufservices/PlatformService.ts @@ -1,3 +1,4 @@ +import crypto from 'node:crypto'; import { PlainMessage } from '@bufbuild/protobuf'; import { ServiceImpl } from '@connectrpc/connect'; import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; @@ -48,6 +49,7 @@ import { IsGitHubAppInstalledResponse, MigrateFromApolloResponse, PublishFederatedSubgraphResponse, + PublishPersistedOperationsResponse, RemoveInvitationResponse, RequestSeriesItem, UpdateFederatedGraphResponse, @@ -62,12 +64,16 @@ import { LeaveOrganizationResponse, DeleteOrganizationResponse, UpdateOrgMemberRoleResponse, + GetClientsResponse, + PublishedOperation, + PublishedOperationStatus, GetLatestValidSubgraphSDLByNameResponse, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { OpenAIGraphql, isValidUrl } from '@wundergraph/cosmo-shared'; -import { parse } from 'graphql'; +import { DocumentNode, buildASTSchema, parse } from 'graphql'; +import { validate } from 'graphql/validation/index.js'; import { uid } from 'uid'; -import { GraphApiKeyDTO, GraphApiKeyJwtPayload } from '../../types/index.js'; +import { ClientDTO, GraphApiKeyDTO, GraphApiKeyJwtPayload, PublishedOperationData } from '../../types/index.js'; import { Composer } from '../composition/composer.js'; import { buildSchema, composeSubgraphs } from '../composition/composition.js'; import { getDiffBetweenGraphs } from '../composition/schemaCheck.js'; @@ -105,6 +111,7 @@ import { FederatedGraphSchemaUpdate, OrganizationWebhookService } from '../webho import { OidcRepository } from '../repositories/OidcRepository.js'; import OidcProvider from '../services/OidcProvider.js'; import { GraphCompositionRepository } from '../repositories/GraphCompositionRepository.js'; +import { OperationsRepository } from '../repositories/OperationsRepository.js'; export default function (opts: RouterOptions): Partial> { return { @@ -1392,6 +1399,9 @@ export default function (opts: RouterOptions): Partial { + /** + * Receives a federated graph name and a list of persisted operation contents. + * First, it validates that the graph exists and all the operations are valid, + * then it stores them. Additionally, if the provided client name for registering + * the operations has never been seen before, we create an entry in the database + * with it. + */ + const logger = opts.logger.child({ + service: ctx.service.typeName, + method: ctx.method.name, + }); + + return handleError>(logger, async () => { + const authContext = await opts.authenticator.authenticate(ctx.requestHeader); + if (!authContext.hasWriteAccess) { + return { + response: { + code: EnumStatusCode.ERR, + details: `The user doesnt have the permissions to perform this operation`, + }, + operations: [], + }; + } + const userId = authContext.userId; + if (!userId) { + return { + response: { + code: EnumStatusCode.ERROR_NOT_AUTHENTICATED, + details: `User not found in the authentication context`, + }, + operations: [], + }; + } + const organizationId = authContext.organizationId; + const federatedGraphRepo = new FederatedGraphRepository(opts.db, organizationId); + + // Validate everything before we update any data + const schema = await federatedGraphRepo.getLatestValidSchemaVersion(req.fedGraphName); + const federatedGraph = await federatedGraphRepo.byName(req.fedGraphName); + if (!schema?.schema || federatedGraph === undefined) { + return { + response: { + code: EnumStatusCode.ERR_NOT_FOUND, + details: `Federated graph '${req.fedGraphName}' does not exist`, + }, + operations: [], + }; + } + const graphAST = parse(schema.schema); + const graphSchema = buildASTSchema(graphAST); + for (const operationContents of req.operations) { + let opAST: DocumentNode; + try { + opAST = parse(operationContents); + } catch (e: any) { + return { + response: { + code: EnumStatusCode.ERR, + details: `Operation ${operationContents} is not valid: ${e}`, + }, + operations: [], + }; + } + const errors = validate(graphSchema, opAST, undefined, { maxErrors: 1 }); + if (errors.length > 0) { + const errorDetails = errors.map((e) => `${e.toString()}`).join(', '); + return { + response: { + code: EnumStatusCode.ERR, + details: `Operation "${operationContents}" is not valid: ${errorDetails}`, + }, + operations: [], + }; + } + } + const operationsRepo = new OperationsRepository(opts.db, federatedGraph.id); + let clientId: string; + try { + clientId = await operationsRepo.registerClient(req.clientName, userId); + } catch (e: any) { + const message = e instanceof Error ? e.message : e.toString(); + return { + response: { + code: EnumStatusCode.ERR, + details: `Could not register client "${req.clientName}": ${message}`, + }, + operations: [], + }; + } + const operations: PublishedOperation[] = []; + const updatedOperations = []; + // Retrieve the operations that have already been published + const operationsResult = await operationsRepo.getPersistedOperations(); + const operationHashes = new Set(operationsResult.map((op) => op.hash)); + for (const operationContents of req.operations) { + const operationHash = crypto.createHash('sha256').update(operationContents).digest('hex'); + const path = `${organizationId}/${federatedGraph.id}/operations/${req.clientName}/${operationHash}.json`; + updatedOperations.push({ + hash: operationHash, + filePath: path, + }); + let status: PublishedOperationStatus; + if (operationHashes.has(operationHash)) { + status = PublishedOperationStatus.UP_TO_DATE; + } else { + const data: PublishedOperationData = { + version: 1, + body: operationContents, + }; + opts.blobStorage.putObject(path, Buffer.from(JSON.stringify(data), 'utf8')); + status = PublishedOperationStatus.CREATED; + } + operations.push( + new PublishedOperation({ + hash: operationHash, + status, + }), + ); + } + + await operationsRepo.updatePersistedOperations(clientId, userId, updatedOperations); + + return { + response: { + code: EnumStatusCode.OK, + }, + operations, + }; + }); + }, + + getClients: (req, ctx) => { + const logger = opts.logger.child({ + service: ctx.service.typeName, + method: ctx.method.name, + }); + + return handleError>(logger, async () => { + const authContext = await opts.authenticator.authenticate(ctx.requestHeader); + const fedRepo = new FederatedGraphRepository(opts.db, authContext.organizationId); + const federatedGraph = await fedRepo.byName(req.fedGraphName); + if (!federatedGraph) { + return { + response: { + code: EnumStatusCode.ERR_NOT_FOUND, + details: `Federated graph '${req.fedGraphName}' does not exist`, + }, + clients: [], + }; + } + const operationsRepo = new OperationsRepository(opts.db, federatedGraph.id); + const clients = await operationsRepo.getRegisteredClients(); + + return { + response: { + code: EnumStatusCode.OK, + }, + clients, + }; + }); + }, }; } diff --git a/controlplane/src/core/build-server.ts b/controlplane/src/core/build-server.ts index ce22b5f31c..c6c6091c91 100644 --- a/controlplane/src/core/build-server.ts +++ b/controlplane/src/core/build-server.ts @@ -1,4 +1,5 @@ import Fastify from 'fastify'; +import { S3Client } from '@aws-sdk/client-s3'; import { fastifyConnectPlugin } from '@connectrpc/connect-fastify'; import { cors } from '@connectrpc/connect'; import fastifyCors from '@fastify/cors'; @@ -23,6 +24,7 @@ import Keycloak from './services/Keycloak.js'; import { PlatformWebhookService } from './webhooks/PlatformWebhookService.js'; import AccessTokenAuthenticator from './services/AccessTokenAuthenticator.js'; import { GitHubRepository } from './repositories/GitHubRepository.js'; +import { S3BlobStorage } from './blobstorage/index.js'; export interface BuildConfig { logger: LoggerOptions; @@ -66,6 +68,7 @@ export interface BuildConfig { privateKey?: string; }; slack: { clientID?: string; clientSecret?: string }; + s3StorageUrl: string; } const developmentLoggerOpts: LoggerOptions = { @@ -199,6 +202,24 @@ export default async function build(opts: BuildConfig) { }); } + if (!opts.s3StorageUrl) { + throw new Error('S3 storage URL is required'); + } + + const url = new URL(opts.s3StorageUrl); + const s3Client = new S3Client({ + // For AWS S3, the region can be set via the endpoint + region: 'auto', + endpoint: url.origin, + credentials: { + accessKeyId: url.username ?? '', + secretAccessKey: url.password ?? '', + }, + forcePathStyle: true, + }); + const bucketName = url.pathname.slice(1); + const blobStorage = new S3BlobStorage(s3Client, bucketName); + /** * Controllers registration */ @@ -241,6 +262,7 @@ export default async function build(opts: BuildConfig) { githubApp, webBaseUrl: opts.auth.webBaseUrl, slack: opts.slack, + blobStorage, }), logLevel: opts.logger.level as pino.LevelWithSilent, // Avoid compression for small requests diff --git a/controlplane/src/core/env.schema.ts b/controlplane/src/core/env.schema.ts index fc48362a84..3679a6ba25 100644 --- a/controlplane/src/core/env.schema.ts +++ b/controlplane/src/core/env.schema.ts @@ -36,4 +36,5 @@ export const envVariables = z.object({ GITHUB_APP_WEBHOOK_SECRET: z.string().optional(), SLACK_APP_CLIENT_ID: z.string().optional(), SLACK_APP_CLIENT_SECRET: z.string().optional(), + S3_STORAGE_URL: z.string(), }); diff --git a/controlplane/src/core/repositories/FederatedGraphRepository.ts b/controlplane/src/core/repositories/FederatedGraphRepository.ts index 94ce2b20d5..5df0c81b54 100644 --- a/controlplane/src/core/repositories/FederatedGraphRepository.ts +++ b/controlplane/src/core/repositories/FederatedGraphRepository.ts @@ -5,7 +5,9 @@ import { RouterConfig } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; import { joinLabel, normalizeURL } from '@wundergraph/cosmo-shared'; import * as schema from '../../db/schema.js'; import { + federatedGraphClients, federatedGraphConfigs, + federatedGraphPersistedOperations, federatedGraphs, graphApiTokens, graphCompositions, @@ -16,17 +18,21 @@ import { targets, } from '../../db/schema.js'; import { + ClientDTO, FederatedGraphChangelogDTO, FederatedGraphDTO, GraphApiKeyDTO, Label, ListFilterOptions, + PersistedOperationDTO, + UserDTO, } from '../../types/index.js'; import { normalizeLabelMatchers, normalizeLabels } from '../util.js'; import { Composer } from '../composition/composer.js'; import { SchemaDiff } from '../composition/schemaCheck.js'; import { Target } from '../../db/models.js'; import { SubgraphRepository } from './SubgraphRepository.js'; +import { UserRepository } from './UserRepository.js'; import { GraphCompositionRepository } from './GraphCompositionRepository.js'; export interface FederatedGraphConfig { diff --git a/controlplane/src/core/repositories/OperationsRepository.ts b/controlplane/src/core/repositories/OperationsRepository.ts new file mode 100644 index 0000000000..3bd360ea98 --- /dev/null +++ b/controlplane/src/core/repositories/OperationsRepository.ts @@ -0,0 +1,121 @@ +import { and, desc, eq, sql } from 'drizzle-orm'; +import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import * as schema from '../../db/schema.js'; +import { federatedGraphClients, federatedGraphPersistedOperations } from '../../db/schema.js'; +import { ClientDTO, PersistedOperationDTO, UpdatedPersistedOperation } from 'src/types/index.js'; + +export class OperationsRepository { + constructor( + private db: PostgresJsDatabase, + private federatedGraphId: string, + ) {} + + public async updatePersistedOperations(clientId: string, userId: string, operations: UpdatedPersistedOperation[]) { + const now = new Date(); + const inserts = operations.map((operation) => { + return { + federatedGraphId: this.federatedGraphId, + clientId, + hash: operation.hash, + filePath: operation.filePath, + createdAt: now, + updatedAt: now, + createdById: userId, + }; + }); + await this.db + .insert(federatedGraphPersistedOperations) + .values(inserts) + .onConflictDoUpdate({ + target: [federatedGraphPersistedOperations.federatedGraphId, federatedGraphPersistedOperations.hash], + set: { updatedAt: now, updatedById: userId }, + }); + } + + public async getPersistedOperations(pagination?: { + limit: number; + offset: number; + }): Promise { + const operationsResult = await this.db.query.federatedGraphPersistedOperations.findMany({ + where: eq(federatedGraphPersistedOperations.federatedGraphId, this.federatedGraphId), + with: { + createdBy: true, + updatedBy: true, + }, + orderBy: desc( + sql`coalesce(${federatedGraphPersistedOperations.updatedAt}, ${federatedGraphPersistedOperations.createdAt})`, + ), + offset: pagination?.offset, + limit: pagination?.limit, + }); + + const operations: PersistedOperationDTO[] = []; + + for (const row of operationsResult) { + operations.push({ + id: row.id, + hash: row.hash, + filePath: row.filePath, + createdAt: row.createdAt.toISOString(), + lastUpdatedAt: row?.updatedAt?.toISOString() || '', + createdBy: row.createdBy.email, + lastUpdatedBy: row.updatedBy?.email ?? '', + }); + } + return operations; + } + + public async registerClient(clientName: string, userId: string): Promise { + if (!clientName) { + throw new Error('client name is empty'); + } + const updatedAt = new Date(); + await this.db + .insert(federatedGraphClients) + .values({ + federatedGraphId: this.federatedGraphId, + name: clientName, + updatedAt, + createdById: userId, + }) + .onConflictDoUpdate({ + target: [federatedGraphClients.federatedGraphId, federatedGraphClients.name], + set: { updatedAt, updatedById: userId }, + }); + + // To avoid depending on postgres, we do a second query to get the inserted client + const result = await this.db.query.federatedGraphClients.findFirst({ + columns: { id: true }, + where: and( + eq(federatedGraphClients.name, clientName), + eq(federatedGraphClients.federatedGraphId, this.federatedGraphId), + ), + }); + return result!.id; + } + + public async getRegisteredClients(): Promise { + const fedGraphClients = await this.db.query.federatedGraphClients.findMany({ + where: eq(federatedGraphClients.federatedGraphId, this.federatedGraphId), + with: { + createdBy: true, + updatedBy: true, + }, + orderBy: desc(sql`coalesce(${federatedGraphClients.updatedAt}, ${federatedGraphClients.createdAt})`), + }); + const clients: ClientDTO[] = []; + + for (const c of fedGraphClients) { + clients.push({ + id: c.id, + name: c.name, + createdAt: c.createdAt.toISOString(), + lastUpdatedAt: c.updatedAt?.toISOString() || '', + createdBy: c.createdBy?.email ?? '', + lastUpdatedBy: c.updatedBy?.email ?? '', + }); + } + + return clients; + } +} diff --git a/controlplane/src/core/routes.ts b/controlplane/src/core/routes.ts index 8230f3efc9..bb45f342d4 100644 --- a/controlplane/src/core/routes.ts +++ b/controlplane/src/core/routes.ts @@ -12,6 +12,7 @@ import { ClickHouseClient } from './clickhouse/index.js'; import { Authenticator } from './services/Authentication.js'; import Keycloak from './services/Keycloak.js'; import { IPlatformWebhookService } from './webhooks/PlatformWebhookService.js'; +import { BlobStorage } from './blobstorage/index.js'; export interface RouterOptions { db: PostgresJsDatabase; @@ -26,6 +27,7 @@ export interface RouterOptions { webBaseUrl: string; githubApp?: App; slack: { clientID?: string; clientSecret?: string }; + blobStorage: BlobStorage; } const handlerOptions: Partial = { maxTimeoutMs: 5000, diff --git a/controlplane/src/db/schema.ts b/controlplane/src/db/schema.ts index 547e381844..cff89880d1 100644 --- a/controlplane/src/db/schema.ts +++ b/controlplane/src/db/schema.ts @@ -12,6 +12,7 @@ import { timestamp, uniqueIndex, uuid, + unique, } from 'drizzle-orm/pg-core'; export const federatedGraphs = pgTable('federated_graphs', { @@ -38,6 +39,93 @@ export const federatedGraphConfigs = pgTable('federated_graph_configs', { trafficCheckDays: integer('traffic_check_days').notNull().default(7), }); +export const federatedGraphClients = pgTable( + 'federated_graph_clients', + { + id: uuid('id').primaryKey().defaultRandom(), + federatedGraphId: uuid('federated_graph_id') + .notNull() + .references(() => federatedGraphs.id, { + onDelete: 'cascade', + }), + name: text('name').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }), + createdById: uuid('created_by_id') + .notNull() + .references(() => users.id, { + onDelete: 'cascade', + }), + updatedById: uuid('updated_by_id').references(() => users.id, { + onDelete: 'cascade', + }), + }, + (t) => ({ + uniqueFederatedGraphClientName: unique('federated_graph_client_name').on(t.federatedGraphId, t.name), + }), +); + +export const federatedGraphClientsRelations = relations(federatedGraphClients, ({ one }) => ({ + createdBy: one(users, { + fields: [federatedGraphClients.createdById], + references: [users.id], + }), + updatedBy: one(users, { + fields: [federatedGraphClients.updatedById], + references: [users.id], + }), +})); + +export const federatedGraphPersistedOperations = pgTable( + 'federated_graph_persisted_operations', + { + id: uuid('id').primaryKey().defaultRandom(), + federatedGraphId: uuid('federated_graph_id') + .notNull() + .references(() => federatedGraphs.id, { + onDelete: 'cascade', + }), + clientId: uuid('client_id') + .notNull() + .references(() => federatedGraphClients.id, { + onDelete: 'cascade', + }), + hash: text('hash').notNull(), + filePath: text('file_path').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }), + createdById: uuid('created_by_id') + .notNull() + .references(() => users.id, { + onDelete: 'cascade', + }), + updatedById: uuid('updated_by_id').references(() => users.id, { + onDelete: 'cascade', + }), + }, + (t) => ({ + uniqueFederatedGraphOperationHash: unique('federated_graph_operation_hash').on(t.federatedGraphId, t.hash), + uniqueFederatedGraphOperationFilePath: unique('federated_graph_operation_file_hash').on( + t.federatedGraphId, + t.filePath, + ), + }), +); + +export const federatedGraphPersistedOperationsRelations = relations( + federatedGraphPersistedOperations, + ({ many, one }) => ({ + createdBy: one(users, { + fields: [federatedGraphPersistedOperations.createdById], + references: [users.id], + }), + updatedBy: one(users, { + fields: [federatedGraphPersistedOperations.updatedById], + references: [users.id], + }), + }), +); + export const subscriptionProtocolEnum = pgEnum('subscription_protocol', ['ws', 'sse', 'sse_post'] as const); export const subgraphs = pgTable('subgraphs', { diff --git a/controlplane/src/index.ts b/controlplane/src/index.ts index 9e69424a66..dd527c4249 100644 --- a/controlplane/src/index.ts +++ b/controlplane/src/index.ts @@ -36,6 +36,7 @@ const { GITHUB_APP_PRIVATE_KEY, SLACK_APP_CLIENT_ID, SLACK_APP_CLIENT_SECRET, + S3_STORAGE_URL, } = envVariables.parse(process.env); const options: BuildConfig = { @@ -80,6 +81,7 @@ const options: BuildConfig = { clientID: SLACK_APP_CLIENT_ID, clientSecret: SLACK_APP_CLIENT_SECRET, }, + s3StorageUrl: S3_STORAGE_URL, }; if (DB_CERT_PATH || DB_KEY_PATH || DB_CA_PATH) { diff --git a/controlplane/src/types/index.ts b/controlplane/src/types/index.ts index 09c255f106..2259db0bf0 100644 --- a/controlplane/src/types/index.ts +++ b/controlplane/src/types/index.ts @@ -267,3 +267,32 @@ export interface SlackAccessTokenResponse { slackChannelName: string; webhookURL: string; } + +export interface ClientDTO { + id: string; + name: string; + createdAt: string; + createdBy: string; + lastUpdatedAt: string; + lastUpdatedBy: string; +} + +export interface PersistedOperationDTO { + id: string; + hash: string; + filePath: string; + createdAt: string; + createdBy: string; + lastUpdatedAt: string; + lastUpdatedBy: string; +} + +export interface PublishedOperationData { + version: 1; + body: string; +} + +export interface UpdatedPersistedOperation { + hash: string; + filePath: string; +} diff --git a/controlplane/test/authenthication.test.ts b/controlplane/test/authenthication.test.ts index 700ef5d3c1..ad926ee18d 100644 --- a/controlplane/test/authenthication.test.ts +++ b/controlplane/test/authenthication.test.ts @@ -49,6 +49,7 @@ describe('Authentication', (ctx) => { clientID: '', clientSecret: '', }, + s3StorageUrl: "http://localhost:9000", }); testContext.onTestFailed(async () => { diff --git a/controlplane/test/check-subgraph-schema.test.ts b/controlplane/test/check-subgraph-schema.test.ts index 18bcc2d70a..a50b291a4f 100644 --- a/controlplane/test/check-subgraph-schema.test.ts +++ b/controlplane/test/check-subgraph-schema.test.ts @@ -20,7 +20,7 @@ import { } from '../src/core/test-util'; import Keycloak from '../src/core/services/Keycloak'; import { MockPlatformWebhookService } from '../src/core/webhooks/PlatformWebhookService'; -import { SetupTest } from './test-util'; +import { InMemoryBlobStorage, SetupTest } from './test-util'; let dbname = ''; @@ -125,6 +125,7 @@ describe('CheckSubgraphSchema', (ctx) => { clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage: new InMemoryBlobStorage(), }), }); @@ -227,6 +228,7 @@ describe('CheckSubgraphSchema', (ctx) => { clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage: new InMemoryBlobStorage(), }), }); diff --git a/controlplane/test/federated-graph.test.ts b/controlplane/test/federated-graph.test.ts index b2e4a5d5d2..424d2819d3 100644 --- a/controlplane/test/federated-graph.test.ts +++ b/controlplane/test/federated-graph.test.ts @@ -19,7 +19,7 @@ import { } from '../src/core/test-util'; import Keycloak from '../src/core/services/Keycloak'; import { MockPlatformWebhookService } from '../src/core/webhooks/PlatformWebhookService'; -import { SetupTest } from './test-util'; +import { InMemoryBlobStorage, SetupTest } from './test-util'; let dbname = ''; @@ -122,6 +122,7 @@ describe('Federated Graph', (ctx) => { clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage: new InMemoryBlobStorage(), }), }); @@ -224,6 +225,7 @@ describe('Federated Graph', (ctx) => { clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage: new InMemoryBlobStorage(), }), }); @@ -326,6 +328,7 @@ describe('Federated Graph', (ctx) => { clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage: new InMemoryBlobStorage(), }), }); @@ -457,6 +460,7 @@ describe('Federated Graph', (ctx) => { clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage: new InMemoryBlobStorage(), }), }); diff --git a/controlplane/test/persisted-operations.test.ts b/controlplane/test/persisted-operations.test.ts new file mode 100644 index 0000000000..78bf506784 --- /dev/null +++ b/controlplane/test/persisted-operations.test.ts @@ -0,0 +1,165 @@ +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { joinLabel } from '@wundergraph/cosmo-shared'; +import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb'; +import { + afterAllSetup, + beforeAllSetup, + genID, + genUniqueLabel, +} from '../src/core/test-util'; +import { SetupTest } from './test-util'; + +let dbname = ''; + +type Client = Awaited>['client']; + +const setupFederatedGraph = async (fedGraphName: string, client: Client) => { + const subgraph1Name = genID('subgraph1'); + const label = genUniqueLabel(); + + const createSubraph1Res = await client.createFederatedSubgraph({ + name: subgraph1Name, + labels: [label], + routingUrl: 'http://localhost:8081', + }); + + expect(createSubraph1Res.response?.code).toBe(EnumStatusCode.OK); + + const publishResp = await client.publishFederatedSubgraph({ + name: subgraph1Name, + labels: [label], + routingUrl: 'http://localhost:8081', + schema: Uint8Array.from(Buffer.from('type Query { hello: String! }')), + }); + + expect(publishResp.response?.code).toBe(EnumStatusCode.OK); + + const createFedGraphRes = await client.createFederatedGraph({ + name: fedGraphName, + routingUrl: 'http://localhost:8080', + labelMatchers: [joinLabel(label)], + }); + + expect(createFedGraphRes.response?.code).toBe(EnumStatusCode.OK); +} + +describe('Persisted operations', (ctx) => { + beforeAll(async () => { + dbname = await beforeAllSetup(); + }); + + afterAll(async () => { + await afterAllSetup(dbname); + }); + + test('Should be able to publish persisted operations', async (testContext) => { + const { client, server } = await SetupTest(testContext, dbname); + const fedGraphName = genID('fedGraph'); + await setupFederatedGraph(fedGraphName, client); + + const publishOperationsResp = await client.publishPersistedOperations({ + fedGraphName, + clientName: 'test-client', + operations: [`query { hello }`], + }); + + expect(publishOperationsResp.response?.code).toBe(EnumStatusCode.OK); + + await server.close(); + }); + + test('Should not publish persisted operations without a client ID', async (testContext) => { + const { client, server } = await SetupTest(testContext, dbname); + const fedGraphName = genID('fedGraph'); + await setupFederatedGraph(fedGraphName, client); + + const publishOperationsResp = await client.publishPersistedOperations({ + fedGraphName, + operations: [`query { hello }`], + }); + + expect(publishOperationsResp.response?.code).not.toBe(EnumStatusCode.OK); + + await server.close(); + }); + + test('Should not publish persisted operations with invalid queries', async (testContext) => { + const { client, server } = await SetupTest(testContext, dbname); + const fedGraphName = genID('fedGraph'); + await setupFederatedGraph(fedGraphName, client); + + const publishOperationsResp = await client.publishPersistedOperations({ + fedGraphName, + operations: [`query { does_not_exist }`], + }); + + expect(publishOperationsResp.response?.code).not.toBe(EnumStatusCode.OK); + + await server.close(); + }); + + test('Should not publish persisted operations with an invalid federated graph name', async (testContext) => { + const { client, server } = await SetupTest(testContext, dbname); + const fedGraphName = genID('fedGraph'); + await setupFederatedGraph(fedGraphName, client); + + const publishOperationsResp = await client.publishPersistedOperations({ + fedGraphName: `not_${fedGraphName}`, + operations: [`query { hello }`], + }); + + expect(publishOperationsResp.response?.code).not.toBe(EnumStatusCode.OK); + + await server.close(); + }); + + test('Should store persisted operations in blob storage', async (testContext) => { + const { client, server, blobStorage } = await SetupTest(testContext, dbname); + const fedGraphName = genID('fedGraph'); + await setupFederatedGraph(fedGraphName, client); + + const query = `query { hello }`; + + const publishOperationsResp = await client.publishPersistedOperations({ + fedGraphName, + clientName: 'test-client', + operations: [query], + }); + + expect(publishOperationsResp.response?.code).toBe(EnumStatusCode.OK); + + const storageKeys = blobStorage.keys(); + expect(storageKeys.length).toBe(1); + + const stream = await blobStorage.getObject(storageKeys[0]); + const text = await new Response(stream).text(); + expect(JSON.parse(text)).toEqual({ version: 1, body: query }); + await server.close(); + }); + + test('Should delete persisted operations from blob storage when the federated graph is deleted', async (testContext) => { + const { client, server, blobStorage } = await SetupTest(testContext, dbname); + const fedGraphName = genID('fedGraph'); + await setupFederatedGraph(fedGraphName, client); + + const query = `query { hello }`; + + const publishOperationsResp = await client.publishPersistedOperations({ + fedGraphName, + clientName: 'test-client', + operations: [query], + }); + + expect(publishOperationsResp.response?.code).toBe(EnumStatusCode.OK); + + expect(blobStorage.keys().length).toBe(1); + + const deleteFederatedGraphResp = await client.deleteFederatedGraph({ + name: fedGraphName, + }); + expect(deleteFederatedGraphResp.response?.code).toBe(EnumStatusCode.OK); + expect(blobStorage.keys().length).toBe(0); + + await server.close(); + }); +}); diff --git a/controlplane/test/test-util.ts b/controlplane/test/test-util.ts index 783f762814..bf093d40b6 100644 --- a/controlplane/test/test-util.ts +++ b/controlplane/test/test-util.ts @@ -12,6 +12,7 @@ import { createTestAuthenticator, seedTest } from '../src/core/test-util'; import Keycloak from '../src/core/services/Keycloak'; import { MockPlatformWebhookService } from '../src/core/webhooks/PlatformWebhookService'; import routes from '../src/core/routes'; +import { BlobNotFoundError, BlobStorage } from '../src/core/blobstorage'; import { Label } from '../src/types'; export const SetupTest = async function (testContext: TestContext, dbname: string) { @@ -47,6 +48,7 @@ export const SetupTest = async function (testContext: TestContext, dbname: strin const platformWebhooks = new MockPlatformWebhookService(); + const blobStorage = new InMemoryBlobStorage(); await server.register(fastifyConnectPlugin, { routes: routes({ db: server.db, @@ -62,6 +64,7 @@ export const SetupTest = async function (testContext: TestContext, dbname: strin clientSecret: '', }, keycloakApiUrl: apiUrl, + blobStorage, }), }); @@ -79,7 +82,7 @@ export const SetupTest = async function (testContext: TestContext, dbname: strin const platformClient = createPromiseClient(PlatformService, transport); const nodeClient = createPromiseClient(NodeService, transport); - return { client: platformClient, nodeClient, server, userTestData }; + return { client: platformClient, nodeClient, server, userTestData, blobStorage }; }; export const createSubgraph = async ( @@ -118,3 +121,41 @@ export const createFederatedGraph = async ( expect(createFedGraphRes.response?.code).toBe(EnumStatusCode.OK); return createFedGraphRes; }; + +export class InMemoryBlobStorage implements BlobStorage { + private objects: Map = new Map(); + + keys() { + return [...this.objects.keys()]; + } + + putObject(key: string, body: Buffer): Promise { + this.objects.set(key, body); + return Promise.resolve(); + } + + getObject(key: string): Promise { + const obj = this.objects.get(key); + if (!obj) { + return Promise.reject(new BlobNotFoundError(`Object with key ${key} not found`)); + } + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(obj); + controller.close(); + }, + }); + return Promise.resolve(stream); + } + + removeDirectory(key: string): Promise { + let count = 0; + for (const objectKey of this.objects.keys()) { + if (objectKey.startsWith(key)) { + this.objects.delete(objectKey); + count++; + } + } + return Promise.resolve(count); + } +} diff --git a/docker-compose.full.yml b/docker-compose.full.yml index 4de83aa4cd..8c43f8363f 100644 --- a/docker-compose.full.yml +++ b/docker-compose.full.yml @@ -126,6 +126,21 @@ services: - default depends_on: - postgres + minio: + image: quay.io/minio/minio:${DC_MINIO_VERSION:-RELEASE.2023-11-06T22-26-08Z} + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER:-minio} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-changeme} + ports: + - "10000:9000" + - "10001:9001" + volumes: + - minio:/data + - ./docker/minio/entrypoint.sh:/entrypoint.sh + entrypoint: /entrypoint.sh + restart: unless-stopped + networks: + - primary # Cosmo Platform @@ -183,6 +198,7 @@ services: KC_API_URL: "http://keycloak:8080" KC_FRONTEND_URL: "http://localhost:8080" PROMETHEUS_API_URL: "http://admin:test@prometheus:9090/api/v1" + S3_STORAGE_URL: http://minio:changeme@localhost:10000/cosmo ports: - '3001:3001' restart: on-failure @@ -301,3 +317,4 @@ volumes: postgres: clickhouse: prometheus: + minio: diff --git a/docker-compose.yml b/docker-compose.yml index 4d8dc3db39..3e4fb0411f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -123,6 +123,21 @@ services: networks: - primary + minio: + image: quay.io/minio/minio:${DC_MINIO_VERSION:-RELEASE.2023-11-06T22-26-08Z} + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER:-minio} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-changeme} + ports: + - "10000:9000" + - "10001:9001" + volumes: + - minio:/data + - ./docker/minio/entrypoint.sh:/entrypoint.sh + entrypoint: /entrypoint.sh + networks: + - primary + # This network is shared between this file and docker-compose.full.yml to # allow the demo subgraphs to communicate with the rest of the infra networks: @@ -134,3 +149,4 @@ volumes: postgres: clickhouse: prometheus: + minio: diff --git a/docker/minio/entrypoint.sh b/docker/minio/entrypoint.sh new file mode 100755 index 0000000000..3bbbbac151 --- /dev/null +++ b/docker/minio/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +mkdir -p /data/cosmo +minio server /data --console-address ":9001" diff --git a/helm/README.md b/helm/README.md index 58dde8b923..016797d814 100644 --- a/helm/README.md +++ b/helm/README.md @@ -9,7 +9,7 @@ - [Minikube](https://minikube.sigs.k8s.io/docs/start/) - Requires enabling `minikube addons enable ingress` - [Helm 3.2.0+](https://helm.sh/docs/intro/install/) installed locally -- [Kapp](https://carvel.dev/kapp/) installed locally +- [Kapp](https://carvel.dev/kapp/docs/latest/install/) installed locally - [Kubectl](https://kubernetes.io/docs/tasks/tools/) installed locally ### Install helm dependencies @@ -18,6 +18,7 @@ # Add bitnami repo to install dependencies like postgresql, keycloak and clickhouse helm repo add bitnami https://charts.bitnami.com/bitnami # Install the helm dependencies (Only needed once) +helm repo add bitnami https://charts.bitnami.com/bitnami && \ helm dependency build ./cosmo ``` @@ -54,6 +55,7 @@ We run several Kubernetes jobs to run migrations. While we provide Helm hook sup #### 3. Make ingress available locally +##### Linux Minikube will automatically expose the ingress controller on your local machine. You can get the IP with `minikube ip`. Now, add the following entries to your `/etc/hosts` file and replace the IP with the IP you get from the previous step. @@ -66,6 +68,23 @@ Now, add the following entries to your `/etc/hosts` file and replace the IP with 192.168.49.2 graphqlmetrics.wundergraph.local ``` +##### macOS + +Minikube needs to set up a tunnel to expose the ingress controller in your local machine. Add the following +entries to `/etc/hosts`: + +``` +127.0.0.1 studio.wundergraph.local +127.0.0.1 controlplane.wundergraph.local +127.0.0.1 router.wundergraph.local +127.0.0.1 keycloak.wundergraph.local +127.0.0.1 otelcollector.wundergraph.local +127.0.0.1 graphqlmetrics.wundergraph.local +``` + +Then start `minikube tunnel` and leave it running. It might ask for your root password in order to open +the tunnel on privileged ports. + #### 4. Access the Cosmo Studio Open [http://studio.local](http://studio.local) in your browser and login with the default credentials: @@ -97,4 +116,4 @@ minikube addons enable ingress ```shell make docs -``` \ No newline at end of file +``` diff --git a/helm/cosmo/CHART.md b/helm/cosmo/CHART.md index 0b0c9cb8ce..39d91df746 100644 --- a/helm/cosmo/CHART.md +++ b/helm/cosmo/CHART.md @@ -21,6 +21,7 @@ This is the official Helm Chart for WunderGraph Cosmo - The Full Lifecycle Graph | | studio | ^0 | | https://charts.bitnami.com/bitnami | clickhouse | ^4.1.5 | | https://charts.bitnami.com/bitnami | keycloak | ^17.3.1 | +| https://charts.bitnami.com/bitnami | minio | 12.10.0 | | https://charts.bitnami.com/bitnami | postgresql | 12.8.0 | ## Values diff --git a/helm/cosmo/Chart.lock b/helm/cosmo/Chart.lock index f1a0d9a29c..026adb3ffb 100644 --- a/helm/cosmo/Chart.lock +++ b/helm/cosmo/Chart.lock @@ -23,5 +23,8 @@ dependencies: - name: clickhouse repository: https://charts.bitnami.com/bitnami version: 4.1.7 -digest: sha256:d4b25197a67d2223e46b8d89db5cdcd5a6cef1e7026789902751610ae5c0ac00 -generated: "2023-11-21T22:21:23.451763679+01:00" +- name: minio + repository: https://charts.bitnami.com/bitnami + version: 12.10.0 +digest: sha256:b659783ca2fb3ed8f1e92ed6121e17158b5d62de3673c6d85148ca55c1c6ea92 +generated: "2023-11-22T11:09:07.627353Z" diff --git a/helm/cosmo/Chart.yaml b/helm/cosmo/Chart.yaml index 38e686ce3d..cf44023bfe 100644 --- a/helm/cosmo/Chart.yaml +++ b/helm/cosmo/Chart.yaml @@ -65,4 +65,8 @@ dependencies: - name: clickhouse version: "^4.1.5" condition: global.clickhouse.enabled - repository: "https://charts.bitnami.com/bitnami" \ No newline at end of file + repository: "https://charts.bitnami.com/bitnami" + - name: minio + version: "12.10.0" + condition: global.minio.enabled + repository: "https://charts.bitnami.com/bitnami" diff --git a/helm/cosmo/charts/controlplane/CHART.md b/helm/cosmo/charts/controlplane/CHART.md index 0b4e1a3dbf..33108aa9aa 100644 --- a/helm/cosmo/charts/controlplane/CHART.md +++ b/helm/cosmo/charts/controlplane/CHART.md @@ -27,6 +27,7 @@ WunderGraph Cosmo Controlplane | configuration.githubAppPrivateKey | string | `""` | | | configuration.githubAppWebhookSecret | string | `""` | | | configuration.logLevel | string | `"info"` | | +| configuration.s3StorageUrl | string | `"http://minio:changeme@minio.wundergraph.local:9000/cosmo"` | | | configuration.slackAppClientId | string | `""` | | | configuration.slackAppClientSecret | string | `""` | | | configuration.webhookSecret | string | `""` | | diff --git a/helm/cosmo/charts/controlplane/templates/deployment.yaml b/helm/cosmo/charts/controlplane/templates/deployment.yaml index 89c5b3b11f..a56e701a0e 100644 --- a/helm/cosmo/charts/controlplane/templates/deployment.yaml +++ b/helm/cosmo/charts/controlplane/templates/deployment.yaml @@ -187,6 +187,12 @@ spec: secretKeyRef: name: {{ include "controlplane.fullname" . }}-secret key: slackAppClientSecret + - name: S3_STORAGE_URL + valueFrom: + secretKeyRef: + name: {{ include "controlplane.fullname" . }}-secret + key: s3StorageUrl + ports: - name: http containerPort: {{ .Values.service.port }} @@ -226,4 +232,4 @@ spec: {{- end }} {{- if .Values.terminationGracePeriodSeconds }} terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/helm/cosmo/charts/controlplane/templates/secret.yaml b/helm/cosmo/charts/controlplane/templates/secret.yaml index adcc26d28e..2e1ff77ffa 100644 --- a/helm/cosmo/charts/controlplane/templates/secret.yaml +++ b/helm/cosmo/charts/controlplane/templates/secret.yaml @@ -23,4 +23,5 @@ stringData: githubAppClientSecret: "{{ .Values.configuration.githubAppClientSecret }}" githubAppPrivateKey: "{{ .Values.configuration.githubAppPrivateKey }}" githubAppWebhookSecret: "{{ .Values.configuration.githubAppWebhookSecret }}" - slackAppClientSecret: "{{ .Values.configuration.slackAppClientSecret }}" \ No newline at end of file + slackAppClientSecret: "{{ .Values.configuration.slackAppClientSecret }}" + s3StorageUrl: "{{ .Values.configuration.s3StorageUrl }}" diff --git a/helm/cosmo/charts/controlplane/values.yaml b/helm/cosmo/charts/controlplane/values.yaml index 19f6690edf..0f1ab05d68 100644 --- a/helm/cosmo/charts/controlplane/values.yaml +++ b/helm/cosmo/charts/controlplane/values.yaml @@ -159,4 +159,5 @@ configuration: githubAppPrivateKey: "" githubAppWebhookSecret: "" slackAppClientId: "" - slackAppClientSecret: "" \ No newline at end of file + slackAppClientSecret: "" + s3StorageUrl: "http://minio:changeme@minio.wundergraph.local:9000/cosmo" diff --git a/helm/cosmo/charts/graphqlmetrics/templates/secret.yaml b/helm/cosmo/charts/graphqlmetrics/templates/secret.yaml index eacad34349..608a4380bf 100644 --- a/helm/cosmo/charts/graphqlmetrics/templates/secret.yaml +++ b/helm/cosmo/charts/graphqlmetrics/templates/secret.yaml @@ -10,4 +10,4 @@ metadata: {{- include "graphqlmetrics.labels" . | nindent 4 }} stringData: clickhouseDsn: "{{ .Values.configuration.clickhouseDsn }}" - jwtSecret: "{{ .Values.global.controlplane.jwtSecret }}" \ No newline at end of file + jwtSecret: "{{ .Values.global.controlplane.jwtSecret }}" diff --git a/helm/cosmo/charts/minio-12.10.0.tgz b/helm/cosmo/charts/minio-12.10.0.tgz new file mode 100644 index 0000000000..7e757b7c0d Binary files /dev/null and b/helm/cosmo/charts/minio-12.10.0.tgz differ diff --git a/helm/cosmo/values.full.yaml b/helm/cosmo/values.full.yaml index 9851f5ea82..4db98c132c 100644 --- a/helm/cosmo/values.full.yaml +++ b/helm/cosmo/values.full.yaml @@ -66,6 +66,9 @@ global: databaseUsername: "postgres" databasePassword: "changeme" + minio: + enabled: true + # Overall Ingress for the Cosmo Platform ingress: enabled: true @@ -100,6 +103,7 @@ controlplane: githubAppWebhookSecret: "" slackAppClientId: "" slackAppClientSecret: "" + s3StorageUrl: "http://minio:changeme@minio.wundergraph.local:9000/cosmo" # Cosmo Router router: @@ -229,4 +233,19 @@ postgresql: scripts: 01_init_keycloak.sql: | -- Create the database for Keycloak - CREATE DATABASE "keycloak"; \ No newline at end of file + CREATE DATABASE "keycloak"; + +# Minio for the Cosmo Controlplane +# https://artifacthub.io/packages/helm/bitnami/minio +minio: + commonAnnotations: + # Support for Kapp. This annotation will form a group to coordinate deployments with kapp. + kapp.k14s.io/change-group: "cosmo.apps.minio.wundergraph.com/deployment" + service: + ports: + minio: 9000 + minio_admin: 9001 + auth: + rootUser: minio + rootPassword: changeme + defaultBuckets: cosmo diff --git a/helm/cosmo/values.yaml b/helm/cosmo/values.yaml index 22b5a7ec30..58971200dd 100644 --- a/helm/cosmo/values.yaml +++ b/helm/cosmo/values.yaml @@ -64,4 +64,4 @@ ingress: # tls: [] # - secretName: chart-example-tls # hosts: - # - chart-example.local \ No newline at end of file + # - chart-example.local diff --git a/lerna.json b/lerna.json index a915002598..c1fbd7f8b3 100644 --- a/lerna.json +++ b/lerna.json @@ -24,7 +24,8 @@ "otelcollector", "graphqlmetrics", "studio", - "keycloak" + "keycloak", + "cdn-server/cdn" ], "npmClient": "pnpm", "loglevel": "verbose" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24a900c892..e56f92609a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,56 @@ importers: specifier: ^3.0.3 version: 3.0.3 + cdn-server: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.445.0 + version: 3.445.0 + '@hono/node-server': + specifier: ^1.2.2 + version: 1.2.2 + '@wundergraph/cosmo-cdn': + specifier: workspace:* + version: link:cdn + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + hono: + specifier: ^3.10.0 + version: 3.10.0 + devDependencies: + '@types/node': + specifier: ^20.9.0 + version: 20.9.0 + eslint: + specifier: ^8.53.0 + version: 8.53.0 + eslint-config-unjs: + specifier: ^0.2.1 + version: 0.2.1(eslint@8.53.0)(typescript@5.2.2) + tsx: + specifier: ^3.12.2 + version: 3.12.2 + + cdn-server/cdn: + dependencies: + hono: + specifier: ^3.10.0 + version: 3.10.0 + jose: + specifier: ^4.15.2 + version: 4.15.2 + devDependencies: + eslint: + specifier: ^8.53.0 + version: 8.53.0 + eslint-config-unjs: + specifier: ^0.2.1 + version: 0.2.1(eslint@8.53.0)(typescript@5.2.2) + vitest: + specifier: ^0.34.6 + version: 0.34.6 + cli: dependencies: '@bufbuild/buf': @@ -86,6 +136,9 @@ importers: date-fns: specifier: ^2.30.0 version: 2.30.0 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 env-paths: specifier: ^3.0.0 version: 3.0.0 @@ -225,6 +278,9 @@ importers: controlplane: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.445.0 + version: 3.445.0 '@bufbuild/buf': specifier: ^1.27.2 version: 1.27.2 @@ -669,6 +725,588 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + /@aws-crypto/crc32@3.0.0: + resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.433.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/crc32c@3.0.0: + resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.433.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection@3.0.0: + resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha1-browser@3.0.0: + resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser@3.0.0: + resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js@3.0.0: + resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.433.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto@3.0.0: + resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/util@3.0.0: + resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + dependencies: + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-sdk/client-s3@3.445.0: + resolution: {integrity: sha512-2G+3MnO78irZRjlfkdvtlKRQ3yuOfrRMg8mztKpMw0q/9WHtwCcmaUUpl1bXwJ+BcNTVHopLQXdbzCeaxxI92w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 3.0.0 + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.445.0 + '@aws-sdk/core': 3.445.0 + '@aws-sdk/credential-provider-node': 3.445.0 + '@aws-sdk/middleware-bucket-endpoint': 3.433.0 + '@aws-sdk/middleware-expect-continue': 3.433.0 + '@aws-sdk/middleware-flexible-checksums': 3.433.0 + '@aws-sdk/middleware-host-header': 3.433.0 + '@aws-sdk/middleware-location-constraint': 3.433.0 + '@aws-sdk/middleware-logger': 3.433.0 + '@aws-sdk/middleware-recursion-detection': 3.433.0 + '@aws-sdk/middleware-sdk-s3': 3.440.0 + '@aws-sdk/middleware-signing': 3.433.0 + '@aws-sdk/middleware-ssec': 3.433.0 + '@aws-sdk/middleware-user-agent': 3.438.0 + '@aws-sdk/region-config-resolver': 3.433.0 + '@aws-sdk/signature-v4-multi-region': 3.437.0 + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-endpoints': 3.438.0 + '@aws-sdk/util-user-agent-browser': 3.433.0 + '@aws-sdk/util-user-agent-node': 3.437.0 + '@aws-sdk/xml-builder': 3.310.0 + '@smithy/config-resolver': 2.0.17 + '@smithy/eventstream-serde-browser': 2.0.12 + '@smithy/eventstream-serde-config-resolver': 2.0.12 + '@smithy/eventstream-serde-node': 2.0.12 + '@smithy/fetch-http-handler': 2.2.4 + '@smithy/hash-blob-browser': 2.0.12 + '@smithy/hash-node': 2.0.12 + '@smithy/hash-stream-node': 2.0.12 + '@smithy/invalid-dependency': 2.0.12 + '@smithy/md5-js': 2.0.12 + '@smithy/middleware-content-length': 2.0.14 + '@smithy/middleware-endpoint': 2.1.4 + '@smithy/middleware-retry': 2.0.19 + '@smithy/middleware-serde': 2.0.12 + '@smithy/middleware-stack': 2.0.6 + '@smithy/node-config-provider': 2.1.4 + '@smithy/node-http-handler': 2.1.8 + '@smithy/protocol-http': 3.0.8 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + '@smithy/url-parser': 2.0.12 + '@smithy/util-base64': 2.0.0 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.16 + '@smithy/util-defaults-mode-node': 2.0.22 + '@smithy/util-endpoints': 1.0.3 + '@smithy/util-retry': 2.0.5 + '@smithy/util-stream': 2.0.17 + '@smithy/util-utf8': 2.0.0 + '@smithy/util-waiter': 2.0.12 + fast-xml-parser: 4.2.5 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.445.0: + resolution: {integrity: sha512-me4LvqNnu6kxi+sW7t0AgMv1Yi64ikas0x2+5jv23o6Csg32w0S0xOjCTKQYahOA5CMFunWvlkFIfxbqs+Uo7w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.445.0 + '@aws-sdk/middleware-host-header': 3.433.0 + '@aws-sdk/middleware-logger': 3.433.0 + '@aws-sdk/middleware-recursion-detection': 3.433.0 + '@aws-sdk/middleware-user-agent': 3.438.0 + '@aws-sdk/region-config-resolver': 3.433.0 + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-endpoints': 3.438.0 + '@aws-sdk/util-user-agent-browser': 3.433.0 + '@aws-sdk/util-user-agent-node': 3.437.0 + '@smithy/config-resolver': 2.0.17 + '@smithy/fetch-http-handler': 2.2.4 + '@smithy/hash-node': 2.0.12 + '@smithy/invalid-dependency': 2.0.12 + '@smithy/middleware-content-length': 2.0.14 + '@smithy/middleware-endpoint': 2.1.4 + '@smithy/middleware-retry': 2.0.19 + '@smithy/middleware-serde': 2.0.12 + '@smithy/middleware-stack': 2.0.6 + '@smithy/node-config-provider': 2.1.4 + '@smithy/node-http-handler': 2.1.8 + '@smithy/protocol-http': 3.0.8 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + '@smithy/url-parser': 2.0.12 + '@smithy/util-base64': 2.0.0 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.16 + '@smithy/util-defaults-mode-node': 2.0.22 + '@smithy/util-endpoints': 1.0.3 + '@smithy/util-retry': 2.0.5 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.445.0: + resolution: {integrity: sha512-ogbdqrS8x9O5BTot826iLnTQ6i4/F5BSi/74gycneCxYmAnYnyUBNOWVnynv6XZiEWyDJQCU2UtMd52aNGW1GA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.445.0 + '@aws-sdk/credential-provider-node': 3.445.0 + '@aws-sdk/middleware-host-header': 3.433.0 + '@aws-sdk/middleware-logger': 3.433.0 + '@aws-sdk/middleware-recursion-detection': 3.433.0 + '@aws-sdk/middleware-sdk-sts': 3.433.0 + '@aws-sdk/middleware-signing': 3.433.0 + '@aws-sdk/middleware-user-agent': 3.438.0 + '@aws-sdk/region-config-resolver': 3.433.0 + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-endpoints': 3.438.0 + '@aws-sdk/util-user-agent-browser': 3.433.0 + '@aws-sdk/util-user-agent-node': 3.437.0 + '@smithy/config-resolver': 2.0.17 + '@smithy/fetch-http-handler': 2.2.4 + '@smithy/hash-node': 2.0.12 + '@smithy/invalid-dependency': 2.0.12 + '@smithy/middleware-content-length': 2.0.14 + '@smithy/middleware-endpoint': 2.1.4 + '@smithy/middleware-retry': 2.0.19 + '@smithy/middleware-serde': 2.0.12 + '@smithy/middleware-stack': 2.0.6 + '@smithy/node-config-provider': 2.1.4 + '@smithy/node-http-handler': 2.1.8 + '@smithy/protocol-http': 3.0.8 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + '@smithy/url-parser': 2.0.12 + '@smithy/util-base64': 2.0.0 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.16 + '@smithy/util-defaults-mode-node': 2.0.22 + '@smithy/util-endpoints': 1.0.3 + '@smithy/util-retry': 2.0.5 + '@smithy/util-utf8': 2.0.0 + fast-xml-parser: 4.2.5 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/core@3.445.0: + resolution: {integrity: sha512-6GYLElUG1QTOdmXG8zXa+Ull9IUeSeItKDYHKzHYfIkbsagMfYlf7wm9XIYlatjtgodNfZ3gPHAJfRyPmwKrsg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/smithy-client': 2.1.12 + tslib: 2.6.1 + dev: false + + /@aws-sdk/credential-provider-env@3.433.0: + resolution: {integrity: sha512-Vl7Qz5qYyxBurMn6hfSiNJeUHSqfVUlMt0C1Bds3tCkl3IzecRWwyBOlxtxO3VCrgVeW3HqswLzCvhAFzPH6nQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/property-provider': 2.0.13 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/credential-provider-ini@3.445.0: + resolution: {integrity: sha512-R7IYSGjNZ5KKJwQJ2HNPemjpAMWvdce91i8w+/aHfqeGfTXrmYJu99PeGRyyBTKEumBaojyjTRvmO8HzS+/l7g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.433.0 + '@aws-sdk/credential-provider-process': 3.433.0 + '@aws-sdk/credential-provider-sso': 3.445.0 + '@aws-sdk/credential-provider-web-identity': 3.433.0 + '@aws-sdk/types': 3.433.0 + '@smithy/credential-provider-imds': 2.1.0 + '@smithy/property-provider': 2.0.13 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.445.0: + resolution: {integrity: sha512-zI4k4foSjQRKNEsouculRcz7IbLfuqdFxypDLYwn+qPNMqJwWJ7VxOOeBSPUpHFcd7CLSfbHN2JAhQ7M02gPTA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.433.0 + '@aws-sdk/credential-provider-ini': 3.445.0 + '@aws-sdk/credential-provider-process': 3.433.0 + '@aws-sdk/credential-provider-sso': 3.445.0 + '@aws-sdk/credential-provider-web-identity': 3.433.0 + '@aws-sdk/types': 3.433.0 + '@smithy/credential-provider-imds': 2.1.0 + '@smithy/property-provider': 2.0.13 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process@3.433.0: + resolution: {integrity: sha512-W7FcGlQjio9Y/PepcZGRyl5Bpwb0uWU7qIUCh+u4+q2mW4D5ZngXg8V/opL9/I/p4tUH9VXZLyLGwyBSkdhL+A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/property-provider': 2.0.13 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/credential-provider-sso@3.445.0: + resolution: {integrity: sha512-gJz7kAiDecdhtApgXnxfZsXKsww8BnifDF9MAx9Dr4X6no47qYsCCS3XPuEyRiF9VebXvHOH0H260Zp3bVyniQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.445.0 + '@aws-sdk/token-providers': 3.438.0 + '@aws-sdk/types': 3.433.0 + '@smithy/property-provider': 2.0.13 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity@3.433.0: + resolution: {integrity: sha512-RlwjP1I5wO+aPpwyCp23Mk8nmRbRL33hqRASy73c4JA2z2YiRua+ryt6MalIxehhwQU6xvXUKulJnPG9VaMFZg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/property-provider': 2.0.13 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-bucket-endpoint@3.433.0: + resolution: {integrity: sha512-Lk1xIu2tWTRa1zDw5hCF1RrpWQYSodUhrS/q3oKz8IAoFqEy+lNaD5jx+fycuZb5EkE4IzWysT+8wVkd0mAnOg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-arn-parser': 3.310.0 + '@smithy/node-config-provider': 2.1.4 + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + '@smithy/util-config-provider': 2.0.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-expect-continue@3.433.0: + resolution: {integrity: sha512-Uq2rPIsjz0CR2sulM/HyYr5WiqiefrSRLdwUZuA7opxFSfE808w5DBWSprHxbH3rbDSQR4nFiOiVYIH8Eth7nA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-flexible-checksums@3.433.0: + resolution: {integrity: sha512-Ptssx373+I7EzFUWjp/i/YiNFt6I6sDuRHz6DOUR9nmmRTlHHqmdcBXlJL2d9wwFxoBRCN8/PXGsTc/DJ4c95Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@aws-crypto/crc32c': 3.0.0 + '@aws-sdk/types': 3.433.0 + '@smithy/is-array-buffer': 2.0.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-host-header@3.433.0: + resolution: {integrity: sha512-mBTq3UWv1UzeHG+OfUQ2MB/5GEkt5LTKFaUqzL7ESwzW8XtpBgXnjZvIwu3Vcd3sEetMwijwaGiJhY0ae/YyaA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-location-constraint@3.433.0: + resolution: {integrity: sha512-2YD860TGntwZifIUbxm+lFnNJJhByR/RB/+fV1I8oGKg+XX2rZU+94pRfHXRywoZKlCA0L+LGDA1I56jxrB9sw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-logger@3.433.0: + resolution: {integrity: sha512-We346Fb5xGonTGVZC9Nvqtnqy74VJzYuTLLiuuftA5sbNzftBDy/22QCfvYSTOAl3bvif+dkDUzQY2ihc5PwOQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.433.0: + resolution: {integrity: sha512-HEvYC9PQlWY/ccUYtLvAlwwf1iCif2TSAmLNr3YTBRVa98x6jKL0hlCrHWYklFeqOGSKy6XhE+NGJMUII0/HaQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-sdk-s3@3.440.0: + resolution: {integrity: sha512-DVTSr+82Z8jR9xTwDN3YHzxX7qvi0n96V92OfxvSRDq2BldCEx/KEL1orUZjw97SAXhINOlUWjRR7j4HpwWQtQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-arn-parser': 3.310.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.433.0: + resolution: {integrity: sha512-ORYbJnBejUyonFl5FwIqhvI3Cq6sAp9j+JpkKZtFNma9tFPdrhmYgfCeNH32H/wGTQV/tUoQ3luh0gA4cuk6DA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.433.0 + '@aws-sdk/types': 3.433.0 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-signing@3.433.0: + resolution: {integrity: sha512-jxPvt59NZo/epMNLNTu47ikmP8v0q217I6bQFGJG7JVFnfl36zDktMwGw+0xZR80qiK47/2BWrNpta61Zd2FxQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/property-provider': 2.0.13 + '@smithy/protocol-http': 3.0.8 + '@smithy/signature-v4': 2.0.12 + '@smithy/types': 2.4.0 + '@smithy/util-middleware': 2.0.5 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-ssec@3.433.0: + resolution: {integrity: sha512-2AMaPx0kYfCiekxoL7aqFqSSoA9du+yI4zefpQNLr+1cZOerYiDxdsZ4mbqStR1CVFaX6U6hrYokXzjInsvETw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/middleware-user-agent@3.438.0: + resolution: {integrity: sha512-a+xHT1wOxT6EA6YyLmrfaroKWOkwwyiktUfXKM0FsUutGzNi4fKhb5NZ2al58NsXzHgHFrasSDp+Lqbd/X2cEw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-endpoints': 3.438.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/region-config-resolver@3.433.0: + resolution: {integrity: sha512-xpjRjCZW+CDFdcMmmhIYg81ST5UAnJh61IHziQEk0FXONrg4kjyYPZAOjEdzXQ+HxJQuGQLKPhRdzxmQnbX7pg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.4 + '@smithy/types': 2.4.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.5 + tslib: 2.6.1 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.437.0: + resolution: {integrity: sha512-MmrqudssOs87JgVg7HGVdvJws/t4kcOrJJd+975ki+DPeSoyK2U4zBDfDkJ+n0tFuZBs3sLwLh0QXE7BV28rRA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/protocol-http': 3.0.8 + '@smithy/signature-v4': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/token-providers@3.438.0: + resolution: {integrity: sha512-G2fUfTtU6/1ayYRMu0Pd9Ln4qYSvwJOWCqJMdkDgvXSwdgcOSOLsnAIk1AHGJDAvgLikdCzuyOsdJiexr9Vnww==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/middleware-host-header': 3.433.0 + '@aws-sdk/middleware-logger': 3.433.0 + '@aws-sdk/middleware-recursion-detection': 3.433.0 + '@aws-sdk/middleware-user-agent': 3.438.0 + '@aws-sdk/region-config-resolver': 3.433.0 + '@aws-sdk/types': 3.433.0 + '@aws-sdk/util-endpoints': 3.438.0 + '@aws-sdk/util-user-agent-browser': 3.433.0 + '@aws-sdk/util-user-agent-node': 3.437.0 + '@smithy/config-resolver': 2.0.17 + '@smithy/fetch-http-handler': 2.2.4 + '@smithy/hash-node': 2.0.12 + '@smithy/invalid-dependency': 2.0.12 + '@smithy/middleware-content-length': 2.0.14 + '@smithy/middleware-endpoint': 2.1.4 + '@smithy/middleware-retry': 2.0.19 + '@smithy/middleware-serde': 2.0.12 + '@smithy/middleware-stack': 2.0.6 + '@smithy/node-config-provider': 2.1.4 + '@smithy/node-http-handler': 2.1.8 + '@smithy/property-provider': 2.0.13 + '@smithy/protocol-http': 3.0.8 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + '@smithy/url-parser': 2.0.12 + '@smithy/util-base64': 2.0.0 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.16 + '@smithy/util-defaults-mode-node': 2.0.22 + '@smithy/util-endpoints': 1.0.3 + '@smithy/util-retry': 2.0.5 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/types@3.433.0: + resolution: {integrity: sha512-0jEE2mSrNDd8VGFjTc1otYrwYPIkzZJEIK90ZxisKvQ/EURGBhNzWn7ejWB9XCMFT6XumYLBR0V9qq5UPisWtA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-arn-parser@3.310.0: + resolution: {integrity: sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-endpoints@3.438.0: + resolution: {integrity: sha512-6VyPTq1kN3GWxwFt5DdZfOsr6cJZPLjWh0troY/0uUv3hK74C9o3Y0Xf/z8UAUvQFkVqZse12O0/BgPVMImvfA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/util-endpoints': 1.0.3 + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-locate-window@3.310.0: + resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-user-agent-browser@3.433.0: + resolution: {integrity: sha512-2Cf/Lwvxbt5RXvWFXrFr49vXv0IddiUwrZoAiwhDYxvsh+BMnh+NUFot+ZQaTrk/8IPZVDeLPWZRdVy00iaVXQ==} + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/types': 2.4.0 + bowser: 2.11.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-user-agent-node@3.437.0: + resolution: {integrity: sha512-JVEcvWaniamtYVPem4UthtCNoTBCfFTwYj7Y3CrWZ2Qic4TqrwLkAfaBGtI2TGrhIClVr77uzLI6exqMTN7orA==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/types': 3.433.0 + '@smithy/node-config-provider': 2.1.4 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@aws-sdk/util-utf8-browser@3.259.0: + resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + dependencies: + tslib: 2.6.1 + dev: false + + /@aws-sdk/xml-builder@3.310.0: + resolution: {integrity: sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + /@babel/code-frame@7.22.5: resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} engines: {node: '>=6.9.0'} @@ -914,15 +1552,15 @@ packages: '@commitlint/execute-rule': 17.4.0 '@commitlint/resolve-extends': 17.6.7 '@commitlint/types': 17.4.4 - '@types/node': 20.3.1 + '@types/node': 20.8.10 chalk: 4.1.2 cosmiconfig: 8.2.0 - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.3.1)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.2.2) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.8.10)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.2.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - '@swc/core' @@ -1029,7 +1667,7 @@ packages: '@bufbuild/protobuf': ^1.3.3 '@connectrpc/connect': ^1.1.2 '@tanstack/react-query': ^5.0.0 - react: 18.2.0 + react: ^18.2.0 react-dom: ^18.2.0 dependencies: '@bufbuild/protobuf': 1.4.1 @@ -1104,6 +1742,13 @@ packages: resolution: {integrity: sha512-ps5qF0tMxWRVu+V5gvCRrQNqlY92aTnIKdq27gm9LZMSdaKYZt6AVvSK1dlUMzs6Rt0Jm80b+eWct6xShBKhIw==} dev: true + /@esbuild-kit/cjs-loader@2.4.4: + resolution: {integrity: sha512-NfsJX4PdzhwSkfJukczyUiZGc7zNNWZcEAyqeISpDnn0PTfzMJR1aR8xAIPskBejIxBJbIgCCMzbaYa9SXepIg==} + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.7.2 + dev: true + /@esbuild-kit/core-utils@3.1.0: resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} dependencies: @@ -1111,6 +1756,13 @@ packages: source-map-support: 0.5.21 dev: true + /@esbuild-kit/core-utils@3.3.2: + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + dev: true + /@esbuild-kit/esm-loader@2.5.5: resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} dependencies: @@ -1908,6 +2560,16 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.53.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@eslint-community/regexpp@4.6.2: resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -1947,16 +2609,38 @@ packages: - supports-color dev: true - /@eslint/js@8.43.0: - resolution: {integrity: sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==} + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.43.0: + resolution: {integrity: sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@eslint/js@8.52.0: resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.53.0: + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@fastify/ajv-compiler@3.5.0: resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} dependencies: @@ -2197,6 +2881,11 @@ packages: react: 18.2.0 dev: false + /@hono/node-server@1.2.2: + resolution: {integrity: sha512-ER8nfFRw7u3BU/44G1GXth9m2Gqa9hneZ4c6yZ4nexGHdBYMJYHlDS6cOo556EugqASi1bBLDpoVbFA9nCgjGQ==} + engines: {node: '>=18.14.1'} + dev: false + /@hookform/resolvers@3.3.1(react-hook-form@7.45.1): resolution: {integrity: sha512-K7KCKRKjymxIB90nHDQ7b9nli474ru99ZbqxiqDAWYsYhOsU3/4qLxW91y+1n04ic13ajjZ66L3aXbNef8PELQ==} peerDependencies: @@ -4629,121 +5318,559 @@ packages: - immer dev: false - /@reactflow/core@11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-/tiE8sPShzeWFnshvi8hc1lbp1C5PLgAFl94JQdBstq94uOBTpdoI//1MN4a+fGp1xwAUP7P0IcLuWqIDZgrZg==} - peerDependencies: - react: 18.2.0 - react-dom: '>=17' + /@reactflow/core@11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/tiE8sPShzeWFnshvi8hc1lbp1C5PLgAFl94JQdBstq94uOBTpdoI//1MN4a+fGp1xwAUP7P0IcLuWqIDZgrZg==} + peerDependencies: + react: 18.2.0 + react-dom: '>=17' + dependencies: + '@types/d3': 7.4.0 + '@types/d3-drag': 3.0.2 + '@types/d3-selection': 3.0.5 + '@types/d3-zoom': 3.0.3 + classcat: 5.0.4 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@reactflow/minimap@11.7.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GqhJ0AoNhYf/GXI7JlWOR4THvi1nEcyo6sL6pGupJu8Ve1b8rpcTKNh4mXIerl8x0oRF8ajGvpIvh4R6rEtLoQ==} + peerDependencies: + react: 18.2.0 + react-dom: '>=17' + dependencies: + '@reactflow/core': 11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + '@types/d3-selection': 3.0.5 + '@types/d3-zoom': 3.0.3 + classcat: 5.0.4 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@reactflow/node-resizer@2.2.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-p8fqfEtMdXVAEdAT74GVpMeIm2v2t92LObKPFvIbOaA11vmcp+jSt45y2mPD6CxP6snzEVHXigYmGZNiujDtlQ==} + peerDependencies: + react: 18.2.0 + react-dom: '>=17' + dependencies: + '@reactflow/core': 11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + classcat: 5.0.4 + d3-drag: 3.0.0 + d3-selection: 3.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@reactflow/node-toolbar@1.3.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-s8gP07HClKDidsBSrcljoK600cdVLLBK1gNK0bSVpCk3hBVKUkEGESwMf7VwpZ1oxhM3859R3pz++7lUrbmF3w==} + peerDependencies: + react: 18.2.0 + react-dom: '>=17' + dependencies: + '@reactflow/core': 11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) + classcat: 5.0.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + - immer + dev: false + + /@rushstack/eslint-patch@1.3.2: + resolution: {integrity: sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==} + dev: true + + /@sigstore/bundle@2.1.0: + resolution: {integrity: sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@sigstore/protobuf-specs': 0.2.1 + dev: true + + /@sigstore/protobuf-specs@0.2.1: + resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /@sigstore/sign@2.1.0: + resolution: {integrity: sha512-4VRpfJxs+8eLqzLVrZngVNExVA/zAhVbi4UT4zmtLi4xRd7vz5qie834OgkrGsLlLB1B2nz/3wUxT1XAUBe8gw==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@sigstore/bundle': 2.1.0 + '@sigstore/protobuf-specs': 0.2.1 + make-fetch-happen: 13.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@sigstore/tuf@2.2.0: + resolution: {integrity: sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==} + engines: {node: ^16.14.0 || >=18.0.0} + dependencies: + '@sigstore/protobuf-specs': 0.2.1 + tuf-js: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + /@smithy/abort-controller@2.0.12: + resolution: {integrity: sha512-YIJyefe1mi3GxKdZxEBEuzYOeQ9xpYfqnFmWzojCssRAuR7ycxwpoRQgp965vuW426xUAQhCV5rCaWElQ7XsaA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/chunked-blob-reader-native@2.0.0: + resolution: {integrity: sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==} + dependencies: + '@smithy/util-base64': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/chunked-blob-reader@2.0.0: + resolution: {integrity: sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==} + dependencies: + tslib: 2.6.1 + dev: false + + /@smithy/config-resolver@2.0.17: + resolution: {integrity: sha512-iQ8Q8ojqiPqRKdybDI1g7HvG8EcnekRnH3DYeNTrT26vDuPq2nomyMCc0DZnPW+uAUcLCGZpAmGTAvEOYX55wA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.4 + '@smithy/types': 2.4.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.5 + tslib: 2.6.1 + dev: false + + /@smithy/credential-provider-imds@2.1.0: + resolution: {integrity: sha512-amqeueHM3i02S6z35WlXp7gejBnRloT5ctR/mQLlg/6LWGd70Avc2epzuuWtCptNg2ak5/yODD1fAVs9NPCyqg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.4 + '@smithy/property-provider': 2.0.13 + '@smithy/types': 2.4.0 + '@smithy/url-parser': 2.0.12 + tslib: 2.6.1 + dev: false + + /@smithy/eventstream-codec@2.0.12: + resolution: {integrity: sha512-ZZQLzHBJkbiAAdj2C5K+lBlYp/XJ+eH2uy+jgJgYIFW/o5AM59Hlj7zyI44/ZTDIQWmBxb3EFv/c5t44V8/g8A==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@smithy/types': 2.4.0 + '@smithy/util-hex-encoding': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/eventstream-serde-browser@2.0.12: + resolution: {integrity: sha512-0pi8QlU/pwutNshoeJcbKR1p7Ie5STd8UFAMX5xhSoSJjNlxIv/OsHbF023jscMRN2Prrqd6ToGgdCnsZVQjvg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/eventstream-serde-config-resolver@2.0.12: + resolution: {integrity: sha512-I0XfwQkIX3gAnbrU5rLMkBSjTM9DHttdbLwf12CXmj7SSI5dT87PxtKLRrZGanaCMbdf2yCep+MW5/4M7IbvQA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/eventstream-serde-node@2.0.12: + resolution: {integrity: sha512-vf1vMHGOkG3uqN9x1zKOhnvW/XgvhJXWqjV6zZiT2FMjlEayugQ1mzpSqr7uf89+BzjTzuZKERmOsEAmewLbxw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/eventstream-serde-universal@2.0.12: + resolution: {integrity: sha512-xZ3ZNpCxIND+q+UCy7y1n1/5VQEYicgSTNCcPqsKawX+Vd+6OcFX7gUHMyPzL8cZr+GdmJuxNleqHlH4giK2tw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/fetch-http-handler@2.2.4: + resolution: {integrity: sha512-gIPRFEGi+c6V52eauGKrjDzPWF2Cu7Z1r5F8A3j2wcwz25sPG/t8kjsbEhli/tS/2zJp/ybCZXe4j4ro3yv/HA==} + dependencies: + '@smithy/protocol-http': 3.0.8 + '@smithy/querystring-builder': 2.0.12 + '@smithy/types': 2.4.0 + '@smithy/util-base64': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/hash-blob-browser@2.0.12: + resolution: {integrity: sha512-riLnV16f27yyePX8UF0deRHAeccUK8SrOxyTykSTrnVkgS3DsjNapZtTbd8OGNKEbI60Ncdb5GwN3rHZudXvog==} + dependencies: + '@smithy/chunked-blob-reader': 2.0.0 + '@smithy/chunked-blob-reader-native': 2.0.0 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/hash-node@2.0.12: + resolution: {integrity: sha512-fDZnTr5j9t5qcbeJ037aMZXxMka13Znqwrgy3PAqYj6Dm3XHXHftTH3q+NWgayUxl1992GFtQt1RuEzRMy3NnQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/hash-stream-node@2.0.12: + resolution: {integrity: sha512-x/DrSynPKrW0k00q7aZ/vy531a3mRw79mOajHp+cIF0TrA1SqEMFoy/B8X0XtoAtlJWt/vvgeDNqt/KAeaAqMw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/invalid-dependency@2.0.12: + resolution: {integrity: sha512-p5Y+iMHV3SoEpy3VSR7mifbreHQwVSvHSAz/m4GdoXfOzKzaYC8hYv10Ks7Deblkf7lhas8U+lAp9ThbBM+ZXA==} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/is-array-buffer@2.0.0: + resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@smithy/md5-js@2.0.12: + resolution: {integrity: sha512-OgDt+Xnrw+W5z3MSl5KZZzebqmXrYl9UdbCiBYnnjErmNywwSjV6QB/Oic3/7hnsPniSU81n7Rvlhz2kH4EREQ==} + dependencies: + '@smithy/types': 2.4.0 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/middleware-content-length@2.0.14: + resolution: {integrity: sha512-poUNgKTw9XwPXfX9nEHpVgrMNVpaSMZbshqvPxFVoalF4wp6kRzYKOfdesSVectlQ51VtigoLfbXcdyPwvxgTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/protocol-http': 3.0.8 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/middleware-endpoint@2.1.4: + resolution: {integrity: sha512-fNUTsdTkM/RUu77AljH7fD3O0sFKDPNn1dFMR1oLAuJLOq4r6yjnL7Uc/F7wOgzgw1KRqqEnqAZccyAX2iEa4Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-serde': 2.0.12 + '@smithy/node-config-provider': 2.1.4 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/types': 2.4.0 + '@smithy/url-parser': 2.0.12 + '@smithy/util-middleware': 2.0.5 + tslib: 2.6.1 + dev: false + + /@smithy/middleware-retry@2.0.19: + resolution: {integrity: sha512-VMS1GHxLpRnuLHrPTj/nb9aD99jJsNzWX07F00fIuV9lkz3lWP7RUM7P1aitm0+4YfhShPn+Wri8/CuoqPOziA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.4 + '@smithy/protocol-http': 3.0.8 + '@smithy/service-error-classification': 2.0.5 + '@smithy/types': 2.4.0 + '@smithy/util-middleware': 2.0.5 + '@smithy/util-retry': 2.0.5 + tslib: 2.6.1 + uuid: 8.3.2 + dev: false + + /@smithy/middleware-serde@2.0.12: + resolution: {integrity: sha512-IBeco157lIScecq2Z+n0gq56i4MTnfKxS7rbfrAORveDJgnbBAaEQgYqMqp/cYqKrpvEXcyTjwKHrBjCCIZh2A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/middleware-stack@2.0.6: + resolution: {integrity: sha512-YSvNZeOKWLJ0M/ycxwDIe2Ztkp6Qixmcml1ggsSv2fdHKGkBPhGrX5tMzPGMI1yyx55UEYBi2OB4s+RriXX48A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/node-config-provider@2.1.4: + resolution: {integrity: sha512-kROLnHFatpimtmZ8YefsRRb5OJ8LVIVNhUWp67KHL4D2Vjd+WpIHMzWtkLLV4p0qXpY+IxmwcL2d2XMPn8ppsQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/property-provider': 2.0.13 + '@smithy/shared-ini-file-loader': 2.2.3 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/node-http-handler@2.1.8: + resolution: {integrity: sha512-KZylM7Wff/So5SmCiwg2kQNXJ+RXgz34wkxS7WNwIUXuZrZZpY/jKJCK+ZaGyuESDu3TxcaY+zeYGJmnFKbQsA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.12 + '@smithy/protocol-http': 3.0.8 + '@smithy/querystring-builder': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/property-provider@2.0.13: + resolution: {integrity: sha512-VJqUf2CbsQX6uUiC5dUPuoEATuFjkbkW3lJHbRnpk9EDC9X+iKqhfTK+WP+lve5EQ9TcCI1Q6R7hrg41FyC54w==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/protocol-http@3.0.8: + resolution: {integrity: sha512-SHJvYeWq8q0FK8xHk+xjV9dzDUDjFMT+G1pZbV+XB6OVoac/FSVshlMNPeUJ8AmSkcDKHRu5vASnRqZHgD3qhw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/querystring-builder@2.0.12: + resolution: {integrity: sha512-cDbF07IuCjiN8CdGvPzfJjXIrmDSelScRfyJYrYBNBbKl2+k7QD/KqiHhtRyEKgID5mmEVrV6KE6L/iPJ98sFw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + '@smithy/util-uri-escape': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/querystring-parser@2.0.12: + resolution: {integrity: sha512-fytyTcXaMzPBuNtPlhj5v6dbl4bJAnwKZFyyItAGt4Tgm9HFPZNo7a9r1SKPr/qdxUEBzvL9Rh+B9SkTX3kFxg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/service-error-classification@2.0.5: + resolution: {integrity: sha512-M0SeJnEgD2ywJyV99Fb1yKFzmxDe9JfpJiYTVSRMyRLc467BPU0qsuuDPzMCdB1mU8M8u1rVOdkqdoyFN8UFTw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + dev: false + + /@smithy/shared-ini-file-loader@2.2.3: + resolution: {integrity: sha512-VDyhCNycPbNkPidMnBgYQeSwJkoATRFm5VrveVqIPAjsdGutf7yZpPycuDWW9bRFnuuwaBhCC0pA7KCH0+2wrg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/signature-v4@2.0.12: + resolution: {integrity: sha512-6Kc2lCZEVmb1nNYngyNbWpq0d82OZwITH11SW/Q0U6PX5fH7B2cIcFe7o6eGEFPkTZTP8itTzmYiGcECL0D0Lw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.12 + '@smithy/is-array-buffer': 2.0.0 + '@smithy/types': 2.4.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-middleware': 2.0.5 + '@smithy/util-uri-escape': 2.0.0 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/smithy-client@2.1.12: + resolution: {integrity: sha512-XXqhridfkKnpj+lt8vM6HRlZbqUAqBjVC74JIi13F/AYQd/zTj9SOyGfxnbp4mjY9q28LityxIuV8CTinr9r5w==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-stack': 2.0.6 + '@smithy/types': 2.4.0 + '@smithy/util-stream': 2.0.17 + tslib: 2.6.1 + dev: false + + /@smithy/types@2.4.0: + resolution: {integrity: sha512-iH1Xz68FWlmBJ9vvYeHifVMWJf82ONx+OybPW8ZGf5wnEv2S0UXcU4zwlwJkRXuLKpcSLHrraHbn2ucdVXLb4g==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@smithy/url-parser@2.0.12: + resolution: {integrity: sha512-qgkW2mZqRvlNUcBkxYB/gYacRaAdck77Dk3/g2iw0S9F0EYthIS3loGfly8AwoWpIvHKhkTsCXXQfzksgZ4zIA==} + dependencies: + '@smithy/querystring-parser': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false + + /@smithy/util-base64@2.0.0: + resolution: {integrity: sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/util-body-length-browser@2.0.0: + resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==} + dependencies: + tslib: 2.6.1 + dev: false + + /@smithy/util-body-length-node@2.1.0: + resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@smithy/util-buffer-from@2.0.0: + resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/is-array-buffer': 2.0.0 + tslib: 2.6.1 + dev: false + + /@smithy/util-config-provider@2.0.0: + resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.1 + dev: false + + /@smithy/util-defaults-mode-browser@2.0.16: + resolution: {integrity: sha512-Uv5Cu8nVkuvLn0puX+R9zWbSNpLIR3AxUlPoLJ7hC5lvir8B2WVqVEkJLwtixKAncVLasnTVjPDCidtAUTGEQw==} + engines: {node: '>= 10.0.0'} dependencies: - '@types/d3': 7.4.0 - '@types/d3-drag': 3.0.2 - '@types/d3-selection': 3.0.5 - '@types/d3-zoom': 3.0.3 - classcat: 5.0.4 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - - immer + '@smithy/property-provider': 2.0.13 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + bowser: 2.11.0 + tslib: 2.6.1 dev: false - /@reactflow/minimap@11.7.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-GqhJ0AoNhYf/GXI7JlWOR4THvi1nEcyo6sL6pGupJu8Ve1b8rpcTKNh4mXIerl8x0oRF8ajGvpIvh4R6rEtLoQ==} - peerDependencies: - react: 18.2.0 - react-dom: '>=17' + /@smithy/util-defaults-mode-node@2.0.22: + resolution: {integrity: sha512-4nNsNBi4pj8nQX/cbRPzomyU/cptFr1OJckxo+nlRZdTZlj+raA8NI5sNF1kD4pyGyARuqDtWc9+xMhFHXIJmw==} + engines: {node: '>= 10.0.0'} dependencies: - '@reactflow/core': 11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) - '@types/d3-selection': 3.0.5 - '@types/d3-zoom': 3.0.3 - classcat: 5.0.4 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - - immer + '@smithy/config-resolver': 2.0.17 + '@smithy/credential-provider-imds': 2.1.0 + '@smithy/node-config-provider': 2.1.4 + '@smithy/property-provider': 2.0.13 + '@smithy/smithy-client': 2.1.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 dev: false - /@reactflow/node-resizer@2.2.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-p8fqfEtMdXVAEdAT74GVpMeIm2v2t92LObKPFvIbOaA11vmcp+jSt45y2mPD6CxP6snzEVHXigYmGZNiujDtlQ==} - peerDependencies: - react: 18.2.0 - react-dom: '>=17' + /@smithy/util-endpoints@1.0.3: + resolution: {integrity: sha512-rMYXLMdAMVbJAEHhNlCSJsAxo3NG3lcPja7WmesjAbNrMSyYZ6FnHHTy8kzRhddn4eAtLvPBSO6LiBB21gCoHQ==} + engines: {node: '>= 14.0.0'} dependencies: - '@reactflow/core': 11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - d3-drag: 3.0.0 - d3-selection: 3.0.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - - immer + '@smithy/node-config-provider': 2.1.4 + '@smithy/types': 2.4.0 + tslib: 2.6.1 dev: false - /@reactflow/node-toolbar@1.3.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-s8gP07HClKDidsBSrcljoK600cdVLLBK1gNK0bSVpCk3hBVKUkEGESwMf7VwpZ1oxhM3859R3pz++7lUrbmF3w==} - peerDependencies: - react: 18.2.0 - react-dom: '>=17' + /@smithy/util-hex-encoding@2.0.0: + resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} + engines: {node: '>=14.0.0'} dependencies: - '@reactflow/core': 11.9.2(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) - classcat: 5.0.4 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.3(@types/react@18.2.14)(react@18.2.0) - transitivePeerDependencies: - - '@types/react' - - immer + tslib: 2.6.1 dev: false - /@rushstack/eslint-patch@1.3.2: - resolution: {integrity: sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw==} - dev: true + /@smithy/util-middleware@2.0.5: + resolution: {integrity: sha512-1lyT3TcaMJQe+OFfVI+TlomDkPuVzb27NZYdYtmSTltVmLaUjdCyt4KE+OH1CnhZKsz4/cdCL420Lg9UH5Z2Mw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false - /@sigstore/bundle@2.1.0: - resolution: {integrity: sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==} - engines: {node: ^16.14.0 || >=18.0.0} + /@smithy/util-retry@2.0.5: + resolution: {integrity: sha512-x3t1+MQAJ6QONk3GTbJNcugCFDVJ+Bkro5YqQQK1EyVesajNDqxFtCx9WdOFNGm/Cbm7tUdwVEmfKQOJoU2Vtw==} + engines: {node: '>= 14.0.0'} dependencies: - '@sigstore/protobuf-specs': 0.2.1 - dev: true + '@smithy/service-error-classification': 2.0.5 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false - /@sigstore/protobuf-specs@0.2.1: - resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true + /@smithy/util-stream@2.0.17: + resolution: {integrity: sha512-fP/ZQ27rRvHsqItds8yB7jerwMpZFTL3QqbQbidUiG0+mttMoKdP0ZqnvM8UK5q0/dfc3/pN7g4XKPXOU7oRWw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/fetch-http-handler': 2.2.4 + '@smithy/node-http-handler': 2.1.8 + '@smithy/types': 2.4.0 + '@smithy/util-base64': 2.0.0 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.1 + dev: false - /@sigstore/sign@2.1.0: - resolution: {integrity: sha512-4VRpfJxs+8eLqzLVrZngVNExVA/zAhVbi4UT4zmtLi4xRd7vz5qie834OgkrGsLlLB1B2nz/3wUxT1XAUBe8gw==} - engines: {node: ^16.14.0 || >=18.0.0} + /@smithy/util-uri-escape@2.0.0: + resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} + engines: {node: '>=14.0.0'} dependencies: - '@sigstore/bundle': 2.1.0 - '@sigstore/protobuf-specs': 0.2.1 - make-fetch-happen: 13.0.0 - transitivePeerDependencies: - - supports-color - dev: true + tslib: 2.6.1 + dev: false - /@sigstore/tuf@2.2.0: - resolution: {integrity: sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==} - engines: {node: ^16.14.0 || >=18.0.0} + /@smithy/util-utf8@2.0.0: + resolution: {integrity: sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==} + engines: {node: '>=14.0.0'} dependencies: - '@sigstore/protobuf-specs': 0.2.1 - tuf-js: 2.1.0 - transitivePeerDependencies: - - supports-color - dev: true + '@smithy/util-buffer-from': 2.0.0 + tslib: 2.6.1 + dev: false - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + /@smithy/util-waiter@2.0.12: + resolution: {integrity: sha512-3sENmyVa1NnOPoiT2NCApPmu7ukP7S/v7kL9IxNmnygkDldn7/yK0TP42oPJLwB2k3mospNsSePIlqdXEUyPHA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.12 + '@smithy/types': 2.4.0 + tslib: 2.6.1 + dev: false /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} @@ -4790,7 +5917,7 @@ packages: /@tanstack/react-query@5.4.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-4aSOrRNa6yEmf7mws5QPTVMn8Lp7L38tFoTZ0c1ZmhIvbr8GIA0WT7X5N3yz/nuK8hUtjw9cAzBr4BPDZZ+tzA==} peerDependencies: - react: 18.2.0 + react: ^18.0.0 react-dom: ^18.0.0 react-native: '*' peerDependenciesMeta: @@ -5145,6 +6272,11 @@ packages: resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} dependencies: undici-types: 5.26.5 + + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 dev: true /@types/normalize-package-data@2.4.1: @@ -5241,6 +6373,34 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + debug: 4.3.4 + eslint: 8.53.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.5.4 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@5.62.0(eslint@8.43.0)(typescript@5.1.3): resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5281,6 +6441,26 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@5.62.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) + debug: 4.3.4 + eslint: 8.53.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@5.62.0: resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5309,6 +6489,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@5.62.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) + '@typescript-eslint/utils': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + debug: 4.3.4 + eslint: 8.53.0 + tsutils: 3.21.0(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@5.62.0: resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5376,12 +6576,32 @@ packages: - typescript dev: true + /@typescript-eslint/utils@5.62.0(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) + eslint: 8.53.0 + eslint-scope: 5.1.1 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@5.62.0: resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.3 dev: true /@typescript/vfs@1.5.0: @@ -6601,7 +7821,7 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true - /cosmiconfig-typescript-loader@4.4.0(@types/node@20.3.1)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.2.2): + /cosmiconfig-typescript-loader@4.4.0(@types/node@20.8.10)(cosmiconfig@8.2.0)(ts-node@10.9.1)(typescript@5.2.2): resolution: {integrity: sha512-BabizFdC3wBHhbI4kJh0VkQP9GkBfoHPydD0COMce1nJ1kJAB3F2TmJ/I7diULBKtmEWSwEbuN/KDtgnmUUVmw==} engines: {node: '>=v14.21.3'} peerDependencies: @@ -6610,9 +7830,9 @@ packages: ts-node: '>=10' typescript: '>=4' dependencies: - '@types/node': 20.3.1 + '@types/node': 20.8.10 cosmiconfig: 8.2.0 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) typescript: 5.2.2 dev: true @@ -7565,6 +8785,15 @@ packages: eslint: 8.52.0 dev: true + /eslint-config-prettier@8.8.0(eslint@8.53.0): + resolution: {integrity: sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.53.0 + dev: true + /eslint-config-standard@17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.52.0): resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} engines: {node: '>=12.0.0'} @@ -7580,6 +8809,21 @@ packages: eslint-plugin-promise: 6.1.1(eslint@8.52.0) dev: true + /eslint-config-standard@17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.53.0): + resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + dependencies: + eslint: 8.53.0 + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.53.0) + eslint-plugin-n: 16.0.1(eslint@8.53.0) + eslint-plugin-promise: 6.1.1(eslint@8.53.0) + dev: true + /eslint-config-unjs@0.2.1(eslint@8.52.0)(typescript@5.2.2): resolution: {integrity: sha512-h17q+WR86glq8yLFuHfEnAFfbEYqXpJAppXc0e0fQz0gsotJQ14BZVrlvIThE2a+stWyh0VT73gbBPfosl2rVA==} peerDependencies: @@ -7604,6 +8848,30 @@ packages: - supports-color dev: true + /eslint-config-unjs@0.2.1(eslint@8.53.0)(typescript@5.2.2): + resolution: {integrity: sha512-h17q+WR86glq8yLFuHfEnAFfbEYqXpJAppXc0e0fQz0gsotJQ14BZVrlvIThE2a+stWyh0VT73gbBPfosl2rVA==} + peerDependencies: + eslint: '*' + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0)(eslint@8.53.0)(typescript@5.2.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + eslint: 8.53.0 + eslint-config-prettier: 8.8.0(eslint@8.53.0) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.53.0) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.53.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.53.0) + eslint-plugin-n: 16.0.1(eslint@8.53.0) + eslint-plugin-node: 11.1.0(eslint@8.53.0) + eslint-plugin-promise: 6.1.1(eslint@8.53.0) + eslint-plugin-unicorn: 47.0.0(eslint@8.53.0) + typescript: 5.2.2 + transitivePeerDependencies: + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: @@ -7662,6 +8930,30 @@ packages: - supports-color dev: true + /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.53.0): + resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4 + enhanced-resolve: 5.15.0 + eslint: 8.53.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.53.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.53.0) + get-tsconfig: 4.7.2 + globby: 13.2.2 + is-core-module: 2.12.1 + is-glob: 4.0.3 + synckit: 0.8.5 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -7721,6 +9013,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint@8.53.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + debug: 3.2.7 + eslint: 8.53.0 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.52.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -7750,6 +9071,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.53.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + debug: 3.2.7 + eslint: 8.53.0 + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.53.0) + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.7)(eslint@8.43.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -7789,6 +9139,17 @@ packages: eslint: 8.52.0 dev: true + /eslint-plugin-es-x@7.2.0(eslint@8.53.0): + resolution: {integrity: sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@eslint-community/regexpp': 4.6.2 + eslint: 8.53.0 + dev: true + /eslint-plugin-es@3.0.1(eslint@8.52.0): resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} engines: {node: '>=8.10.0'} @@ -7800,6 +9161,17 @@ packages: regexpp: 3.2.0 dev: true + /eslint-plugin-es@3.0.1(eslint@8.53.0): + resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.53.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.52.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -7833,6 +9205,39 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0)(eslint@8.53.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.62.0(eslint@8.53.0)(typescript@5.2.2) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.53.0 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint@8.53.0) + has: 1.0.3 + is-core-module: 2.12.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.2 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-import@2.27.5(eslint@8.43.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -7907,6 +9312,23 @@ packages: semver: 7.5.4 dev: true + /eslint-plugin-n@16.0.1(eslint@8.53.0): + resolution: {integrity: sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==} + engines: {node: '>=16.0.0'} + peerDependencies: + eslint: '>=7.0.0' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + builtins: 5.0.1 + eslint: 8.53.0 + eslint-plugin-es-x: 7.2.0(eslint@8.53.0) + ignore: 5.2.4 + is-core-module: 2.12.1 + minimatch: 3.1.2 + resolve: 1.22.2 + semver: 7.5.4 + dev: true + /eslint-plugin-node@11.1.0(eslint@8.52.0): resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} engines: {node: '>=8.10.0'} @@ -7922,6 +9344,21 @@ packages: semver: 6.3.1 dev: true + /eslint-plugin-node@11.1.0(eslint@8.53.0): + resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=5.16.0' + dependencies: + eslint: 8.53.0 + eslint-plugin-es: 3.0.1(eslint@8.53.0) + eslint-utils: 2.1.0 + ignore: 5.2.4 + minimatch: 3.1.2 + resolve: 1.22.2 + semver: 6.3.1 + dev: true + /eslint-plugin-promise@6.1.1(eslint@8.52.0): resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7931,6 +9368,15 @@ packages: eslint: 8.52.0 dev: true + /eslint-plugin-promise@6.1.1(eslint@8.53.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.53.0 + dev: true + /eslint-plugin-react-hooks@4.6.0(eslint@8.43.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} @@ -7989,6 +9435,31 @@ packages: strip-indent: 3.0.0 dev: true + /eslint-plugin-unicorn@47.0.0(eslint@8.53.0): + resolution: {integrity: sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA==} + engines: {node: '>=16'} + peerDependencies: + eslint: '>=8.38.0' + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + ci-info: 3.8.0 + clean-regexp: 1.0.0 + eslint: 8.53.0 + esquery: 1.5.0 + indent-string: 4.0.0 + is-builtin-module: 3.2.1 + jsesc: 3.0.2 + lodash: 4.17.21 + pluralize: 8.0.0 + read-pkg-up: 7.0.1 + regexp-tree: 0.1.27 + regjsparser: 0.10.0 + safe-regex: 2.1.1 + semver: 7.5.4 + strip-indent: 3.0.0 + dev: true + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -8130,6 +9601,53 @@ packages: - supports-color dev: true + /eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@eslint-community/regexpp': 4.6.2 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.53.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8333,6 +9851,13 @@ packages: resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} dev: false + /fast-xml-parser@4.2.5: + resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fastify-graceful-shutdown@3.5.1: resolution: {integrity: sha512-fNwwCn8ob+Ykjg1iFgQrhbxNryXxUAGWD8yxz93gUgwAFgavd7rsOO/RN1oQskzltWBjWTB2KAqeF8q4XrJIkA==} engines: {node: '>=16.0.0'} @@ -8605,7 +10130,6 @@ packages: /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true /get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} @@ -9044,6 +10568,11 @@ packages: react-is: 16.13.1 dev: false + /hono@3.10.0: + resolution: {integrity: sha512-rN8PNMt66U58Qj3+L8gr7EsaYnj56cBDS2MjhUnMgDaXoyw44/OeFDsmXXzn/YQ7CAWeENC3Dq/kp+4RJSNE3Q==} + engines: {node: '>=16.0.0'} + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true @@ -10028,7 +11557,7 @@ packages: /loupe@2.3.6: resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -11354,7 +12883,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.24 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) yaml: 2.3.1 /postcss-load-config@4.0.1(ts-node@10.9.1): @@ -11370,7 +12899,7 @@ packages: optional: true dependencies: lilconfig: 2.1.0 - ts-node: 10.9.1(@types/node@20.3.1)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) yaml: 2.3.1 dev: true @@ -11420,7 +12949,6 @@ packages: nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: false /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} @@ -11690,7 +13218,7 @@ packages: /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: - react: 18.2.0 + react: ^18.2.0 dependencies: loose-envify: 1.4.0 react: 18.2.0 @@ -12738,6 +14266,10 @@ packages: dependencies: acorn: 8.10.0 + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + /strong-log-transformer@2.1.0: resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==} engines: {node: '>=4'} @@ -13035,7 +14567,7 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - /ts-node@10.9.1(@types/node@20.3.1)(typescript@5.2.2): + /ts-node@10.9.1(@types/node@20.8.10)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -13054,7 +14586,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.3.1 + '@types/node': 20.8.10 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -13090,7 +14622,6 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib@2.6.0: resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} @@ -13155,6 +14686,17 @@ packages: typescript: 5.2.2 dev: true + /tsx@3.12.2: + resolution: {integrity: sha512-ykAEkoBg30RXxeOMVeZwar+JH632dZn9EUJVyJwhfag62k6UO/dIyJEV58YuLF6e5BTdV/qmbQrpkWqjq9cUnQ==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.4.4 + '@esbuild-kit/core-utils': 3.1.0 + '@esbuild-kit/esm-loader': 2.5.5 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /tsx@3.14.0: resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} hasBin: true @@ -13336,7 +14878,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /undici@5.27.2: resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} @@ -13476,6 +15017,11 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true @@ -13546,7 +15092,7 @@ packages: - supports-color - terser - /vite-node@0.34.6(@types/node@20.3.1): + /vite-node@0.34.6(@types/node@20.9.0): resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -13556,7 +15102,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.3.9(@types/node@20.3.1) + vite: 4.3.9(@types/node@20.9.0) transitivePeerDependencies: - '@types/node' - less @@ -13599,6 +15145,39 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vite@4.3.9(@types/node@20.9.0): + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.9.0 + esbuild: 0.17.19 + postcss: 8.4.31 + rollup: 3.26.3 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vitest@0.34.1: resolution: {integrity: sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ==} engines: {node: '>=v14.18.0'} @@ -13695,7 +15274,7 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.3.1 + '@types/node': 20.9.0 '@vitest/expect': 0.34.6 '@vitest/runner': 0.34.6 '@vitest/snapshot': 0.34.6 @@ -13714,8 +15293,8 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.3.9(@types/node@20.3.1) - vite-node: 0.34.6(@types/node@20.3.1) + vite: 4.3.9(@types/node@20.9.0) + vite-node: 0.34.6(@types/node@20.9.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c84e23441b..42b3f235b1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,3 +7,5 @@ packages: - shared - studio - connect + - cdn-server + - cdn-server/cdn diff --git a/proto/wg/cosmo/platform/v1/platform.proto b/proto/wg/cosmo/platform/v1/platform.proto index c11c00ee1f..fdde6386a1 100644 --- a/proto/wg/cosmo/platform/v1/platform.proto +++ b/proto/wg/cosmo/platform/v1/platform.proto @@ -709,6 +709,27 @@ message DeleteRouterTokenResponse { Response response = 1; } +message PublishPersistedOperationsRequest { + string fedGraphName = 1; + string clientName = 2; + repeated string operations = 3; +} + +enum PublishedOperationStatus { + UP_TO_DATE = 0; + CREATED = 1; +} + +message PublishedOperation { + string hash = 1; + PublishedOperationStatus status = 2; +} + +message PublishPersistedOperationsResponse { + Response response = 1; + repeated PublishedOperation operations = 2; +} + message Header{ string key = 1; string value = 2; @@ -986,6 +1007,24 @@ message DeleteOIDCProviderResponse { Response response = 1; } +message ClientInfo{ + string name = 1; + string id = 2; + string createdAt = 3; + string lastUpdatedAt = 4; + string createdBy = 5; + string lastUpdatedBy = 6; +} + +message GetClientsRequest { + string fedGraphName = 1; +} + +message GetClientsResponse { + Response response = 1; + repeated ClientInfo clients = 2; +} + message GetFieldUsageRequest { string graph_name = 1; optional string namedType = 2; @@ -1046,6 +1085,8 @@ service PlatformService { rpc GetRouterTokens(GetRouterTokensRequest) returns (GetRouterTokensResponse) {} // DeleteRouterToken deletes the router token of a federated graph. rpc DeleteRouterToken(DeleteRouterTokenRequest) returns (DeleteRouterTokenResponse) {} + // Add persisted operations + rpc PublishPersistedOperations(PublishPersistedOperationsRequest) returns (PublishPersistedOperationsResponse) {} // For Studio and CLI @@ -1136,6 +1177,9 @@ service PlatformService { rpc GetOIDCProvider(GetOIDCProviderRequest) returns (GetOIDCProviderResponse) {} // DeleteOIDCProvider deletes the oidc provider connected the organization rpc DeleteOIDCProvider(DeleteOIDCProviderRequest) returns (DeleteOIDCProviderResponse) {} + // GetClients returns all the clients of the organization + rpc GetClients(GetClientsRequest) returns (GetClientsResponse) {} + // Analytics diff --git a/router-tests/authentication_test.go b/router-tests/authentication_test.go index 047b164014..f83e3d9e48 100644 --- a/router-tests/authentication_test.go +++ b/router-tests/authentication_test.go @@ -39,7 +39,7 @@ func setupServerWithJWKS(tb testing.TB, jwksOpts *authentication.JWKSAuthenticat core.WithAccessController(core.NewAccessController(authenticators, authRequired)), } serverOpts = append(serverOpts, opts...) - return prepareServer(tb, serverOpts...), authServer + return setupServer(tb, serverOpts...), authServer } func assertHasGraphQLErrors(t *testing.T, rr *httptest.ResponseRecorder) { @@ -187,7 +187,7 @@ func TestAuthenticationMultipleProviders(t *testing.T) { require.NoError(t, err) authenticators := []authentication.Authenticator{authenticator1, authenticator2} accessController := core.NewAccessController(authenticators, false) - server := prepareServer(t, core.WithAccessController(accessController)) + server := setupServer(t, core.WithAccessController(accessController)) t.Run("authenticate with first provider", func(t *testing.T) { for _, prefix := range authenticator1HeaderValuePrefixes { diff --git a/router-tests/integration_test.go b/router-tests/integration_test.go index ab27321603..8d6ebe07bd 100644 --- a/router-tests/integration_test.go +++ b/router-tests/integration_test.go @@ -19,6 +19,7 @@ import ( "time" "github.com/buger/jsonparser" + "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wundergraph/cosmo/router-tests/routerconfig" @@ -123,8 +124,15 @@ func sendQueryOK(tb testing.TB, server *core.Server, query *testQuery) string { } func sendData(server *core.Server, data []byte) *httptest.ResponseRecorder { + return sendDataWithHeader(server, data, nil) +} + +func sendDataWithHeader(server *core.Server, data []byte, header http.Header) *httptest.ResponseRecorder { rr := httptest.NewRecorder() req := httptest.NewRequest("POST", "/graphql", bytes.NewBuffer(data)) + if header != nil { + req.Header = header + } server.Server.Handler.ServeHTTP(rr, req) return rr } @@ -137,7 +145,14 @@ func sendCustomData(server *core.Server, data []byte, reqMw func(r *http.Request return rr } -func prepareServer(tb testing.TB, opts ...core.Option) *core.Server { +// setupServer sets up the router server without making it listen on a local +// port, allowing tests by calling the server directly via server.Server.Handler.ServeHTTP +func setupServer(tb testing.TB, opts ...core.Option) *core.Server { + server, _ := setupServerConfig(tb, opts...) + return server +} + +func setupServerConfig(tb testing.TB, opts ...core.Option) (*core.Server, config.Config) { ctx := context.Background() cfg := config.Config{ Graph: config.Graph{ @@ -163,10 +178,46 @@ func prepareServer(tb testing.TB, opts ...core.Option) *core.Server { zapcore.ErrorLevel, )) + t := jwt.New(jwt.SigningMethodHS256) + t.Claims = jwt.MapClaims{ + "federated_graph_id": "graph", + "organization_id": "organization", + } + graphApiToken, err := t.SignedString([]byte("hunter2")) + require.NoError(tb, err) + + cdnFileServer := http.FileServer(http.Dir(filepath.Join("testdata", "cdn"))) + var cdnRequestLog []string + cdnServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + requestLog, err := json.Marshal(cdnRequestLog) + require.NoError(tb, err) + w.Header().Set("Content-Type", "application/json") + w.Write(requestLog) + return + } + cdnRequestLog = append(cdnRequestLog, r.Method+" "+r.URL.Path) + // Ensure we have an authorization header with a valid token + authorization := r.Header.Get("Authorization") + token := authorization[len("Bearer "):] + parsedClaims := make(jwt.MapClaims) + jwtParser := new(jwt.Parser) + _, _, err = jwtParser.ParseUnverified(token, parsedClaims) + assert.NoError(tb, err) + assert.Equal(tb, t.Claims, parsedClaims) + cdnFileServer.ServeHTTP(w, r) + })) + + tb.Cleanup(cdnServer.Close) + + cfg.CDN.URL = cdnServer.URL + routerOpts := []core.Option{ core.WithFederatedGraphName(cfg.Graph.Name), core.WithStaticRouterConfig(routerConfig), core.WithLogger(zapLogger), + core.WithGraphApiToken(graphApiToken), + core.WithCDNURL(cfg.CDN.URL), core.WithEngineExecutionConfig(config.EngineExecutionConfiguration{ EnableSingleFlight: true, EnableRequestTracing: true, @@ -181,13 +232,7 @@ func prepareServer(tb testing.TB, opts ...core.Option) *core.Server { server, err := rs.NewTestServer(ctx) require.NoError(tb, err) - return server -} - -// setupServer sets up the router server without making it listen on a local -// port, allowing tests by calling the server directly via server.Server.Handler.ServeHTTP -func setupServer(tb testing.TB) *core.Server { - return prepareServer(tb) + return server, cfg } // setupListeningServer calls setupServer to set up the server but makes it listen @@ -202,7 +247,7 @@ func setupListeningServer(tb testing.TB, opts ...core.Option) (*core.Server, int serverOpts := append([]core.Option{ core.WithListenerAddr(":" + strconv.Itoa(port)), }, opts...) - server := prepareServer(tb, serverOpts...) + server := setupServer(tb, serverOpts...) go func() { err := server.Server.ListenAndServe() if err != http.ErrServerClosed { diff --git a/router-tests/persisted_operations_test.go b/router-tests/persisted_operations_test.go new file mode 100644 index 0000000000..6cfa21ce80 --- /dev/null +++ b/router-tests/persisted_operations_test.go @@ -0,0 +1,71 @@ +package integration_test + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" +) + +func persistedOperationPayload(sha56Hash string) []byte { + return []byte(fmt.Sprintf(`{ + "extensions": { + "persistedQuery": { + "version":1, + "sha256Hash": "%s" + } + } + }`, sha56Hash)) +} + +func TestPersistedOperationNotFound(t *testing.T) { + server := setupServer(t) + result := sendData(server, persistedOperationPayload("does-not-exist")) + assert.Equal(t, http.StatusOK, result.Code) + assert.JSONEq(t, `{"data": null, "errors": [{ "message": "PersistedQueryNotFound" }]}`, result.Body.String()) +} + +func TestPersistedOperation(t *testing.T) { + const ( + operationID = "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f" + operationName = "Employees" + ) + server := setupServer(t) + header := make(http.Header) + header.Add("graphql-client-name", "my-client") + payload := persistedOperationPayload(operationID) + payload, _ = sjson.SetBytes(payload, "operationName", operationName) + res := sendDataWithHeader(server, payload, header) + assert.JSONEq(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12}]}}`, res.Body.String()) +} + +func TestPersistedOperationsCache(t *testing.T) { + // Requesting the same persisted operation twice should only make one request to the CDN + const ( + operationID = "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f" + operationName = "Employees" + ) + server, cfg := setupServerConfig(t) + header := make(http.Header) + header.Add("graphql-client-name", "my-client") + payload := persistedOperationPayload(operationID) + payload, _ = sjson.SetBytes(payload, "operationName", operationName) + res1 := sendDataWithHeader(server, payload, header) + assert.JSONEq(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12}]}}`, res1.Body.String()) + res2 := sendDataWithHeader(server, payload, header) + assert.JSONEq(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12}]}}`, res2.Body.String()) + + // Check how many requests we made to the CDN + requestLogResp, err := http.Get(cfg.CDN.URL) + require.NoError(t, err) + defer requestLogResp.Body.Close() + var requestLog []string + if err := json.NewDecoder(requestLogResp.Body).Decode(&requestLog); err != nil { + t.Fatal(err) + } + assert.Len(t, requestLog, 1) +} diff --git a/router-tests/testdata/cdn/organization/graph/operations/my-client/dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f.json b/router-tests/testdata/cdn/organization/graph/operations/my-client/dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f.json new file mode 100644 index 0000000000..04f076a03e --- /dev/null +++ b/router-tests/testdata/cdn/organization/graph/operations/my-client/dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "body": "query Employees {\n employees {\n id\n }\n}" +} diff --git a/router/cmd/instance.go b/router/cmd/instance.go index d4af592ad5..9f45dd4b21 100644 --- a/router/cmd/instance.go +++ b/router/cmd/instance.go @@ -137,6 +137,7 @@ func NewRouter(params Params) (*core.Router, error) { core.WithEngineExecutionConfig(cfg.EngineExecutionConfiguration), core.WithAccessController(core.NewAccessController(authenticators, cfg.Authorization.RequireAuthentication)), core.WithLocalhostFallbackInsideDocker(cfg.LocalhostFallbackInsideDocker), + core.WithCDNURL(cfg.CDN.URL), ) } diff --git a/router/config/config.go b/router/config/config.go index 73e081d9bd..99cdff4450 100644 --- a/router/config/config.go +++ b/router/config/config.go @@ -188,6 +188,10 @@ type AuthorizationConfiguration struct { RequireAuthentication bool `yaml:"require_authentication" default:"false" envconfig:"REQUIRE_AUTHENTICATION"` } +type CDNConfiguration struct { + URL string `yaml:"url" validate:"url" envconfig:"CDN_URL" default:"https://cosmo-cdn.wundergraph.com"` +} + type Config struct { Version string `yaml:"version"` @@ -216,6 +220,7 @@ type Config struct { Authentication AuthenticationConfiguration `yaml:"authentication"` Authorization AuthorizationConfiguration `yaml:"authorization"` LocalhostFallbackInsideDocker bool `yaml:"localhost_fallback_inside_docker" default:"true" envconfig:"LOCALHOST_FALLBACK_INSIDE_DOCKER"` + CDN CDNConfiguration `yaml:"cdn"` ConfigPath string `envconfig:"CONFIG_PATH" validate:"omitempty,filepath"` RouterConfigPath string `yaml:"router_config_path" envconfig:"ROUTER_CONFIG_PATH" validate:"omitempty,filepath"` diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index 6534437035..39b34ba2be 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -6,6 +6,7 @@ import ( "time" "github.com/go-chi/chi/middleware" + "github.com/wundergraph/cosmo/router/internal/cdn" "github.com/wundergraph/cosmo/router/internal/logging" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" "github.com/wundergraph/graphql-go-tools/v2/pkg/graphql" @@ -87,12 +88,13 @@ func (h *PreHandler) Handler(next http.Handler) http.Handler { } r = validatedReq - operation, err := h.parser.ParseReader(r.Body) + operation, err := h.parser.ParseReader(r.Context(), clientInfo, r.Body, requestLogger) if err != nil { hasRequestError = true var reportErr ReportError var inputErr InputError + var poNotFoundErr cdn.PersistentOperationNotFoundError switch { case errors.As(err, &inputErr): requestLogger.Error(inputErr.Error()) @@ -101,6 +103,12 @@ func (h *PreHandler) Handler(next http.Handler) http.Handler { report := reportErr.Report() logInternalErrorsFromReport(reportErr.Report(), requestLogger) writeRequestErrors(r, graphql.RequestErrorsFromOperationReport(*report), w, requestLogger) + case errors.As(err, &poNotFoundErr): + requestLogger.Debug("persisted operation not found", + zap.String("sha256Hash", poNotFoundErr.Sha256Hash()), + zap.String("clientName", poNotFoundErr.ClientName())) + writeRequestErrors(r, graphql.RequestErrorsFromError(errors.New(cdn.PersistedOperationNotFoundErrorCode)), w, requestLogger) + default: // If we have an unknown error, we log it and return an internal server error requestLogger.Error(err.Error()) writeRequestErrors(r, graphql.RequestErrorsFromError(errInternalServer), w, requestLogger) diff --git a/router/core/operation_parser.go b/router/core/operation_parser.go index 367956bbd0..4ad9f21ace 100644 --- a/router/core/operation_parser.go +++ b/router/core/operation_parser.go @@ -2,6 +2,7 @@ package core import ( "bytes" + "context" "fmt" "hash" "io" @@ -13,6 +14,7 @@ import ( "github.com/pkg/errors" "github.com/wundergraph/graphql-go-tools/v2/pkg/lexer/literal" + "github.com/wundergraph/cosmo/router/internal/cdn" "github.com/wundergraph/cosmo/router/internal/pool" "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" "github.com/wundergraph/graphql-go-tools/v2/pkg/astjson" @@ -20,6 +22,7 @@ import ( "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" "github.com/wundergraph/graphql-go-tools/v2/pkg/astprinter" "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" + "go.uber.org/zap" ) type ParsedOperation struct { @@ -42,6 +45,7 @@ type ParsedOperation struct { type OperationParser struct { executor *Executor maxOperationSizeInBytes int64 + cdn *cdn.CDN parseKitPool *sync.Pool } @@ -55,10 +59,17 @@ type parseKit struct { unescapedDocument []byte } -func NewOperationParser(executor *Executor, maxOperationSizeInBytes int64) *OperationParser { +type OperationParserOptions struct { + Executor *Executor + MaxOperationSizeInBytes int64 + CDN *cdn.CDN +} + +func NewOperationParser(opts OperationParserOptions) *OperationParser { return &OperationParser{ - executor: executor, - maxOperationSizeInBytes: maxOperationSizeInBytes, + executor: opts.Executor, + maxOperationSizeInBytes: opts.MaxOperationSizeInBytes, + cdn: opts.CDN, parseKitPool: &sync.Pool{ New: func() interface{} { return &parseKit{ @@ -98,7 +109,7 @@ func (p *OperationParser) entityTooLarge() error { } } -func (p *OperationParser) ParseReader(r io.Reader) (*ParsedOperation, error) { +func (p *OperationParser) ParseReader(ctx context.Context, clientInfo *ClientInfo, r io.Reader, log *zap.Logger) (*ParsedOperation, error) { // Use an extra byte for the max size. This way we can check if N became // zero to detect if the request body was too large. limitedReader := &io.LimitedReader{R: r, N: p.maxOperationSizeInBytes + 1} @@ -112,14 +123,14 @@ func (p *OperationParser) ParseReader(r io.Reader) (*ParsedOperation, error) { if limitedReader.N == 0 { return nil, p.entityTooLarge() } - return p.parse(buf.Bytes()) + return p.parse(ctx, clientInfo, buf.Bytes(), log) } -func (p *OperationParser) Parse(data []byte) (*ParsedOperation, error) { +func (p *OperationParser) Parse(ctx context.Context, clientInfo *ClientInfo, data []byte, log *zap.Logger) (*ParsedOperation, error) { if len(data) > int(p.maxOperationSizeInBytes) { return nil, p.entityTooLarge() } - return p.parse(data) + return p.parse(ctx, clientInfo, data, log) } var ( @@ -130,10 +141,28 @@ var ( {"query"}, {"variables"}, {"operationName"}, + {"extensions"}, + } + + persistedQueryKeys = [][]string{ + {"version"}, + {"sha256Hash"}, } ) -func (p *OperationParser) parse(body []byte) (*ParsedOperation, error) { +const ( + parseOperationKeysQueryIndex = iota + parseOperationKeysVariablesIndex + parseOperationKeysOperationNameIndex + parseOperationKeysExtensionsIndex +) + +const ( + persistedQueryKeysVersionIndex = iota + persistedQueryKeysSha256HashIndex +) + +func (p *OperationParser) parse(ctx context.Context, clientInfo *ClientInfo, body []byte, log *zap.Logger) (*ParsedOperation, error) { var ( requestOperationType string @@ -145,28 +174,76 @@ func (p *OperationParser) parse(body []byte) (*ParsedOperation, error) { originalOperationNameRef ast.ByteSliceReference requestDocumentBytes []byte requestVariableBytes []byte + persistedQueryVersion []byte + persistedQuerySha256Hash []byte ) kit := p.getKit() defer p.freeKit(kit) + var parseErr error + jsonparser.EachKey(body, func(i int, value []byte, valueType jsonparser.ValueType, err error) { if err != nil { + parseErr = err return } switch i { - case 0: + case parseOperationKeysQueryIndex: requestDocumentBytes, err = jsonparser.Unescape(value, kit.unescapedDocument) if err != nil { + parseErr = fmt.Errorf("error unescaping query: %w", err) return } - case 1: + case parseOperationKeysVariablesIndex: requestVariableBytes = value - case 2: + case parseOperationKeysOperationNameIndex: requestOperationNameBytes = value + case parseOperationKeysExtensionsIndex: + persistedQuery, _, _, err := jsonparser.Get(value, "persistedQuery") + if err != nil { + parseErr = err + return + } + if len(persistedQuery) > 0 { + jsonparser.EachKey(persistedQuery, func(i int, value []byte, valueType jsonparser.ValueType, err error) { + if err != nil { + parseErr = err + return + } + switch i { + case persistedQueryKeysVersionIndex: + persistedQueryVersion = value + case persistedQueryKeysSha256HashIndex: + persistedQuerySha256Hash = value + } + }, persistedQueryKeys...) + if persistedQueryVersion == nil { + log.Warn("persistedQuery.version is missing") + persistedQuerySha256Hash = nil + return + } + if len(persistedQueryVersion) != 1 || persistedQueryVersion[0] != '1' { + log.Warn("unsupported persistedQuery.version", zap.String("version", string(persistedQueryVersion))) + persistedQuerySha256Hash = nil + return + } + } } }, parseOperationKeys...) + if parseErr != nil { + return nil, errors.WithStack(parseErr) + } + + if len(persistedQuerySha256Hash) > 0 { + persistedOperationData, err := p.cdn.PersistentOperation(ctx, clientInfo.Name, persistedQuerySha256Hash) + if err != nil { + return nil, errors.WithStack(err) + } + requestDocumentBytes = persistedOperationData + } + requestHasOperationName := requestOperationNameBytes != nil && !bytes.Equal(requestOperationNameBytes, literal.NULL) if !requestHasOperationName { requestOperationNameBytes = nil diff --git a/router/core/router.go b/router/core/router.go index be1425bb2d..7285077ae2 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -12,6 +12,7 @@ import ( "connectrpc.com/connect" "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/graphqlmetrics/v1/graphqlmetricsv1connect" + "github.com/wundergraph/cosmo/router/internal/cdn" "github.com/wundergraph/cosmo/router/internal/docker" "github.com/wundergraph/cosmo/router/internal/graphqlmetrics" brotli "go.withmatt.com/connect-brotli" @@ -94,6 +95,8 @@ type ( healthCheckPath string readinessCheckPath string livenessCheckPath string + cdnURL string + cdn *cdn.CDN prometheusServer *http.Server modulesConfig map[string]interface{} routerMiddlewares []func(http.Handler) http.Handler @@ -267,6 +270,14 @@ func NewRouter(opts ...Option) (*Router, error) { }) } } + routerCDN, err := cdn.New(cdn.CDNOptions{ + URL: r.cdnURL, + AuthenticationToken: r.graphApiToken, + }) + if err != nil { + return nil, err + } + r.cdn = routerCDN return r, nil } @@ -673,7 +684,11 @@ func (r *Router) newServer(ctx context.Context, routerConfig *nodev1.RouterConfi return nil, fmt.Errorf("failed to build plan configuration: %w", err) } - operationParser := NewOperationParser(executor, int64(r.routerTrafficConfig.MaxRequestBodyBytes)) + operationParser := NewOperationParser(OperationParserOptions{ + Executor: executor, + MaxOperationSizeInBytes: int64(r.routerTrafficConfig.MaxRequestBodyBytes), + CDN: r.cdn, + }) operationPlanner := NewOperationPlanner(executor, planCache) var graphqlPlaygroundHandler http.Handler @@ -1029,6 +1044,13 @@ func WithLivenessCheckPath(path string) Option { } } +// WithCDNURL sets the root URL for the CDN to use +func WithCDNURL(cdnURL string) Option { + return func(r *Router) { + r.cdnURL = cdnURL + } +} + func WithHeaderRules(headers config.HeaderRules) Option { return func(r *Router) { r.headerRules = headers diff --git a/router/core/websocket.go b/router/core/websocket.go index 379e96dd71..ba8efca384 100644 --- a/router/core/websocket.go +++ b/router/core/websocket.go @@ -414,7 +414,7 @@ func (h *WebSocketConnectionHandler) writeErrorMessage(operationID string, err e } func (h *WebSocketConnectionHandler) parseAndPlan(payload []byte) (*ParsedOperation, *operationContext, error) { - operation, err := h.parser.Parse(payload) + operation, err := h.parser.Parse(h.ctx, h.clientInfo, payload, h.logger) if err != nil { return nil, nil, err } diff --git a/router/internal/cdn/cdn.go b/router/internal/cdn/cdn.go new file mode 100644 index 0000000000..d1bc2244e4 --- /dev/null +++ b/router/internal/cdn/cdn.go @@ -0,0 +1,203 @@ +package cdn + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "sync" + + "github.com/buger/jsonparser" + "github.com/golang-jwt/jwt/v5" + "github.com/wundergraph/cosmo/router/internal/unsafebytes" +) + +const ( + FederatedGraphIDClaim = "federated_graph_id" + OrganizationIDClaim = "organization_id" + + PersistedOperationNotFoundErrorCode = "PersistedQueryNotFound" +) + +var ( + persistedOperationKeys = [][]string{ + {"version"}, + {"body"}, + } +) + +const ( + persistedOperationKeyIndexVersion = iota + persistedOperationKeyIndexBody +) + +type PersistentOperationNotFoundError interface { + error + ClientName() string + Sha256Hash() string +} + +type persistentOperationNotFoundError struct { + clientName string + sha256Hash []byte +} + +func (e *persistentOperationNotFoundError) ClientName() string { + return e.clientName +} + +func (e *persistentOperationNotFoundError) Sha256Hash() string { + return string(e.sha256Hash) +} + +func (e *persistentOperationNotFoundError) Error() string { + return fmt.Sprintf("operation %s for client %s not found", unsafebytes.BytesToString(e.sha256Hash), e.clientName) +} + +type cdnPersistedOperationsCache struct { + operations map[string][]byte + mu sync.RWMutex +} + +func (c *cdnPersistedOperationsCache) Get(clientName string, operationHash []byte) []byte { + c.mu.RLock() + defer c.mu.RUnlock() + return c.operations[clientName+unsafebytes.BytesToString(operationHash)] +} + +func (c *cdnPersistedOperationsCache) Set(clientName string, operationHash []byte, operationBody []byte) { + c.mu.Lock() + defer c.mu.Unlock() + if c.operations == nil { + c.operations = make(map[string][]byte) + } + // operationBody might be a subslice of something else, make a copy + data := make([]byte, len(operationBody)) + copy(data, operationBody) + c.operations[clientName+unsafebytes.BytesToString(operationHash)] = data +} + +type CDNOptions struct { + // URL is the root URL of the CDN + URL string + // AuthenticationToken is the token used to authenticate with the CDN, + // usually the same as the GRAPH_API_TOKEN + AuthenticationToken string + HTTPClient *http.Client +} + +type CDN struct { + cdnURL *url.URL + authenticationToken string + // federatedGraphID is the ID of the federated graph that was obtained + // from the token, already url-escaped + federatedGraphID string + // organizationID is the ID of the organization for this graph that was obtained + // from the token, already url-escaped + organizationID string + httpClient *http.Client + operationsCache cdnPersistedOperationsCache +} + +func (cdn *CDN) PersistentOperation(ctx context.Context, clientName string, sha256Hash []byte) ([]byte, error) { + if data := cdn.operationsCache.Get(clientName, sha256Hash); data != nil { + return data, nil + } + operationPath := fmt.Sprintf("/%s/%s/operations/%s/%s.json", + cdn.organizationID, + cdn.federatedGraphID, + url.PathEscape(clientName), + url.PathEscape(unsafebytes.BytesToString(sha256Hash))) + operationURL := cdn.cdnURL.ResolveReference(&url.URL{Path: operationPath}) + req, err := http.NewRequestWithContext(ctx, "GET", operationURL.String(), nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", "Bearer "+cdn.authenticationToken) + resp, err := cdn.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, &persistentOperationNotFoundError{ + clientName: clientName, + sha256Hash: sha256Hash, + } + } + data, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code when loading persisted operation %d: %s", resp.StatusCode, string(data)) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading persisted operation body: %w", err) + } + var operationVersion []byte + var operationBody []byte + jsonparser.EachKey(data, func(idx int, value []byte, vt jsonparser.ValueType, err error) { + switch idx { + case persistedOperationKeyIndexVersion: + operationVersion = value + case persistedOperationKeyIndexBody: + operationBody = value + } + }, persistedOperationKeys...) + if len(operationVersion) != 1 || operationVersion[0] != '1' { + return nil, fmt.Errorf("invalid persisted operation version %q", string(operationVersion)) + } + unscaped, err := jsonparser.Unescape(operationBody, nil) + if err != nil { + return nil, fmt.Errorf("error unescaping persisted operation body: %w", err) + } + cdn.operationsCache.Set(clientName, sha256Hash, unscaped) + return unscaped, nil +} + +func New(opts CDNOptions) (*CDN, error) { + u, err := url.Parse(opts.URL) + if err != nil { + return nil, fmt.Errorf("invalid CDN URL %q: %w", opts.URL, err) + } + // Don't validate the token here, just extract the claims + // that we need to talk to the CDN + jwtParser := new(jwt.Parser) + claims := make(jwt.MapClaims) + var federatedGraphID string + var organizationID string + if opts.AuthenticationToken != "" { + _, _, err = jwtParser.ParseUnverified(opts.AuthenticationToken, claims) + if err != nil { + return nil, fmt.Errorf("invalid CDN authentication token %q: %w", opts.AuthenticationToken, err) + } + federatedGraphIDValue := claims[FederatedGraphIDClaim] + if federatedGraphIDValue == nil { + return nil, fmt.Errorf("invalid CDN authentication token claims, missing %q", FederatedGraphIDClaim) + } + var ok bool + federatedGraphID, ok = federatedGraphIDValue.(string) + if !ok { + return nil, fmt.Errorf("invalid CDN authentication token claims, %q is not a string, it's %T", FederatedGraphIDClaim, federatedGraphIDValue) + } + organizationIDValue := claims[OrganizationIDClaim] + if organizationIDValue == nil { + return nil, fmt.Errorf("invalid CDN authentication token claims, missing %q", OrganizationIDClaim) + } + organizationID, ok = organizationIDValue.(string) + if !ok { + return nil, fmt.Errorf("invalid CDN authentication token claims, %q is not a string, it's %T", OrganizationIDClaim, organizationIDValue) + } + } + httpClient := opts.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + return &CDN{ + cdnURL: u, + authenticationToken: opts.AuthenticationToken, + federatedGraphID: url.PathEscape(federatedGraphID), + organizationID: url.PathEscape(organizationID), + httpClient: httpClient, + }, nil +} diff --git a/router/internal/cdn/doc.go b/router/internal/cdn/doc.go new file mode 100644 index 0000000000..c9dc8ea61d --- /dev/null +++ b/router/internal/cdn/doc.go @@ -0,0 +1,2 @@ +// Package cdn implements primitives to talk to the Cosmo CDN +package cdn diff --git a/router/internal/cdn/persisted_operation.go b/router/internal/cdn/persisted_operation.go new file mode 100644 index 0000000000..467a5c2bec --- /dev/null +++ b/router/internal/cdn/persisted_operation.go @@ -0,0 +1 @@ +package cdn diff --git a/studio/src/components/layout/graph-layout.tsx b/studio/src/components/layout/graph-layout.tsx index da9356a87b..d55e520376 100644 --- a/studio/src/components/layout/graph-layout.tsx +++ b/studio/src/components/layout/graph-layout.tsx @@ -25,6 +25,7 @@ import { addDays, formatDistance } from "date-fns"; import { UserContext } from "../app-provider"; import { calURL } from "@/lib/constants"; import Link from "next/link"; +import { MdDevices } from "react-icons/md"; const icons: { [key: string]: ReactNode } = { Overview: , @@ -34,6 +35,7 @@ const icons: { [key: string]: ReactNode } = { Changelog: , Checks: , Analytics: , + Clients: , }; export interface GraphContextProps { @@ -90,6 +92,11 @@ const GraphLayout = ({ children }: LayoutProps) => { matchExact: false, icon: , }, + { + title: "Clients", + href: basePath + "/clients", + icon: , + }, { title: "Analytics", href: basePath + "/analytics", diff --git a/studio/src/pages/[organizationSlug]/graph/[slug]/clients.tsx b/studio/src/pages/[organizationSlug]/graph/[slug]/clients.tsx new file mode 100644 index 0000000000..24e92737ce --- /dev/null +++ b/studio/src/pages/[organizationSlug]/graph/[slug]/clients.tsx @@ -0,0 +1,215 @@ +import { EmptyState } from "@/components/empty-state"; +import { getGraphLayout } from "@/components/layout/graph-layout"; +import { PageHeader } from "@/components/layout/head"; +import { TitleLayout } from "@/components/layout/title-layout"; +import { Button } from "@/components/ui/button"; +import { CLI } from "@/components/ui/cli"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { docsBaseURL } from "@/lib/constants"; +import { NextPageWithLayout } from "@/lib/page"; +import { cn } from "@/lib/utils"; +import { CommandLineIcon } from "@heroicons/react/24/outline"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { useQuery } from "@tanstack/react-query"; +import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common/common_pb"; +import { getClients } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery"; +import { formatDistanceToNow } from "date-fns"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { BiAnalyse } from "react-icons/bi"; +import { IoBarcodeSharp } from "react-icons/io5"; + +const ClientsPage: NextPageWithLayout = () => { + const router = useRouter(); + const organizationSlug = router.query.organizationSlug as string; + const slug = router.query.slug as string; + + const constructLink = (name: string, mode: "metrics" | "traces") => { + const filters = []; + const value = { + label: name, + value: name, + operator: 0, + }; + + const filter = { + id: "clientName", + value: [JSON.stringify(value)], + }; + filters.push(filter); + + if (mode === "metrics") { + return `/${organizationSlug}/graph/${slug}/analytics?filterState=${JSON.stringify( + filters, + )}`; + } else { + return `/${organizationSlug}/graph/${slug}/analytics/traces?filterState=${JSON.stringify( + filters, + )}`; + } + }; + + const { data, isLoading, error, refetch } = useQuery( + getClients.useQuery({ + fedGraphName: slug, + }), + ); + + if (!data) return null; + + if (!data || error || data.response?.code !== EnumStatusCode.OK) + return ( + } + title="Could not retrieve changelog" + description={ + data?.response?.details || error?.message || "Please try again" + } + actions={} + /> + ); + + return ( +
+ {data.clients.length === 0 ? ( + } + title="Push new operations to the registry using the CLI" + description={ + <> + No clients found. Use the CLI tool to create one.{" "} + + Learn more. + + + } + actions={ + -f `} + /> + } + /> + ) : ( + <> +

+ Registered clients can be created by publishing persisted operations + for them.{" "} + + Learn more + +

+ + + + Name + Created By + Updated By + Created At + Last Push + + + + + {data.clients.map( + ({ + id, + name, + createdAt, + lastUpdatedAt, + createdBy, + lastUpdatedBy, + }) => { + return ( + + +

+ {name} +

+
+ {createdBy} + +

+ {lastUpdatedBy !== "" ? lastUpdatedBy : "-"} +

+
+ + {formatDistanceToNow(new Date(createdAt))} + + + {lastUpdatedAt + ? formatDistanceToNow(new Date(lastUpdatedAt)) + : "Never"} + + + + + + + + + Metrics + + + + + + + + Traces + + +
+ ); + }, + )} +
+
+ + )} +
+ ); +}; + +ClientsPage.getLayout = (page) => + getGraphLayout( + + + {page} + + , + ); + +export default ClientsPage;