@furystack/rest 8.0.42 → 8.1.1

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 (54) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +37 -1
  3. package/esm/api-endpoint-schema.d.ts +47 -2
  4. package/esm/api-endpoint-schema.d.ts.map +1 -1
  5. package/esm/index.d.ts +4 -1
  6. package/esm/index.d.ts.map +1 -1
  7. package/esm/index.js +4 -1
  8. package/esm/index.js.map +1 -1
  9. package/esm/openapi-document.d.ts +303 -0
  10. package/esm/openapi-document.d.ts.map +1 -0
  11. package/esm/openapi-document.js +2 -0
  12. package/esm/openapi-document.js.map +1 -0
  13. package/esm/openapi-resolve-refs.d.ts +20 -0
  14. package/esm/openapi-resolve-refs.d.ts.map +1 -0
  15. package/esm/openapi-resolve-refs.js +68 -0
  16. package/esm/openapi-resolve-refs.js.map +1 -0
  17. package/esm/openapi-resolve-refs.spec.d.ts +2 -0
  18. package/esm/openapi-resolve-refs.spec.d.ts.map +1 -0
  19. package/esm/openapi-resolve-refs.spec.js +294 -0
  20. package/esm/openapi-resolve-refs.spec.js.map +1 -0
  21. package/esm/openapi-to-rest-api.d.ts +197 -0
  22. package/esm/openapi-to-rest-api.d.ts.map +1 -0
  23. package/esm/openapi-to-rest-api.js +2 -0
  24. package/esm/openapi-to-rest-api.js.map +1 -0
  25. package/esm/openapi-to-rest-api.spec.d.ts +2 -0
  26. package/esm/openapi-to-rest-api.spec.d.ts.map +1 -0
  27. package/esm/openapi-to-rest-api.spec.js +665 -0
  28. package/esm/openapi-to-rest-api.spec.js.map +1 -0
  29. package/esm/openapi-to-schema.d.ts +24 -0
  30. package/esm/openapi-to-schema.d.ts.map +1 -0
  31. package/esm/openapi-to-schema.js +145 -0
  32. package/esm/openapi-to-schema.js.map +1 -0
  33. package/esm/openapi-to-schema.spec.d.ts +2 -0
  34. package/esm/openapi-to-schema.spec.d.ts.map +1 -0
  35. package/esm/openapi-to-schema.spec.js +610 -0
  36. package/esm/openapi-to-schema.spec.js.map +1 -0
  37. package/esm/rest-api.d.ts +21 -4
  38. package/esm/rest-api.d.ts.map +1 -1
  39. package/esm/swagger-document.d.ts +2 -195
  40. package/esm/swagger-document.d.ts.map +1 -1
  41. package/esm/swagger-document.js +2 -1
  42. package/esm/swagger-document.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/api-endpoint-schema.ts +56 -3
  45. package/src/index.ts +4 -1
  46. package/src/openapi-document.ts +328 -0
  47. package/src/openapi-resolve-refs.spec.ts +324 -0
  48. package/src/openapi-resolve-refs.ts +71 -0
  49. package/src/openapi-to-rest-api.spec.ts +823 -0
  50. package/src/openapi-to-rest-api.ts +263 -0
  51. package/src/openapi-to-schema.spec.ts +707 -0
  52. package/src/openapi-to-schema.ts +163 -0
  53. package/src/rest-api.ts +26 -5
  54. package/src/swagger-document.ts +2 -220
@@ -0,0 +1,163 @@
1
+ import type { ApiDocumentMetadata, ApiEndpointSchema } from './api-endpoint-schema.js'
2
+ import type { Method } from './methods.js'
3
+ import type { OpenApiDocument, Operation, ReferenceObject, SecuritySchemeObject } from './openapi-document.js'
4
+
5
+ const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace'] as const
6
+
7
+ const isReferenceObject = (obj: unknown): obj is ReferenceObject =>
8
+ typeof obj === 'object' && obj !== null && '$ref' in obj
9
+
10
+ /**
11
+ * Converts an OpenAPI `{param}` path format to FuryStack `:param` format.
12
+ *
13
+ * @param path - The OpenAPI path (e.g. `/users/{id}`)
14
+ * @returns The FuryStack path (e.g. `/users/:id`)
15
+ */
16
+ export const convertOpenApiPathToFuryStack = (path: string): string => path.replace(/\{([^{}]+)\}/g, ':$1')
17
+
18
+ const extractResponseSchema = (operation: Operation): unknown => {
19
+ for (const statusCode of ['200', '201', '2XX', 'default']) {
20
+ const response = operation.responses?.[statusCode]
21
+ if (!response || isReferenceObject(response)) continue
22
+ const jsonContent = response.content?.['application/json']
23
+ if (jsonContent?.schema) return jsonContent.schema
24
+ }
25
+ return undefined
26
+ }
27
+
28
+ const extractSchemaName = (operation: Operation, method: string, path: string): string => {
29
+ if (operation.operationId) return operation.operationId
30
+ const cleanPath = path
31
+ .replace(/\{([^{}]+)\}/g, '$1')
32
+ .replace(/\//g, '_')
33
+ .replace(/^_/, '')
34
+ return `${method}_${cleanPath}`
35
+ }
36
+
37
+ const isOperationAuthenticated = (operation: Operation, docSecurity?: OpenApiDocument['security']): boolean => {
38
+ if (operation.security !== undefined) {
39
+ return operation.security.length > 0
40
+ }
41
+ if (docSecurity !== undefined) {
42
+ return docSecurity.length > 0
43
+ }
44
+ return false
45
+ }
46
+
47
+ const extractSecuritySchemeNames = (
48
+ operation: Operation,
49
+ docSecurity?: OpenApiDocument['security'],
50
+ ): string[] | undefined => {
51
+ const security = operation.security !== undefined ? operation.security : docSecurity
52
+ if (!security || security.length === 0) return undefined
53
+ const names = security.flatMap((req) => Object.keys(req))
54
+ return names.length > 0 ? names : undefined
55
+ }
56
+
57
+ const extractDocumentMetadata = (doc: OpenApiDocument): ApiDocumentMetadata | undefined => {
58
+ const metadata: ApiDocumentMetadata = {}
59
+ let hasMetadata = false
60
+
61
+ if (doc.info.summary) {
62
+ metadata.summary = doc.info.summary
63
+ hasMetadata = true
64
+ }
65
+ if (doc.info.termsOfService) {
66
+ metadata.termsOfService = doc.info.termsOfService
67
+ hasMetadata = true
68
+ }
69
+ if (doc.info.contact) {
70
+ metadata.contact = doc.info.contact
71
+ hasMetadata = true
72
+ }
73
+ if (doc.info.license) {
74
+ metadata.license = doc.info.license
75
+ hasMetadata = true
76
+ }
77
+ if (doc.servers?.length) {
78
+ metadata.servers = doc.servers
79
+ hasMetadata = true
80
+ }
81
+ if (doc.tags?.length) {
82
+ metadata.tags = doc.tags
83
+ hasMetadata = true
84
+ }
85
+ if (doc.externalDocs) {
86
+ metadata.externalDocs = doc.externalDocs
87
+ hasMetadata = true
88
+ }
89
+ const schemes = doc.components?.securitySchemes
90
+ if (schemes) {
91
+ const resolved: Record<string, SecuritySchemeObject> = {}
92
+ for (const [name, scheme] of Object.entries(schemes)) {
93
+ if (!isReferenceObject(scheme)) {
94
+ resolved[name] = scheme
95
+ }
96
+ }
97
+ if (Object.keys(resolved).length > 0) {
98
+ metadata.securitySchemes = resolved
99
+ hasMetadata = true
100
+ }
101
+ }
102
+
103
+ return hasMetadata ? metadata : undefined
104
+ }
105
+
106
+ /**
107
+ * Converts an OpenAPI 3.x document to a FuryStack `ApiEndpointSchema`.
108
+ *
109
+ * This enables consuming external OpenAPI documents with FuryStack's runtime pipeline.
110
+ * Preserves operation-level metadata (tags, deprecated, summary, description) and
111
+ * document-level metadata (servers, tags, contact, license, securitySchemes).
112
+ *
113
+ * **Important:** If the document contains `$ref` pointers, call `resolveOpenApiRefs(doc)`
114
+ * first to inline them. This function does not resolve `$ref` on its own.
115
+ *
116
+ * @param doc - The OpenAPI document to convert
117
+ * @returns An ApiEndpointSchema that can be used with FuryStack's API tools
118
+ */
119
+ export const openApiToSchema = (doc: OpenApiDocument): ApiEndpointSchema => {
120
+ const endpoints: ApiEndpointSchema['endpoints'] = {}
121
+
122
+ if (doc.paths) {
123
+ for (const [openApiPath, pathItemOrRef] of Object.entries(doc.paths)) {
124
+ if (isReferenceObject(pathItemOrRef)) continue
125
+ const pathItem = pathItemOrRef
126
+ const furyStackPath = convertOpenApiPathToFuryStack(openApiPath)
127
+
128
+ for (const method of HTTP_METHODS) {
129
+ const operation = pathItem[method]
130
+ if (!operation) continue
131
+
132
+ const upperMethod = method.toUpperCase() as Method
133
+ const methodEndpoints = endpoints[upperMethod] ?? {}
134
+ endpoints[upperMethod] = methodEndpoints
135
+
136
+ const responseSchema = extractResponseSchema(operation)
137
+ const schemaName = extractSchemaName(operation, method, openApiPath)
138
+
139
+ const securitySchemeNames = extractSecuritySchemeNames(operation, doc.security)
140
+
141
+ methodEndpoints[furyStackPath] = {
142
+ path: furyStackPath,
143
+ schema: responseSchema ?? {},
144
+ schemaName,
145
+ isAuthenticated: isOperationAuthenticated(operation, doc.security),
146
+ ...(securitySchemeNames ? { securitySchemes: securitySchemeNames } : {}),
147
+ ...(operation.tags?.length ? { tags: operation.tags } : {}),
148
+ ...(operation.deprecated ? { deprecated: true } : {}),
149
+ ...(operation.summary ? { summary: operation.summary } : {}),
150
+ ...(operation.description ? { description: operation.description } : {}),
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ return {
157
+ name: doc.info.title,
158
+ description: doc.info.description ?? '',
159
+ version: doc.info.version,
160
+ metadata: extractDocumentMetadata(doc),
161
+ endpoints,
162
+ }
163
+ }
package/src/rest-api.ts CHANGED
@@ -1,20 +1,41 @@
1
1
  import type { ApiEndpointSchema } from './api-endpoint-schema.js'
2
2
  import type { Method } from './methods.js'
3
- import type { SwaggerDocument } from './swagger-document.js'
3
+ import type { OpenApiDocument } from './openapi-document.js'
4
4
 
5
+ /**
6
+ * Defines the shape of a REST API as a mapping of HTTP methods to path-endpoint pairs.
7
+ * Each endpoint describes its result type and optionally its URL parameters, query parameters,
8
+ * request body, headers, and metadata like tags/deprecated/summary/description.
9
+ *
10
+ * This type is the shared contract between `@furystack/rest-service` (server) and
11
+ * `@furystack/rest-client-fetch` (client). It can also be derived from an OpenAPI document
12
+ * using `OpenApiToRestApi<T>`.
13
+ */
5
14
  export type RestApi = {
6
15
  [TMethod in Method]?: {
7
- [TUrl: string]: { result: unknown; url?: unknown; query?: unknown; body?: unknown; headers?: unknown }
16
+ [TUrl: string]: {
17
+ result: unknown
18
+ url?: unknown
19
+ query?: unknown
20
+ body?: unknown
21
+ headers?: unknown
22
+ tags?: string[]
23
+ deprecated?: boolean
24
+ summary?: string
25
+ description?: string
26
+ }
8
27
  }
9
28
  }
10
29
 
11
30
  /**
12
- * Represents an API with a GET action to retrieve its schema.
13
- * This type extends the base RestApi type to include a specific GET endpoint for schema retrieval.
31
+ * Represents an API with a GET action to retrieve its schema and OpenAPI document.
32
+ * This type extends the base RestApi type to include specific GET endpoints for schema and OpenAPI retrieval.
14
33
  */
15
34
  export type WithSchemaAction<T extends RestApi> = T & {
16
35
  GET: {
17
36
  '/schema': { result: ApiEndpointSchema<T>; headers: { accept: 'application/schema+json' } }
18
- '/swagger.json': { result: SwaggerDocument }
37
+ '/openapi.json': { result: OpenApiDocument }
38
+ /** @deprecated Use `/openapi.json` instead. This endpoint will be removed in a future major version. */
39
+ '/swagger.json': { result: OpenApiDocument }
19
40
  }
20
41
  }
@@ -1,220 +1,2 @@
1
- export type SwaggerDocument = {
2
- openapi: string
3
- info: InfoObject
4
- jsonSchemaDialect?: string
5
- externalDocs?: ExternalDocumentationObject
6
- servers?: ServerObject[]
7
- tags?: TagObject[]
8
- security?: SecurityRequirementObject[]
9
- paths?: Record<string, PathItem>
10
- webhooks?: Record<string, PathItem>
11
- components?: ComponentsObject
12
- [key: `x-${string}`]: unknown
13
- }
14
-
15
- export type InfoObject = {
16
- title: string
17
- version: string
18
- description?: string
19
- summary?: string
20
- termsOfService?: string
21
- contact?: ContactObject
22
- license?: LicenseObject
23
- [key: `x-${string}`]: unknown
24
- }
25
-
26
- export type ContactObject = {
27
- name?: string
28
- url?: string
29
- email?: string
30
- [key: `x-${string}`]: unknown
31
- }
32
-
33
- export type LicenseObject = {
34
- name: string
35
- identifier?: string
36
- url?: string
37
- [key: `x-${string}`]: unknown
38
- }
39
-
40
- export type ExternalDocumentationObject = {
41
- url: string
42
- description?: string
43
- [key: `x-${string}`]: unknown
44
- }
45
-
46
- export type ServerObject = {
47
- url: string
48
- description?: string
49
- variables?: Record<string, ServerVariableObject>
50
- [key: `x-${string}`]: unknown
51
- }
52
-
53
- export type ServerVariableObject = {
54
- default: string
55
- description?: string
56
- enum?: string[]
57
- [key: `x-${string}`]: unknown
58
- }
59
-
60
- export type TagObject = {
61
- name: string
62
- description?: string
63
- externalDocs?: ExternalDocumentationObject
64
- [key: `x-${string}`]: unknown
65
- }
66
-
67
- export type SecurityRequirementObject = Record<string, string[]>
68
-
69
- export type ComponentsObject = {
70
- schemas?: Record<string, object | boolean>
71
- responses?: Record<string, ResponseObject | ReferenceObject>
72
- parameters?: Record<string, ParameterObject | ReferenceObject>
73
- examples?: Record<string, ExampleObject | ReferenceObject>
74
- requestBodies?: Record<string, RequestBodyObject | ReferenceObject>
75
- headers?: Record<string, HeaderObject | ReferenceObject>
76
- securitySchemes?: Record<string, SecuritySchemeObject | ReferenceObject>
77
- links?: Record<string, LinkObject | ReferenceObject>
78
- callbacks?: Record<string, CallbackObject | ReferenceObject>
79
- pathItems?: Record<string, PathItem | ReferenceObject>
80
- [key: `x-${string}`]: unknown
81
- }
82
-
83
- export type ReferenceObject = {
84
- $ref: string
85
- description?: string
86
- summary?: string
87
- }
88
-
89
- export type PathItem = {
90
- summary?: string
91
- description?: string
92
- get?: Operation
93
- put?: Operation
94
- post?: Operation
95
- delete?: Operation
96
- options?: Operation
97
- head?: Operation
98
- patch?: Operation
99
- trace?: Operation
100
- servers?: ServerObject[]
101
- parameters?: Array<ParameterObject | ReferenceObject>
102
- [key: `x-${string}`]: unknown
103
- }
104
-
105
- export type Operation = {
106
- tags?: string[]
107
- summary?: string
108
- description?: string
109
- externalDocs?: ExternalDocumentationObject
110
- operationId?: string
111
- parameters?: Array<ParameterObject | ReferenceObject>
112
- requestBody?: RequestBodyObject | ReferenceObject
113
- responses: ResponsesObject
114
- callbacks?: Record<string, CallbackObject | ReferenceObject>
115
- deprecated?: boolean
116
- security?: SecurityRequirementObject[]
117
- servers?: ServerObject[]
118
- [key: `x-${string}`]: unknown
119
- }
120
-
121
- export type ParameterObject = {
122
- name: string
123
- in: 'query' | 'header' | 'path' | 'cookie'
124
- description?: string
125
- required?: boolean
126
- deprecated?: boolean
127
- allowEmptyValue?: boolean
128
- style?: string
129
- explode?: boolean
130
- allowReserved?: boolean
131
- schema?: object | boolean
132
- example?: unknown
133
- examples?: Record<string, ExampleObject | ReferenceObject>
134
- content?: Record<string, MediaTypeObject>
135
- [key: `x-${string}`]: unknown
136
- }
137
-
138
- export type RequestBodyObject = {
139
- description?: string
140
- content: Record<string, MediaTypeObject>
141
- required?: boolean
142
- [key: `x-${string}`]: unknown
143
- }
144
-
145
- export type MediaTypeObject = {
146
- schema?: object | boolean
147
- example?: unknown
148
- examples?: Record<string, ExampleObject | ReferenceObject>
149
- encoding?: Record<string, EncodingObject>
150
- [key: `x-${string}`]: unknown
151
- }
152
-
153
- export type EncodingObject = {
154
- contentType?: string
155
- headers?: Record<string, HeaderObject | ReferenceObject>
156
- style?: string
157
- explode?: boolean
158
- allowReserved?: boolean
159
- [key: `x-${string}`]: unknown
160
- }
161
-
162
- export type ResponsesObject = Record<string, ResponseObject | ReferenceObject>
163
-
164
- export type ResponseObject = {
165
- description: string
166
- headers?: Record<string, HeaderObject | ReferenceObject>
167
- content?: Record<string, MediaTypeObject>
168
- links?: Record<string, LinkObject | ReferenceObject>
169
- [key: `x-${string}`]: unknown
170
- }
171
-
172
- export type HeaderObject = Omit<ParameterObject, 'name' | 'in'>
173
-
174
- export type ExampleObject = {
175
- summary?: string
176
- description?: string
177
- value?: unknown
178
- externalValue?: string
179
- [key: `x-${string}`]: unknown
180
- }
181
-
182
- export type LinkObject = {
183
- operationRef?: string
184
- operationId?: string
185
- parameters?: Record<string, unknown>
186
- requestBody?: unknown
187
- description?: string
188
- server?: ServerObject
189
- [key: `x-${string}`]: unknown
190
- }
191
-
192
- export type CallbackObject = Record<string, PathItem>
193
-
194
- export type SecuritySchemeObject = {
195
- type: 'apiKey' | 'http' | 'mutualTLS' | 'oauth2' | 'openIdConnect'
196
- description?: string
197
- name?: string
198
- in?: 'query' | 'header' | 'cookie'
199
- scheme?: string
200
- bearerFormat?: string
201
- flows?: OAuthFlowsObject
202
- openIdConnectUrl?: string
203
- [key: `x-${string}`]: unknown
204
- }
205
-
206
- export type OAuthFlowsObject = {
207
- implicit?: OAuthFlowObject
208
- password?: OAuthFlowObject
209
- clientCredentials?: OAuthFlowObject
210
- authorizationCode?: OAuthFlowObject
211
- [key: `x-${string}`]: unknown
212
- }
213
-
214
- export type OAuthFlowObject = {
215
- authorizationUrl?: string
216
- tokenUrl?: string
217
- refreshUrl?: string
218
- scopes: Record<string, string>
219
- [key: `x-${string}`]: unknown
220
- }
1
+ /** @deprecated Use imports from './openapi-document.js' instead */
2
+ export * from './openapi-document.js'