Skip to content

Commit 458bdfd

Browse files
authored
Merge pull request #340 from objectstack-ai/copilot/fix-job-step-error
2 parents d744a7e + 051b5c8 commit 458bdfd

2 files changed

Lines changed: 119 additions & 38 deletions

File tree

packages/protocols/graphql/src/index.ts

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { RuntimePlugin, RuntimeContext } from '@objectql/types';
1212
import { ApolloServer, HeaderMap } from '@apollo/server';
1313
import { expressMiddleware } from '@apollo/server/express4';
1414
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
15+
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';
1516
import { buildSubgraphSchema } from '@apollo/subgraph';
1617
import { gql } from 'graphql-tag';
1718
import { createServer } from 'http';
@@ -35,6 +36,8 @@ export interface GraphQLPluginConfig {
3536
port?: number;
3637
/** Enable introspection (also enables Apollo Sandbox in development) */
3738
introspection?: boolean;
39+
/** Enable GraphQL Playground/landing page (Apollo Sandbox). When false, disables the landing page. */
40+
playground?: boolean;
3841
/** Custom type definitions (optional) */
3942
typeDefs?: string;
4043
/** Enable subscriptions via WebSocket */
@@ -219,21 +222,28 @@ export class GraphQLPlugin implements RuntimePlugin {
219222
}
220223

221224
// Create Apollo Server with WebSocket support
225+
const plugins: any[] = [
226+
ApolloServerPluginDrainHttpServer({ httpServer: this.httpServer }),
227+
{
228+
async serverWillStart() {
229+
return {
230+
async drainServer() {
231+
// Cleanup WebSocket server on shutdown
232+
}
233+
};
234+
}
235+
}
236+
];
237+
238+
// Disable landing page in test mode when playground is explicitly set to false
239+
if (this.config.playground === false) {
240+
plugins.push(ApolloServerPluginLandingPageDisabled());
241+
}
242+
222243
this.server = new ApolloServer({
223244
schema,
224245
introspection: this.config.introspection,
225-
plugins: [
226-
ApolloServerPluginDrainHttpServer({ httpServer: this.httpServer }),
227-
{
228-
async serverWillStart() {
229-
return {
230-
async drainServer() {
231-
// Cleanup WebSocket server on shutdown
232-
}
233-
};
234-
}
235-
}
236-
],
246+
plugins,
237247
includeStacktraceInErrorResponses: process.env.NODE_ENV !== 'production',
238248
formatError: (formattedError, error) => {
239249
return {
@@ -296,16 +306,23 @@ export class GraphQLPlugin implements RuntimePlugin {
296306
const schema = makeExecutableSchema({ typeDefs, resolvers });
297307

298308
// Initialize Apollo Server (without express middleware yet)
309+
const plugins: any[] = [
310+
{
311+
async serverWillStart() {
312+
return { async drainServer() {} };
313+
}
314+
}
315+
];
316+
317+
// Disable landing page in test mode when playground is explicitly set to false
318+
if (this.config.playground === false) {
319+
plugins.push(ApolloServerPluginLandingPageDisabled());
320+
}
321+
299322
this.server = new ApolloServer({
300323
schema,
301324
introspection: this.config.introspection,
302-
plugins: [
303-
{
304-
async serverWillStart() {
305-
return { async drainServer() {} };
306-
}
307-
}
308-
],
325+
plugins,
309326
includeStacktraceInErrorResponses: process.env.NODE_ENV !== 'production',
310327
});
311328

@@ -646,6 +663,13 @@ export class GraphQLPlugin implements RuntimePlugin {
646663
const objectTypes = this.getMetaTypes();
647664

648665
let typeDefs = `#graphql
666+
# Custom scalars
667+
"""
668+
Represents arbitrary JSON values. Can be used for dynamic data structures.
669+
Supports objects, arrays, strings, numbers, booleans, and null.
670+
"""
671+
scalar JSON
672+
649673
# Common filter types
650674
input StringFilter {
651675
eq: String
@@ -959,7 +983,46 @@ export class GraphQLPlugin implements RuntimePlugin {
959983
private generateResolvers(): any {
960984
const objectTypes = this.getMetaTypes();
961985

986+
// Helper function for parsing GraphQL literals to JSON
987+
const parseLiteral = (ast: any): any => {
988+
if (ast.kind === 'StringValue') {
989+
return JSON.parse(ast.value);
990+
}
991+
if (ast.kind === 'IntValue' || ast.kind === 'FloatValue') {
992+
return parseFloat(ast.value);
993+
}
994+
if (ast.kind === 'BooleanValue') {
995+
return ast.value;
996+
}
997+
if (ast.kind === 'NullValue') {
998+
return null;
999+
}
1000+
if (ast.kind === 'ObjectValue') {
1001+
const obj: any = {};
1002+
ast.fields.forEach((field: any) => {
1003+
obj[field.name.value] = parseLiteral(field.value);
1004+
});
1005+
return obj;
1006+
}
1007+
if (ast.kind === 'ListValue') {
1008+
return ast.values.map((value: any) => parseLiteral(value));
1009+
}
1010+
return null;
1011+
};
1012+
9621013
const resolvers: any = {
1014+
// JSON scalar resolver - passes through any value
1015+
JSON: {
1016+
__parseValue(value: any) {
1017+
return value; // value from the client input variables
1018+
},
1019+
__serialize(value: any) {
1020+
return value; // value sent to the client
1021+
},
1022+
__parseLiteral(ast: any) {
1023+
return parseLiteral(ast);
1024+
}
1025+
},
9631026
Query: {
9641027
hello: () => 'Hello from GraphQL Protocol Plugin with Subscriptions!',
9651028

packages/protocols/graphql/src/tck.test.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class GraphQLEndpoint implements ProtocolEndpoint {
2525
constructor(plugin: GraphQLPlugin, kernel: ObjectKernel) {
2626
this.plugin = plugin;
2727
this.kernel = kernel;
28-
this.baseUrl = `http://localhost:${plugin.config.port || 4000}`;
28+
this.baseUrl = `http://localhost:${plugin.config.port || 4000}/graphql`;
2929
}
3030

3131
async execute(operation: ProtocolOperation): Promise<ProtocolResponse> {
@@ -66,12 +66,12 @@ class GraphQLEndpoint implements ProtocolEndpoint {
6666
private async executeCreate(operation: ProtocolOperation): Promise<ProtocolResponse> {
6767
const entityName = this.capitalize(operation.entity);
6868
const mutation = `
69-
mutation Create${entityName}($data: JSON!) {
70-
create${entityName}(data: $data)
69+
mutation Create${entityName}($input: ${entityName}Input!) {
70+
create${entityName}(input: $input)
7171
}
7272
`;
7373

74-
const result = await this.graphqlRequest(mutation, { data: operation.data });
74+
const result = await this.graphqlRequest(mutation, { input: operation.data });
7575

7676
if (result.errors) {
7777
return {
@@ -91,9 +91,10 @@ class GraphQLEndpoint implements ProtocolEndpoint {
9191

9292
private async executeRead(operation: ProtocolOperation): Promise<ProtocolResponse> {
9393
const entityName = this.capitalize(operation.entity);
94+
const camelName = this.toCamelCase(operation.entity);
9495
const query = `
95-
query Get${entityName}($id: String!) {
96-
${operation.entity}(id: $id)
96+
query Get${entityName}($id: ID!) {
97+
${camelName}(id: $id)
9798
}
9899
`;
99100

@@ -111,21 +112,21 @@ class GraphQLEndpoint implements ProtocolEndpoint {
111112

112113
return {
113114
success: true,
114-
data: result.data[operation.entity]
115+
data: result.data[camelName]
115116
};
116117
}
117118

118119
private async executeUpdate(operation: ProtocolOperation): Promise<ProtocolResponse> {
119120
const entityName = this.capitalize(operation.entity);
120121
const mutation = `
121-
mutation Update${entityName}($id: String!, $data: JSON!) {
122-
update${entityName}(id: $id, data: $data)
122+
mutation Update${entityName}($id: ID!, $input: ${entityName}UpdateInput!) {
123+
update${entityName}(id: $id, input: $input)
123124
}
124125
`;
125126

126127
const result = await this.graphqlRequest(mutation, {
127128
id: operation.id,
128-
data: operation.data
129+
input: operation.data
129130
});
130131

131132
if (result.errors) {
@@ -147,7 +148,7 @@ class GraphQLEndpoint implements ProtocolEndpoint {
147148
private async executeDelete(operation: ProtocolOperation): Promise<ProtocolResponse> {
148149
const entityName = this.capitalize(operation.entity);
149150
const mutation = `
150-
mutation Delete${entityName}($id: String!) {
151+
mutation Delete${entityName}($id: ID!) {
151152
delete${entityName}(id: $id)
152153
}
153154
`;
@@ -172,13 +173,14 @@ class GraphQLEndpoint implements ProtocolEndpoint {
172173

173174
private async executeQuery(operation: ProtocolOperation): Promise<ProtocolResponse> {
174175
const entityName = this.capitalize(operation.entity);
176+
const camelName = this.toCamelCase(operation.entity);
175177

176178
let queryArgs = '';
177179
const variables: any = {};
178180

179181
if (operation.filter) {
180-
queryArgs += '$filter: JSON';
181-
variables.filter = operation.filter;
182+
queryArgs += `$where: ${entityName}Filter`;
183+
variables.where = operation.filter;
182184
}
183185

184186
if (operation.options?.limit) {
@@ -195,7 +197,7 @@ class GraphQLEndpoint implements ProtocolEndpoint {
195197

196198
const query = `
197199
query List${entityName}${queryArgs ? `(${queryArgs})` : ''} {
198-
${operation.entity}List${this.buildQueryArgs(variables)}
200+
${camelName}List${this.buildQueryArgs(variables)}
199201
}
200202
`;
201203

@@ -213,7 +215,7 @@ class GraphQLEndpoint implements ProtocolEndpoint {
213215

214216
return {
215217
success: true,
216-
data: result.data[`${operation.entity}List`] || []
218+
data: result.data[`${camelName}List`] || []
217219
};
218220
}
219221

@@ -232,16 +234,16 @@ class GraphQLEndpoint implements ProtocolEndpoint {
232234
}
233235

234236
const mutations = operation.data.map((item, index) => `
235-
item${index}: create${entityName}(data: $data${index})
237+
item${index}: create${entityName}(input: $input${index})
236238
`).join('\n');
237239

238240
const variables: any = {};
239241
const variableDefinitions = operation.data.map((_, index) =>
240-
`$data${index}: JSON!`
242+
`$input${index}: ${entityName}Input!`
241243
).join(', ');
242244

243245
operation.data.forEach((item, index) => {
244-
variables[`data${index}`] = item;
246+
variables[`input${index}`] = item;
245247
});
246248

247249
const mutation = `
@@ -307,11 +309,27 @@ class GraphQLEndpoint implements ProtocolEndpoint {
307309
body: JSON.stringify({ query, variables })
308310
});
309311

312+
const contentType = response.headers.get('content-type');
313+
if (!contentType || !contentType.includes('application/json')) {
314+
const text = await response.text();
315+
throw new Error(`Expected JSON response but got: ${contentType}. Response: ${text.substring(0, 200)}`);
316+
}
317+
310318
return await response.json();
311319
}
312320

313321
private capitalize(str: string): string {
314-
return str.charAt(0).toUpperCase() + str.slice(1);
322+
// Convert to PascalCase (handle underscores and hyphens)
323+
return str
324+
.split(/[_-]/)
325+
.filter(word => word.length > 0) // Remove empty segments
326+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
327+
.join('');
328+
}
329+
330+
private toCamelCase(str: string): string {
331+
const pascal = this.capitalize(str);
332+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
315333
}
316334

317335
private buildQueryArgs(variables: any): string {

0 commit comments

Comments
 (0)