@supalive/codegen 1.8.1 → 1.9.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.
- package/README.md +148 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-CTW5fpJ-.js → src-C8EGEwt7.js} +177 -302
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,154 @@ const { files } = generate({ entry: "src/server/procedures.ts" });
|
|
|
85
85
|
`extractClient` (build the intermediate representation) and `emit` (turn the IR
|
|
86
86
|
into files) are also exported for advanced use.
|
|
87
87
|
|
|
88
|
+
## Return type overrides
|
|
89
|
+
|
|
90
|
+
By default the codegen infers everything from TypeScript types: `number` →
|
|
91
|
+
`double`, `bigint` → `BigInt`, `Date` → `DateTime`, etc. The `returns` property
|
|
92
|
+
on procedure configs lets you override specific fields for the generated client
|
|
93
|
+
without changing your handler's return type.
|
|
94
|
+
|
|
95
|
+
Model naming uses the `.modelName()` Zod extension method — no extra imports
|
|
96
|
+
needed (it patches all Zod types when you import from `@supalive/core/procedure`).
|
|
97
|
+
|
|
98
|
+
### Default (no `returns`)
|
|
99
|
+
|
|
100
|
+
Everything inferred from the handler's TS return type:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const listCards = query({
|
|
104
|
+
args: z.object({ limit: z.number().int().default(20) }),
|
|
105
|
+
handler: async (_ctx, _input): Promise<{ items: { id: string; name: string }[] }> => ({
|
|
106
|
+
items: [],
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
// → ListCardsResult { items: List<ListCardsItem> }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `.modelName("Name")` — rename a model
|
|
113
|
+
|
|
114
|
+
Give a generated model a custom name instead of the auto-derived one:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const listCards = query({
|
|
118
|
+
args: z.object({ limit: z.number().int().default(20) }),
|
|
119
|
+
returns: z.array(
|
|
120
|
+
z.object({ id: z.string(), name: z.string() }).modelName("CardItem")
|
|
121
|
+
),
|
|
122
|
+
handler: async (_ctx, _input) => [] as { id: string; name: string }[],
|
|
123
|
+
});
|
|
124
|
+
// → result type: List<CardItem> (not List<ListCardsResultItem>)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`.modelName("Foo")` on any Zod schema tells the codegen to rename the
|
|
128
|
+
model to `Foo`. Combine with field-level overrides like `z.number().int()` to
|
|
129
|
+
also patch the TS-inferred types.
|
|
130
|
+
|
|
131
|
+
### Partial field overrides (implicit)
|
|
132
|
+
|
|
133
|
+
Only the fields you list in the `returns` schema are overridden; the rest stay
|
|
134
|
+
inferred from TS. No `partial()` wrapper needed:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const storeCards = query({
|
|
138
|
+
args: z.object({}),
|
|
139
|
+
returns: z.object({
|
|
140
|
+
total: z.number().int(), // override: number → int
|
|
141
|
+
// 'cards' is NOT listed → stays inferred from TS
|
|
142
|
+
}),
|
|
143
|
+
handler: async (_ctx, _input): Promise<{
|
|
144
|
+
cards: { id: string; name: string; balance: number }[];
|
|
145
|
+
total: number;
|
|
146
|
+
}> => ({ cards: [], total: 0 }),
|
|
147
|
+
});
|
|
148
|
+
// → StoreCardsResult { cards: List<...>, total: int }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Combining `.modelName()` + `.int()`
|
|
152
|
+
|
|
153
|
+
The most common pattern: rename nested models and patch numeric types while
|
|
154
|
+
keeping the parent result inferred from TS:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const storeCards = query({
|
|
158
|
+
args: z.object({}),
|
|
159
|
+
returns: z.object({
|
|
160
|
+
value: z.array(
|
|
161
|
+
z.object({
|
|
162
|
+
balanceCents: z.number().int(), // double → int
|
|
163
|
+
}).modelName("StoreCardItem")
|
|
164
|
+
),
|
|
165
|
+
}),
|
|
166
|
+
handler: async (ctx, input) => {
|
|
167
|
+
const cards = await ctx.db.query(GiftCardSchema).select().get();
|
|
168
|
+
return {
|
|
169
|
+
cursor: "...",
|
|
170
|
+
value: cards,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
// → StoreCardsResult { cursor: String, value: List<StoreCardItem> }
|
|
175
|
+
// StoreCardItem { id, balanceCents: int, ... }
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Procedure-level list return with model naming
|
|
179
|
+
|
|
180
|
+
When a procedure returns a bare list (not wrapped in an object), use
|
|
181
|
+
`z.array()` at the top level of `returns`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const cardLedger = query({
|
|
185
|
+
args: z.object({ cardId: z.string() }),
|
|
186
|
+
returns: z.array(
|
|
187
|
+
z.object({
|
|
188
|
+
amountCents: z.number().int(),
|
|
189
|
+
balanceAfterCents: z.number().int(),
|
|
190
|
+
}).modelName("CardLedgerItem")
|
|
191
|
+
),
|
|
192
|
+
handler: async (ctx, { cardId }) =>
|
|
193
|
+
ctx.db.query(CardTransactionSchema).select()
|
|
194
|
+
.where((f) => f.eq("cardId", cardId)).get(),
|
|
195
|
+
});
|
|
196
|
+
// → result type: List<CardLedgerItem>
|
|
197
|
+
// CardLedgerItem { ..., amountCents: int, balanceAfterCents: int, ... }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Nested model overrides
|
|
201
|
+
|
|
202
|
+
Override fields inside a list element's model by nesting `.modelName()` +
|
|
203
|
+
field overrides:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const nestedOverride = query({
|
|
207
|
+
args: z.object({}),
|
|
208
|
+
returns: z.object({
|
|
209
|
+
items: z.array(
|
|
210
|
+
z.object({
|
|
211
|
+
id: z.string(),
|
|
212
|
+
balance: z.number().int(), // override int
|
|
213
|
+
name: z.string(),
|
|
214
|
+
}).modelName("NestedItem")
|
|
215
|
+
),
|
|
216
|
+
}),
|
|
217
|
+
handler: async (_ctx, _input) => ({
|
|
218
|
+
items: [] as { id: string; balance: number; name: string }[],
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
// → NestedItem { id: String, balance: int, name: String }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Summary of helpers
|
|
225
|
+
|
|
226
|
+
| Helper | Purpose | Example |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| `.modelName("Name")` | Rename a model in generated output | `z.object({ ... }).modelName("CardItem")` |
|
|
229
|
+
| `z.number().int()` | Patch a field's type from `double` to `int` | `z.object({ balance: z.number().int() })` |
|
|
230
|
+
| `z.bigint()` | Patch a field's type to `BigInt` | `z.object({ revision: z.bigint() })` |
|
|
231
|
+
|
|
232
|
+
All `returns` helpers are purely for codegen — they have no effect at runtime.
|
|
233
|
+
The Zod schemas are not used for runtime validation of the handler's return
|
|
234
|
+
value.
|
|
235
|
+
|
|
88
236
|
## License
|
|
89
237
|
|
|
90
238
|
MIT
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as emit, c as loadSchemaEnums, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, s as SchemaEnumRegistry, t as generate } from "./src-
|
|
1
|
+
import { a as emit, c as loadSchemaEnums, i as resolveConfig, n as generateToDisk, o as extractClient, r as readEntryConfig, s as SchemaEnumRegistry, t as generate } from "./src-C8EGEwt7.js";
|
|
2
2
|
export { SchemaEnumRegistry, emit, extractClient, generate, generateToDisk, loadSchemaEnums, readEntryConfig, resolveConfig };
|
|
@@ -80,6 +80,139 @@ function scanZodArgMeta(routerDecl) {
|
|
|
80
80
|
} catch {}
|
|
81
81
|
return result;
|
|
82
82
|
}
|
|
83
|
+
/** From a `query({ returns: ..., handler })` call, get the output schema. */
|
|
84
|
+
function getOutputSchema(call) {
|
|
85
|
+
if (!Node.isCallExpression(call)) return void 0;
|
|
86
|
+
const arg = call.getArguments()[0];
|
|
87
|
+
if (!arg || !Node.isObjectLiteralExpression(arg)) return void 0;
|
|
88
|
+
const returnsProp = arg.getProperty("returns");
|
|
89
|
+
if (!returnsProp || !Node.isPropertyAssignment(returnsProp)) return void 0;
|
|
90
|
+
return returnsProp.getInitializer();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Scan each procedure's `returns` Zod schema into a structured metadata tree.
|
|
94
|
+
* Returns `Map<procName, OutputSchemaMeta>`.
|
|
95
|
+
*/
|
|
96
|
+
function scanZodOutputMeta(routerDecl) {
|
|
97
|
+
const result = /* @__PURE__ */ new Map();
|
|
98
|
+
try {
|
|
99
|
+
const proceduresObj = getProceduresObjectLiteral(routerDecl);
|
|
100
|
+
if (!proceduresObj) return result;
|
|
101
|
+
for (const prop of proceduresObj.getProperties()) {
|
|
102
|
+
const key = getPropertyKey(prop);
|
|
103
|
+
if (!key) continue;
|
|
104
|
+
const call = resolveProcedureCall(prop);
|
|
105
|
+
if (!call) continue;
|
|
106
|
+
const outputSchema = getOutputSchema(call);
|
|
107
|
+
if (!outputSchema) continue;
|
|
108
|
+
const meta = scanOutputSchemaNode(outputSchema);
|
|
109
|
+
if (meta) result.set(key, meta);
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Recursively scan a Zod schema expression, extracting field metadata and
|
|
116
|
+
* `__genType` model names. Returns `null` if no metadata found.
|
|
117
|
+
*/
|
|
118
|
+
function scanOutputSchemaNode(node) {
|
|
119
|
+
if (!node) return null;
|
|
120
|
+
let resolved = node;
|
|
121
|
+
if (Node.isIdentifier(resolved)) {
|
|
122
|
+
const init = resolveIdentifierInit(resolved);
|
|
123
|
+
if (init) resolved = init;
|
|
124
|
+
}
|
|
125
|
+
let modelName;
|
|
126
|
+
let isObject = false;
|
|
127
|
+
let baseCallNode;
|
|
128
|
+
let arrayElementNode;
|
|
129
|
+
let cur = resolved;
|
|
130
|
+
while (cur && Node.isCallExpression(cur)) {
|
|
131
|
+
const expr = cur.getExpression();
|
|
132
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
133
|
+
const method = expr.getName();
|
|
134
|
+
if (method === "meta") {
|
|
135
|
+
const metaArg = cur.getArguments()[0];
|
|
136
|
+
if (metaArg && Node.isObjectLiteralExpression(metaArg)) {
|
|
137
|
+
const genTypeProp = metaArg.getProperty("__genType");
|
|
138
|
+
if (genTypeProp && Node.isPropertyAssignment(genTypeProp)) {
|
|
139
|
+
const init = genTypeProp.getInitializer();
|
|
140
|
+
if (init && Node.isStringLiteral(init)) modelName = init.getLiteralValue();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
cur = expr.getExpression();
|
|
144
|
+
} else if (method === "modelName") {
|
|
145
|
+
const nameArg = cur.getArguments()[0];
|
|
146
|
+
if (nameArg && Node.isStringLiteral(nameArg)) modelName = nameArg.getLiteralValue();
|
|
147
|
+
cur = expr.getExpression();
|
|
148
|
+
} else if (method === "array") {
|
|
149
|
+
arrayElementNode = cur.getArguments()[0];
|
|
150
|
+
cur = expr.getExpression();
|
|
151
|
+
} else if (/^(strict|loose)?[oO]bject$/.test(method)) {
|
|
152
|
+
isObject = true;
|
|
153
|
+
baseCallNode = cur;
|
|
154
|
+
break;
|
|
155
|
+
} else cur = expr.getExpression();
|
|
156
|
+
} else break;
|
|
157
|
+
}
|
|
158
|
+
if (!isObject && cur && Node.isCallExpression(cur)) {
|
|
159
|
+
const expr = cur.getExpression();
|
|
160
|
+
if (Node.isIdentifier(expr) || Node.isPropertyAccessExpression(expr)) {
|
|
161
|
+
const exprText = expr.getText();
|
|
162
|
+
if (/(^|\.)(strict|loose)?[oO]bject$/.test(exprText)) {
|
|
163
|
+
isObject = true;
|
|
164
|
+
baseCallNode = cur;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (isObject && baseCallNode && Node.isCallExpression(baseCallNode)) {
|
|
169
|
+
const arg = baseCallNode.getArguments()[0];
|
|
170
|
+
if (!arg || !Node.isObjectLiteralExpression(arg)) return modelName ? {
|
|
171
|
+
fieldMeta: /* @__PURE__ */ new Map(),
|
|
172
|
+
modelName,
|
|
173
|
+
nested: /* @__PURE__ */ new Map()
|
|
174
|
+
} : null;
|
|
175
|
+
const fieldMeta = /* @__PURE__ */ new Map();
|
|
176
|
+
const nested = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const prop of arg.getProperties()) {
|
|
178
|
+
if (!Node.isPropertyAssignment(prop)) continue;
|
|
179
|
+
const key = prop.getName();
|
|
180
|
+
const init = prop.getInitializer();
|
|
181
|
+
if (!init) continue;
|
|
182
|
+
const methods = topLevelZodMethods(init);
|
|
183
|
+
const meta = {};
|
|
184
|
+
if (methods.has("bigint")) meta.numeric = "bigint";
|
|
185
|
+
else if (methods.has("int")) meta.numeric = "int";
|
|
186
|
+
if (methods.has("default") || methods.has("catch") || methods.has("prefault")) meta.optional = true;
|
|
187
|
+
const description = topLevelZodDescription(init);
|
|
188
|
+
if (description) meta.description = description;
|
|
189
|
+
if (meta.numeric || meta.optional || meta.description) fieldMeta.set(key, meta);
|
|
190
|
+
const nestedMeta = scanOutputSchemaNode(init);
|
|
191
|
+
if (nestedMeta) nested.set(key, nestedMeta);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
fieldMeta,
|
|
195
|
+
modelName,
|
|
196
|
+
nested
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (arrayElementNode) {
|
|
200
|
+
const elementMeta = scanOutputSchemaNode(arrayElementNode);
|
|
201
|
+
if (elementMeta || modelName) return {
|
|
202
|
+
fieldMeta: /* @__PURE__ */ new Map(),
|
|
203
|
+
modelName,
|
|
204
|
+
nested: /* @__PURE__ */ new Map([["__element", elementMeta ?? {
|
|
205
|
+
fieldMeta: /* @__PURE__ */ new Map(),
|
|
206
|
+
nested: /* @__PURE__ */ new Map()
|
|
207
|
+
}]])
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return modelName ? {
|
|
211
|
+
fieldMeta: /* @__PURE__ */ new Map(),
|
|
212
|
+
modelName,
|
|
213
|
+
nested: /* @__PURE__ */ new Map()
|
|
214
|
+
} : null;
|
|
215
|
+
}
|
|
83
216
|
function getProceduresObjectLiteral(routerDecl) {
|
|
84
217
|
let init;
|
|
85
218
|
if (Node.isVariableDeclaration(routerDecl)) init = routerDecl.getInitializer();
|
|
@@ -123,9 +256,6 @@ function followToCall(node, depth) {
|
|
|
123
256
|
/**
|
|
124
257
|
* Given an Identifier that may be an import binding, follow through the import
|
|
125
258
|
* chain to the actual exported declaration's initializer node.
|
|
126
|
-
*
|
|
127
|
-
* Handles: `import { listItems } from "./procs.js"` → the exported VariableDeclaration
|
|
128
|
-
* `import { x } from "./reexport.js"` where that file re-exports → recursive follow
|
|
129
259
|
*/
|
|
130
260
|
function followImport(id) {
|
|
131
261
|
const sourceFile = id.getSourceFile();
|
|
@@ -167,9 +297,6 @@ function followImport(id) {
|
|
|
167
297
|
}
|
|
168
298
|
}
|
|
169
299
|
}
|
|
170
|
-
/** Extract the initializer expression from a definition node, unwrapping any
|
|
171
|
-
* container nodes (VariableStatement, ExportNamedDeclaration, etc.) that
|
|
172
|
-
* ts-morph may return for cross-file imports. */
|
|
173
300
|
function definitionInitializer(def) {
|
|
174
301
|
if (Node.isVariableDeclaration(def)) return def.getInitializer();
|
|
175
302
|
if (Node.isVariableStatement(def)) for (const d of def.getDeclarationList().getDeclarations()) {
|
|
@@ -181,7 +308,6 @@ function definitionInitializer(def) {
|
|
|
181
308
|
if (init) return init;
|
|
182
309
|
}
|
|
183
310
|
}
|
|
184
|
-
/** Resolve an Identifier to its initializer, following definitions across files. */
|
|
185
311
|
function resolveIdentifierInit(id) {
|
|
186
312
|
if (!Node.isIdentifier(id)) return void 0;
|
|
187
313
|
for (const def of id.getDefinitionNodes()) {
|
|
@@ -226,11 +352,6 @@ function scanObjectSchema(schema) {
|
|
|
226
352
|
}
|
|
227
353
|
return map;
|
|
228
354
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Names of the methods called at the top level of a Zod field expression.
|
|
231
|
-
* For `z.number().int().default(0)` → `{ number, int, default }`. Nested
|
|
232
|
-
* schemas (inside an object argument) are not descended into.
|
|
233
|
-
*/
|
|
234
355
|
function topLevelZodMethods(node) {
|
|
235
356
|
const methods = /* @__PURE__ */ new Set();
|
|
236
357
|
let cur = node;
|
|
@@ -243,11 +364,6 @@ function topLevelZodMethods(node) {
|
|
|
243
364
|
}
|
|
244
365
|
return methods;
|
|
245
366
|
}
|
|
246
|
-
/**
|
|
247
|
-
* The description string from a field's top-level `.describe("...")` or
|
|
248
|
-
* `.meta({ description: "..." })`, walking the method chain (outermost first,
|
|
249
|
-
* so the outermost description wins).
|
|
250
|
-
*/
|
|
251
367
|
function topLevelZodDescription(node) {
|
|
252
368
|
let cur = node;
|
|
253
369
|
while (cur && Node.isCallExpression(cur)) {
|
|
@@ -285,79 +401,6 @@ function findZodObjectLiteral(node) {
|
|
|
285
401
|
}
|
|
286
402
|
return cur && Node.isObjectLiteralExpression(cur) ? cur : void 0;
|
|
287
403
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Read `@genType` JSDoc tags from procedure declarations for return type overrides.
|
|
290
|
-
*
|
|
291
|
-
* Syntax:
|
|
292
|
-
* - Object shape: `/** @genType {{value: List<StoreCardItem>}} */` — overrides specific fields
|
|
293
|
-
* - Direct type: `/** @genType List<StoreCardItem> */` — overrides the whole return type
|
|
294
|
-
*
|
|
295
|
-
* Returns a map of procedure name → raw `@genType` value string.
|
|
296
|
-
*/
|
|
297
|
-
function scanProcedureTypeOverrides(routerDecl) {
|
|
298
|
-
const result = /* @__PURE__ */ new Map();
|
|
299
|
-
try {
|
|
300
|
-
const proceduresObj = getProceduresObjectLiteral(routerDecl);
|
|
301
|
-
if (!proceduresObj) return result;
|
|
302
|
-
for (const prop of proceduresObj.getProperties()) {
|
|
303
|
-
const key = getPropertyKey(prop);
|
|
304
|
-
if (!key) continue;
|
|
305
|
-
const typeValue = procedureTypeTag(prop);
|
|
306
|
-
if (typeValue) result.set(key, typeValue);
|
|
307
|
-
}
|
|
308
|
-
} catch {}
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
/** Read the `@genType` tag from a procedure declaration's JSDoc. */
|
|
312
|
-
function procedureTypeTag(prop) {
|
|
313
|
-
const tag = findTypeTagOnNode(prop);
|
|
314
|
-
if (tag) return tag;
|
|
315
|
-
let idNode;
|
|
316
|
-
if (Node.isShorthandPropertyAssignment(prop)) idNode = prop.getNameNode();
|
|
317
|
-
else if (Node.isPropertyAssignment(prop)) idNode = prop.getInitializer();
|
|
318
|
-
if (idNode && Node.isIdentifier(idNode)) {
|
|
319
|
-
for (const def of idNode.getDefinitionNodes()) if (Node.isVariableDeclaration(def)) {
|
|
320
|
-
const stmt = def.getVariableStatement();
|
|
321
|
-
if (stmt) {
|
|
322
|
-
const tag = findTypeTagOnNode(stmt);
|
|
323
|
-
if (tag) return tag;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
for (const def of idNode.getDefinitionNodes()) if (Node.isImportSpecifier(def)) {
|
|
327
|
-
const sourceFile = def.getImportDeclaration().getModuleSpecifierSourceFile();
|
|
328
|
-
if (sourceFile) {
|
|
329
|
-
const exported = sourceFile.getExportedDeclarations().get(def.getName());
|
|
330
|
-
if (exported && exported.length > 0) {
|
|
331
|
-
const decl = exported[0];
|
|
332
|
-
if (Node.isVariableDeclaration(decl)) {
|
|
333
|
-
const stmt = decl.getVariableStatement();
|
|
334
|
-
if (stmt) {
|
|
335
|
-
const tag = findTypeTagOnNode(stmt);
|
|
336
|
-
if (tag) return tag;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
/** Extract the `@genType` tag value from a JSDocable node, if present. */
|
|
345
|
-
function findTypeTagOnNode(node) {
|
|
346
|
-
if (!Node.isJSDocable(node)) return void 0;
|
|
347
|
-
const docs = node.getJsDocs();
|
|
348
|
-
if (docs.length === 0) return void 0;
|
|
349
|
-
const jd = docs[docs.length - 1];
|
|
350
|
-
for (const tag of jd.getTags()) if (tag.getTagName() === "genType") {
|
|
351
|
-
const text = tag.compilerNode.typeExpression?.getText?.()?.trim();
|
|
352
|
-
if (text) return text;
|
|
353
|
-
const comment = tag.getComment();
|
|
354
|
-
if (typeof comment === "string") return comment.trim();
|
|
355
|
-
if (Array.isArray(comment)) {
|
|
356
|
-
const joined = comment.map((c) => c.getText?.() ?? "").join("").trim();
|
|
357
|
-
if (joined) return joined;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
404
|
//#endregion
|
|
362
405
|
//#region src/names.ts
|
|
363
406
|
/** PascalCase an arbitrary string (splitting on any non-alphanumeric run). */
|
|
@@ -548,8 +591,9 @@ function extractClient(config, schemaEnums = SchemaEnumRegistry.empty()) {
|
|
|
548
591
|
if (!proceduresSym) throw new Error(`Export "${config.router}" does not look like a Supalive router (no "procedures" property).`);
|
|
549
592
|
const proceduresType = proceduresSym.getTypeAtLocation(routerDecl);
|
|
550
593
|
const argMeta = scanZodArgMeta(routerDecl);
|
|
594
|
+
const outputMeta = scanZodOutputMeta(routerDecl);
|
|
551
595
|
const procDocs = scanProcedureDocs(routerDecl);
|
|
552
|
-
const extractor = new Extractor(routerDecl, config, argMeta,
|
|
596
|
+
const extractor = new Extractor(routerDecl, config, argMeta, outputMeta, project.getTypeChecker(), schemaEnums);
|
|
553
597
|
const procedures = [];
|
|
554
598
|
for (const procSym of proceduresType.getProperties()) {
|
|
555
599
|
const name = procSym.getName();
|
|
@@ -573,7 +617,7 @@ var Extractor = class {
|
|
|
573
617
|
locationNode;
|
|
574
618
|
config;
|
|
575
619
|
argMeta;
|
|
576
|
-
|
|
620
|
+
outputMeta;
|
|
577
621
|
tc;
|
|
578
622
|
schemaEnums;
|
|
579
623
|
modelRegistry = /* @__PURE__ */ new Map();
|
|
@@ -583,11 +627,11 @@ var Extractor = class {
|
|
|
583
627
|
enumsFinalized = false;
|
|
584
628
|
nameCounts = /* @__PURE__ */ new Map();
|
|
585
629
|
visiting = /* @__PURE__ */ new Set();
|
|
586
|
-
constructor(locationNode, config, argMeta,
|
|
630
|
+
constructor(locationNode, config, argMeta, outputMeta, tc, schemaEnums) {
|
|
587
631
|
this.locationNode = locationNode;
|
|
588
632
|
this.config = config;
|
|
589
633
|
this.argMeta = argMeta;
|
|
590
|
-
this.
|
|
634
|
+
this.outputMeta = outputMeta;
|
|
591
635
|
this.tc = tc;
|
|
592
636
|
this.schemaEnums = schemaEnums;
|
|
593
637
|
}
|
|
@@ -601,36 +645,6 @@ var Extractor = class {
|
|
|
601
645
|
return;
|
|
602
646
|
}
|
|
603
647
|
}
|
|
604
|
-
/**
|
|
605
|
-
* Read an `@genType` JSDoc tag from a property and parse it into a DartType.
|
|
606
|
-
*
|
|
607
|
-
* Supported formats:
|
|
608
|
-
* - `/** @genType StoreCardItem */` → model with that name
|
|
609
|
-
* - `/** @genType List<StoreCardItem> */` → list with model element
|
|
610
|
-
* - `/** @genType String */`, `/** @genType int */`, etc. → primitive
|
|
611
|
-
*/
|
|
612
|
-
readTypeOverride(prop) {
|
|
613
|
-
try {
|
|
614
|
-
const decls = prop.getDeclarations();
|
|
615
|
-
if (!decls || decls.length === 0) return null;
|
|
616
|
-
const node = decls[0];
|
|
617
|
-
if (!Node.isJSDocable(node)) return null;
|
|
618
|
-
const jsDocs = node.getJsDocs();
|
|
619
|
-
if (jsDocs.length === 0) return null;
|
|
620
|
-
const typeTag = jsDocs[jsDocs.length - 1].getTags().find((t) => t.getTagName() === "genType");
|
|
621
|
-
if (!typeTag) return null;
|
|
622
|
-
let raw = typeTag.compilerNode.typeExpression?.getText?.()?.trim();
|
|
623
|
-
if (!raw) {
|
|
624
|
-
const comment = typeTag.getComment();
|
|
625
|
-
if (typeof comment === "string") raw = comment.trim();
|
|
626
|
-
else if (Array.isArray(comment)) raw = comment.map((c) => c.getText?.() ?? "").join("").trim();
|
|
627
|
-
}
|
|
628
|
-
if (!raw) return null;
|
|
629
|
-
return parseTypeOverride(raw);
|
|
630
|
-
} catch {
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
648
|
models() {
|
|
635
649
|
this.finalizeEnums();
|
|
636
650
|
return [...this.modelRegistry.values()];
|
|
@@ -663,77 +677,9 @@ var Extractor = class {
|
|
|
663
677
|
}
|
|
664
678
|
const ret = unwrapPromise(sig.getReturnType());
|
|
665
679
|
const { hasNull, hasUndefined } = splitNullish(ret);
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
if (
|
|
669
|
-
const parsed = parseProcTypeOverride(procOverrideRaw);
|
|
670
|
-
if (parsed) if (parsed.kind === "direct") {
|
|
671
|
-
const core = splitNullish(ret).core;
|
|
672
|
-
if (core.length > 0) {
|
|
673
|
-
if (parsed.dartType.kind === "model") this.walkSingle(core[0], parsed.dartType.name);
|
|
674
|
-
else if (parsed.dartType.kind === "list" && parsed.dartType.element.kind === "model") {
|
|
675
|
-
const actualEl = core[0].isArray() ? core[0].getArrayElementTypeOrThrow() : core[0];
|
|
676
|
-
this.walkSingle(actualEl, parsed.dartType.element.name);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
resultType = parsed.dartType;
|
|
680
|
-
} else {
|
|
681
|
-
const core = splitNullish(ret).core;
|
|
682
|
-
const actualType = core.length > 0 ? core[0] : void 0;
|
|
683
|
-
const modelName = pascal(name) + "Result";
|
|
684
|
-
const fields = [];
|
|
685
|
-
const overrideKeys = new Set(parsed.fields.keys());
|
|
686
|
-
for (const [fieldName, fieldType] of parsed.fields) {
|
|
687
|
-
if (actualType && actualType.isObject()) {
|
|
688
|
-
const actualProp = actualType.getProperty(fieldName);
|
|
689
|
-
if (actualProp) {
|
|
690
|
-
const actualPropType = actualProp.getTypeAtLocation(this.locationNode);
|
|
691
|
-
if (fieldType.kind === "model") this.walkSingle(actualPropType, fieldType.name);
|
|
692
|
-
else if (fieldType.kind === "list" && fieldType.element.kind === "model") {
|
|
693
|
-
const core2 = splitNullish(actualPropType).core;
|
|
694
|
-
if (core2.length > 0) {
|
|
695
|
-
const el = core2[0].isArray() ? core2[0].getArrayElementTypeOrThrow() : core2[0];
|
|
696
|
-
this.walkSingle(el, fieldType.element.name);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
fields.push({
|
|
702
|
-
name: camel(fieldName),
|
|
703
|
-
jsonKey: fieldName,
|
|
704
|
-
type: fieldType,
|
|
705
|
-
optional: false,
|
|
706
|
-
nullable: false
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
if (actualType && actualType.isObject()) for (const prop of actualType.getProperties()) {
|
|
710
|
-
const jsonKey = prop.getName();
|
|
711
|
-
if (overrideKeys.has(jsonKey)) continue;
|
|
712
|
-
if (prop.getFlags() & ts.SymbolFlags.Method) continue;
|
|
713
|
-
const { core: propCore, hasNull, hasUndefined } = splitNullish(prop.getTypeAtLocation(this.locationNode));
|
|
714
|
-
const optional = hasUndefined;
|
|
715
|
-
const nullable = hasNull;
|
|
716
|
-
const fieldHint = modelName.replace(/Result$/, "") + pascal(jsonKey);
|
|
717
|
-
const dartType = propCore.length > 0 ? propCore.length === 1 ? this.walkSingle(propCore[0], fieldHint) : this.walkUnion(propCore, { hint: fieldHint }) : { kind: "dynamic" };
|
|
718
|
-
fields.push({
|
|
719
|
-
name: camel(jsonKey),
|
|
720
|
-
jsonKey,
|
|
721
|
-
type: dartType,
|
|
722
|
-
optional,
|
|
723
|
-
nullable
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
this.modelRegistry.set(modelName, {
|
|
727
|
-
name: modelName,
|
|
728
|
-
fields
|
|
729
|
-
});
|
|
730
|
-
resultType = {
|
|
731
|
-
kind: "model",
|
|
732
|
-
name: modelName
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
else resultType = this.walk(ret, pascal(name) + "Result");
|
|
736
|
-
} else resultType = this.walk(ret, pascal(name) + "Result");
|
|
680
|
+
const resultType = this.walk(ret, pascal(name) + "Result");
|
|
681
|
+
const outputSchemaMeta = this.outputMeta.get(name);
|
|
682
|
+
if (outputSchemaMeta) this.applyOutputMeta(resultType, outputSchemaMeta);
|
|
737
683
|
return {
|
|
738
684
|
name,
|
|
739
685
|
kind,
|
|
@@ -744,6 +690,41 @@ var Extractor = class {
|
|
|
744
690
|
resultNullable: hasNull || hasUndefined
|
|
745
691
|
};
|
|
746
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Recursively apply output schema metadata to a DartType tree.
|
|
695
|
+
* Patches numeric types (int/bigint), renames models via __genType,
|
|
696
|
+
* and recurses into nested objects and lists.
|
|
697
|
+
*/
|
|
698
|
+
applyOutputMeta(dt, meta) {
|
|
699
|
+
if (meta.modelName && dt.kind === "model") {
|
|
700
|
+
const existing = this.modelRegistry.get(dt.name);
|
|
701
|
+
if (existing && !this.modelRegistry.has(meta.modelName)) {
|
|
702
|
+
const renamed = {
|
|
703
|
+
...existing,
|
|
704
|
+
name: meta.modelName
|
|
705
|
+
};
|
|
706
|
+
this.modelRegistry.set(meta.modelName, renamed);
|
|
707
|
+
this.modelRegistry.delete(dt.name);
|
|
708
|
+
}
|
|
709
|
+
dt.name = meta.modelName;
|
|
710
|
+
}
|
|
711
|
+
if (dt.kind === "model") {
|
|
712
|
+
const model = this.modelRegistry.get(dt.name);
|
|
713
|
+
if (model) for (const f of model.fields) {
|
|
714
|
+
const fMeta = meta.fieldMeta.get(f.jsonKey);
|
|
715
|
+
if (fMeta) {
|
|
716
|
+
if (fMeta.numeric && (f.type.kind === "double" || f.type.kind === "int")) f.type = { kind: fMeta.numeric };
|
|
717
|
+
if (fMeta.optional) f.optional = true;
|
|
718
|
+
if (fMeta.description && !f.doc) f.doc = fMeta.description;
|
|
719
|
+
}
|
|
720
|
+
const nestedMeta = meta.nested.get(f.jsonKey);
|
|
721
|
+
if (nestedMeta) this.applyOutputMeta(f.type, nestedMeta);
|
|
722
|
+
}
|
|
723
|
+
} else if (dt.kind === "list") {
|
|
724
|
+
const elMeta = meta.nested.get("__element");
|
|
725
|
+
if (elMeta) this.applyOutputMeta(dt.element, elMeta);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
747
728
|
walk(type, hint) {
|
|
748
729
|
const { core } = splitNullish(type);
|
|
749
730
|
if (core.length === 0) return { kind: "dynamic" };
|
|
@@ -817,19 +798,8 @@ var Extractor = class {
|
|
|
817
798
|
const { core, hasNull, hasUndefined } = splitNullish(propType);
|
|
818
799
|
const optional = declaredOptional || hasUndefined;
|
|
819
800
|
const nullable = hasNull;
|
|
820
|
-
const typeOverride = this.readTypeOverride(prop);
|
|
821
801
|
let dartType;
|
|
822
|
-
if (
|
|
823
|
-
const actualEl = core[0].isArray() ? core[0].getArrayElementTypeOrThrow() : core[0];
|
|
824
|
-
dartType = {
|
|
825
|
-
kind: "list",
|
|
826
|
-
element: this.walkSingle(actualEl, typeOverride.element.name)
|
|
827
|
-
};
|
|
828
|
-
} else dartType = typeOverride;
|
|
829
|
-
else if (typeOverride.kind === "model") if (core.length > 0) dartType = this.walkSingle(core[0], typeOverride.name);
|
|
830
|
-
else dartType = typeOverride;
|
|
831
|
-
else dartType = typeOverride;
|
|
832
|
-
else if (core.length === 0) dartType = { kind: "dynamic" };
|
|
802
|
+
if (core.length === 0) dartType = { kind: "dynamic" };
|
|
833
803
|
else if (core.length === 1) {
|
|
834
804
|
const fieldHint = name.replace(/Result$/, "") + pascal(jsonKey);
|
|
835
805
|
dartType = this.walkSingle(core[0], fieldHint);
|
|
@@ -1118,101 +1088,6 @@ function cleanStem(modelName) {
|
|
|
1118
1088
|
}
|
|
1119
1089
|
return singularizeWord(s);
|
|
1120
1090
|
}
|
|
1121
|
-
const PRIMITIVE_MAP = {
|
|
1122
|
-
string: "string",
|
|
1123
|
-
int: "int",
|
|
1124
|
-
double: "double",
|
|
1125
|
-
bool: "bool",
|
|
1126
|
-
bigint: "bigint",
|
|
1127
|
-
bytes: "bytes",
|
|
1128
|
-
datetime: "datetime",
|
|
1129
|
-
dynamic: "dynamic"
|
|
1130
|
-
};
|
|
1131
|
-
/**
|
|
1132
|
-
* Parse an `@type` override value into a DartType.
|
|
1133
|
-
*
|
|
1134
|
-
* - `"StoreCardItem"` → `{ kind: "model", name: "StoreCardItem" }`
|
|
1135
|
-
* - `"List<StoreCardItem>"` → `{ kind: "list", element: { kind: "model", name: "StoreCardItem" } }`
|
|
1136
|
-
* - `"String"` → `{ kind: "string" }`
|
|
1137
|
-
* - `"int"` → `{ kind: "int" }`
|
|
1138
|
-
*/
|
|
1139
|
-
function parseTypeOverride(raw) {
|
|
1140
|
-
const listMatch = raw.match(/^List<(.+)>$/);
|
|
1141
|
-
if (listMatch) {
|
|
1142
|
-
const innerType = parseTypeOverride(listMatch[1].trim());
|
|
1143
|
-
return innerType ? {
|
|
1144
|
-
kind: "list",
|
|
1145
|
-
element: innerType
|
|
1146
|
-
} : null;
|
|
1147
|
-
}
|
|
1148
|
-
const prim = PRIMITIVE_MAP[raw.toLowerCase()];
|
|
1149
|
-
if (prim) return { kind: prim };
|
|
1150
|
-
return {
|
|
1151
|
-
kind: "model",
|
|
1152
|
-
name: raw
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Parse a procedure-level `@genType` value.
|
|
1157
|
-
*
|
|
1158
|
-
* - `{{value: List<StoreCardItem>}}` → object shape with field overrides
|
|
1159
|
-
* - `List<StoreCardItem>` → direct type override
|
|
1160
|
-
* - `StoreCardResult` → direct model name override
|
|
1161
|
-
*/
|
|
1162
|
-
function parseProcTypeOverride(raw) {
|
|
1163
|
-
const objectMatch = raw.match(/^\{([\s\S]+)\}$/);
|
|
1164
|
-
if (objectMatch) {
|
|
1165
|
-
let inner = objectMatch[1];
|
|
1166
|
-
if (inner.startsWith("{")) inner = inner.slice(1);
|
|
1167
|
-
if (inner.endsWith("}")) inner = inner.slice(0, -1);
|
|
1168
|
-
const fields = parseFieldOverrides(inner);
|
|
1169
|
-
return fields.size > 0 ? {
|
|
1170
|
-
kind: "object",
|
|
1171
|
-
fields
|
|
1172
|
-
} : null;
|
|
1173
|
-
}
|
|
1174
|
-
const dartType = parseTypeOverride(raw);
|
|
1175
|
-
return dartType ? {
|
|
1176
|
-
kind: "direct",
|
|
1177
|
-
dartType
|
|
1178
|
-
} : null;
|
|
1179
|
-
}
|
|
1180
|
-
/**
|
|
1181
|
-
* Parse comma-separated `field: Type` entries inside an object shape override.
|
|
1182
|
-
*
|
|
1183
|
-
* Handles nested `List<T>` types (the colon inside `List<...>` must not split
|
|
1184
|
-
* the field name). Examples:
|
|
1185
|
-
* - `"value: List<StoreCardItem>"` → `{ value: { kind: "list", ... } }`
|
|
1186
|
-
* - `"name: String, count: int"` → two entries
|
|
1187
|
-
*/
|
|
1188
|
-
function parseFieldOverrides(raw) {
|
|
1189
|
-
const fields = /* @__PURE__ */ new Map();
|
|
1190
|
-
const parts = splitOutsideAngles(raw);
|
|
1191
|
-
for (const part of parts) {
|
|
1192
|
-
const colonIdx = part.indexOf(":");
|
|
1193
|
-
if (colonIdx === -1) continue;
|
|
1194
|
-
const fieldName = part.slice(0, colonIdx).trim();
|
|
1195
|
-
const typeStr = part.slice(colonIdx + 1).trim();
|
|
1196
|
-
if (!fieldName || !typeStr) continue;
|
|
1197
|
-
const dartType = parseTypeOverride(typeStr);
|
|
1198
|
-
if (dartType) fields.set(fieldName, dartType);
|
|
1199
|
-
}
|
|
1200
|
-
return fields;
|
|
1201
|
-
}
|
|
1202
|
-
/** Split a string by commas, ignoring commas inside `<...>` angle brackets. */
|
|
1203
|
-
function splitOutsideAngles(raw) {
|
|
1204
|
-
const parts = [];
|
|
1205
|
-
let depth = 0;
|
|
1206
|
-
let start = 0;
|
|
1207
|
-
for (let i = 0; i < raw.length; i++) if (raw[i] === "<") depth++;
|
|
1208
|
-
else if (raw[i] === ">") depth = Math.max(0, depth - 1);
|
|
1209
|
-
else if (raw[i] === "," && depth === 0) {
|
|
1210
|
-
parts.push(raw.slice(start, i));
|
|
1211
|
-
start = i + 1;
|
|
1212
|
-
}
|
|
1213
|
-
parts.push(raw.slice(start));
|
|
1214
|
-
return parts;
|
|
1215
|
-
}
|
|
1216
1091
|
/** The bare candidate name for a structural (non-schema) enum group. */
|
|
1217
1092
|
function fallbackBase(g) {
|
|
1218
1093
|
return pascal(g.field ?? g.hint);
|