@supalive/codegen 1.8.0 → 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-BRGy8CeY.js → src-C8EGEwt7.js} +190 -81
- 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 };
|
|
@@ -39,6 +39,20 @@ function procedureDocText(prop) {
|
|
|
39
39
|
const doc = stmt ? jsDocText(stmt) : void 0;
|
|
40
40
|
if (doc) return doc;
|
|
41
41
|
}
|
|
42
|
+
for (const def of idNode.getDefinitionNodes()) if (Node.isImportSpecifier(def)) {
|
|
43
|
+
const sourceFile = def.getImportDeclaration().getModuleSpecifierSourceFile();
|
|
44
|
+
if (sourceFile) {
|
|
45
|
+
const exported = sourceFile.getExportedDeclarations().get(def.getName());
|
|
46
|
+
if (exported && exported.length > 0) {
|
|
47
|
+
const decl = exported[0];
|
|
48
|
+
if (Node.isVariableDeclaration(decl)) {
|
|
49
|
+
const stmt = decl.getVariableStatement();
|
|
50
|
+
const doc = stmt ? jsDocText(stmt) : void 0;
|
|
51
|
+
if (doc) return doc;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
/** The trimmed description of a node's last JSDoc block, if any. */
|
|
@@ -66,6 +80,139 @@ function scanZodArgMeta(routerDecl) {
|
|
|
66
80
|
} catch {}
|
|
67
81
|
return result;
|
|
68
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
|
+
}
|
|
69
216
|
function getProceduresObjectLiteral(routerDecl) {
|
|
70
217
|
let init;
|
|
71
218
|
if (Node.isVariableDeclaration(routerDecl)) init = routerDecl.getInitializer();
|
|
@@ -109,9 +256,6 @@ function followToCall(node, depth) {
|
|
|
109
256
|
/**
|
|
110
257
|
* Given an Identifier that may be an import binding, follow through the import
|
|
111
258
|
* chain to the actual exported declaration's initializer node.
|
|
112
|
-
*
|
|
113
|
-
* Handles: `import { listItems } from "./procs.js"` → the exported VariableDeclaration
|
|
114
|
-
* `import { x } from "./reexport.js"` where that file re-exports → recursive follow
|
|
115
259
|
*/
|
|
116
260
|
function followImport(id) {
|
|
117
261
|
const sourceFile = id.getSourceFile();
|
|
@@ -153,9 +297,6 @@ function followImport(id) {
|
|
|
153
297
|
}
|
|
154
298
|
}
|
|
155
299
|
}
|
|
156
|
-
/** Extract the initializer expression from a definition node, unwrapping any
|
|
157
|
-
* container nodes (VariableStatement, ExportNamedDeclaration, etc.) that
|
|
158
|
-
* ts-morph may return for cross-file imports. */
|
|
159
300
|
function definitionInitializer(def) {
|
|
160
301
|
if (Node.isVariableDeclaration(def)) return def.getInitializer();
|
|
161
302
|
if (Node.isVariableStatement(def)) for (const d of def.getDeclarationList().getDeclarations()) {
|
|
@@ -167,7 +308,6 @@ function definitionInitializer(def) {
|
|
|
167
308
|
if (init) return init;
|
|
168
309
|
}
|
|
169
310
|
}
|
|
170
|
-
/** Resolve an Identifier to its initializer, following definitions across files. */
|
|
171
311
|
function resolveIdentifierInit(id) {
|
|
172
312
|
if (!Node.isIdentifier(id)) return void 0;
|
|
173
313
|
for (const def of id.getDefinitionNodes()) {
|
|
@@ -212,11 +352,6 @@ function scanObjectSchema(schema) {
|
|
|
212
352
|
}
|
|
213
353
|
return map;
|
|
214
354
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Names of the methods called at the top level of a Zod field expression.
|
|
217
|
-
* For `z.number().int().default(0)` → `{ number, int, default }`. Nested
|
|
218
|
-
* schemas (inside an object argument) are not descended into.
|
|
219
|
-
*/
|
|
220
355
|
function topLevelZodMethods(node) {
|
|
221
356
|
const methods = /* @__PURE__ */ new Set();
|
|
222
357
|
let cur = node;
|
|
@@ -229,11 +364,6 @@ function topLevelZodMethods(node) {
|
|
|
229
364
|
}
|
|
230
365
|
return methods;
|
|
231
366
|
}
|
|
232
|
-
/**
|
|
233
|
-
* The description string from a field's top-level `.describe("...")` or
|
|
234
|
-
* `.meta({ description: "..." })`, walking the method chain (outermost first,
|
|
235
|
-
* so the outermost description wins).
|
|
236
|
-
*/
|
|
237
367
|
function topLevelZodDescription(node) {
|
|
238
368
|
let cur = node;
|
|
239
369
|
while (cur && Node.isCallExpression(cur)) {
|
|
@@ -461,8 +591,9 @@ function extractClient(config, schemaEnums = SchemaEnumRegistry.empty()) {
|
|
|
461
591
|
if (!proceduresSym) throw new Error(`Export "${config.router}" does not look like a Supalive router (no "procedures" property).`);
|
|
462
592
|
const proceduresType = proceduresSym.getTypeAtLocation(routerDecl);
|
|
463
593
|
const argMeta = scanZodArgMeta(routerDecl);
|
|
594
|
+
const outputMeta = scanZodOutputMeta(routerDecl);
|
|
464
595
|
const procDocs = scanProcedureDocs(routerDecl);
|
|
465
|
-
const extractor = new Extractor(routerDecl, config, argMeta, project.getTypeChecker(), schemaEnums);
|
|
596
|
+
const extractor = new Extractor(routerDecl, config, argMeta, outputMeta, project.getTypeChecker(), schemaEnums);
|
|
466
597
|
const procedures = [];
|
|
467
598
|
for (const procSym of proceduresType.getProperties()) {
|
|
468
599
|
const name = procSym.getName();
|
|
@@ -486,6 +617,7 @@ var Extractor = class {
|
|
|
486
617
|
locationNode;
|
|
487
618
|
config;
|
|
488
619
|
argMeta;
|
|
620
|
+
outputMeta;
|
|
489
621
|
tc;
|
|
490
622
|
schemaEnums;
|
|
491
623
|
modelRegistry = /* @__PURE__ */ new Map();
|
|
@@ -495,10 +627,11 @@ var Extractor = class {
|
|
|
495
627
|
enumsFinalized = false;
|
|
496
628
|
nameCounts = /* @__PURE__ */ new Map();
|
|
497
629
|
visiting = /* @__PURE__ */ new Set();
|
|
498
|
-
constructor(locationNode, config, argMeta, tc, schemaEnums) {
|
|
630
|
+
constructor(locationNode, config, argMeta, outputMeta, tc, schemaEnums) {
|
|
499
631
|
this.locationNode = locationNode;
|
|
500
632
|
this.config = config;
|
|
501
633
|
this.argMeta = argMeta;
|
|
634
|
+
this.outputMeta = outputMeta;
|
|
502
635
|
this.tc = tc;
|
|
503
636
|
this.schemaEnums = schemaEnums;
|
|
504
637
|
}
|
|
@@ -512,31 +645,6 @@ var Extractor = class {
|
|
|
512
645
|
return;
|
|
513
646
|
}
|
|
514
647
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Read an `@type` JSDoc tag from a property and parse it into a DartType.
|
|
517
|
-
*
|
|
518
|
-
* Supported formats:
|
|
519
|
-
* - `/** @type StoreCardItem */` → model with that name
|
|
520
|
-
* - `/** @type List<StoreCardItem> */` → list with model element
|
|
521
|
-
* - `/** @type String */`, `/** @type int */`, etc. → primitive
|
|
522
|
-
*/
|
|
523
|
-
readTypeOverride(prop) {
|
|
524
|
-
try {
|
|
525
|
-
const decls = prop.getDeclarations();
|
|
526
|
-
if (!decls || decls.length === 0) return null;
|
|
527
|
-
const node = decls[0];
|
|
528
|
-
if (!Node.isJSDocable(node)) return null;
|
|
529
|
-
const jsDocs = node.getJsDocs();
|
|
530
|
-
if (jsDocs.length === 0) return null;
|
|
531
|
-
const typeTag = jsDocs[jsDocs.length - 1].getTags().find((t) => t.getTagName() === "type");
|
|
532
|
-
if (!typeTag) return null;
|
|
533
|
-
const raw = typeTag.compilerNode.typeExpression?.getText?.()?.trim();
|
|
534
|
-
if (!raw) return null;
|
|
535
|
-
return parseTypeOverride(raw);
|
|
536
|
-
} catch {
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
648
|
models() {
|
|
541
649
|
this.finalizeEnums();
|
|
542
650
|
return [...this.modelRegistry.values()];
|
|
@@ -570,6 +678,8 @@ var Extractor = class {
|
|
|
570
678
|
const ret = unwrapPromise(sig.getReturnType());
|
|
571
679
|
const { hasNull, hasUndefined } = splitNullish(ret);
|
|
572
680
|
const resultType = this.walk(ret, pascal(name) + "Result");
|
|
681
|
+
const outputSchemaMeta = this.outputMeta.get(name);
|
|
682
|
+
if (outputSchemaMeta) this.applyOutputMeta(resultType, outputSchemaMeta);
|
|
573
683
|
return {
|
|
574
684
|
name,
|
|
575
685
|
kind,
|
|
@@ -580,6 +690,41 @@ var Extractor = class {
|
|
|
580
690
|
resultNullable: hasNull || hasUndefined
|
|
581
691
|
};
|
|
582
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
|
+
}
|
|
583
728
|
walk(type, hint) {
|
|
584
729
|
const { core } = splitNullish(type);
|
|
585
730
|
if (core.length === 0) return { kind: "dynamic" };
|
|
@@ -653,10 +798,8 @@ var Extractor = class {
|
|
|
653
798
|
const { core, hasNull, hasUndefined } = splitNullish(propType);
|
|
654
799
|
const optional = declaredOptional || hasUndefined;
|
|
655
800
|
const nullable = hasNull;
|
|
656
|
-
const typeOverride = this.readTypeOverride(prop);
|
|
657
801
|
let dartType;
|
|
658
|
-
if (
|
|
659
|
-
else if (core.length === 0) dartType = { kind: "dynamic" };
|
|
802
|
+
if (core.length === 0) dartType = { kind: "dynamic" };
|
|
660
803
|
else if (core.length === 1) {
|
|
661
804
|
const fieldHint = name.replace(/Result$/, "") + pascal(jsonKey);
|
|
662
805
|
dartType = this.walkSingle(core[0], fieldHint);
|
|
@@ -945,40 +1088,6 @@ function cleanStem(modelName) {
|
|
|
945
1088
|
}
|
|
946
1089
|
return singularizeWord(s);
|
|
947
1090
|
}
|
|
948
|
-
const PRIMITIVE_MAP = {
|
|
949
|
-
string: "string",
|
|
950
|
-
int: "int",
|
|
951
|
-
double: "double",
|
|
952
|
-
bool: "bool",
|
|
953
|
-
bigint: "bigint",
|
|
954
|
-
bytes: "bytes",
|
|
955
|
-
datetime: "datetime",
|
|
956
|
-
dynamic: "dynamic"
|
|
957
|
-
};
|
|
958
|
-
/**
|
|
959
|
-
* Parse an `@type` override value into a DartType.
|
|
960
|
-
*
|
|
961
|
-
* - `"StoreCardItem"` → `{ kind: "model", name: "StoreCardItem" }`
|
|
962
|
-
* - `"List<StoreCardItem>"` → `{ kind: "list", element: { kind: "model", name: "StoreCardItem" } }`
|
|
963
|
-
* - `"String"` → `{ kind: "string" }`
|
|
964
|
-
* - `"int"` → `{ kind: "int" }`
|
|
965
|
-
*/
|
|
966
|
-
function parseTypeOverride(raw) {
|
|
967
|
-
const listMatch = raw.match(/^List<(.+)>$/);
|
|
968
|
-
if (listMatch) {
|
|
969
|
-
const innerType = parseTypeOverride(listMatch[1].trim());
|
|
970
|
-
return innerType ? {
|
|
971
|
-
kind: "list",
|
|
972
|
-
element: innerType
|
|
973
|
-
} : null;
|
|
974
|
-
}
|
|
975
|
-
const prim = PRIMITIVE_MAP[raw.toLowerCase()];
|
|
976
|
-
if (prim) return { kind: prim };
|
|
977
|
-
return {
|
|
978
|
-
kind: "model",
|
|
979
|
-
name: raw
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
1091
|
/** The bare candidate name for a structural (non-schema) enum group. */
|
|
983
1092
|
function fallbackBase(g) {
|
|
984
1093
|
return pascal(g.field ?? g.hint);
|