Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/protographic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ on:
pull_request:
paths:
- 'pnpm-lock.yaml'
- "protographic/**/*"
- ".github/workflows/protographic.yaml"
- 'protographic/**/*'
- '.github/workflows/protographic.yaml'

concurrency:
group: ${{github.workflow}}-${{github.head_ref}}
Expand All @@ -29,6 +29,10 @@ jobs:
- name: Generate code
run: pnpm generate

# We run linter + formatting and fail CI if uncommitted changes are detected
- name: Lint & format
run: pnpm run --filter ./protographic lint:fix

- uses: ./.github/actions/git-dirty-check
with:
package-name: protographic
Expand All @@ -39,9 +43,6 @@ jobs:
- name: Test
run: pnpm run --filter ./protographic test:coverage

- name: Lint
run: pnpm run --filter ./protographic lint

- name: Upload test results to Codecov
uses: ./.github/actions/codecov-upload-pr
with:
Expand Down
122 changes: 65 additions & 57 deletions protographic/OPERATIONS_TO_PROTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The operations-to-proto compiler generates Protocol Buffer service definitions d
### When to Use Operations-Based Generation

Use operations-based generation when:

- You want to minimize the proto API surface area
- You have a large GraphQL schema but only use a subset of it
- You want proto definitions that exactly match your client operations
Expand All @@ -57,6 +58,7 @@ Use operations-based generation when:
All operations must have a name. The operation name becomes the RPC method name in the generated proto.

**✅ Correct: Named operation**

```graphql
query GetUser($id: ID!) {
user(id: $id) {
Expand All @@ -66,6 +68,7 @@ query GetUser($id: ID!) {
```

**❌ Incorrect: Anonymous operation**

```graphql
query {
user(id: "123") {
Expand All @@ -88,6 +91,7 @@ wgc grpc-service generate MyService \
```

**Generates:**

- Proto messages only for fields used in operations
- Request/response messages per operation
- `service.proto.lock.json` for field number stability
Expand Down Expand Up @@ -133,36 +137,36 @@ wgc grpc-service generate [name] [options]

#### Required Arguments

| Argument | Description |
|----------|-------------|
| `name` | The name of the proto service (e.g., `UserService`) |
| Argument | Description |
| -------- | --------------------------------------------------- |
| `name` | The name of the proto service (e.g., `UserService`) |

#### Required Options

| Option | Description |
|--------|-------------|
| Option | Description |
| -------------------- | ------------------------------- |
| `-i, --input <path>` | Path to the GraphQL schema file |

#### Output Options

| Option | Default | Description |
|--------|---------|-------------|
| `-o, --output <path>` | `.` | Output directory for generated files |
| `-p, --package-name <name>` | `service.v1` | Proto package name |
| Option | Default | Description |
| --------------------------- | ------------ | ------------------------------------ |
| `-o, --output <path>` | `.` | Output directory for generated files |
| `-p, --package-name <name>` | `service.v1` | Proto package name |

#### Operations Mode Options

| Option | Description |
|--------|-------------|
| `-w, --with-operations <path>` | Path to directory containing `.graphql` or `.gql` operation files. Subdirectories are traversed recursively. Enables operations-based generation. |
| `--prefix-operation-type` | Prefix RPC method names with operation type (Query/Mutation/Subscription) |
| `--custom-scalar-mapping <json>` | Custom scalar type mappings as inline JSON string. Example: `'{"DateTime":"google.protobuf.Timestamp","UUID":"string"}'` |
| `--custom-scalar-mapping-file <path>` | Path to JSON file containing custom scalar type mappings. Example: `./mappings.json` |
| Option | Description |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-w, --with-operations <path>` | Path to directory containing `.graphql` or `.gql` operation files. Subdirectories are traversed recursively. Enables operations-based generation. |
| `--prefix-operation-type` | Prefix RPC method names with operation type (Query/Mutation/Subscription) |
| `--custom-scalar-mapping <json>` | Custom scalar type mappings as inline JSON string. Example: `'{"DateTime":"google.protobuf.Timestamp","UUID":"string"}'` |
| `--custom-scalar-mapping-file <path>` | Path to JSON file containing custom scalar type mappings. Example: `./mappings.json` |

#### Language-Specific Options

| Option | Description |
|--------|-------------|
| Option | Description |
| ------------------------- | ------------------------------------------ |
| `-g, --go-package <name>` | Adds `option go_package` to the proto file |

### Examples
Expand Down Expand Up @@ -216,7 +220,6 @@ wgc grpc-service generate UserService \
--go-package github.com/myorg/myapp/proto/user/v1
```


---

## API Reference
Expand All @@ -229,25 +232,25 @@ Compiles GraphQL operations to Protocol Buffer definitions.
function compileOperationsToProto(
operationSource: string | DocumentNode,
schemaOrSDL: GraphQLSchema | string,
options?: OperationsToProtoOptions
): CompileOperationsToProtoResult
options?: OperationsToProtoOptions,
): CompileOperationsToProtoResult;
```

#### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `operationSource` | `string \| DocumentNode` | GraphQL operations as a string or parsed DocumentNode |
| `schemaOrSDL` | `GraphQLSchema \| string` | GraphQL schema or SDL string |
| `options` | `OperationsToProtoOptions` | Optional configuration |
| Parameter | Type | Description |
| ----------------- | -------------------------- | ----------------------------------------------------- |
| `operationSource` | `string \| DocumentNode` | GraphQL operations as a string or parsed DocumentNode |
| `schemaOrSDL` | `GraphQLSchema \| string` | GraphQL schema or SDL string |
| `options` | `OperationsToProtoOptions` | Optional configuration |

#### Returns

```typescript
interface CompileOperationsToProtoResult {
proto: string; // Generated proto text
root: protobuf.Root; // Protobufjs AST root
lockData: ProtoLock; // Lock data for field stability
proto: string; // Generated proto text
root: protobuf.Root; // Protobufjs AST root
lockData: ProtoLock; // Lock data for field stability
}
```

Expand All @@ -256,9 +259,9 @@ interface CompileOperationsToProtoResult {
```typescript
interface OperationsToProtoOptions {
// Service Configuration
serviceName?: string; // Default: "DefaultService"
packageName?: string; // Default: "service.v1"
serviceName?: string; // Default: "DefaultService"
packageName?: string; // Default: "service.v1"

// Language Options
goPackage?: string;
javaPackage?: string;
Expand All @@ -270,15 +273,15 @@ interface OperationsToProtoOptions {
phpMetadataNamespace?: string;
objcClassPrefix?: string;
swiftPrefix?: string;

// Generation Options
includeComments?: boolean; // Default: true
prefixOperationType?: boolean; // Default: false
queryIdempotency?: 'NO_SIDE_EFFECTS' | 'DEFAULT'; // Optional
maxDepth?: number; // Default: 50
includeComments?: boolean; // Default: true
prefixOperationType?: boolean; // Default: false
queryIdempotency?: 'NO_SIDE_EFFECTS' | 'DEFAULT'; // Optional
maxDepth?: number; // Default: 50

// Field Stability
lockData?: ProtoLock; // Previous lock data
lockData?: ProtoLock; // Previous lock data
}
```

Expand All @@ -296,11 +299,11 @@ const result = compileOperationsToProto(operations, schema, {
packageName: 'myorg.user.v1',
goPackage: 'github.com/myorg/myapp/proto/user/v1',
prefixOperationType: true,
queryIdempotency: 'NO_SIDE_EFFECTS', // All queries are marked as idempotent
queryIdempotency: 'NO_SIDE_EFFECTS', // All queries are marked as idempotent
includeComments: true,
customScalarMappings: {
'DateTime': 'google.protobuf.Timestamp',
'UUID': 'string'
DateTime: 'google.protobuf.Timestamp',
UUID: 'string',
},
});

Expand Down Expand Up @@ -426,9 +429,9 @@ option go_package = "github.com/myorg/myapp/proto/user/v1";

service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}

rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {}

rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
}

Expand All @@ -442,7 +445,7 @@ message GetUserResponse {
string name = 2;
google.protobuf.StringValue email = 3;
}

User user = 1;
}

Expand Down Expand Up @@ -539,7 +542,7 @@ query GetUser($id: ID!) {
id
name
email
age # New field
age # New field
}
}
```
Expand All @@ -550,20 +553,19 @@ Regenerate - the lock file preserves existing field numbers and assigns the next

## Advanced Topics


### Custom Scalar Mappings

GraphQL custom scalars can be mapped to proto types using either inline JSON or a separate configuration file.

#### Common Scalar Mappings

| GraphQL Scalar | Recommended Proto Type |
|----------------|----------------------|
| `DateTime` | `google.protobuf.Timestamp` |
| `Date` | `google.protobuf.Timestamp` |
| `JSON` | `google.protobuf.Struct` |
| `UUID` | `string` |
| `BigInt` | `int64` |
| GraphQL Scalar | Recommended Proto Type |
| -------------- | --------------------------- |
| `DateTime` | `google.protobuf.Timestamp` |
| `Date` | `google.protobuf.Timestamp` |
| `JSON` | `google.protobuf.Struct` |
| `UUID` | `string` |
| `BigInt` | `int64` |

#### Using Inline JSON

Expand All @@ -582,6 +584,7 @@ wgc grpc-service generate UserService \
Create a JSON file with your scalar mappings:

**scalar-mappings.json:**

```json
{
"DateTime": "google.protobuf.Timestamp",
Expand Down Expand Up @@ -611,10 +614,10 @@ When using the API directly, pass the mappings as an object:
```typescript
const result = compileOperationsToProto(operations, schema, {
customScalarMappings: {
'DateTime': 'google.protobuf.Timestamp',
'UUID': 'string',
'JSON': 'google.protobuf.Struct'
}
DateTime: 'google.protobuf.Timestamp',
UUID: 'string',
JSON: 'google.protobuf.Struct',
},
});
```

Expand Down Expand Up @@ -655,11 +658,13 @@ The lock file maintains field number stability across generations.
#### No Operation Files Found

**Error:**

```text
No GraphQL operation files (.graphql, .gql) found in ./operations
```

**Solution:**

- Ensure your operation files have `.graphql`, `.gql`, `.graphqls`, or `.gqls` extensions
- Check the path to your operations directory
- Verify files contain valid GraphQL operations
Expand All @@ -668,6 +673,7 @@ No GraphQL operation files (.graphql, .gql) found in ./operations
#### Anonymous Operations Not Supported

**Error:**

```text
Operations must be named
```
Expand All @@ -694,11 +700,13 @@ query GetUser {
#### Field Number Conflicts

**Error:**

```text
Field number conflict in message X
```

**Solution:**

- Delete the lock file and regenerate (breaking change)
- Or manually resolve conflicts in the lock file (advanced)

Expand Down
17 changes: 9 additions & 8 deletions protographic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const protoOutput = compileGraphQLToProto(
packageName: 'user.v1', // Package name
goPackage: 'cosmo/pkg/my_package', // Go package name
lockFilePath: './proto.lock.json', // Optional: Path to proto.lock.json for deterministic field ordering
}
},
);
```

Expand All @@ -68,21 +68,21 @@ import { compileGraphQLToProto } from '@wundergraph/protographic';
// First generation with a new lock file
const result1 = compileGraphQLToProto(initialSchema, {
serviceName: 'MyService',
lockFilePath: './proto.lock.json' // Creates lock file if it doesn't exist
lockFilePath: './proto.lock.json', // Creates lock file if it doesn't exist
});

// Later generation with schema changes but preserving field order
const result2 = compileGraphQLToProto(updatedSchema, {
serviceName: 'MyService',
lockFilePath: './proto.lock.json' // Uses existing lock file
lockFilePath: './proto.lock.json', // Uses existing lock file
});
```

When providing a `lockFilePath`, the function returns an object with both the proto definition and the lock data:

```typescript
const { proto, lockData } = compileGraphQLToProto(schema, {
lockFilePath: './proto.lock.json'
const { proto, lockData } = compileGraphQLToProto(schema, {
lockFilePath: './proto.lock.json',
});
```

Expand All @@ -93,7 +93,7 @@ import { compileGraphQLToProto, ProtoLock } from '@wundergraph/protographic';

// First generation - creates initial lock data
const result1 = compileGraphQLToProto(initialSchema, {
serviceName: 'MyService'
serviceName: 'MyService',
});
const proto1 = result1.proto;
const lockData = result1.lockData;
Expand All @@ -104,11 +104,12 @@ const lockData = result1.lockData;
// Later generation with the saved lock data
const result2 = compileGraphQLToProto(updatedSchema, {
serviceName: 'MyService',
lockData: lockData // Use previously generated lock data
lockData: lockData, // Use previously generated lock data
});
```

The lock data records the order of:

- Service methods
- Message fields
- Enum values
Expand Down Expand Up @@ -141,7 +142,7 @@ query GetUser($userId: ID!) {
const result = compileOperationsToProto(operation, schema, {
serviceName: 'UserService',
packageName: 'user.v1',
prefixOperationType: true
prefixOperationType: true,
});
```

Expand Down
Loading
Loading