@ankhorage/data-sources 0.0.0 → 0.1.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.
@@ -0,0 +1,158 @@
1
+ const REST_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'];
2
+ export function isManualRestMethod(method) {
3
+ return REST_METHODS.some((restMethod) => restMethod === method);
4
+ }
5
+ export function normalizeManualRestMethod(method) {
6
+ return method.toUpperCase();
7
+ }
8
+ export function extractRestPathParams(path) {
9
+ const params = new Set();
10
+ const bracedParamPattern = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
11
+ const colonParamPattern = /(^|\/):([A-Za-z_][A-Za-z0-9_]*)/g;
12
+ for (const match of path.matchAll(bracedParamPattern)) {
13
+ const [, param] = match;
14
+ if (param !== undefined)
15
+ params.add(param);
16
+ }
17
+ for (const match of path.matchAll(colonParamPattern)) {
18
+ const [, , param] = match;
19
+ if (param !== undefined)
20
+ params.add(param);
21
+ }
22
+ return [...params];
23
+ }
24
+ export function validateManualRestDataSource(definition) {
25
+ const diagnostics = [];
26
+ if (definition.baseUrl.trim().length === 0) {
27
+ diagnostics.push({
28
+ code: 'invalid-config',
29
+ dataSourceId: definition.id,
30
+ message: 'Manual REST data source requires a non-empty baseUrl.',
31
+ path: 'baseUrl',
32
+ severity: 'error',
33
+ });
34
+ }
35
+ for (const endpoint of definition.endpoints) {
36
+ if (!endpoint.path.startsWith('/')) {
37
+ diagnostics.push({
38
+ code: 'invalid-config',
39
+ dataSourceId: definition.id,
40
+ endpointId: endpoint.id,
41
+ message: 'Manual REST endpoint paths must start with `/`.',
42
+ path: `endpoints.${endpoint.id}.path`,
43
+ severity: 'error',
44
+ });
45
+ }
46
+ for (const operation of endpoint.operations) {
47
+ const normalizedMethod = normalizeManualRestMethod(operation.method);
48
+ const operationPath = operation.path ?? endpoint.path;
49
+ const templateParams = extractRestPathParams(operationPath);
50
+ const pathParameters = operation.parameters?.filter((parameter) => parameter.location === 'path') ?? [];
51
+ const pathParameterNames = new Set(pathParameters.map((parameter) => parameter.name));
52
+ if (!isManualRestMethod(normalizedMethod)) {
53
+ diagnostics.push({
54
+ code: 'invalid-config',
55
+ dataSourceId: definition.id,
56
+ endpointId: endpoint.id,
57
+ operationId: operation.id,
58
+ message: `Manual REST operation method must be one of ${REST_METHODS.join(', ')}.`,
59
+ path: `endpoints.${endpoint.id}.operations.${operation.id}.method`,
60
+ severity: 'error',
61
+ });
62
+ }
63
+ for (const templateParam of templateParams) {
64
+ if (!pathParameterNames.has(templateParam)) {
65
+ diagnostics.push({
66
+ code: 'invalid-config',
67
+ dataSourceId: definition.id,
68
+ endpointId: endpoint.id,
69
+ operationId: operation.id,
70
+ message: `Path template parameter '${templateParam}' must have a matching path parameter definition.`,
71
+ path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters`,
72
+ severity: 'error',
73
+ });
74
+ }
75
+ }
76
+ for (const pathParameter of pathParameters) {
77
+ if (!templateParams.includes(pathParameter.name)) {
78
+ diagnostics.push({
79
+ code: 'invalid-config',
80
+ dataSourceId: definition.id,
81
+ endpointId: endpoint.id,
82
+ operationId: operation.id,
83
+ message: `Path parameter '${pathParameter.name}' is not referenced by the operation path template.`,
84
+ path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters.${pathParameter.name}`,
85
+ severity: 'error',
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return diagnostics;
92
+ }
93
+ export function createManualRestDataSource(definition) {
94
+ const diagnostics = validateManualRestDataSource(definition);
95
+ const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');
96
+ if (hasErrors) {
97
+ return { ok: false, diagnostics };
98
+ }
99
+ return {
100
+ ok: true,
101
+ data: normalizeManualRestDataSource(definition),
102
+ diagnostics,
103
+ };
104
+ }
105
+ export function normalizeManualRestDataSource(definition) {
106
+ const endpoints = {};
107
+ for (const endpoint of definition.endpoints) {
108
+ const operations = {};
109
+ for (const operation of endpoint.operations) {
110
+ const operationPath = operation.path ?? endpoint.path;
111
+ const { parameters } = operation;
112
+ const request = operation.request === undefined && parameters === undefined
113
+ ? undefined
114
+ : {
115
+ ...operation.request,
116
+ parameters,
117
+ };
118
+ operations[operation.id] = {
119
+ id: operation.id,
120
+ endpointId: endpoint.id,
121
+ name: operation.name,
122
+ description: operation.description,
123
+ protocol: 'http',
124
+ intent: operation.intent,
125
+ method: normalizeManualRestMethod(operation.method),
126
+ path: operationPath,
127
+ request,
128
+ response: operation.response,
129
+ pagination: operation.pagination,
130
+ credential: operation.credential,
131
+ metadata: operation.metadata,
132
+ };
133
+ }
134
+ endpoints[endpoint.id] = {
135
+ id: endpoint.id,
136
+ kind: 'http',
137
+ name: endpoint.name,
138
+ description: endpoint.description,
139
+ baseUrl: definition.baseUrl,
140
+ path: endpoint.path,
141
+ credential: endpoint.credential,
142
+ operations,
143
+ metadata: endpoint.metadata,
144
+ };
145
+ }
146
+ return {
147
+ id: definition.id,
148
+ kind: 'rest',
149
+ name: definition.name,
150
+ description: definition.description,
151
+ baseUrl: definition.baseUrl,
152
+ credential: definition.credential,
153
+ endpoints,
154
+ schemas: definition.schemas,
155
+ metadata: definition.metadata,
156
+ };
157
+ }
158
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rest/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n CredentialRef,\n DataContractValue,\n DataEndpointConfig,\n DataOperationConfig,\n DataOperationIntent,\n DataOperationMethod,\n DataOperationPagination,\n DataOperationParameter,\n DataOperationRequest,\n DataOperationResponse,\n DataSchemaRegistry,\n DataSourceDiagnostic,\n DataSourceDiagnosticResult,\n EndpointId,\n OperationId,\n RestDataSourceConfig,\n} from '@ankhorage/contracts/data';\n\nconst REST_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'] as const;\n\nexport type ManualRestMethod = (typeof REST_METHODS)[number];\n\nexport interface ManualRestOperationDefinition {\n readonly id: OperationId;\n readonly intent: DataOperationIntent;\n readonly method: string;\n readonly name?: string;\n readonly description?: string;\n readonly path?: string;\n readonly credential?: CredentialRef;\n readonly parameters?: readonly DataOperationParameter[];\n readonly request?: DataOperationRequest;\n readonly response?: DataOperationResponse;\n readonly pagination?: DataOperationPagination;\n readonly metadata?: DataContractValue;\n}\n\nexport interface ManualRestEndpointDefinition {\n readonly id: EndpointId;\n readonly path: string;\n readonly name?: string;\n readonly description?: string;\n readonly credential?: CredentialRef;\n readonly operations: readonly ManualRestOperationDefinition[];\n readonly metadata?: DataContractValue;\n}\n\nexport interface ManualRestDataSourceDefinition {\n readonly id: string;\n readonly baseUrl: string;\n readonly name?: string;\n readonly description?: string;\n readonly credential?: CredentialRef;\n readonly endpoints: readonly ManualRestEndpointDefinition[];\n readonly schemas?: DataSchemaRegistry;\n readonly metadata?: DataContractValue;\n}\n\nexport function isManualRestMethod(method: string): method is ManualRestMethod {\n return REST_METHODS.some((restMethod) => restMethod === method);\n}\n\nexport function normalizeManualRestMethod(method: string): DataOperationMethod {\n return method.toUpperCase();\n}\n\nexport function extractRestPathParams(path: string): readonly string[] {\n const params = new Set<string>();\n const bracedParamPattern = /\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n const colonParamPattern = /(^|\\/):([A-Za-z_][A-Za-z0-9_]*)/g;\n\n for (const match of path.matchAll(bracedParamPattern)) {\n const [, param] = match;\n if (param !== undefined) params.add(param);\n }\n\n for (const match of path.matchAll(colonParamPattern)) {\n const [, , param] = match;\n if (param !== undefined) params.add(param);\n }\n\n return [...params];\n}\n\nexport function validateManualRestDataSource(\n definition: ManualRestDataSourceDefinition,\n): readonly DataSourceDiagnostic[] {\n const diagnostics: DataSourceDiagnostic[] = [];\n\n if (definition.baseUrl.trim().length === 0) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n message: 'Manual REST data source requires a non-empty baseUrl.',\n path: 'baseUrl',\n severity: 'error',\n });\n }\n\n for (const endpoint of definition.endpoints) {\n if (!endpoint.path.startsWith('/')) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: endpoint.id,\n message: 'Manual REST endpoint paths must start with `/`.',\n path: `endpoints.${endpoint.id}.path`,\n severity: 'error',\n });\n }\n\n for (const operation of endpoint.operations) {\n const normalizedMethod = normalizeManualRestMethod(operation.method);\n const operationPath = operation.path ?? endpoint.path;\n const templateParams = extractRestPathParams(operationPath);\n const pathParameters =\n operation.parameters?.filter((parameter) => parameter.location === 'path') ?? [];\n const pathParameterNames = new Set(pathParameters.map((parameter) => parameter.name));\n\n if (!isManualRestMethod(normalizedMethod)) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: endpoint.id,\n operationId: operation.id,\n message: `Manual REST operation method must be one of ${REST_METHODS.join(', ')}.`,\n path: `endpoints.${endpoint.id}.operations.${operation.id}.method`,\n severity: 'error',\n });\n }\n\n for (const templateParam of templateParams) {\n if (!pathParameterNames.has(templateParam)) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: endpoint.id,\n operationId: operation.id,\n message: `Path template parameter '${templateParam}' must have a matching path parameter definition.`,\n path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters`,\n severity: 'error',\n });\n }\n }\n\n for (const pathParameter of pathParameters) {\n if (!templateParams.includes(pathParameter.name)) {\n diagnostics.push({\n code: 'invalid-config',\n dataSourceId: definition.id,\n endpointId: endpoint.id,\n operationId: operation.id,\n message: `Path parameter '${pathParameter.name}' is not referenced by the operation path template.`,\n path: `endpoints.${endpoint.id}.operations.${operation.id}.parameters.${pathParameter.name}`,\n severity: 'error',\n });\n }\n }\n }\n }\n\n return diagnostics;\n}\n\nexport function createManualRestDataSource(\n definition: ManualRestDataSourceDefinition,\n): DataSourceDiagnosticResult<RestDataSourceConfig> {\n const diagnostics = validateManualRestDataSource(definition);\n const hasErrors = diagnostics.some((diagnostic) => diagnostic.severity === 'error');\n\n if (hasErrors) {\n return { ok: false, diagnostics };\n }\n\n return {\n ok: true,\n data: normalizeManualRestDataSource(definition),\n diagnostics,\n };\n}\n\nexport function normalizeManualRestDataSource(\n definition: ManualRestDataSourceDefinition,\n): RestDataSourceConfig {\n const endpoints: Record<EndpointId, DataEndpointConfig> = {};\n\n for (const endpoint of definition.endpoints) {\n const operations: Record<OperationId, DataOperationConfig> = {};\n\n for (const operation of endpoint.operations) {\n const operationPath = operation.path ?? endpoint.path;\n const { parameters } = operation;\n const request: DataOperationRequest | undefined =\n operation.request === undefined && parameters === undefined\n ? undefined\n : {\n ...operation.request,\n parameters,\n };\n\n operations[operation.id] = {\n id: operation.id,\n endpointId: endpoint.id,\n name: operation.name,\n description: operation.description,\n protocol: 'http',\n intent: operation.intent,\n method: normalizeManualRestMethod(operation.method),\n path: operationPath,\n request,\n response: operation.response,\n pagination: operation.pagination,\n credential: operation.credential,\n metadata: operation.metadata,\n };\n }\n\n endpoints[endpoint.id] = {\n id: endpoint.id,\n kind: 'http',\n name: endpoint.name,\n description: endpoint.description,\n baseUrl: definition.baseUrl,\n path: endpoint.path,\n credential: endpoint.credential,\n operations,\n metadata: endpoint.metadata,\n };\n }\n\n return {\n id: definition.id,\n kind: 'rest',\n name: definition.name,\n description: definition.description,\n baseUrl: definition.baseUrl,\n credential: definition.credential,\n endpoints,\n schemas: definition.schemas,\n metadata: definition.metadata,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/data-sources",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.1.0",
5
5
  "description": "Headless toolkit for importing, generating, normalizing, and testing Ankhorage data-source configs.",
6
6
  "homepage": "https://github.com/ankhorage/data-sources#readme",
7
7
  "bugs": {
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { DataSourceConfig, DataSourceKind } from '@ankhorage/contracts/data';
2
2
 
3
+ export * from './openapi';
4
+ export * from './rest';
5
+
3
6
  export const DATA_SOURCES_PACKAGE_NAME = '@ankhorage/data-sources' as const;
4
7
 
5
8
  export const SUPPORTED_DATA_SOURCE_KINDS = [
@@ -0,0 +1,296 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import {
4
+ importOpenApiDocument,
5
+ normalizeOpenApiEndpointId,
6
+ normalizeOpenApiOperationId,
7
+ normalizeOpenApiSchema,
8
+ type OpenApiDocumentObject,
9
+ } from './index';
10
+
11
+ function assertSerializable<TValue>(value: TValue): void {
12
+ expect(JSON.parse(JSON.stringify(value))).toEqual(value);
13
+ }
14
+
15
+ function createPetStoreDocument(): OpenApiDocumentObject {
16
+ return {
17
+ openapi: '3.1.0',
18
+ info: {
19
+ title: 'Pet Store',
20
+ version: '2026-05-19',
21
+ description: 'Example API.',
22
+ },
23
+ servers: [
24
+ {
25
+ url: 'https://api.example.com',
26
+ },
27
+ ],
28
+ components: {
29
+ schemas: {
30
+ Pet: {
31
+ type: 'object',
32
+ required: ['id', 'name'],
33
+ properties: {
34
+ id: { type: 'string' },
35
+ name: { type: 'string' },
36
+ age: { type: 'integer' },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ paths: {
42
+ '/pets': {
43
+ get: {
44
+ operationId: 'listPets',
45
+ summary: 'List pets',
46
+ parameters: [
47
+ {
48
+ name: 'limit',
49
+ in: 'query',
50
+ schema: { type: 'integer', default: 20 },
51
+ },
52
+ ],
53
+ responses: {
54
+ '200': {
55
+ description: 'Pets',
56
+ content: {
57
+ 'application/json': {
58
+ schema: {
59
+ type: 'array',
60
+ items: { $ref: '#/components/schemas/Pet' },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ },
66
+ },
67
+ post: {
68
+ operationId: 'createPet',
69
+ summary: 'Create pet',
70
+ requestBody: {
71
+ required: true,
72
+ content: {
73
+ 'application/json': {
74
+ schema: { $ref: '#/components/schemas/Pet' },
75
+ },
76
+ },
77
+ },
78
+ responses: {
79
+ '201': {
80
+ description: 'Created pet',
81
+ content: {
82
+ 'application/json': {
83
+ schema: { $ref: '#/components/schemas/Pet' },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ },
90
+ '/pets/{petId}': {
91
+ parameters: [
92
+ {
93
+ name: 'petId',
94
+ in: 'path',
95
+ required: true,
96
+ schema: { type: 'string' },
97
+ },
98
+ ],
99
+ get: {
100
+ operationId: 'getPet',
101
+ responses: {
102
+ '200': {
103
+ description: 'Pet',
104
+ content: {
105
+ 'application/json': {
106
+ schema: { $ref: '#/components/schemas/Pet' },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ },
112
+ patch: {
113
+ operationId: 'updatePet',
114
+ requestBody: {
115
+ content: {
116
+ 'application/json': {
117
+ schema: { $ref: '#/components/schemas/Pet' },
118
+ },
119
+ },
120
+ },
121
+ responses: {
122
+ '200': {
123
+ description: 'Updated pet',
124
+ content: {
125
+ 'application/json': {
126
+ schema: { $ref: '#/components/schemas/Pet' },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ },
132
+ delete: {
133
+ operationId: 'deletePet',
134
+ responses: {
135
+ '204': {
136
+ description: 'Deleted pet',
137
+ },
138
+ },
139
+ },
140
+ },
141
+ },
142
+ };
143
+ }
144
+
145
+ describe('OpenAPI import normalization', () => {
146
+ it('normalizes deterministic endpoint and operation ids', () => {
147
+ expect(normalizeOpenApiEndpointId('/pets/{petId}')).toBe('pets-petid');
148
+ expect(normalizeOpenApiOperationId('get', '/pets/{petId}')).toBe('get-pets-petid');
149
+ expect(normalizeOpenApiOperationId('get', '/', '')).toBe('get-root');
150
+ });
151
+
152
+ it('normalizes OpenAPI schemas into data schemas', () => {
153
+ const schema = normalizeOpenApiSchema({
154
+ type: 'object',
155
+ properties: {
156
+ id: { type: 'string' },
157
+ tags: { type: 'array', items: { type: 'string' } },
158
+ },
159
+ required: ['id'],
160
+ });
161
+
162
+ assertSerializable(schema);
163
+ expect(schema.type).toBe('object');
164
+ expect(schema.properties?.tags?.items?.type).toBe('string');
165
+ });
166
+
167
+ it('imports a minimal OpenAPI document into a normalized OpenAPI data source', () => {
168
+ const result = importOpenApiDocument({
169
+ id: 'pet-store',
170
+ document: createPetStoreDocument(),
171
+ documentUrl: 'https://api.example.com/openapi.json',
172
+ credential: {
173
+ id: 'pet-store-token',
174
+ kind: 'bearer',
175
+ },
176
+ });
177
+
178
+ expect(result.ok).toBe(true);
179
+ if (result.ok) {
180
+ assertSerializable(result.data);
181
+ expect(result.data.kind).toBe('openapi');
182
+ expect(result.data.baseUrl).toBe('https://api.example.com');
183
+ expect(result.data.import?.version).toBe('2026-05-19');
184
+ expect(result.data.schemas?.Pet?.properties?.name?.type).toBe('string');
185
+ expect(result.data.endpoints.pets?.operations.listpets?.intent).toBe('read');
186
+ expect(result.data.endpoints.pets?.operations.createpet?.intent).toBe('create');
187
+ expect(
188
+ result.data.endpoints['pets-petid']?.operations.getpet?.request?.parameters?.[0]?.name,
189
+ ).toBe('petId');
190
+ expect(result.data.endpoints['pets-petid']?.operations.updatepet?.intent).toBe('update');
191
+ expect(result.data.endpoints['pets-petid']?.operations.deletepet?.intent).toBe('delete');
192
+ }
193
+ });
194
+
195
+ it('uses an explicit baseUrl override and reports ambiguous servers', () => {
196
+ const document: OpenApiDocumentObject = {
197
+ ...createPetStoreDocument(),
198
+ servers: [{ url: 'https://first.example.com' }, { url: 'https://second.example.com' }],
199
+ };
200
+ const result = importOpenApiDocument({
201
+ id: 'pet-store',
202
+ document,
203
+ baseUrl: 'https://override.example.com',
204
+ });
205
+
206
+ expect(result.ok).toBe(true);
207
+ if (result.ok) {
208
+ expect(result.data.baseUrl).toBe('https://override.example.com');
209
+ expect(result.diagnostics).toEqual([]);
210
+ }
211
+ });
212
+
213
+ it('reports ambiguous server diagnostics when no baseUrl override exists', () => {
214
+ const document: OpenApiDocumentObject = {
215
+ ...createPetStoreDocument(),
216
+ servers: [{ url: 'https://first.example.com' }, { url: 'https://second.example.com' }],
217
+ };
218
+ const result = importOpenApiDocument({ id: 'pet-store', document });
219
+
220
+ expect(result.ok).toBe(true);
221
+ if (result.ok) {
222
+ expect(result.data.baseUrl).toBe('https://first.example.com');
223
+ expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain(
224
+ 'ambiguous-server',
225
+ );
226
+ }
227
+ });
228
+
229
+ it('deduplicates duplicate operation IDs deterministically', () => {
230
+ const document: OpenApiDocumentObject = {
231
+ openapi: '3.1.0',
232
+ paths: {
233
+ '/one': {
234
+ get: { operationId: 'duplicate', responses: { '200': { description: 'OK' } } },
235
+ },
236
+ '/two': {
237
+ get: { operationId: 'duplicate', responses: { '200': { description: 'OK' } } },
238
+ },
239
+ },
240
+ };
241
+ const result = importOpenApiDocument({
242
+ id: 'duplicates',
243
+ document,
244
+ baseUrl: 'https://api.example.com',
245
+ });
246
+
247
+ expect(result.ok).toBe(true);
248
+ if (result.ok) {
249
+ expect(result.data.endpoints.one?.operations.duplicate?.id).toBe('duplicate');
250
+ expect(result.data.endpoints.two?.operations['duplicate-2']?.id).toBe('duplicate-2');
251
+ expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain(
252
+ 'duplicate-operation-id',
253
+ );
254
+ }
255
+ });
256
+
257
+ it('returns an error diagnostic when paths are missing', () => {
258
+ const result = importOpenApiDocument({
259
+ id: 'empty',
260
+ document: { openapi: '3.1.0' },
261
+ baseUrl: 'https://api.example.com',
262
+ });
263
+
264
+ expect(result.ok).toBe(false);
265
+ if (!result.ok) {
266
+ expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toContain('invalid-config');
267
+ }
268
+ });
269
+
270
+ it('reports unsupported schema types as warnings', () => {
271
+ const result = importOpenApiDocument({
272
+ id: 'unsupported-schema',
273
+ baseUrl: 'https://api.example.com',
274
+ document: {
275
+ openapi: '3.1.0',
276
+ components: {
277
+ schemas: {
278
+ Weird: { type: 'file' },
279
+ },
280
+ },
281
+ paths: {
282
+ '/weird': {
283
+ get: { responses: { '200': { description: 'OK' } } },
284
+ },
285
+ },
286
+ },
287
+ });
288
+
289
+ expect(result.ok).toBe(true);
290
+ if (result.ok) {
291
+ expect(result.diagnostics?.map((diagnostic) => diagnostic.code)).toContain(
292
+ 'unsupported-schema',
293
+ );
294
+ }
295
+ });
296
+ });