@open-mercato/shared 0.9.1-develop.7477.1.64ba865101 → 0.9.1-develop.7497.1.543ea44c40

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.
@@ -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, optional } of merged) {
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: !optional,
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; optional: boolean }> = []
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 method of availableMethods) {
1072
- const methodLower = method.toLowerCase() as Lowercase<HttpMethod>
1073
- const existing = doc.paths[pathKey][methodLower]
1074
- if (existing) continue
1075
-
1076
- const metadata = 'handlers' in api ? (api as ModuleApiRouteFile).metadata?.[method] : undefined
1077
- const methodDoc = routeDoc?.methods?.[method]
1078
- const summary = methodDoc?.summary ?? routeDoc?.summary ?? `${method} ${pathKey}`
1079
- const baseDescription = methodDoc?.description ?? routeDoc?.description
1080
- const meta = metadata && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : undefined
1081
- const requireFeatures = Array.isArray(meta?.['requireFeatures'])
1082
- ? (meta!['requireFeatures'] as string[])
1083
- : undefined
1084
- const requireRoles = Array.isArray(meta?.['requireRoles'])
1085
- ? (meta!['requireRoles'] as string[])
1086
- : undefined
1087
- const requireAuth = meta?.['requireAuth'] === true
1088
- const descriptionParts: string[] = []
1089
- if (baseDescription) descriptionParts.push(baseDescription)
1090
- if (includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length) {
1091
- descriptionParts.push(`Requires features: ${requireFeatures.join(', ')}`)
1092
- }
1093
- if (includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length) {
1094
- descriptionParts.push(`Requires roles: ${requireRoles.join(', ')}`)
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
- const querySchema = methodDoc?.query
1098
- const pathSchema = methodDoc?.pathParams ?? routeDoc?.pathParams
1099
- const headerSchema = methodDoc?.headers
1100
- const requestBody = methodDoc?.requestBody
1101
- const examples = collectExamples(querySchema, requestBody?.schema, pathSchema, headerSchema, metadata)
1102
- const curlSample = buildCurlSample(
1103
- method,
1104
- pathKey,
1105
- normalized.params,
1106
- examples,
1107
- options.baseUrlForExamples ?? 'https://api.open-mercato.local',
1108
- metadata,
1109
- requestBody
1110
- )
1111
-
1112
- doc.paths[pathKey][methodLower] = {
1113
- operationId: methodDoc?.operationId ?? resolveOperationId(moduleEntry.id, pathKey, method),
1114
- summary,
1115
- description: descriptionParts.length ? descriptionParts.join('\n\n') : undefined,
1116
- tags: methodDoc?.tags ?? (moduleTag ? [moduleTag] : undefined),
1117
- deprecated: methodDoc?.deprecated,
1118
- externalDocs: methodDoc?.externalDocs,
1119
- parameters: [
1120
- ...buildParameters(pathSchema, 'path', normalized.params),
1121
- ...buildParameters(querySchema, 'query'),
1122
- ...buildParameters(headerSchema, 'header'),
1123
- ].filter(Boolean),
1124
- requestBody: buildRequestBody(requestBody),
1125
- responses: buildResponses(method, methodDoc?.responses, methodDoc?.errors, metadata),
1126
- security: buildSecurity(metadata, methodDoc, options.defaultSecurity),
1127
- 'x-codeSamples': methodDoc?.codeSamples ?? [
1128
- {
1129
- lang: 'curl',
1130
- label: 'cURL',
1131
- source: curlSample,
1132
- },
1133
- ],
1134
- ...(includeAccessControlMetadata && Array.isArray(requireFeatures) && requireFeatures.length
1135
- ? { 'x-require-features': requireFeatures }
1136
- : {}),
1137
- ...(includeAccessControlMetadata && Array.isArray(requireRoles) && requireRoles.length
1138
- ? { 'x-require-roles': requireRoles }
1139
- : {}),
1140
- ...(requireAuth ? { 'x-require-auth': true } : {}),
1141
- ...(methodDoc?.extensions ?? {}),
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
  }
@@ -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 = {