chore: improve development environment seeding (#7162)

Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
This commit is contained in:
jdolle 2025-10-27 02:23:56 -07:00 committed by GitHub
parent 8ab8421ea5
commit 2f1051eed0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 615 additions and 482 deletions

View file

@ -111,19 +111,19 @@ We have a script to feed your local instance of Hive with initial seed data. Thi
1. Use `Start Hive` to run your local Hive instance
2. Make sure `usage` and `usage-ingestor` are running as well (with `pnpm dev`)
3. Open Hive app, create a project and a target, then create a token
4. Run the seed script: `TOKEN="MY_TOKEN_HERE" pnpm seed`
5. This should report a dummy schema and some dummy usage data to your local instance of Hive,
allowing you to test features e2e
4. Run the seed script: `FEDERATION=<0|1> TOKEN=<access_token> TARGET=<target_id> pnpm seed:schemas`
5. This should report a dummy schema
6. Run the usage seed to generate some dummy usage data to your local instance of Hive, allowing you
to test features e2e: `FEDERATION=<0|1> TOKEN=<access_token> TARGET=<target_id> pnpm seed:usage`
> Note: You can set `STAGING=1` in order to target staging env and seed a target there. Same for
> development env, you can use `DEV=1`
> Note: You can set `STAGE=<dev|staging|local>` in order to target a specific Hive environment and
> seed a target there.
> Note: You can set `FEDERATION=1` in order to publish multiple subgraphs.
> To send more operations and test heavy load on Hive instance, you can also set `OPERATIONS`
> (amount of operations in each interval round, default is `1`) and `INTERVAL` (frequency of sending
> operations, default: `1000`ms). For example, using `INTERVAL=1000 OPERATIONS=1000` will send 1000
> requests per second.
> To send more operations with `seed:usage`, and test heavy load on Hive instance, you can also set
> `OPERATIONS` (amount of operations in each interval round, default is `10`) and `INTERVAL`
> (frequency of sending operations, default: `1000`ms). For example, using
> `INTERVAL=1000 OPERATIONS=1000` will send 1000 requests per second. And set `BATCHES` to set the
> total number of batches to run before the seed exits. Default: 10.
### Troubleshooting

View file

@ -43,7 +43,8 @@
"prettier": "prettier --cache --write --list-different --ignore-unknown \"**/*\"",
"release": "pnpm build:libraries && changeset publish",
"release:version": "changeset version && pnpm --filter hive-apollo-router-plugin sync-cargo-file && pnpm build:libraries && pnpm --filter @graphql-hive/cli oclif:readme",
"seed": "tsx scripts/seed-local-env.ts",
"seed:schemas": "tsx scripts/seed-schemas.ts",
"seed:usage": "tsx scripts/seed-usage.ts",
"start": "pnpm run local:setup",
"test": "vitest",
"test:e2e": "CYPRESS_BASE_URL=$HIVE_APP_BASE_URL cypress run --browser chrome",
@ -69,6 +70,7 @@
"@graphql-codegen/urql-introspection": "3.0.1",
"@graphql-eslint/eslint-plugin": "3.20.1",
"@graphql-inspector/cli": "4.0.3",
"@graphql-tools/load": "8.1.2",
"@manypkg/get-packages": "2.2.2",
"@next/eslint-plugin-next": "14.2.23",
"@parcel/watcher": "2.5.1",

View file

@ -122,6 +122,9 @@ importers:
'@graphql-inspector/cli':
specifier: 4.0.3
version: 4.0.3(@types/node@22.10.5)(encoding@0.1.13)(graphql@16.9.0)
'@graphql-tools/load':
specifier: 8.1.2
version: 8.1.2(graphql@16.9.0)
'@manypkg/get-packages':
specifier: 2.2.2
version: 2.2.2
@ -1424,7 +1427,7 @@ importers:
devDependencies:
'@graphql-inspector/core':
specifier: 5.1.0-alpha-20231208113249-34700c8a
version: 5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0)
version: 5.1.0-alpha-20231208113249-34700c8a(graphql@16.11.0)
'@hive/service-common':
specifier: workspace:*
version: link:../service-common
@ -3663,6 +3666,7 @@ packages:
'@fastify/vite@6.0.7':
resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==}
bundledDependencies: []
'@floating-ui/core@1.2.6':
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
@ -17594,8 +17598,8 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.596.0
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/middleware-host-header': 3.577.0
@ -17702,11 +17706,11 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-sso-oidc@3.596.0':
'@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/middleware-host-header': 3.577.0
@ -17745,6 +17749,7 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/client-sso-oidc@3.723.0(@aws-sdk/client-sts@3.723.0)':
@ -17878,11 +17883,11 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)':
'@aws-sdk/client-sts@3.596.0':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.596.0
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/middleware-host-header': 3.577.0
@ -17921,7 +17926,6 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
'@aws-sdk/client-sts@3.723.0':
@ -18035,7 +18039,7 @@ snapshots:
'@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)':
dependencies:
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/credential-provider-env': 3.587.0
'@aws-sdk/credential-provider-http': 3.596.0
'@aws-sdk/credential-provider-process': 3.587.0
@ -18154,7 +18158,7 @@ snapshots:
'@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)':
dependencies:
'@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)
'@aws-sdk/client-sts': 3.596.0
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.1.11
'@smithy/types': 3.7.2
@ -18329,7 +18333,7 @@ snapshots:
'@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)':
dependencies:
'@aws-sdk/client-sso-oidc': 3.596.0
'@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0)
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
@ -19961,6 +19965,13 @@ snapshots:
object-inspect: 1.12.3
tslib: 2.6.2
'@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@16.11.0)':
dependencies:
dependency-graph: 0.11.0
graphql: 16.11.0
object-inspect: 1.12.3
tslib: 2.6.2
'@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0)':
dependencies:
dependency-graph: 0.11.0

View file

@ -1,459 +0,0 @@
import { buildSchema, parse } from 'graphql';
import { createHive } from '@graphql-hive/core';
const isFederation = process.env.FEDERATION === '1';
const isSchemaReportingEnabled = process.env.SCHEMA_REPORTING !== '0';
const isUsageReportingEnabled = process.env.USAGE_REPORTING !== '0';
const envName = process.env.STAGING ? 'staging' : process.env.DEV ? 'dev' : 'local';
const schemaReportingEndpoint =
envName === 'staging'
? 'https://app.staging.graphql-hive.com/registry'
: envName === 'dev'
? 'https://app.dev.graphql-hive.com/registry'
: 'http://localhost:3001/graphql';
const usageReportingEndpoint =
envName === 'staging'
? 'https://app.staging.graphql-hive.com/usage'
: envName === 'dev'
? 'https://app.dev.graphql-hive.com/usage'
: 'http://localhost:4001';
const target = process.env.TARGET;
console.log(`
Environment: ${envName}
Schema reporting endpoint: ${schemaReportingEndpoint}
Usage reporting endpoint: ${usageReportingEndpoint}
Schema reporting: ${isSchemaReportingEnabled ? 'enabled' : 'disabled'}
Usage reporting: ${isUsageReportingEnabled ? 'enabled' : 'disabled'}
`);
const createInstance = (
service: null | {
name: string;
url: string;
},
) => {
return createHive({
token: process.env.TOKEN!,
agent: {
name: 'Hive Seed Script',
maxSize: 10,
},
debug: true,
enabled: true,
reporting: isSchemaReportingEnabled
? {
endpoint: schemaReportingEndpoint,
author: 'Hive Seed Script',
commit: '1',
serviceName: service?.name,
serviceUrl: service?.url,
}
: false,
usage: isUsageReportingEnabled
? {
target: target || undefined,
clientInfo: () => ({
name: 'Fake Hive Client',
version: '1.1.1',
}),
endpoint: usageReportingEndpoint,
max: 10,
sampleRate: 1,
}
: false,
});
};
async function single() {
const hiveInstance = createInstance(null);
await hiveInstance.info();
const schema = buildSchema(/* GraphQL */ `
type Query {
field(arg: String): String
nested: NestedQuery!
}
type NestedQuery {
test: String
}
`);
const query1 = parse(/* GraphQL */ `
query test {
field
withArg: field(arg: "test")
nested {
test
}
}
`);
const query2 = parse(/* GraphQL */ `
query testAnother {
field
}
`);
hiveInstance.reportSchema({ schema });
const operationsPerBatch = process.env.OPERATIONS ? parseInt(process.env.OPERATIONS) : 1;
setInterval(
() => {
for (let i = 0; i < operationsPerBatch; i++) {
const randNumber = Math.random() * 100;
console.log('Reporting usage query...');
const done = hiveInstance.collectUsage();
done(
{
document: randNumber > 50 ? query1 : query2,
schema,
variableValues: {},
contextValue: {},
},
randNumber > 90
? {
errors: undefined,
}
: {
errors: [{ message: 'oops' }],
},
);
}
},
process.env.INTERVAL ? parseInt(process.env.INTERVAL) : 1000,
);
}
const publishMutationDocument =
/* GraphQL */
`
mutation schemaPublish($input: SchemaPublishInput!) {
schemaPublish(input: $input) {
__typename
... on SchemaPublishSuccess {
initial
valid
message
linkToWebsite
changes {
nodes {
message
criticality
}
total
}
}
... on SchemaPublishError {
valid
linkToWebsite
changes {
nodes {
message
criticality
}
total
}
errors {
nodes {
message
}
total
}
}
}
}
`;
async function federation() {
const instance = createInstance(null);
const schemaInventory = /* GraphQL */ `
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
import: ["@key", "@shareable", "@external", "@requires"]
)
type Product implements ProductItf @key(fields: "id") {
id: ID!
dimensions: ProductDimension @external
delivery(zip: String): DeliveryEstimates @requires(fields: "dimensions { size weight }")
}
type ProductDimension @shareable {
size: String
weight: Float
}
type DeliveryEstimates {
estimatedDelivery: String
fastestDelivery: String
}
interface ProductItf {
id: ID!
dimensions: ProductDimension
delivery(zip: String): DeliveryEstimates
}
enum ShippingClass {
STANDARD
EXPRESS
OVERNIGHT
}
`;
const schemaPandas = /* GraphQL */ `
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@tag"])
directive @tag(name: String!) repeatable on FIELD_DEFINITION
type Query {
allPandas: [Panda]
panda(name: ID!): Panda
}
type Panda {
name: ID!
favoriteFood: String @tag(name: "nom-nom-nom")
}
`;
const schemaProducts = /* GraphQL */ `
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
import: ["@key", "@shareable", "@inaccessible", "@tag"]
)
directive @myDirective(a: String!) on FIELD_DEFINITION
directive @hello on FIELD_DEFINITION
type Query {
allProducts: [ProductItf]
product(id: ID!): ProductItf
}
interface ProductItf implements SkuItf {
id: ID!
sku: String
name: String
package: String
variation: ProductVariation
dimensions: ProductDimension
createdBy: User
hidden: String @inaccessible
oldField: String @deprecated(reason: "refactored out")
}
interface SkuItf {
sku: String
}
type Product implements ProductItf & SkuItf
@key(fields: "id")
@key(fields: "sku package")
@key(fields: "sku variation { id }") {
id: ID! @tag(name: "hi-from-products")
sku: String
name: String @hello
package: String
variation: ProductVariation
dimensions: ProductDimension
createdBy: User
hidden: String
reviewsScore: Float!
oldField: String
}
enum ShippingClass {
STANDARD
EXPRESS
}
type ProductVariation {
id: ID!
name: String
}
type ProductDimension @shareable {
size: String
weight: Float
}
type User @key(fields: "email") {
email: ID!
totalProductsCreated: Int @shareable
}
`;
const schemaReviews = /* GraphQL */ `
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
import: ["@key", "@shareable", "@override"]
)
type Product implements ProductItf @key(fields: "id") {
id: ID!
reviewsCount: Int!
reviewsScore: Float! @shareable @override(from: "products")
reviews: [Review!]!
}
interface ProductItf {
id: ID!
reviewsCount: Int!
reviewsScore: Float!
reviews: [Review!]!
}
type Query {
review(id: Int!): Review
}
type Review {
id: Int!
body: String!
}
`;
const schemaUsers = /* GraphQL */ `
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@tag", "@shareable"])
directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT
type User @key(fields: "email") {
email: ID! @tag(name: "test-from-users")
name: String
totalProductsCreated: Int @shareable
createdAt: DateTime
}
scalar DateTime
`;
let res = await fetch(schemaReportingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TOKEN}`,
},
body: JSON.stringify({
query: publishMutationDocument,
variables: {
input: {
author: 'MoneyBoy',
commit: '1977',
sdl: schemaInventory,
service: 'Inventory',
url: 'https://inventory.localhost/graphql',
target: target ? { byId: target } : null,
},
},
}),
}).then(res => res.json());
console.log(JSON.stringify(res, null, 2));
res = await fetch(schemaReportingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TOKEN}`,
},
body: JSON.stringify({
query: publishMutationDocument,
variables: {
input: {
author: 'MoneyBoy',
commit: '1977',
sdl: schemaPandas,
service: 'Panda',
url: 'https://panda.localhost/graphql',
target: target ? { byId: target } : null,
},
},
}),
}).then(res => res.json());
console.log(JSON.stringify(res, null, 2));
res = await fetch(schemaReportingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TOKEN}`,
},
body: JSON.stringify({
query: publishMutationDocument,
variables: {
input: {
author: 'MoneyBoy',
commit: '1977',
sdl: schemaProducts,
service: 'Products',
url: 'https://products.localhost/graphql',
target: target ? { byId: target } : null,
},
},
}),
}).then(res => res.json());
console.log(JSON.stringify(res, null, 2));
res = await fetch(schemaReportingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TOKEN}`,
},
body: JSON.stringify({
query: publishMutationDocument,
variables: {
input: {
author: 'MoneyBoy',
commit: '1977',
sdl: schemaReviews,
service: 'Reviews',
url: 'https://reviews.localhost/graphql',
target: target ? { byId: target } : null,
},
},
}),
}).then(res => res.json());
console.log(JSON.stringify(res, null, 2));
res = await fetch(schemaReportingEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.TOKEN}`,
},
body: JSON.stringify({
query: publishMutationDocument,
variables: {
input: {
author: 'MoneyBoy',
commit: '1977',
sdl: schemaUsers,
service: 'Users',
url: 'https://users.localhost/graphql',
target: target ? { byId: target } : null,
},
},
}),
}).then(res => res.json());
console.log(JSON.stringify(res, null, 2));
await instance.info();
}
if (isFederation === false) {
await single();
} else {
await federation();
}

168
scripts/seed-schemas.ts Normal file
View file

@ -0,0 +1,168 @@
/**
* Example:
* `TARGET=<target_id> FEDERATION=1 STAGE=local TOKEN=<access_token> pnpm seed:schemas`
*/
import { parse as parsePath } from 'path';
import { printSchema } from 'graphql';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { loadSchema, loadTypedefs } from '@graphql-tools/load';
const token = process.env.TOKEN || process.env.HIVE_TOKEN;
if (!token) {
throw new Error('Missing "TOKEN"');
}
const target = process.env.TARGET;
const isFederation = process.env.FEDERATION === '1';
const stage = process.env.STAGE || 'local';
let graphqlEndpoint: string;
let environment: string;
switch (stage.toLowerCase()) {
case 'staging': {
graphqlEndpoint = 'https://app.hiveready.dev/graphql';
environment = 'staging';
break;
}
case 'dev': {
graphqlEndpoint = 'https://app.buzzcheck.dev/graphql';
environment = 'dev';
break;
}
default: {
graphqlEndpoint = 'http://localhost:3001/graphql';
environment = 'local';
}
}
console.log(`
Environment: ${environment}
Hive GraphQL endpoint: ${graphqlEndpoint}
Schema type: ${isFederation ? 'federation' : 'single'}
`);
const publishMutationDocument =
/* GraphQL */
`
mutation schemaPublish($input: SchemaPublishInput!) {
schemaPublish(input: $input) {
__typename
... on SchemaPublishSuccess {
initial
valid
message
linkToWebsite
changes {
nodes {
message
criticality
}
total
}
}
... on SchemaPublishError {
valid
linkToWebsite
changes {
nodes {
message
criticality
}
total
}
errors {
nodes {
message
}
total
}
}
}
}
`;
async function publishSchema(args: { sdl: string; service?: string; target?: string }) {
const response = await fetch(graphqlEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: publishMutationDocument,
variables: {
input: {
author: 'MoneyBoy',
commit: '1977',
sdl: args.sdl,
service: args.service,
url: `https://${args.service ? `${args.service}.` : ''}localhost/graphql`,
target: args.target ? { byId: args.target } : null,
},
},
}),
}).then(res => res.json());
return response as {
data: {
schemaPublish: {
valid: boolean;
} | null;
} | null;
errors?: any[];
};
}
async function single() {
const schema = await loadSchema('scripts/seed-schemas/mono.graphql', {
loaders: [new GraphQLFileLoader()],
});
const sdl = printSchema(schema);
const result = await publishSchema({
sdl,
target,
});
if (result?.errors || result?.data?.schemaPublish?.valid !== true) {
console.error(`Published schema is invalid.`);
} else {
console.log(`Published successfully.`);
}
return result;
}
async function federation() {
const schemaDocs = await loadTypedefs('scripts/seed-schemas/federated/*.graphql', {
loaders: [new GraphQLFileLoader()],
});
const uploads = schemaDocs
.map(async d => {
if (!d.rawSDL) {
console.error(`Missing SDL at "${d.location}"`);
return null;
}
const service = d.location ? parsePath(d.location).name.replaceAll('.', '-') : undefined;
const result = await publishSchema({
sdl: d.rawSDL,
service,
target,
});
if (result?.errors || result?.data?.schemaPublish?.valid !== true) {
console.error(`Published schema is invalid for "${service}".`);
} else {
console.log(`Published "${service}" successfully.`);
}
return result;
})
.filter(Boolean);
return Promise.all(uploads);
}
if (isFederation === false) {
await single();
} else {
await federation();
}

View file

@ -0,0 +1 @@
Each file in this directory is treated as a separate subgraph in the Federated seed.

View file

@ -0,0 +1,27 @@
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
import: ["@key", "@shareable", "@external", "@requires"]
)
type Product @key(fields: "upc dimensions") {
upc: String!
dimensions: ProductDimension @external
delivery(zip: String): DeliveryEstimates @requires(fields: "dimensions { size weight }")
}
type ProductDimension @shareable {
size: String
weight: Float
}
type DeliveryEstimates {
estimatedDelivery: String
fastestDelivery: String
}
enum ShippingClass {
STANDARD
EXPRESS
OVERNIGHT
}

View file

@ -0,0 +1,63 @@
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
import: ["@key", "@shareable", "@inaccessible", "@tag", "@external"]
)
directive @example(text: String!) on FIELD_DEFINITION
type Query {
allProducts: [ProductItf]
product(upc: String!): ProductItf
topProducts(first: Int = 5): [ProductItf]
}
interface ProductItf implements SkuItf {
upc: String!
sku: String
name: String
package: String
variations: [ProductItf]
dimensions: ProductDimension
createdBy: User
hidden: String @inaccessible
oldField: String @deprecated(reason: "refactored out")
}
interface SkuItf {
sku: String
}
type Product implements ProductItf & SkuItf @key(fields: "upc") @key(fields: "sku package") {
"""
Universal Product Code. A standardized numeric global identifier
"""
upc: String!
"""
SKUs are unique to the company and are used internally. Alphanumeric.
"""
sku: String @tag(name: "internal")
name: String @example(text: "Foo Bar")
package: String
variations: [ProductItf]
dimensions: ProductDimension
createdBy: User
hidden: String
reviewsScore: Float!
oldField: String
}
enum ShippingClass {
STANDARD
EXPRESS
}
type ProductDimension @shareable {
size: String
weight: Float
}
type User @key(fields: "id") {
id: ID!
totalProductsCreated: Int @shareable
}

View file

@ -0,0 +1,28 @@
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.3"
import: ["@key", "@override", "@external", "@provides"]
)
type Query {
review(id: Int!): Review
}
type Review @key(fields: "id") {
id: ID!
body: String
author: User
product: Product
}
extend type User @key(fields: "id") {
id: ID!
reviews: [Review]
}
extend type Product @key(fields: "upc") {
upc: String!
reviews: [Review]
reviewsCount: Int!
reviewsScore: Float! @override(from: "products")
}

View file

@ -0,0 +1,20 @@
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@tag", "@shareable"])
directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT
type User @key(fields: "id") @key(fields: "email") {
id: ID!
email: String! @tag(name: "pii")
name: String
alias: String
totalProductsCreated: Int @shareable
createdAt: DateTime
}
scalar DateTime
extend type Query {
me: User
user(id: ID!): User
users: [User]
}

View file

@ -0,0 +1,72 @@
interface Node {
id: ID!
}
interface Character implements Node {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
type Human implements Character & Node {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character & Node {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
enum LengthUnit {
METER
LIGHT_YEAR
}
enum Episode {
"Star Wars Episode IV: A New Hope, released in 1977."
NEWHOPE
"Star Wars Episode V: The Empire Strikes Back, released in 1980."
EMPIRE
"Star Wars Episode VI: Return of the Jedi, released in 1983."
JEDI
}
"""
The query type, represents all of the entry points into our object graph
"""
type Query {
"""
Fetches the hero of a specified Star Wars film.
"""
hero("The name of the film that the hero appears in." episode: Episode): Character
}
type Review {
episode: Episode
stars: Int!
commentary: String
}
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode, review: ReviewInput!): Review
}

175
scripts/seed-usage.ts Normal file
View file

@ -0,0 +1,175 @@
/**
* Example:
* `TARGET=<target_id> FEDERATION=1 STAGE=local TOKEN=<access_token> pnpm seed:usage`
*/
import { parse as parsePath } from 'path';
import { buildSchema, DocumentNode, GraphQLSchema, parse, print } from 'graphql';
import { createHive, HiveClient } from '@graphql-hive/core';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { loadDocuments, loadSchema, loadTypedefs } from '@graphql-tools/load';
import {
composeServices,
ServiceDefinition,
transformSupergraphToPublicSchema,
} from '@theguild/federation-composition';
const token = process.env.TOKEN || process.env.HIVE_TOKEN;
if (!token) {
throw new Error('Missing "TOKEN"');
}
const target = process.env.TARGET;
const isFederation = process.env.FEDERATION === '1';
const stage = process.env.STAGE || 'local';
const batches = parseInt(process.env.BATCHES || '10', 10) || 10;
const operationsPerBatch = parseInt(process.env.OPERATIONS || '10', 10) || 10;
const interval = parseInt(process.env.INTERVAL || '1000') || 1000;
let usageReportingEndpoint: string;
let environment: string;
switch (stage.toLowerCase()) {
case 'staging': {
usageReportingEndpoint = 'https://app.hiveready.dev/usage';
environment = 'staging';
break;
}
case 'dev': {
usageReportingEndpoint = 'https://app.buzzcheck.dev/usage';
environment = 'dev';
break;
}
default: {
usageReportingEndpoint = 'http://localhost:4001';
environment = 'local';
}
}
console.log(`
Environment: ${environment}
Usage reporting endpoint: ${usageReportingEndpoint}
`);
const createInstance = () => {
return createHive({
token,
agent: {
name: 'Hive Seed Script',
maxSize: 10,
},
debug: true,
enabled: true,
usage: {
target: target || undefined,
clientInfo: () => ({
// @todo create multiple clients with different versions and/or names
name: 'Fake Hive Client',
version: '1.1.1',
}),
endpoint: usageReportingEndpoint,
max: 10,
sampleRate: 1,
},
});
};
/**
* Not very precise, but provides enough randomization to weight the queries reasonably well.
*/
const chooseQuery = (queries: DocumentNode[]) => {
for (let i = 0; i < queries.length; i++) {
const randNumber = Math.random() * 100;
if (randNumber <= 25) {
return queries[i];
}
}
return queries[queries.length - 1];
};
function start(args: { instance: HiveClient; schema: GraphQLSchema; queries: DocumentNode[] }) {
let sentBatches = 0;
let intervalId: NodeJS.Timeout | null = setInterval(async () => {
if (sentBatches >= batches) {
console.log('Done.');
if (!!intervalId) {
clearInterval(intervalId);
await args.instance.dispose();
}
intervalId = null;
return;
}
sentBatches++;
for (let i = 0; i < operationsPerBatch; i++) {
const randNumber = Math.random() * 100;
const done = args.instance.collectUsage();
await done(
{
document: chooseQuery(args.queries),
schema: args.schema,
variableValues: {},
contextValue: {},
},
randNumber > 95
? {
errors: undefined,
}
: {
errors: [{ message: 'oops' }],
},
);
}
}, interval);
}
const instance = createInstance();
await instance.info();
if (isFederation === false) {
const schema = await loadSchema('scripts/seed-schemas/mono.graphql', {
loaders: [new GraphQLFileLoader()],
});
const documents = await loadDocuments('scripts/seed-usage/mono/*.graphql', {
loaders: [new GraphQLFileLoader()],
});
const queries = documents.map(({ document }) => {
if (!document) {
throw new Error('Unexpected error. Could not find document.');
}
return document;
});
start({ instance, schema, queries });
} else {
const schemaDocs = await loadTypedefs('scripts/seed-schemas/federated/*.graphql', {
loaders: [new GraphQLFileLoader()],
});
const services = schemaDocs.map((d): ServiceDefinition => {
if (!d.rawSDL) {
throw new Error(`Missing SDL at "${d.location}"`);
}
if (!d.location) {
throw new Error(`Unexpected error. Missing "location".`);
}
const service = parsePath(d.location).name.replaceAll('.', '-');
return {
typeDefs: parse(d.rawSDL),
name: service,
url: `https://${service ? `${service}.` : ''}localhost/graphql`,
};
});
const { supergraphSdl, errors } = composeServices(services);
if (errors) {
throw new Error(`Could not compose:\n - ${errors.map(e => e.message).join('\n - ')}`);
}
const apiSchema = print(transformSupergraphToPublicSchema(parse(supergraphSdl)));
const documents = await loadDocuments('scripts/seed-usage/federated/*.graphql', {
loaders: [new GraphQLFileLoader()],
});
const queries = documents.map(({ document }) => {
if (!document) {
throw new Error('Unexpected error. Could not find document.');
}
return document;
});
start({ instance, schema: buildSchema(apiSchema), queries });
}

View file

@ -0,0 +1,4 @@
seed-usage.ts will randomly select operations from these files to send as usage metrics.
Technically, these operations dont need to be valid against your schema(s). But they are separated
into folders to create more realistic usage examples.

View file

@ -0,0 +1,11 @@
query AllProducts {
allProducts {
id
sku
name
variation {
id
name
}
}
}

View file

@ -0,0 +1,5 @@
query FirstReview {
review(id: 1) {
body
}
}

View file

@ -0,0 +1,5 @@
query NewHopeHero {
hero(episode: NEWHOPE) {
name
}
}