Skip to content

Commit 1f36533

Browse files
Copilothotlong
andcommitted
Add Protocol TCK tests and fix TypeScript compilation errors
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent fbfcf24 commit 1f36533

6 files changed

Lines changed: 216 additions & 5 deletions

File tree

packages/protocols/graphql/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"express": "^4.18.2",
2929
"graphql": "^16.8.1",
3030
"graphql-subscriptions": "^2.0.0",
31+
"graphql-tag": "^2.12.6",
3132
"graphql-ws": "^5.14.0",
3233
"ws": "^8.14.0"
3334
},

packages/protocols/graphql/src/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ApolloServer, HeaderMap } from '@apollo/server';
1313
import { expressMiddleware } from '@apollo/server/express4';
1414
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
1515
import { buildSubgraphSchema } from '@apollo/subgraph';
16+
import { gql } from 'graphql-tag';
1617
import { createServer } from 'http';
1718
import { WebSocketServer } from 'ws';
1819
import { useServer } from 'graphql-ws/lib/use/ws';
@@ -172,10 +173,13 @@ export class GraphQLPlugin implements RuntimePlugin {
172173
let schema;
173174
if (this.config.enableFederation) {
174175
// Build federated subgraph schema
175-
schema = buildSubgraphSchema({
176-
typeDefs,
177-
resolvers
178-
});
176+
// buildSubgraphSchema expects parsed GraphQL documents
177+
schema = buildSubgraphSchema([
178+
{
179+
typeDefs: gql(typeDefs),
180+
resolvers
181+
}
182+
]);
179183
console.log(`[${this.name}] 🔗 Apollo Federation enabled - service: ${this.config.federationServiceName}`);
180184
} else {
181185
// Build standard GraphQL schema

packages/protocols/odata-v4/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1336,7 +1336,7 @@ export class ODataV4Plugin implements RuntimePlugin {
13361336
const url = part.url;
13371337

13381338
// Parse URL to extract entity set and key
1339-
const urlParts = url.replace(this.config.basePath, '').split('/').filter(p => p);
1339+
const urlParts = url.replace(this.config.basePath, '').split('/').filter((p: string) => p);
13401340

13411341
if (urlParts.length === 0) {
13421342
return 'HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\n\r\n{"error":{"code":"400","message":"Invalid URL"}}';
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Example Protocol TCK Test
3+
*
4+
* Demonstrates how to use the Protocol TCK with a mock protocol endpoint
5+
*/
6+
7+
import { describe, expect, test, beforeEach } from 'vitest';
8+
import { runProtocolTCK, ProtocolEndpoint, ProtocolOperation, ProtocolResponse } from '../src/index';
9+
10+
/**
11+
* Mock in-memory protocol endpoint for testing the TCK itself
12+
*/
13+
class MockProtocolEndpoint implements ProtocolEndpoint {
14+
private data: Map<string, Map<string, any>> = new Map();
15+
private idCounter = 0;
16+
17+
async execute(operation: ProtocolOperation): Promise<ProtocolResponse> {
18+
try {
19+
const { type, entity, data, id, filter, options } = operation;
20+
21+
// Ensure entity collection exists
22+
if (!this.data.has(entity)) {
23+
this.data.set(entity, new Map());
24+
}
25+
26+
const collection = this.data.get(entity)!;
27+
28+
switch (type) {
29+
case 'create': {
30+
const newId = data?.id || `mock-${++this.idCounter}`;
31+
const newData = {
32+
...data,
33+
id: newId,
34+
created_at: new Date().toISOString(),
35+
updated_at: new Date().toISOString()
36+
};
37+
collection.set(newId, newData);
38+
return { success: true, data: newData };
39+
}
40+
41+
case 'read': {
42+
if (!id) {
43+
return { success: false, error: { code: '400', message: 'ID required' } };
44+
}
45+
const item = collection.get(id);
46+
return { success: true, data: item || null };
47+
}
48+
49+
case 'update': {
50+
if (!id) {
51+
return { success: false, error: { code: '400', message: 'ID required' } };
52+
}
53+
const existing = collection.get(id);
54+
if (!existing) {
55+
return { success: false, error: { code: '404', message: 'Not found' } };
56+
}
57+
const updated = {
58+
...existing,
59+
...data,
60+
id, // Don't allow ID change
61+
updated_at: new Date().toISOString()
62+
};
63+
collection.set(id, updated);
64+
return { success: true, data: updated };
65+
}
66+
67+
case 'delete': {
68+
if (!id) {
69+
return { success: false, error: { code: '400', message: 'ID required' } };
70+
}
71+
const deleted = collection.delete(id);
72+
return { success: deleted, data: deleted ? { id } : null };
73+
}
74+
75+
case 'query': {
76+
let results = Array.from(collection.values());
77+
78+
// Apply filter
79+
if (filter) {
80+
results = results.filter(item => {
81+
return Object.entries(filter).every(([key, value]) => item[key] === value);
82+
});
83+
}
84+
85+
// Apply sorting
86+
if (options?.orderBy) {
87+
const sortField = options.orderBy[0]?.field;
88+
const sortOrder = options.orderBy[0]?.order?.toLowerCase();
89+
if (sortField) {
90+
results.sort((a, b) => {
91+
const aVal = a[sortField];
92+
const bVal = b[sortField];
93+
const compare = aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
94+
return sortOrder === 'desc' ? -compare : compare;
95+
});
96+
}
97+
}
98+
99+
// Apply pagination
100+
if (options?.offset) {
101+
results = results.slice(options.offset);
102+
}
103+
if (options?.limit) {
104+
results = results.slice(0, options.limit);
105+
}
106+
107+
return { success: true, data: results };
108+
}
109+
110+
case 'batch': {
111+
if (!Array.isArray(data)) {
112+
return { success: false, error: { code: '400', message: 'Batch requires array' } };
113+
}
114+
115+
const batchResults = [];
116+
for (const item of data) {
117+
const result = await this.execute({
118+
type: 'create',
119+
entity,
120+
data: item
121+
});
122+
if (result.data) {
123+
batchResults.push(result.data);
124+
}
125+
}
126+
127+
return { success: true, data: batchResults };
128+
}
129+
130+
default:
131+
return {
132+
success: false,
133+
error: { code: '400', message: `Unsupported operation: ${type}` }
134+
};
135+
}
136+
} catch (error) {
137+
return {
138+
success: false,
139+
error: {
140+
code: '500',
141+
message: error instanceof Error ? error.message : 'Internal error'
142+
}
143+
};
144+
}
145+
}
146+
147+
async getMetadata(): Promise<any> {
148+
return {
149+
entities: Array.from(this.data.keys()),
150+
protocol: 'mock',
151+
version: '1.0.0'
152+
};
153+
}
154+
155+
async close(): Promise<void> {
156+
this.data.clear();
157+
}
158+
}
159+
160+
// Run the TCK tests
161+
describe('Protocol TCK - Mock Protocol Example', () => {
162+
runProtocolTCK(
163+
() => new MockProtocolEndpoint(),
164+
'Mock Protocol',
165+
{
166+
timeout: 10000,
167+
performance: {
168+
enabled: true,
169+
thresholds: {
170+
create: 10,
171+
read: 5,
172+
update: 10,
173+
delete: 5,
174+
query: 20,
175+
batch: 50
176+
}
177+
}
178+
}
179+
);
180+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
globals: true,
6+
environment: 'node',
7+
coverage: {
8+
provider: 'v8',
9+
reporter: ['text', 'json', 'html'],
10+
},
11+
},
12+
});

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)