@roundtable-bb/sdk 0.1.2 → 0.1.4

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.
@@ -1,298 +0,0 @@
1
- /**
2
- * Reads openapi.json and generates packages/sdk/src/generated/client.ts.
3
- * Run from workspace root: tsx packages/sdk/codegen/generate.ts
4
- */
5
-
6
- import { readFileSync, writeFileSync, mkdirSync } from 'fs'
7
- import { join, dirname } from 'path'
8
- import { fileURLToPath } from 'url'
9
- import { $ZodRegistry, $ZodType, toJSONSchema } from 'zod/v4/core'
10
- import * as types from '@roundtable-bb/types'
11
- import type { ZodTypeAny } from 'zod'
12
-
13
- const __dirname = dirname(fileURLToPath(import.meta.url))
14
-
15
- // ─── Convert Zod schema → OpenAPI 3.0 JSON Schema (same logic as fastify-type-provider-zod) ─
16
-
17
- const IDENTITY_KEYWORDS = new Set(['id', '$id', '$schema'])
18
-
19
- function sanitize(value: unknown): unknown {
20
- if (Array.isArray(value)) return value.map(sanitize)
21
- if (value === null || typeof value !== 'object') return value
22
- const result: Record<string, unknown> = {}
23
- for (const [k, child] of Object.entries(value as Record<string, unknown>)) {
24
- if (IDENTITY_KEYWORDS.has(k)) continue
25
- if (k === 'properties' && child !== null && typeof child === 'object') {
26
- result[k] = Object.fromEntries(
27
- Object.entries(child as Record<string, unknown>).map(([n, s]) => [n, sanitize(s)]),
28
- )
29
- } else {
30
- result[k] = sanitize(child)
31
- }
32
- }
33
- return result
34
- }
35
-
36
- function zodToOpenApi(schema: ZodTypeAny, io: 'input' | 'output'): Record<string, unknown> {
37
- const PLACEHOLDER = '__ID__'
38
- const reg = new $ZodRegistry()
39
- reg.add(schema as unknown as InstanceType<typeof $ZodType>, { id: PLACEHOLDER })
40
- const { schemas } = toJSONSchema(reg, {
41
- target: 'openapi-3.0',
42
- io,
43
- cycles: 'ref',
44
- reused: 'inline',
45
- unrepresentable: 'any',
46
- uri: () => '__URI__',
47
- })
48
- return sanitize(schemas[PLACEHOLDER]) as Record<string, unknown>
49
- }
50
-
51
- // ─── Build known-schema registry (input = request bodies, output = responses) ─
52
-
53
- type SchemaEntry = { typeName: string; input: Record<string, unknown>; output: Record<string, unknown> }
54
- const registry: SchemaEntry[] = []
55
-
56
- for (const [key, value] of Object.entries(types)) {
57
- if (!key.endsWith('Schema') || key === 'PaginatedResponseSchema') continue
58
- if (typeof value !== 'object' || !value || !('_zod' in value)) continue
59
- const typeName = key.replace(/Schema$/, '')
60
- try {
61
- const input = zodToOpenApi(value as ZodTypeAny, 'input')
62
- const output = zodToOpenApi(value as ZodTypeAny, 'output')
63
- if (Object.keys(output).length > 0) registry.push({ typeName, input, output })
64
- } catch {
65
- // skip unconvertible schemas
66
- }
67
- }
68
-
69
- // ─── Deep equality ───────────────────────────────────────────────────────────
70
-
71
- function deepEqual(a: unknown, b: unknown): boolean {
72
- if (a === b) return true
73
- if (typeof a !== typeof b || typeof a !== 'object') return false
74
- if (a === null || b === null) return false
75
- const ka = Object.keys(a as object).sort()
76
- const kb = Object.keys(b as object).sort()
77
- if (ka.length !== kb.length || ka.join('\0') !== kb.join('\0')) return false
78
- return ka.every((k) =>
79
- deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),
80
- )
81
- }
82
-
83
- // ─── Schema → TypeScript type ─────────────────────────────────────────────────
84
-
85
- const usedTypes = new Set<string>()
86
-
87
- function primitiveType(schema: Record<string, unknown>): string {
88
- if (schema.type === 'string') return 'string'
89
- if (schema.type === 'integer' || schema.type === 'number') return 'number'
90
- if (schema.type === 'boolean') return 'boolean'
91
- if (schema.type === 'null') return 'null'
92
- if (schema.anyOf) {
93
- const variants = (schema.anyOf as Record<string, unknown>[]).map(primitiveType)
94
- return variants.join(' | ')
95
- }
96
- return 'unknown'
97
- }
98
-
99
- function matchSchema(schema: Record<string, unknown>, io: 'input' | 'output' = 'output'): string | null {
100
- // Direct match against known schemas from @roundtable-bb/types
101
- for (const { typeName, input, output } of registry) {
102
- if (deepEqual(schema, io === 'input' ? input : output)) {
103
- usedTypes.add(typeName)
104
- return typeName
105
- }
106
- }
107
-
108
- // Array: { type: 'array', items: X }
109
- if (schema.type === 'array' && schema.items && typeof schema.items === 'object') {
110
- const itemsSchema = schema.items as Record<string, unknown>
111
- const inner = matchSchema(itemsSchema, io) ?? primitiveType(itemsSchema)
112
- if (inner !== 'unknown') return `${inner}[]`
113
- }
114
-
115
- // Paginated: { type: 'object', properties: { items: { type: 'array', items: X }, nextCursor: ... } }
116
- if (schema.type === 'object' && schema.properties && typeof schema.properties === 'object') {
117
- const props = schema.properties as Record<string, Record<string, unknown>>
118
- const itemsProp = props['items']
119
- const nextCursorProp = props['nextCursor']
120
- if (
121
- itemsProp?.type === 'array' &&
122
- typeof itemsProp.items === 'object' &&
123
- nextCursorProp !== undefined
124
- ) {
125
- const inner = matchSchema(itemsProp.items as Record<string, unknown>, io)
126
- if (inner) {
127
- usedTypes.add('PaginatedResponse')
128
- return `PaginatedResponse<${inner}>`
129
- }
130
- }
131
- }
132
-
133
- return null
134
- }
135
-
136
- function resolveType(schema: Record<string, unknown> | undefined, io: 'input' | 'output' = 'output'): string {
137
- if (!schema) return 'void'
138
- return matchSchema(schema, io) ?? primitiveType(schema)
139
- }
140
-
141
- // ─── Parse OpenAPI spec ───────────────────────────────────────────────────────
142
-
143
- type Param = { name: string; tsType: string; required: boolean }
144
-
145
- type Operation = {
146
- operationId: string
147
- method: string
148
- path: string
149
- pathParams: Param[]
150
- queryParams: Param[]
151
- bodyType: string | null
152
- responseType: string
153
- summary: string
154
- }
155
-
156
- const specPath = join(__dirname, '../openapi.json')
157
- const spec = JSON.parse(readFileSync(specPath, 'utf-8')) as {
158
- paths: Record<
159
- string,
160
- Record<
161
- string,
162
- {
163
- operationId?: string
164
- summary?: string
165
- parameters?: Array<{
166
- name: string
167
- in: string
168
- required?: boolean
169
- schema?: Record<string, unknown>
170
- }>
171
- requestBody?: {
172
- content?: { 'application/json'?: { schema?: Record<string, unknown> } }
173
- }
174
- responses?: Record<
175
- string,
176
- { content?: { 'application/json'?: { schema?: Record<string, unknown> } } }
177
- >
178
- }
179
- >
180
- >
181
- }
182
-
183
- const operations: Operation[] = []
184
-
185
- for (const [rawPath, methods] of Object.entries(spec.paths ?? {})) {
186
- for (const [method, op] of Object.entries(methods)) {
187
- if (!op.operationId) continue
188
-
189
- const pathParams: Param[] = (op.parameters ?? [])
190
- .filter((p) => p.in === 'path')
191
- .map((p) => ({ name: p.name, tsType: 'string', required: true }))
192
-
193
- const queryParams: Param[] = (op.parameters ?? [])
194
- .filter((p) => p.in === 'query')
195
- .map((p) => ({
196
- name: p.name,
197
- tsType: p.schema ? resolveType(p.schema, 'input') : 'string',
198
- required: !!p.required,
199
- }))
200
-
201
- const bodySchema = op.requestBody?.content?.['application/json']?.schema
202
- const bodyType = bodySchema ? resolveType(bodySchema, 'input') : null
203
-
204
- const [responseCode, responseEntry] =
205
- Object.entries(op.responses ?? {}).find(([code]) => code.startsWith('2')) ?? [
206
- '200',
207
- undefined,
208
- ]
209
-
210
- const responseType =
211
- responseCode === '204'
212
- ? 'void'
213
- : resolveType(
214
- (
215
- responseEntry as {
216
- content?: { 'application/json'?: { schema?: Record<string, unknown> } }
217
- }
218
- )?.content?.['application/json']?.schema,
219
- )
220
-
221
- operations.push({
222
- operationId: op.operationId,
223
- method: method.toUpperCase(),
224
- path: rawPath,
225
- pathParams,
226
- queryParams,
227
- bodyType,
228
- responseType,
229
- summary: op.summary ?? '',
230
- })
231
- }
232
- }
233
-
234
- // ─── Emit client class ────────────────────────────────────────────────────────
235
-
236
- function buildTemplatePath(path: string): string {
237
- const converted = path.replace(/\{([^}]+)\}/g, '${params.$1}')
238
- return converted.includes('${') ? `\`${converted}\`` : `'${converted}'`
239
- }
240
-
241
- function emitMethod(op: Operation): string {
242
- const args: string[] = []
243
-
244
- if (op.pathParams.length > 0) {
245
- const shape = op.pathParams.map((p) => `${p.name}: ${p.tsType}`).join('; ')
246
- args.push(`params: { ${shape} }`)
247
- }
248
- if (op.bodyType) args.push(`body: ${op.bodyType}`)
249
- if (op.queryParams.length > 0) {
250
- const shape = op.queryParams
251
- .map((p) => `${p.name}${p.required ? '' : '?'}: ${p.tsType}`)
252
- .join('; ')
253
- args.push(`query?: { ${shape} }`)
254
- }
255
-
256
- const requestArgs: string[] = ['this.opts', `'${op.method}'`, buildTemplatePath(op.path)]
257
- const requestOpts: string[] = []
258
- if (op.bodyType) requestOpts.push('body')
259
- if (op.queryParams.length > 0) requestOpts.push('query')
260
- if (requestOpts.length > 0) requestArgs.push(`{ ${requestOpts.join(', ')} }`)
261
-
262
- return [
263
- ` /** ${op.summary} */`,
264
- ` ${op.operationId}(${args.join(', ')}): Promise<${op.responseType}> {`,
265
- ` return request(${requestArgs.join(', ')})`,
266
- ` }`,
267
- ].join('\n')
268
- }
269
-
270
- const importedTypes = [...usedTypes].sort()
271
- const importLine =
272
- importedTypes.length > 0
273
- ? `import type { ${importedTypes.join(', ')} } from '@roundtable-bb/types'`
274
- : ''
275
-
276
- const methods = operations
277
- .sort((a, b) => a.operationId.localeCompare(b.operationId))
278
- .map(emitMethod)
279
- .join('\n\n')
280
-
281
- const output = `\
282
- // AUTO-GENERATED — do not edit manually.
283
- // Run \`nx run @roundtable-bb/sdk:codegen\` to regenerate.
284
-
285
- ${importLine}
286
- import { request, type RoundtableClientOpts } from '../request.js'
287
-
288
- export class RoundtableClient {
289
- constructor(private readonly opts: RoundtableClientOpts) {}
290
-
291
- ${methods}
292
- }
293
- `
294
-
295
- const outDir = join(__dirname, '../src/generated')
296
- mkdirSync(outDir, { recursive: true })
297
- writeFileSync(join(outDir, 'client.ts'), output)
298
- console.log(`✓ Generated ${operations.length} operations → src/generated/client.ts`)