@open-mercato/shared 0.9.1-develop.7476.1.342138d35e → 0.9.1-develop.7496.1.3346415f94
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/bootstrap/dynamicLoader.js +46 -6
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +6 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +7 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +62 -0
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +7 -0
- package/dist/lib/crud/ids.js +13 -1
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/openapi/generator.js +84 -67
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/registry.js.map +2 -2
- package/package.json +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +141 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +174 -0
- package/src/lib/bootstrap/dynamicLoader.ts +63 -5
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +3 -0
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +130 -0
- package/src/lib/crud/__tests__/ids.test.ts +34 -1
- package/src/lib/crud/ids.ts +24 -0
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +102 -0
- package/src/lib/openapi/generator.ts +106 -77
- package/src/modules/registry.ts +5 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MAX_IDS_PER_REQUEST,
|
|
3
|
+
mergeIdFilter,
|
|
4
|
+
parseIdsParam,
|
|
5
|
+
isIdsParamProvided,
|
|
6
|
+
isUuidListParam,
|
|
7
|
+
splitIdListParam,
|
|
8
|
+
} from '@open-mercato/shared/lib/crud/ids'
|
|
2
9
|
|
|
3
10
|
describe('crud ids helpers', () => {
|
|
4
11
|
const idA = '550e8400-e29b-41d4-a716-446655440001'
|
|
@@ -168,4 +175,30 @@ describe('crud ids helpers', () => {
|
|
|
168
175
|
tenantId: 't1',
|
|
169
176
|
})
|
|
170
177
|
})
|
|
178
|
+
|
|
179
|
+
it('splitIdListParam trims entries and drops empty ones', () => {
|
|
180
|
+
expect(splitIdListParam(` ${idA} ,, ${idB} ,`)).toEqual([idA, idB])
|
|
181
|
+
expect(splitIdListParam(undefined)).toEqual([])
|
|
182
|
+
expect(splitIdListParam(['not', 'a', 'string'])).toEqual([])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('isUuidListParam accepts absent, empty, and all-UUID lists', () => {
|
|
186
|
+
expect(isUuidListParam(undefined)).toBe(true)
|
|
187
|
+
expect(isUuidListParam('')).toBe(true)
|
|
188
|
+
expect(isUuidListParam(` ${idA}, ${idB.toUpperCase()} `)).toBe(true)
|
|
189
|
+
expect(isUuidListParam('00000000-0000-0000-0000-000000000000')).toBe(true)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('isUuidListParam rejects malformed and mixed lists', () => {
|
|
193
|
+
expect(isUuidListParam('not-a-uuid')).toBe(false)
|
|
194
|
+
expect(isUuidListParam(`${idA},not-a-uuid`)).toBe(false)
|
|
195
|
+
expect(isUuidListParam(`${idA}0`)).toBe(false)
|
|
196
|
+
expect(isUuidListParam(`{${idA}}`)).toBe(false)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('isUuidListParam fails closed on non-string values', () => {
|
|
200
|
+
expect(isUuidListParam(['not-a-uuid'])).toBe(false)
|
|
201
|
+
expect(isUuidListParam([idA])).toBe(false)
|
|
202
|
+
expect(isUuidListParam(null)).toBe(false)
|
|
203
|
+
})
|
|
171
204
|
})
|
package/src/lib/crud/ids.ts
CHANGED
|
@@ -51,6 +51,30 @@ export function parseIdsParam(raw: unknown, maxIds: number = MAX_IDS_PER_REQUEST
|
|
|
51
51
|
return parsed.slice(0, safeMax)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const UUID_SHAPE_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Splits a comma-separated id list query param into its trimmed, non-empty entries.
|
|
58
|
+
*/
|
|
59
|
+
export function splitIdListParam(raw: unknown): string[] {
|
|
60
|
+
if (typeof raw !== 'string') return []
|
|
61
|
+
return raw
|
|
62
|
+
.split(',')
|
|
63
|
+
.map((value) => value.trim())
|
|
64
|
+
.filter((value) => value.length > 0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether every entry of a comma-separated id list query param is UUID-shaped, so it can be
|
|
69
|
+
* bound to a uuid column without a database error. Use it to reject a malformed or mixed list
|
|
70
|
+
* with a 400 instead of dropping the bad entries and widening the result set (#7265).
|
|
71
|
+
*/
|
|
72
|
+
export function isUuidListParam(raw: unknown): boolean {
|
|
73
|
+
if (raw === undefined) return true
|
|
74
|
+
if (typeof raw !== 'string') return false
|
|
75
|
+
return splitIdListParam(raw).every((value) => UUID_SHAPE_REGEX.test(value))
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
/**
|
|
55
79
|
* Whether an `?ids=` param was supplied at all (a non-empty string, or repeated
|
|
56
80
|
* `?ids=` occurrences), regardless of whether any value survived UUID
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { buildOpenApiDocument } from '../generator'
|
|
3
|
+
import type { Module } from '../../../modules/registry'
|
|
4
|
+
|
|
5
|
+
type ParameterNode = { name?: string; in?: string; required?: boolean }
|
|
6
|
+
type OperationNode = { operationId?: string; parameters?: ParameterNode[] }
|
|
7
|
+
|
|
8
|
+
const noopHandler = async () => new Response(null)
|
|
9
|
+
|
|
10
|
+
function imageModule(extraApis: unknown[] = [], operationId?: string): Module {
|
|
11
|
+
return {
|
|
12
|
+
id: 'attachments',
|
|
13
|
+
apis: [
|
|
14
|
+
{
|
|
15
|
+
path: '/attachments/image/[id]/[[...slug]]',
|
|
16
|
+
handlers: { GET: noopHandler },
|
|
17
|
+
docs: {
|
|
18
|
+
methods: {
|
|
19
|
+
GET: {
|
|
20
|
+
summary: 'Serve image',
|
|
21
|
+
...(operationId ? { operationId } : {}),
|
|
22
|
+
pathParams: z.object({ id: z.string().uuid() }),
|
|
23
|
+
responses: [{ status: 200, description: 'Image' }],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
...extraApis,
|
|
29
|
+
],
|
|
30
|
+
} as unknown as Module
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function operation(doc: ReturnType<typeof buildOpenApiDocument>, path: string): OperationNode | undefined {
|
|
34
|
+
return (doc.paths[path] as Record<string, OperationNode> | undefined)?.get
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectPathParameterViolations(doc: ReturnType<typeof buildOpenApiDocument>): string[] {
|
|
38
|
+
const violations: string[] = []
|
|
39
|
+
const operationIds = new Set<string>()
|
|
40
|
+
for (const [path, item] of Object.entries(doc.paths)) {
|
|
41
|
+
const templateNames = Array.from(path.matchAll(/\{([^}]+)\}/g), (match) => match[1])
|
|
42
|
+
for (const [method, op] of Object.entries(item as Record<string, OperationNode>)) {
|
|
43
|
+
const pathParams = (op.parameters ?? []).filter((param) => param.in === 'path')
|
|
44
|
+
for (const param of pathParams) {
|
|
45
|
+
if (param.required !== true) violations.push(`${method} ${path}: path parameter ${param.name} is not required`)
|
|
46
|
+
if (!templateNames.includes(param.name ?? '')) violations.push(`${method} ${path}: path parameter ${param.name} is not in the template`)
|
|
47
|
+
}
|
|
48
|
+
for (const name of templateNames) {
|
|
49
|
+
if (!pathParams.some((param) => param.name === name)) violations.push(`${method} ${path}: template variable ${name} is not declared`)
|
|
50
|
+
}
|
|
51
|
+
if (op.operationId) {
|
|
52
|
+
if (operationIds.has(op.operationId)) violations.push(`${method} ${path}: duplicate operationId ${op.operationId}`)
|
|
53
|
+
operationIds.add(op.operationId)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return violations
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('buildOpenApiDocument optional catch-all path parameters', () => {
|
|
61
|
+
it('emits id-only and id-plus-slug paths with only required path parameters', () => {
|
|
62
|
+
const doc = buildOpenApiDocument([imageModule()])
|
|
63
|
+
|
|
64
|
+
expect(collectPathParameterViolations(doc)).toEqual([])
|
|
65
|
+
|
|
66
|
+
const withSlug = operation(doc, '/attachments/image/{id}/{slug}')
|
|
67
|
+
const withoutSlug = operation(doc, '/attachments/image/{id}')
|
|
68
|
+
expect(withSlug?.parameters?.filter((param) => param.in === 'path').map((param) => param.name)).toEqual(['id', 'slug'])
|
|
69
|
+
expect(withoutSlug?.parameters?.filter((param) => param.in === 'path').map((param) => param.name)).toEqual(['id'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('keeps the published operationId on the slug path and gives the id-only path a distinct one', () => {
|
|
73
|
+
const doc = buildOpenApiDocument([imageModule()])
|
|
74
|
+
|
|
75
|
+
expect(operation(doc, '/attachments/image/{id}/{slug}')?.operationId).toBe('attachments_get_attachments_image_id_slug')
|
|
76
|
+
expect(operation(doc, '/attachments/image/{id}')?.operationId).toBe('attachments_get_attachments_image_id')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('derives a distinct id-only operationId from an explicit operationId', () => {
|
|
80
|
+
const doc = buildOpenApiDocument([imageModule([], 'serveImage')])
|
|
81
|
+
|
|
82
|
+
expect(operation(doc, '/attachments/image/{id}/{slug}')?.operationId).toBe('serveImage')
|
|
83
|
+
expect(operation(doc, '/attachments/image/{id}')?.operationId).toBe('serveImage_without_slug')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('never replaces a concrete route that owns the id-only path, whichever is registered first', () => {
|
|
87
|
+
const concreteRoute = {
|
|
88
|
+
path: '/attachments/image/[id]',
|
|
89
|
+
handlers: { GET: noopHandler },
|
|
90
|
+
docs: { methods: { GET: { summary: 'Concrete', operationId: 'concreteImage', responses: [{ status: 200, description: 'OK' }] } } },
|
|
91
|
+
}
|
|
92
|
+
const after = buildOpenApiDocument([imageModule([concreteRoute])])
|
|
93
|
+
const before = buildOpenApiDocument([
|
|
94
|
+
{ id: 'other', apis: [concreteRoute] } as unknown as Module,
|
|
95
|
+
imageModule(),
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
expect(operation(after, '/attachments/image/{id}')?.operationId).toBe('concreteImage')
|
|
99
|
+
expect(operation(before, '/attachments/image/{id}')?.operationId).toBe('concreteImage')
|
|
100
|
+
expect(collectPathParameterViolations(after)).toEqual([])
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -15,6 +15,12 @@ type PathParamInfo = {
|
|
|
15
15
|
optional?: boolean
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
type PathVariant = {
|
|
19
|
+
path: string
|
|
20
|
+
params: PathParamInfo[]
|
|
21
|
+
omittedParam?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
type ParameterLocation = 'query' | 'path' | 'header'
|
|
19
25
|
|
|
20
26
|
export type JsonSchema = Record<string, unknown>
|
|
@@ -119,6 +125,20 @@ function normalizePath(path: string): { path: string; params: PathParamInfo[] }
|
|
|
119
125
|
return { path: '/' + normalized, params }
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
function expandOptionalPathParams(normalized: { path: string; params: PathParamInfo[] }): PathVariant[] {
|
|
129
|
+
const required = normalized.params.filter((param) => !param.optional)
|
|
130
|
+
const optionalParam = normalized.params.find((param) => param.optional)
|
|
131
|
+
const fullVariant: PathVariant = {
|
|
132
|
+
path: normalized.path,
|
|
133
|
+
params: normalized.params.map((param) => ({ ...param, optional: false })),
|
|
134
|
+
}
|
|
135
|
+
if (!optionalParam) return [fullVariant]
|
|
136
|
+
const placeholder = `/{${optionalParam.name}}`
|
|
137
|
+
if (!normalized.path.endsWith(placeholder)) return [fullVariant]
|
|
138
|
+
const basePath = normalized.path.slice(0, -placeholder.length) || '/'
|
|
139
|
+
return [fullVariant, { path: basePath, params: required, omittedParam: optionalParam.name }]
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
function unwrap(schema?: ZodTypeAny): {
|
|
123
143
|
schema: ZodTypeAny | undefined
|
|
124
144
|
optional: boolean
|
|
@@ -681,14 +701,14 @@ function buildParameters(
|
|
|
681
701
|
|
|
682
702
|
if (location === 'path' && pathParamNames && pathParamNames.length) {
|
|
683
703
|
const merged = mergePathParamSchemas(schema, pathParamNames)
|
|
684
|
-
for (const { name, schema: paramSchema
|
|
704
|
+
for (const { name, schema: paramSchema } of merged) {
|
|
685
705
|
const jsonSchema = zodToJsonSchema(paramSchema)
|
|
686
706
|
const example = generateExample(paramSchema)
|
|
687
707
|
const description = extractZodDescription(paramSchema)
|
|
688
708
|
params.push({
|
|
689
709
|
name,
|
|
690
710
|
in: 'path',
|
|
691
|
-
required:
|
|
711
|
+
required: true,
|
|
692
712
|
schema: jsonSchema ?? { type: 'string' },
|
|
693
713
|
example,
|
|
694
714
|
...(description ? { description } : {}),
|
|
@@ -738,7 +758,7 @@ function buildParameters(
|
|
|
738
758
|
}
|
|
739
759
|
|
|
740
760
|
function mergePathParamSchemas(schema: ZodTypeAny | undefined, params: PathParamInfo[]) {
|
|
741
|
-
const merged: Array<{ name: string; schema: ZodTypeAny | undefined
|
|
761
|
+
const merged: Array<{ name: string; schema: ZodTypeAny | undefined }> = []
|
|
742
762
|
const map: Record<string, ZodTypeAny> = {}
|
|
743
763
|
if (schema) {
|
|
744
764
|
const { schema: unwrapped } = unwrap(schema)
|
|
@@ -753,7 +773,6 @@ function mergePathParamSchemas(schema: ZodTypeAny | undefined, params: PathParam
|
|
|
753
773
|
merged.push({
|
|
754
774
|
name: param.name,
|
|
755
775
|
schema: map[param.name],
|
|
756
|
-
optional: !!param.optional,
|
|
757
776
|
})
|
|
758
777
|
}
|
|
759
778
|
return merged
|
|
@@ -1051,6 +1070,7 @@ export function buildOpenApiDocument(modules: Module[], options: OpenApiDocument
|
|
|
1051
1070
|
|
|
1052
1071
|
const includeAccessControlMetadata = options.includeAccessControlMetadata !== false
|
|
1053
1072
|
const tags = new Map<string, string | undefined>()
|
|
1073
|
+
const derivedOperations = new Set<string>()
|
|
1054
1074
|
|
|
1055
1075
|
for (const moduleEntry of modules) {
|
|
1056
1076
|
const defaultTag = moduleEntry.info?.title ?? toTitle(moduleEntry.id)
|
|
@@ -1060,85 +1080,94 @@ export function buildOpenApiDocument(modules: Module[], options: OpenApiDocument
|
|
|
1060
1080
|
for (const api of apis) {
|
|
1061
1081
|
const routeDoc = collectRouteDoc(api, moduleEntry.id)
|
|
1062
1082
|
const moduleTag = routeDoc?.tag ?? defaultTag
|
|
1063
|
-
const normalized = normalizePath((api as any).path ?? (api as any).path ?? '')
|
|
1064
|
-
const pathKey = normalized.path
|
|
1065
|
-
if (!doc.paths[pathKey]) doc.paths[pathKey] = {}
|
|
1066
1083
|
const availableMethods: HttpMethod[] =
|
|
1067
1084
|
'handlers' in api
|
|
1068
1085
|
? HTTP_METHODS.filter((method) => typeof (api as ModuleApiRouteFile).handlers?.[method] === 'function')
|
|
1069
1086
|
: [api.method as HttpMethod]
|
|
1070
1087
|
|
|
1071
|
-
for (const
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
: undefined
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1088
|
+
for (const normalized of expandOptionalPathParams(normalizePath((api as any).path ?? ''))) {
|
|
1089
|
+
const pathKey = normalized.path
|
|
1090
|
+
if (!doc.paths[pathKey]) doc.paths[pathKey] = {}
|
|
1091
|
+
|
|
1092
|
+
for (const method of availableMethods) {
|
|
1093
|
+
const methodLower = method.toLowerCase() as Lowercase<HttpMethod>
|
|
1094
|
+
const operationKey = `${methodLower} ${pathKey}`
|
|
1095
|
+
const existing = doc.paths[pathKey][methodLower]
|
|
1096
|
+
if (existing && (normalized.omittedParam || !derivedOperations.has(operationKey))) continue
|
|
1097
|
+
if (normalized.omittedParam) derivedOperations.add(operationKey)
|
|
1098
|
+
else derivedOperations.delete(operationKey)
|
|
1099
|
+
|
|
1100
|
+
const metadata = 'handlers' in api ? (api as ModuleApiRouteFile).metadata?.[method] : undefined
|
|
1101
|
+
const methodDoc = routeDoc?.methods?.[method]
|
|
1102
|
+
const summary = methodDoc?.summary ?? routeDoc?.summary ?? `${method} ${pathKey}`
|
|
1103
|
+
const baseDescription = methodDoc?.description ?? routeDoc?.description
|
|
1104
|
+
const meta = metadata && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : undefined
|
|
1105
|
+
const requireFeatures = Array.isArray(meta?.['requireFeatures'])
|
|
1106
|
+
? (meta!['requireFeatures'] as string[])
|
|
1107
|
+
: undefined
|
|
1108
|
+
const requireRoles = Array.isArray(meta?.['requireRoles'])
|
|
1109
|
+
? (meta!['requireRoles'] as string[])
|
|
1110
|
+
: undefined
|
|
1111
|
+
const requireAuth = meta?.['requireAuth'] === true
|
|
1112
|
+
const descriptionParts: string[] = []
|
|
1113
|
+
if (baseDescription) descriptionParts.push(baseDescription)
|
|
1114
|
+
if (includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length) {
|
|
1115
|
+
descriptionParts.push(`Requires features: ${requireFeatures.join(', ')}`)
|
|
1116
|
+
}
|
|
1117
|
+
if (includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length) {
|
|
1118
|
+
descriptionParts.push(`Requires roles: ${requireRoles.join(', ')}`)
|
|
1119
|
+
}
|
|
1096
1120
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1121
|
+
const querySchema = methodDoc?.query
|
|
1122
|
+
const pathSchema = methodDoc?.pathParams ?? routeDoc?.pathParams
|
|
1123
|
+
const headerSchema = methodDoc?.headers
|
|
1124
|
+
const requestBody = methodDoc?.requestBody
|
|
1125
|
+
const examples = collectExamples(querySchema, requestBody?.schema, pathSchema, headerSchema, metadata)
|
|
1126
|
+
const curlSample = buildCurlSample(
|
|
1127
|
+
method,
|
|
1128
|
+
pathKey,
|
|
1129
|
+
normalized.params,
|
|
1130
|
+
examples,
|
|
1131
|
+
options.baseUrlForExamples ?? 'https://api.open-mercato.local',
|
|
1132
|
+
metadata,
|
|
1133
|
+
requestBody
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
doc.paths[pathKey][methodLower] = {
|
|
1137
|
+
operationId: methodDoc?.operationId
|
|
1138
|
+
? normalized.omittedParam
|
|
1139
|
+
? `${methodDoc.operationId}_without_${normalizeOperationIdSegment(normalized.omittedParam)}`
|
|
1140
|
+
: methodDoc.operationId
|
|
1141
|
+
: resolveOperationId(moduleEntry.id, pathKey, method),
|
|
1142
|
+
summary,
|
|
1143
|
+
description: descriptionParts.length ? descriptionParts.join('\n\n') : undefined,
|
|
1144
|
+
tags: methodDoc?.tags ?? (moduleTag ? [moduleTag] : undefined),
|
|
1145
|
+
deprecated: methodDoc?.deprecated,
|
|
1146
|
+
externalDocs: methodDoc?.externalDocs,
|
|
1147
|
+
parameters: [
|
|
1148
|
+
...buildParameters(pathSchema, 'path', normalized.params),
|
|
1149
|
+
...buildParameters(querySchema, 'query'),
|
|
1150
|
+
...buildParameters(headerSchema, 'header'),
|
|
1151
|
+
].filter(Boolean),
|
|
1152
|
+
requestBody: buildRequestBody(requestBody),
|
|
1153
|
+
responses: buildResponses(method, methodDoc?.responses, methodDoc?.errors, metadata),
|
|
1154
|
+
security: buildSecurity(metadata, methodDoc, options.defaultSecurity),
|
|
1155
|
+
'x-codeSamples': methodDoc?.codeSamples ?? [
|
|
1156
|
+
{
|
|
1157
|
+
lang: 'curl',
|
|
1158
|
+
label: 'cURL',
|
|
1159
|
+
source: curlSample,
|
|
1160
|
+
},
|
|
1161
|
+
],
|
|
1162
|
+
...(includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length
|
|
1163
|
+
? { 'x-require-features': requireFeatures }
|
|
1164
|
+
: {}),
|
|
1165
|
+
...(includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length
|
|
1166
|
+
? { 'x-require-roles': requireRoles }
|
|
1167
|
+
: {}),
|
|
1168
|
+
...(requireAuth ? { 'x-require-auth': true } : {}),
|
|
1169
|
+
...(methodDoc?.extensions ?? {}),
|
|
1170
|
+
}
|
|
1142
1171
|
}
|
|
1143
1172
|
}
|
|
1144
1173
|
}
|
package/src/modules/registry.ts
CHANGED
|
@@ -178,6 +178,11 @@ export type ApiRouteManifestEntry = {
|
|
|
178
178
|
export type ModuleCli = {
|
|
179
179
|
command: string
|
|
180
180
|
run: (argv: string[]) => Promise<void> | void
|
|
181
|
+
// Usage text printed by the `mercato` dispatcher when the command is invoked with --help/-h.
|
|
182
|
+
help?: string
|
|
183
|
+
// Set when the command parses --help/-h itself; the dispatcher then forwards the flag
|
|
184
|
+
// instead of intercepting it, so the command can print its own richer usage text.
|
|
185
|
+
handlesHelp?: boolean
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
export type ModuleSubscriber = {
|