@ankhorage/data-sources 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +6 -41
  3. package/dist/ankh.provider.js +1 -1
  4. package/dist/ankh.provider.js.map +1 -1
  5. package/dist/discovery/http.d.ts +10 -0
  6. package/dist/discovery/http.d.ts.map +1 -0
  7. package/dist/discovery/http.js +35 -0
  8. package/dist/discovery/http.js.map +1 -0
  9. package/dist/discovery/index.d.ts +9 -9
  10. package/dist/discovery/index.d.ts.map +1 -1
  11. package/dist/discovery/index.js +27 -55
  12. package/dist/discovery/index.js.map +1 -1
  13. package/dist/graphql/index.d.ts +8 -76
  14. package/dist/graphql/index.d.ts.map +1 -1
  15. package/dist/graphql/index.js +17 -197
  16. package/dist/graphql/index.js.map +1 -1
  17. package/dist/graphql/introspection.d.ts +5 -0
  18. package/dist/graphql/introspection.d.ts.map +1 -0
  19. package/dist/graphql/introspection.js +136 -0
  20. package/dist/graphql/introspection.js.map +1 -0
  21. package/dist/graphql/operation.d.ts +5 -0
  22. package/dist/graphql/operation.d.ts.map +1 -0
  23. package/dist/graphql/operation.js +39 -0
  24. package/dist/graphql/operation.js.map +1 -0
  25. package/dist/graphql/types.d.ts +71 -0
  26. package/dist/graphql/types.d.ts.map +1 -0
  27. package/dist/graphql/types.js +2 -0
  28. package/dist/graphql/types.js.map +1 -0
  29. package/dist/index.d.ts +1 -17
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -34
  32. package/dist/index.js.map +1 -1
  33. package/dist/openapi/endpoints.d.ts +5 -0
  34. package/dist/openapi/endpoints.d.ts.map +1 -0
  35. package/dist/openapi/endpoints.js +77 -0
  36. package/dist/openapi/endpoints.js.map +1 -0
  37. package/dist/openapi/index.d.ts +7 -87
  38. package/dist/openapi/index.d.ts.map +1 -1
  39. package/dist/openapi/index.js +8 -267
  40. package/dist/openapi/index.js.map +1 -1
  41. package/dist/openapi/operation.d.ts +5 -0
  42. package/dist/openapi/operation.d.ts.map +1 -0
  43. package/dist/openapi/operation.js +141 -0
  44. package/dist/openapi/operation.js.map +1 -0
  45. package/dist/openapi/schema.d.ts +5 -0
  46. package/dist/openapi/schema.d.ts.map +1 -0
  47. package/dist/openapi/schema.js +84 -0
  48. package/dist/openapi/schema.js.map +1 -0
  49. package/dist/openapi/types.d.ts +82 -0
  50. package/dist/openapi/types.d.ts.map +1 -0
  51. package/dist/openapi/types.js +2 -0
  52. package/dist/openapi/types.js.map +1 -0
  53. package/dist/packageInfo.d.ts +18 -0
  54. package/dist/packageInfo.d.ts.map +1 -0
  55. package/dist/packageInfo.js +30 -0
  56. package/dist/packageInfo.js.map +1 -0
  57. package/dist/rest/index.d.ts +5 -5
  58. package/dist/rest/index.d.ts.map +1 -1
  59. package/dist/rest/index.js +49 -48
  60. package/dist/rest/index.js.map +1 -1
  61. package/dist/test-runner/index.d.ts +2 -57
  62. package/dist/test-runner/index.d.ts.map +1 -1
  63. package/dist/test-runner/index.js +58 -160
  64. package/dist/test-runner/index.js.map +1 -1
  65. package/dist/test-runner/response.d.ts +3 -0
  66. package/dist/test-runner/response.d.ts.map +1 -0
  67. package/dist/test-runner/response.js +29 -0
  68. package/dist/test-runner/response.js.map +1 -0
  69. package/dist/test-runner/result.d.ts +6 -0
  70. package/dist/test-runner/result.d.ts.map +1 -0
  71. package/dist/test-runner/result.js +17 -0
  72. package/dist/test-runner/result.js.map +1 -0
  73. package/dist/test-runner/serialization.d.ts +12 -0
  74. package/dist/test-runner/serialization.d.ts.map +1 -0
  75. package/dist/test-runner/serialization.js +99 -0
  76. package/dist/test-runner/serialization.js.map +1 -0
  77. package/dist/test-runner/types.d.ts +58 -0
  78. package/dist/test-runner/types.d.ts.map +1 -0
  79. package/dist/test-runner/types.js +2 -0
  80. package/dist/test-runner/types.js.map +1 -0
  81. package/package.json +5 -5
  82. package/src/ankh.provider.ts +1 -1
  83. package/src/api-phase1.contract.test.ts +24 -0
  84. package/src/discovery/http.ts +38 -0
  85. package/src/discovery/index.test.ts +76 -75
  86. package/src/discovery/index.ts +49 -72
  87. package/src/graphql/index.test.ts +10 -11
  88. package/src/graphql/index.ts +35 -315
  89. package/src/graphql/introspection.ts +163 -0
  90. package/src/graphql/operation.ts +60 -0
  91. package/src/graphql/types.ts +79 -0
  92. package/src/index.test.ts +21 -13
  93. package/src/index.ts +1 -54
  94. package/src/openapi/endpoints.ts +124 -0
  95. package/src/openapi/index.test.ts +47 -47
  96. package/src/openapi/index.ts +12 -467
  97. package/src/openapi/operation.ts +231 -0
  98. package/src/openapi/schema.ts +127 -0
  99. package/src/openapi/types.ts +93 -0
  100. package/src/packageInfo.ts +50 -0
  101. package/src/rest/index.test.ts +17 -14
  102. package/src/rest/index.ts +67 -61
  103. package/src/test-runner/index.test.ts +153 -120
  104. package/src/test-runner/index.ts +97 -299
  105. package/src/test-runner/response.ts +32 -0
  106. package/src/test-runner/result.ts +35 -0
  107. package/src/test-runner/serialization.ts +151 -0
  108. package/src/test-runner/types.ts +81 -0
  109. package/dist/generated-api/index.d.ts +0 -10
  110. package/dist/generated-api/index.d.ts.map +0 -1
  111. package/dist/generated-api/index.js +0 -274
  112. package/dist/generated-api/index.js.map +0 -1
  113. package/dist/generated-api/validateGeneratedApiCollection.d.ts +0 -3
  114. package/dist/generated-api/validateGeneratedApiCollection.d.ts.map +0 -1
  115. package/dist/generated-api/validateGeneratedApiCollection.js +0 -35
  116. package/dist/generated-api/validateGeneratedApiCollection.js.map +0 -1
  117. package/src/generated-api/index.test.ts +0 -173
  118. package/src/generated-api/index.ts +0 -414
  119. package/src/generated-api/validateGeneratedApiCollection.test.ts +0 -91
  120. package/src/generated-api/validateGeneratedApiCollection.ts +0 -76
package/src/rest/index.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  DataSourceDiagnostic,
14
14
  DataSourceDiagnosticResult,
15
15
  EndpointId,
16
- ExternalRestApiDataSourceConfig,
16
+ ExternalRestApiDefinition,
17
17
  OperationId,
18
18
  } from '@ankhorage/contracts/data';
19
19
 
@@ -46,7 +46,7 @@ export interface ManualRestEndpointDefinition {
46
46
  readonly metadata?: DataContractValue;
47
47
  }
48
48
 
49
- export interface ManualRestDataSourceDefinition {
49
+ export interface ManualRestApiDefinition {
50
50
  readonly id: string;
51
51
  readonly baseUrl: string;
52
52
  readonly name?: string;
@@ -83,16 +83,16 @@ export function extractRestPathParams(path: string): readonly string[] {
83
83
  return [...params];
84
84
  }
85
85
 
86
- export function validateManualRestDataSource(
87
- definition: ManualRestDataSourceDefinition,
86
+ export function validateManualRestApi(
87
+ definition: ManualRestApiDefinition,
88
88
  ): readonly DataSourceDiagnostic[] {
89
89
  const diagnostics: DataSourceDiagnostic[] = [];
90
90
 
91
91
  if (definition.baseUrl.trim().length === 0) {
92
92
  diagnostics.push({
93
93
  code: 'invalid-config',
94
- dataSourceId: definition.id,
95
- message: 'Manual REST data source requires a non-empty baseUrl.',
94
+ apiId: definition.id,
95
+ message: 'Manual REST API requires a non-empty baseUrl.',
96
96
  path: 'baseUrl',
97
97
  severity: 'error',
98
98
  });
@@ -102,7 +102,7 @@ export function validateManualRestDataSource(
102
102
  if (!endpoint.path.startsWith('/')) {
103
103
  diagnostics.push({
104
104
  code: 'invalid-config',
105
- dataSourceId: definition.id,
105
+ apiId: definition.id,
106
106
  endpointId: endpoint.id,
107
107
  message: 'Manual REST endpoint paths must start with `/`.',
108
108
  path: `endpoints.${endpoint.id}.path`,
@@ -118,69 +118,30 @@ export function validateManualRestDataSource(
118
118
  return diagnostics;
119
119
  }
120
120
 
121
- export function createManualRestDataSource(
122
- definition: ManualRestDataSourceDefinition,
123
- ): DataSourceDiagnosticResult<ExternalRestApiDataSourceConfig> {
124
- const diagnostics = validateManualRestDataSource(definition);
121
+ export function createManualRestApi(
122
+ definition: ManualRestApiDefinition,
123
+ ): DataSourceDiagnosticResult<ExternalRestApiDefinition> {
124
+ const diagnostics = validateManualRestApi(definition);
125
125
  if (diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
126
126
  return { ok: false, diagnostics };
127
127
  }
128
128
 
129
129
  return {
130
130
  ok: true,
131
- data: normalizeManualRestDataSource(definition),
131
+ data: normalizeManualRestApi(definition),
132
132
  diagnostics,
133
133
  };
134
134
  }
135
135
 
136
- export function normalizeManualRestDataSource(
137
- definition: ManualRestDataSourceDefinition,
138
- ): ExternalRestApiDataSourceConfig {
139
- const endpoints: Record<EndpointId, DataEndpointConfig> = {};
140
-
141
- for (const endpoint of definition.endpoints) {
142
- const operations: Record<OperationId, DataOperationConfig> = {};
143
-
144
- for (const operation of endpoint.operations) {
145
- const { parameters } = operation;
146
- const request: DataOperationRequest | undefined =
147
- operation.request === undefined && parameters === undefined
148
- ? undefined
149
- : { ...operation.request, parameters };
150
-
151
- operations[operation.id] = {
152
- id: operation.id,
153
- endpointId: endpoint.id,
154
- name: operation.name,
155
- description: operation.description,
156
- protocol: 'http',
157
- intent: operation.intent,
158
- method: normalizeManualRestMethod(operation.method),
159
- path: operation.path ?? endpoint.path,
160
- request,
161
- response: operation.response,
162
- pagination: operation.pagination,
163
- credential: operation.credential,
164
- metadata: operation.metadata,
165
- };
166
- }
167
-
168
- endpoints[endpoint.id] = {
169
- id: endpoint.id,
170
- kind: 'http',
171
- name: endpoint.name,
172
- description: endpoint.description,
173
- baseUrl: definition.baseUrl,
174
- path: endpoint.path,
175
- credential: endpoint.credential,
176
- operations,
177
- metadata: endpoint.metadata,
178
- };
179
- }
136
+ export function normalizeManualRestApi(
137
+ definition: ManualRestApiDefinition,
138
+ ): ExternalRestApiDefinition {
139
+ const endpoints = Object.fromEntries(
140
+ definition.endpoints.map((endpoint) => [endpoint.id, normalizeManualRestEndpoint(endpoint)]),
141
+ );
180
142
 
181
143
  return {
182
144
  id: definition.id,
183
- kind: 'api',
184
145
  origin: 'external',
185
146
  protocol: 'rest',
186
147
  name: definition.name,
@@ -193,8 +154,53 @@ export function normalizeManualRestDataSource(
193
154
  };
194
155
  }
195
156
 
157
+ function normalizeManualRestEndpoint(endpoint: ManualRestEndpointDefinition): DataEndpointConfig {
158
+ const operations = Object.fromEntries(
159
+ endpoint.operations.map((operation) => [
160
+ operation.id,
161
+ normalizeManualRestOperation(endpoint, operation),
162
+ ]),
163
+ );
164
+ return {
165
+ id: endpoint.id,
166
+ kind: 'http',
167
+ name: endpoint.name,
168
+ description: endpoint.description,
169
+ path: endpoint.path,
170
+ credential: endpoint.credential,
171
+ operations,
172
+ metadata: endpoint.metadata,
173
+ };
174
+ }
175
+
176
+ function normalizeManualRestOperation(
177
+ endpoint: ManualRestEndpointDefinition,
178
+ operation: ManualRestOperationDefinition,
179
+ ): DataOperationConfig {
180
+ const { parameters } = operation;
181
+ const request: DataOperationRequest | undefined =
182
+ operation.request === undefined && parameters === undefined
183
+ ? undefined
184
+ : { ...operation.request, parameters };
185
+ return {
186
+ id: operation.id,
187
+ endpointId: endpoint.id,
188
+ name: operation.name,
189
+ description: operation.description,
190
+ protocol: 'http',
191
+ intent: operation.intent,
192
+ method: normalizeManualRestMethod(operation.method),
193
+ path: operation.path ?? endpoint.path,
194
+ request,
195
+ response: operation.response,
196
+ pagination: operation.pagination,
197
+ credential: operation.credential,
198
+ metadata: operation.metadata,
199
+ };
200
+ }
201
+
196
202
  function validateManualRestOperation(
197
- definition: ManualRestDataSourceDefinition,
203
+ definition: ManualRestApiDefinition,
198
204
  endpoint: ManualRestEndpointDefinition,
199
205
  operation: ManualRestOperationDefinition,
200
206
  diagnostics: DataSourceDiagnostic[],
@@ -209,7 +215,7 @@ function validateManualRestOperation(
209
215
  if (!isManualRestMethod(method)) {
210
216
  diagnostics.push({
211
217
  code: 'invalid-config',
212
- dataSourceId: definition.id,
218
+ apiId: definition.id,
213
219
  endpointId: endpoint.id,
214
220
  operationId: operation.id,
215
221
  message: `Manual REST operation method must be one of ${REST_METHODS.join(', ')}.`,
@@ -222,7 +228,7 @@ function validateManualRestOperation(
222
228
  if (pathParameterNames.has(templateParam)) continue;
223
229
  diagnostics.push({
224
230
  code: 'invalid-config',
225
- dataSourceId: definition.id,
231
+ apiId: definition.id,
226
232
  endpointId: endpoint.id,
227
233
  operationId: operation.id,
228
234
  message: `Path template parameter '${templateParam}' must have a matching path parameter definition.`,
@@ -235,7 +241,7 @@ function validateManualRestOperation(
235
241
  if (templateParams.includes(pathParameter.name)) continue;
236
242
  diagnostics.push({
237
243
  code: 'invalid-config',
238
- dataSourceId: definition.id,
244
+ apiId: definition.id,
239
245
  endpointId: endpoint.id,
240
246
  operationId: operation.id,
241
247
  message: `Path parameter '${pathParameter.name}' is not referenced by the operation path template.`,
@@ -1,12 +1,37 @@
1
- import type { DataSourceConfig } from '@ankhorage/contracts/data';
2
- import { describe, expect, it } from 'bun:test';
1
+ import type { ApiDefinition, ExternalRestApiDefinition } from '@ankhorage/contracts/data';
2
+ import { expect, it } from 'bun:test';
3
3
 
4
4
  import { buildEndpointTestRequest, type EndpointTestFetch, testEndpoint } from './index';
5
5
 
6
- function restSource(): DataSourceConfig {
6
+ function nutritionApi(): ExternalRestApiDefinition {
7
+ return {
8
+ id: 'nutrition',
9
+ origin: 'external',
10
+ protocol: 'rest',
11
+ baseUrl: 'https://api.ankhorage.com/v1/nutrition',
12
+ endpoints: {
13
+ products: {
14
+ id: 'products',
15
+ kind: 'http',
16
+ path: '/products',
17
+ operations: {
18
+ 'products.list': {
19
+ id: 'products.list',
20
+ endpointId: 'products',
21
+ protocol: 'http',
22
+ intent: 'read',
23
+ method: 'GET',
24
+ path: '/products',
25
+ },
26
+ },
27
+ },
28
+ },
29
+ };
30
+ }
31
+
32
+ function parameterApi(): ExternalRestApiDefinition {
7
33
  return {
8
34
  id: 'cms',
9
- kind: 'api',
10
35
  origin: 'external',
11
36
  protocol: 'rest',
12
37
  baseUrl: 'https://cms.example.com',
@@ -15,7 +40,6 @@ function restSource(): DataSourceConfig {
15
40
  posts: {
16
41
  id: 'posts',
17
42
  kind: 'http',
18
- path: '/posts/{postId}',
19
43
  operations: {
20
44
  'posts.get': {
21
45
  id: 'posts.get',
@@ -26,13 +50,9 @@ function restSource(): DataSourceConfig {
26
50
  path: '/posts/{postId}',
27
51
  request: {
28
52
  parameters: [
29
- {
30
- name: 'postId',
31
- location: 'path',
32
- required: true,
33
- schema: { type: 'string' },
34
- },
53
+ { name: 'postId', location: 'path', required: true, schema: { type: 'string' } },
35
54
  { name: 'preview', location: 'query', schema: { type: 'boolean' } },
55
+ { name: 'x-locale', location: 'header', schema: { type: 'string' } },
36
56
  ],
37
57
  },
38
58
  },
@@ -43,6 +63,13 @@ function restSource(): DataSourceConfig {
43
63
  intent: 'create',
44
64
  method: 'POST',
45
65
  path: '/posts',
66
+ request: {
67
+ contentType: 'application/json',
68
+ parameters: [
69
+ { name: 'title', location: 'body', required: true, schema: { type: 'string' } },
70
+ { name: 'published', location: 'body', schema: { type: 'boolean' } },
71
+ ],
72
+ },
46
73
  },
47
74
  },
48
75
  },
@@ -50,10 +77,9 @@ function restSource(): DataSourceConfig {
50
77
  };
51
78
  }
52
79
 
53
- function graphQlSource(): DataSourceConfig {
80
+ function graphQlApi(): ApiDefinition {
54
81
  return {
55
82
  id: 'content',
56
- kind: 'api',
57
83
  origin: 'external',
58
84
  protocol: 'graphql',
59
85
  endpointUrl: 'https://content.example.com/graphql',
@@ -75,124 +101,131 @@ function graphQlSource(): DataSourceConfig {
75
101
  };
76
102
  }
77
103
 
78
- describe('endpoint test runner', () => {
79
- it('builds REST dry runs with params and credential headers', async () => {
80
- const result = await buildEndpointTestRequest({
81
- dataSource: restSource(),
82
- endpointId: 'posts',
83
- operationId: 'posts.get',
84
- dryRun: true,
85
- values: { postId: 'post 1', preview: true },
86
- credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
87
- });
88
- expect(result.ok).toBe(true);
89
- if (result.ok) {
90
- expect(result.request.url).toBe('https://cms.example.com/posts/post%201?preview=true');
91
- expect(result.request.headers.authorization).toBe('Bearer token');
92
- }
104
+ it('builds the canonical Nutrition products URL', async () => {
105
+ const result = await buildEndpointTestRequest({
106
+ api: nutritionApi(),
107
+ endpointId: 'products',
108
+ operationId: 'products.list',
109
+ dryRun: true,
93
110
  });
111
+ expect(result.ok).toBe(true);
112
+ if (result.ok) {
113
+ expect(result.request.apiId).toBe('nutrition');
114
+ expect(result.request.method).toBe('GET');
115
+ expect(result.request.url).toBe('https://api.ankhorage.com/v1/nutrition/products');
116
+ }
117
+ });
94
118
 
95
- it('executes REST requests and parses responses', async () => {
96
- const fetch: EndpointTestFetch = () =>
97
- Promise.resolve({ status: 200, text: () => Promise.resolve('{"id":"post-1"}') });
98
- const result = await testEndpoint({
99
- dataSource: restSource(),
100
- endpointId: 'posts',
101
- operationId: 'posts.get',
102
- values: { postId: 'post-1' },
103
- credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
104
- fetch,
119
+ it('serializes path, query and header parameters with credentials', async () => {
120
+ const result = await buildEndpointTestRequest({
121
+ api: parameterApi(),
122
+ endpointId: 'posts',
123
+ operationId: 'posts.get',
124
+ dryRun: true,
125
+ values: { postId: 'post 1', preview: true, 'x-locale': 'de-CH' },
126
+ credentialResolver: () => ({ headers: { authorization: 'Bearer token' } }),
127
+ });
128
+ expect(result.ok).toBe(true);
129
+ if (result.ok) {
130
+ expect(result.request.url).toBe('https://cms.example.com/posts/post%201?preview=true');
131
+ expect(result.request.headers).toEqual({
132
+ authorization: 'Bearer token',
133
+ 'x-locale': 'de-CH',
105
134
  });
106
- expect(result.ok).toBe(true);
107
- if (result.ok) expect(result.data).toEqual({ id: 'post-1' });
135
+ }
136
+ });
137
+
138
+ it('serializes body parameters without provider-specific handling', async () => {
139
+ const result = await buildEndpointTestRequest({
140
+ api: parameterApi(),
141
+ endpointId: 'posts',
142
+ operationId: 'posts.create',
143
+ dryRun: true,
144
+ values: { title: 'Hello', published: true },
145
+ credentialResolver: () => ({ headers: {} }),
108
146
  });
147
+ expect(result.ok).toBe(true);
148
+ if (result.ok) {
149
+ expect(result.request.body).toBe('{"title":"Hello","published":true}');
150
+ expect(result.request.headers['content-type']).toBe('application/json');
151
+ }
152
+ });
109
153
 
110
- it('returns HTTP, network, credential and input diagnostics', async () => {
111
- const http = await testEndpoint({
112
- dataSource: restSource(),
113
- endpointId: 'posts',
114
- operationId: 'posts.get',
115
- values: { postId: 'missing' },
116
- credentialResolver: () => ({ headers: {} }),
117
- fetch: () => Promise.resolve({ status: 404, text: () => Promise.resolve('{}') }),
118
- });
119
- const network = await testEndpoint({
120
- dataSource: restSource(),
121
- endpointId: 'posts',
122
- operationId: 'posts.get',
123
- values: { postId: 'x' },
124
- credentialResolver: () => ({ headers: {} }),
125
- fetch: () => Promise.reject(new Error('Connection failed')),
126
- });
127
- const credential = await buildEndpointTestRequest({
128
- dataSource: restSource(),
129
- endpointId: 'posts',
130
- operationId: 'posts.get',
131
- values: { postId: 'x' },
132
- });
133
- const input = await buildEndpointTestRequest({
134
- dataSource: restSource(),
135
- endpointId: 'posts',
136
- operationId: 'posts.get',
137
- credentialResolver: () => ({ headers: {} }),
138
- });
139
- expect(http.ok).toBe(false);
140
- expect(network.ok).toBe(false);
141
- expect(credential.ok).toBe(false);
142
- expect(input.ok).toBe(false);
154
+ it('executes external REST requests and parses responses', async () => {
155
+ const fetch: EndpointTestFetch = () =>
156
+ Promise.resolve({ status: 200, text: () => Promise.resolve('{"products":[]}') });
157
+ const result = await testEndpoint({
158
+ api: nutritionApi(),
159
+ endpointId: 'products',
160
+ operationId: 'products.list',
161
+ fetch,
143
162
  });
163
+ expect(result.ok).toBe(true);
164
+ if (result.ok) expect(result.data).toEqual({ products: [] });
165
+ });
144
166
 
145
- it('builds POST bodies and GraphQL requests', async () => {
146
- const post = await buildEndpointTestRequest({
147
- dataSource: restSource(),
148
- endpointId: 'posts',
149
- operationId: 'posts.create',
150
- dryRun: true,
151
- values: { body: { title: 'Hello' } },
152
- credentialResolver: () => ({ headers: {} }),
153
- });
154
- const graphql = await buildEndpointTestRequest({
155
- dataSource: graphQlSource(),
156
- endpointId: 'graphql',
157
- operationId: 'query.viewer',
158
- dryRun: true,
159
- values: { variables: { id: 'viewer-1' } },
160
- });
161
- expect(post.ok).toBe(true);
162
- if (post.ok) expect(post.request.body).toBe('{"title":"Hello"}');
163
- expect(graphql.ok).toBe(true);
164
- if (graphql.ok) expect(graphql.request.url).toBe('https://content.example.com/graphql');
167
+ it('builds GraphQL requests through the same canonical API input', async () => {
168
+ const result = await buildEndpointTestRequest({
169
+ api: graphQlApi(),
170
+ endpointId: 'graphql',
171
+ operationId: 'query.viewer',
172
+ dryRun: true,
173
+ values: { variables: { id: 'viewer-1' } },
165
174
  });
175
+ expect(result.ok).toBe(true);
176
+ if (result.ok) {
177
+ expect(result.request.url).toBe('https://content.example.com/graphql');
178
+ expect(result.request.method).toBe('POST');
179
+ }
180
+ });
166
181
 
167
- it('does not pretend database-backed generated operations are HTTP endpoints', async () => {
168
- const source: DataSourceConfig = {
169
- id: 'catalog',
170
- kind: 'api',
171
- origin: 'generated',
172
- protocol: 'rest',
173
- generatedApiId: 'catalog',
174
- adapter: { id: 'db', kind: 'database' },
175
- endpoints: {
176
- products: {
177
- id: 'products',
178
- kind: 'database',
179
- operations: {
180
- 'products.list': {
181
- id: 'products.list',
182
- endpointId: 'products',
183
- protocol: 'database',
184
- intent: 'read',
185
- },
182
+ it('rejects internal APIs during phase 1', async () => {
183
+ const internal: ApiDefinition = {
184
+ id: 'future-api',
185
+ origin: 'internal',
186
+ protocol: 'rest',
187
+ basePath: '/v1/future',
188
+ endpoints: {},
189
+ };
190
+ const result = await buildEndpointTestRequest({
191
+ api: internal,
192
+ endpointId: 'products',
193
+ operationId: 'products.list',
194
+ });
195
+ expect(result.ok).toBe(false);
196
+ if (!result.ok) {
197
+ expect(result.diagnostics[0]?.apiId).toBe('future-api');
198
+ expect(result.diagnostics[0]?.message).toContain('not executable in API phase 1');
199
+ }
200
+ });
201
+
202
+ it('never treats database protocol operations as API HTTP operations', async () => {
203
+ const api: ExternalRestApiDefinition = {
204
+ id: 'broken-api',
205
+ origin: 'external',
206
+ protocol: 'rest',
207
+ baseUrl: 'https://example.com',
208
+ endpoints: {
209
+ products: {
210
+ id: 'products',
211
+ kind: 'http',
212
+ operations: {
213
+ 'products.list': {
214
+ id: 'products.list',
215
+ endpointId: 'products',
216
+ protocol: 'database',
217
+ intent: 'read',
218
+ path: '/products',
186
219
  },
187
220
  },
188
221
  },
189
- };
190
- const result = await buildEndpointTestRequest({
191
- dataSource: source,
192
- endpointId: 'products',
193
- operationId: 'products.list',
194
- });
195
- expect(result.ok).toBe(false);
196
- if (!result.ok) expect(result.diagnostics[0]?.message).toContain('adapter executor');
222
+ },
223
+ };
224
+ const result = await buildEndpointTestRequest({
225
+ api,
226
+ endpointId: 'products',
227
+ operationId: 'products.list',
197
228
  });
229
+ expect(result.ok).toBe(false);
230
+ if (!result.ok) expect(result.diagnostics[0]?.message).toContain("protocol 'database'");
198
231
  });