nuxt-openapi-hyperfetch 0.3.0-beta → 0.3.1-beta
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/dist/generators/components/connector-generator/generator.js +1 -0
- package/dist/generators/components/connector-generator/templates.d.ts +1 -1
- package/dist/generators/components/connector-generator/templates.js +125 -43
- package/dist/generators/shared/runtime/connector-types.d.ts +104 -0
- package/dist/generators/shared/runtime/connector-types.js +10 -0
- package/dist/generators/shared/runtime/useListConnector.d.ts +5 -3
- package/dist/generators/shared/runtime/useListConnector.js +6 -4
- package/dist/generators/use-async-data/runtime/useApiAsyncData.d.ts +8 -2
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.d.ts +9 -3
- package/dist/generators/use-async-data/templates.js +24 -8
- package/dist/generators/use-fetch/runtime/useApiRequest.d.ts +9 -2
- package/dist/generators/use-fetch/templates.js +9 -5
- package/package.json +1 -1
- package/src/generators/components/connector-generator/generator.ts +1 -0
- package/src/generators/components/connector-generator/templates.ts +157 -43
- package/src/generators/shared/runtime/connector-types.ts +142 -0
- package/src/generators/shared/runtime/useListConnector.ts +6 -4
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +33 -5
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +30 -8
- package/src/generators/use-async-data/templates.ts +24 -9
- package/src/generators/use-fetch/runtime/useApiRequest.ts +34 -4
- package/src/generators/use-fetch/templates.ts +9 -6
|
@@ -15,7 +15,6 @@ function generateFileHeader(): string {
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/* eslint-disable */
|
|
18
|
-
// @ts-nocheck
|
|
19
18
|
`;
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -42,18 +41,37 @@ function toFileName(composableName: string): string {
|
|
|
42
41
|
/**
|
|
43
42
|
* Build all `import` lines for a resource connector.
|
|
44
43
|
*/
|
|
45
|
-
function buildImports(resource: ResourceInfo, composablesRelDir: string): string {
|
|
44
|
+
function buildImports(resource: ResourceInfo, composablesRelDir: string, sdkRelDir: string): string {
|
|
46
45
|
const lines: string[] = [];
|
|
47
46
|
|
|
48
47
|
// zod
|
|
49
48
|
lines.push(`import { z } from 'zod';`);
|
|
50
49
|
lines.push('');
|
|
51
50
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
51
|
+
// connector-types — structural interfaces for return types
|
|
52
|
+
const connectorTypeImports: string[] = ['ListConnectorReturn'];
|
|
53
|
+
if (resource.detailEndpoint) {
|
|
54
|
+
connectorTypeImports.push('DetailConnectorReturn');
|
|
55
|
+
}
|
|
56
|
+
if (resource.createEndpoint || resource.updateEndpoint) {
|
|
57
|
+
connectorTypeImports.push('FormConnectorReturn');
|
|
58
|
+
}
|
|
59
|
+
if (resource.deleteEndpoint) {
|
|
60
|
+
connectorTypeImports.push('DeleteConnectorReturn');
|
|
61
|
+
}
|
|
62
|
+
lines.push(`import type { ${connectorTypeImports.join(', ')} } from '#nxh/runtime/connector-types';`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
|
|
65
|
+
// SDK request/response types (for the params overload signature)
|
|
54
66
|
if (resource.listEndpoint) {
|
|
55
|
-
|
|
67
|
+
const requestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
|
|
68
|
+
lines.push(`import type { ${requestTypeName} } from '${sdkRelDir}';`);
|
|
69
|
+
lines.push('');
|
|
56
70
|
}
|
|
71
|
+
|
|
72
|
+
// runtime helpers (Nuxt alias — set up by the Nuxt module)
|
|
73
|
+
// useListConnector is always imported to support the optional factory pattern
|
|
74
|
+
const runtimeHelpers: string[] = ['useListConnector'];
|
|
57
75
|
if (resource.detailEndpoint) {
|
|
58
76
|
runtimeHelpers.push('useDetailConnector');
|
|
59
77
|
}
|
|
@@ -141,6 +159,68 @@ function buildColumns(resource: ResourceInfo): string {
|
|
|
141
159
|
return `const ${varName} = [\n${entries},\n];`;
|
|
142
160
|
}
|
|
143
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Build the TypeScript options interface for a connector.
|
|
164
|
+
* Only includes fields relevant to the endpoints present on the resource.
|
|
165
|
+
*/
|
|
166
|
+
function buildOptionsInterface(resource: ResourceInfo): string {
|
|
167
|
+
const typeName = `${pascalCase(resource.composableName)}Options`;
|
|
168
|
+
const hasColumns = resource.columns && resource.columns.length > 0;
|
|
169
|
+
const fields: string[] = [];
|
|
170
|
+
|
|
171
|
+
if (resource.listEndpoint && hasColumns) {
|
|
172
|
+
fields.push(` columnLabels?: Record<string, string>;`);
|
|
173
|
+
fields.push(` columnLabel?: (key: string) => string;`);
|
|
174
|
+
}
|
|
175
|
+
if (resource.createEndpoint && resource.zodSchemas.create) {
|
|
176
|
+
fields.push(` createSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
|
|
177
|
+
}
|
|
178
|
+
if (resource.updateEndpoint && resource.zodSchemas.update) {
|
|
179
|
+
fields.push(` updateSchema?: z.ZodTypeAny | ((base: z.ZodTypeAny) => z.ZodTypeAny);`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (fields.length === 0) {
|
|
183
|
+
return `type ${typeName} = Record<string, never>;`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [`interface ${typeName} {`, ...fields, `}`].join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build the TypeScript return type for a connector.
|
|
191
|
+
*/
|
|
192
|
+
function buildReturnType(resource: ResourceInfo): string {
|
|
193
|
+
const pascal = pascalCase(resource.name);
|
|
194
|
+
const typeName = `${pascalCase(resource.composableName)}Return`;
|
|
195
|
+
const fields: string[] = [];
|
|
196
|
+
|
|
197
|
+
// table is always present in the return type:
|
|
198
|
+
// - if listEndpoint exists → ListConnectorReturn<T> (always defined)
|
|
199
|
+
// - if no listEndpoint → ListConnectorReturn<unknown> | undefined (only when factory passed)
|
|
200
|
+
if (resource.listEndpoint) {
|
|
201
|
+
fields.push(` table: ListConnectorReturn<${pascal}>;`);
|
|
202
|
+
} else {
|
|
203
|
+
fields.push(` table: ListConnectorReturn<unknown> | undefined;`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (resource.detailEndpoint) {
|
|
207
|
+
fields.push(` detail: DetailConnectorReturn<${pascal}>;`);
|
|
208
|
+
}
|
|
209
|
+
if (resource.createEndpoint) {
|
|
210
|
+
const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
|
|
211
|
+
fields.push(` createForm: FormConnectorReturn<${inputType}>;`);
|
|
212
|
+
}
|
|
213
|
+
if (resource.updateEndpoint) {
|
|
214
|
+
const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
|
|
215
|
+
fields.push(` updateForm: FormConnectorReturn<${inputType}>;`);
|
|
216
|
+
}
|
|
217
|
+
if (resource.deleteEndpoint) {
|
|
218
|
+
fields.push(` deleteAction: DeleteConnectorReturn<${pascal}>;`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return [`type ${typeName} = {`, ...fields, `};`].join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
144
224
|
/**
|
|
145
225
|
* Build the body of the exported connector function.
|
|
146
226
|
*/
|
|
@@ -154,6 +234,10 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
154
234
|
const columnsVar = `${camel}Columns`;
|
|
155
235
|
const subConnectors: string[] = [];
|
|
156
236
|
|
|
237
|
+
// Derived type names — must match buildOptionsInterface / buildReturnType
|
|
238
|
+
const optionsTypeName = `${pascalCase(resource.composableName)}Options`;
|
|
239
|
+
const returnTypeName = `${pascalCase(resource.composableName)}Return`;
|
|
240
|
+
|
|
157
241
|
// Destructure options param — only what's relevant for this resource
|
|
158
242
|
const optionKeys: string[] = [];
|
|
159
243
|
if (resource.listEndpoint && hasColumns) {
|
|
@@ -169,67 +253,71 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
169
253
|
const optionsDestructure =
|
|
170
254
|
optionKeys.length > 0 ? ` const { ${optionKeys.join(', ')} } = options;\n` : '';
|
|
171
255
|
|
|
256
|
+
// ── List / table sub-connector ─────────────────────────────────────────────
|
|
172
257
|
if (resource.listEndpoint) {
|
|
173
258
|
const fn = toAsyncDataName(resource.listEndpoint.operationId);
|
|
174
|
-
|
|
175
|
-
// (goToPage, nextPage, prevPage, setPerPage, pagination ref).
|
|
176
|
-
// We set it whenever the spec declares a list endpoint that has a response schema,
|
|
177
|
-
// which is a reliable proxy for "this API returns structured data worth paginating".
|
|
259
|
+
const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
|
|
178
260
|
const paginatedFlag = resource.listEndpoint.responseSchema ? 'paginated: true' : '';
|
|
179
261
|
const columnsArg = hasColumns ? `columns: ${columnsVar}` : '';
|
|
180
262
|
const labelArgs = hasColumns ? 'columnLabels, columnLabel' : '';
|
|
181
263
|
const allArgs = [paginatedFlag, columnsArg, labelArgs].filter(Boolean).join(', ');
|
|
182
264
|
const opts = allArgs ? `{ ${allArgs} }` : '{}';
|
|
183
|
-
|
|
265
|
+
|
|
266
|
+
// Factory: if the first arg is a function the user provided their own composable;
|
|
267
|
+
// otherwise build a default factory from the plain params object.
|
|
268
|
+
subConnectors.push(
|
|
269
|
+
` const isFactory = typeof paramsOrSource === 'function';`,
|
|
270
|
+
` const listFactory = isFactory`,
|
|
271
|
+
` ? (paramsOrSource as () => unknown)`,
|
|
272
|
+
` : () => ${fn}((paramsOrSource ?? {}) as ${listRequestTypeName});`,
|
|
273
|
+
` const table = useListConnector(listFactory, ${opts}) as unknown as ListConnectorReturn<${pascal}>;`
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
// No list endpoint — support optional factory for developer-provided list
|
|
277
|
+
subConnectors.push(
|
|
278
|
+
` const table = paramsOrSource`,
|
|
279
|
+
` ? (useListConnector(paramsOrSource as () => unknown, {}) as unknown as ListConnectorReturn<unknown>)`,
|
|
280
|
+
` : undefined;`
|
|
281
|
+
);
|
|
184
282
|
}
|
|
185
283
|
|
|
186
284
|
if (resource.detailEndpoint) {
|
|
187
285
|
const fn = toAsyncDataName(resource.detailEndpoint.operationId);
|
|
188
|
-
subConnectors.push(` const detail = useDetailConnector(${fn})
|
|
286
|
+
subConnectors.push(` const detail = useDetailConnector(${fn}) as unknown as DetailConnectorReturn<${pascal}>;`);
|
|
189
287
|
}
|
|
190
288
|
|
|
191
289
|
if (resource.createEndpoint) {
|
|
192
290
|
const fn = toAsyncDataName(resource.createEndpoint.operationId);
|
|
291
|
+
const inputType = resource.zodSchemas.create ? `${pascal}CreateInput` : `Record<string, unknown>`;
|
|
193
292
|
const schemaArg = resource.zodSchemas.create
|
|
194
293
|
? `{ schema: ${pascal}CreateSchema, schemaOverride: createSchema }`
|
|
195
294
|
: '{}';
|
|
196
|
-
subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg})
|
|
295
|
+
subConnectors.push(` const createForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
|
|
197
296
|
}
|
|
198
297
|
|
|
199
298
|
if (resource.updateEndpoint) {
|
|
200
299
|
const fn = toAsyncDataName(resource.updateEndpoint.operationId);
|
|
201
300
|
const hasDetail = !!resource.detailEndpoint;
|
|
301
|
+
const inputType = resource.zodSchemas.update ? `${pascal}UpdateInput` : `Record<string, unknown>`;
|
|
202
302
|
|
|
203
|
-
// Build the options argument for useFormConnector:
|
|
204
|
-
// schema → Zod schema for client-side validation before submission
|
|
205
|
-
// loadWith → reference to the detail connector so the form auto-fills
|
|
206
|
-
// when detail.item changes (user clicks "Edit" on a row)
|
|
207
|
-
//
|
|
208
|
-
// Four combinations are possible depending on what the spec provides:
|
|
209
303
|
let schemaArg = '{}';
|
|
210
304
|
if (resource.zodSchemas.update && hasDetail) {
|
|
211
|
-
// Best case: validate AND pre-fill from detail
|
|
212
305
|
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema, loadWith: detail }`;
|
|
213
306
|
} else if (resource.zodSchemas.update) {
|
|
214
|
-
// Validate, but no detail endpoint to pre-fill from
|
|
215
307
|
schemaArg = `{ schema: ${pascal}UpdateSchema, schemaOverride: updateSchema }`;
|
|
216
308
|
} else if (hasDetail) {
|
|
217
|
-
// No Zod schema (no request body in spec), but still pre-fill from detail
|
|
218
309
|
schemaArg = `{ loadWith: detail }`;
|
|
219
310
|
}
|
|
220
|
-
subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg})
|
|
311
|
+
subConnectors.push(` const updateForm = useFormConnector(${fn}, ${schemaArg}) as unknown as FormConnectorReturn<${inputType}>;`);
|
|
221
312
|
}
|
|
222
313
|
|
|
223
314
|
if (resource.deleteEndpoint) {
|
|
224
315
|
const fn = toAsyncDataName(resource.deleteEndpoint.operationId);
|
|
225
|
-
subConnectors.push(` const deleteAction = useDeleteConnector(${fn})
|
|
316
|
+
subConnectors.push(` const deleteAction = useDeleteConnector(${fn}) as unknown as DeleteConnectorReturn<${pascal}>;`);
|
|
226
317
|
}
|
|
227
318
|
|
|
228
|
-
// Return object —
|
|
229
|
-
const returnKeys: string[] = [];
|
|
230
|
-
if (resource.listEndpoint) {
|
|
231
|
-
returnKeys.push('table');
|
|
232
|
-
}
|
|
319
|
+
// Return object — always includes table (undefined when no list + no factory)
|
|
320
|
+
const returnKeys: string[] = ['table'];
|
|
233
321
|
if (resource.detailEndpoint) {
|
|
234
322
|
returnKeys.push('detail');
|
|
235
323
|
}
|
|
@@ -243,17 +331,36 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
243
331
|
returnKeys.push('deleteAction');
|
|
244
332
|
}
|
|
245
333
|
|
|
246
|
-
const returnStatement = ` return { ${returnKeys.join(', ')} };`;
|
|
334
|
+
const returnStatement = ` return { ${returnKeys.join(', ')} } as ${returnTypeName};`;
|
|
247
335
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.
|
|
256
|
-
|
|
336
|
+
// ── Function signature ─────────────────────────────────────────────────────
|
|
337
|
+
// Resources WITH a list endpoint: two overloads (factory | params).
|
|
338
|
+
// Resources WITHOUT a list endpoint: single signature with optional factory.
|
|
339
|
+
const lines: string[] = [];
|
|
340
|
+
|
|
341
|
+
if (resource.listEndpoint) {
|
|
342
|
+
const listRequestTypeName = `${pascalCase(resource.listEndpoint.operationId)}Request`;
|
|
343
|
+
lines.push(
|
|
344
|
+
`export function ${resource.composableName}(source: () => unknown, options?: ${optionsTypeName}): ${returnTypeName};`,
|
|
345
|
+
`export function ${resource.composableName}(params?: ${listRequestTypeName}, options?: ${optionsTypeName}): ${returnTypeName};`,
|
|
346
|
+
`export function ${resource.composableName}(paramsOrSource?: ${listRequestTypeName} | (() => unknown), options: ${optionsTypeName} = {}): ${returnTypeName} {`
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
lines.push(
|
|
350
|
+
`export function ${resource.composableName}(source?: () => unknown, options: ${optionsTypeName} = {}): ${returnTypeName} {`
|
|
351
|
+
);
|
|
352
|
+
// Alias so the body can use paramsOrSource uniformly
|
|
353
|
+
lines.push(` const paramsOrSource = source;`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (optionsDestructure.trim()) {
|
|
357
|
+
lines.push(optionsDestructure.trimEnd());
|
|
358
|
+
}
|
|
359
|
+
lines.push(...subConnectors);
|
|
360
|
+
lines.push(returnStatement);
|
|
361
|
+
lines.push(`}`);
|
|
362
|
+
|
|
363
|
+
return lines.join('\n');
|
|
257
364
|
}
|
|
258
365
|
|
|
259
366
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -265,16 +372,21 @@ function buildFunctionBody(resource: ResourceInfo): string {
|
|
|
265
372
|
* @param composablesRelDir Relative path from the connector dir to the
|
|
266
373
|
* useAsyncData composables dir (e.g. '../use-async-data')
|
|
267
374
|
*/
|
|
268
|
-
export function generateConnectorFile(
|
|
375
|
+
export function generateConnectorFile(
|
|
376
|
+
resource: ResourceInfo,
|
|
377
|
+
composablesRelDir: string,
|
|
378
|
+
sdkRelDir = '../..'
|
|
379
|
+
): string {
|
|
269
380
|
const header = generateFileHeader();
|
|
270
|
-
const imports = buildImports(resource, composablesRelDir);
|
|
381
|
+
const imports = buildImports(resource, composablesRelDir, sdkRelDir);
|
|
271
382
|
const schemas = buildZodSchemas(resource);
|
|
272
383
|
const columns = buildColumns(resource);
|
|
384
|
+
const optionsInterface = buildOptionsInterface(resource);
|
|
385
|
+
const returnType = buildReturnType(resource);
|
|
273
386
|
const fn = buildFunctionBody(resource);
|
|
274
387
|
|
|
275
|
-
// Assemble file: header + imports + (optional) Zod blocks + columns const +
|
|
276
|
-
//
|
|
277
|
-
// line between sections, which matches Prettier's output for this structure.
|
|
388
|
+
// Assemble file: header + imports + (optional) Zod blocks + columns const +
|
|
389
|
+
// options interface + return type + function body.
|
|
278
390
|
const parts: string[] = [header, imports];
|
|
279
391
|
if (schemas.trim()) {
|
|
280
392
|
parts.push(schemas);
|
|
@@ -282,6 +394,8 @@ export function generateConnectorFile(resource: ResourceInfo, composablesRelDir:
|
|
|
282
394
|
if (columns.trim()) {
|
|
283
395
|
parts.push(columns);
|
|
284
396
|
}
|
|
397
|
+
parts.push(optionsInterface);
|
|
398
|
+
parts.push(returnType);
|
|
285
399
|
parts.push(fn);
|
|
286
400
|
|
|
287
401
|
return parts.join('\n') + '\n';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connector-types.ts — Structural return type interfaces for the 4 runtime connectors.
|
|
3
|
+
*
|
|
4
|
+
* Uses locally-defined minimal type aliases for Ref/ComputedRef/ShallowRef so this
|
|
5
|
+
* file compiles in the CLI context (where Vue is not installed) and remains
|
|
6
|
+
* structurally compatible with Vue's actual types in the user's Nuxt project.
|
|
7
|
+
*
|
|
8
|
+
* Copied to the user's project alongside the generated connectors and runtime helpers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Minimal structural aliases — compatible with Vue's Ref<T>, ComputedRef<T>, ShallowRef<T>.
|
|
12
|
+
// These are intentionally kept as simple as possible to avoid coupling to Vue's internals.
|
|
13
|
+
type Ref<T> = { value: T };
|
|
14
|
+
type ShallowRef<T> = { value: T };
|
|
15
|
+
type ComputedRef<T> = { readonly value: T };
|
|
16
|
+
|
|
17
|
+
// ─── Column / field defs (mirrors schema-analyzer output) ────────────────────
|
|
18
|
+
|
|
19
|
+
export interface ColumnDef {
|
|
20
|
+
key: string;
|
|
21
|
+
label: string;
|
|
22
|
+
type: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FormFieldDef {
|
|
26
|
+
key: string;
|
|
27
|
+
label: string;
|
|
28
|
+
type: string;
|
|
29
|
+
required: boolean;
|
|
30
|
+
options?: { label: string; value: string }[];
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
hidden?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Pagination ───────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export interface PaginationState {
|
|
38
|
+
page: number;
|
|
39
|
+
perPage: number;
|
|
40
|
+
total: number;
|
|
41
|
+
totalPages: number;
|
|
42
|
+
goToPage: (page: number) => void;
|
|
43
|
+
nextPage: () => void;
|
|
44
|
+
prevPage: () => void;
|
|
45
|
+
setPerPage: (n: number) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── ListConnectorReturn ──────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export interface ListConnectorReturn<TRow = unknown> {
|
|
51
|
+
// State
|
|
52
|
+
rows: ComputedRef<TRow[]>;
|
|
53
|
+
columns: ComputedRef<ColumnDef[]>;
|
|
54
|
+
loading: ComputedRef<boolean>;
|
|
55
|
+
error: ComputedRef<unknown>;
|
|
56
|
+
|
|
57
|
+
// Pagination
|
|
58
|
+
pagination: ComputedRef<PaginationState | null>;
|
|
59
|
+
goToPage: (page: number) => void;
|
|
60
|
+
nextPage: () => void;
|
|
61
|
+
prevPage: () => void;
|
|
62
|
+
setPerPage: (n: number) => void;
|
|
63
|
+
|
|
64
|
+
// Selection
|
|
65
|
+
selected: Ref<TRow[]>;
|
|
66
|
+
onRowSelect: (row: TRow) => void;
|
|
67
|
+
clearSelection: () => void;
|
|
68
|
+
|
|
69
|
+
// Actions
|
|
70
|
+
refresh: () => void;
|
|
71
|
+
|
|
72
|
+
// CRUD coordination — public methods
|
|
73
|
+
create: () => void;
|
|
74
|
+
update: (row: TRow) => void;
|
|
75
|
+
remove: (row: TRow) => void;
|
|
76
|
+
|
|
77
|
+
// CRUD coordination — internal triggers (watch in the parent component)
|
|
78
|
+
_createTrigger: Ref<number>;
|
|
79
|
+
_updateTarget: ShallowRef<TRow | null>;
|
|
80
|
+
_deleteTarget: ShallowRef<TRow | null>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── DetailConnectorReturn ────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export interface DetailConnectorReturn<TItem = unknown> {
|
|
86
|
+
// State
|
|
87
|
+
item: ComputedRef<TItem | null>;
|
|
88
|
+
loading: ComputedRef<boolean>;
|
|
89
|
+
error: ComputedRef<unknown>;
|
|
90
|
+
fields: ComputedRef<FormFieldDef[]>;
|
|
91
|
+
|
|
92
|
+
// Actions
|
|
93
|
+
load: (id: string | number) => Promise<void>;
|
|
94
|
+
clear: () => void;
|
|
95
|
+
|
|
96
|
+
// Internals (advanced use)
|
|
97
|
+
_composable: unknown;
|
|
98
|
+
_currentId: Ref<string | number | null>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── FormConnectorReturn ──────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export interface FormConnectorReturn<TInput = Record<string, unknown>> {
|
|
104
|
+
// State
|
|
105
|
+
model: Ref<Partial<TInput>>;
|
|
106
|
+
errors: Ref<Record<string, string[]>>;
|
|
107
|
+
loading: Ref<boolean>;
|
|
108
|
+
submitError: Ref<unknown>;
|
|
109
|
+
submitted: Ref<boolean>;
|
|
110
|
+
isValid: ComputedRef<boolean>;
|
|
111
|
+
hasErrors: ComputedRef<boolean>;
|
|
112
|
+
fields: ComputedRef<FormFieldDef[]>;
|
|
113
|
+
|
|
114
|
+
// Callbacks (developer-assignable)
|
|
115
|
+
onSuccess: Ref<((data: unknown) => void) | null>;
|
|
116
|
+
onError: Ref<((err: unknown) => void) | null>;
|
|
117
|
+
|
|
118
|
+
// Actions
|
|
119
|
+
submit: () => Promise<void>;
|
|
120
|
+
reset: () => void;
|
|
121
|
+
setValues: (data: Partial<TInput>) => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── DeleteConnectorReturn ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export interface DeleteConnectorReturn<TItem = unknown> {
|
|
127
|
+
// State
|
|
128
|
+
target: Ref<TItem | null>;
|
|
129
|
+
isOpen: Ref<boolean>;
|
|
130
|
+
loading: Ref<boolean>;
|
|
131
|
+
error: Ref<unknown>;
|
|
132
|
+
hasTarget: ComputedRef<boolean>;
|
|
133
|
+
|
|
134
|
+
// Callbacks (developer-assignable)
|
|
135
|
+
onSuccess: Ref<((item: TItem) => void) | null>;
|
|
136
|
+
onError: Ref<((err: unknown) => void) | null>;
|
|
137
|
+
|
|
138
|
+
// Actions
|
|
139
|
+
setTarget: (item: TItem) => void;
|
|
140
|
+
cancel: () => void;
|
|
141
|
+
confirm: () => Promise<void>;
|
|
142
|
+
}
|
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
import { ref, computed, shallowRef } from 'vue';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* @param
|
|
17
|
-
*
|
|
16
|
+
* @param factory A zero-argument function that calls and returns the underlying
|
|
17
|
+
* useAsyncData composable, e.g. () => useAsyncDataGetPets(params)
|
|
18
|
+
* The factory is called once during connector setup (inside setup()).
|
|
19
|
+
* @param options Configuration for the list connector
|
|
18
20
|
*/
|
|
19
|
-
export function useListConnector(
|
|
21
|
+
export function useListConnector(factory, options = {}) {
|
|
20
22
|
const { paginated = false, columns = [], columnLabels = {}, columnLabel = null } = options;
|
|
21
23
|
|
|
22
24
|
// ── Execute the underlying composable ──────────────────────────────────────
|
|
23
|
-
const composable =
|
|
25
|
+
const composable = factory();
|
|
24
26
|
|
|
25
27
|
// ── Derived state ──────────────────────────────────────────────────────────
|
|
26
28
|
|
|
@@ -37,14 +37,39 @@ type MaybeTransformed<T, Options> = Options extends { transform: (...args: any)
|
|
|
37
37
|
? any // With nested paths, type inference is complex, so we use any
|
|
38
38
|
: T;
|
|
39
39
|
|
|
40
|
+
type PickInput = ReadonlyArray<string> | undefined;
|
|
41
|
+
|
|
42
|
+
type HasNestedPath<K extends ReadonlyArray<string>> =
|
|
43
|
+
Extract<K[number], `${string}.${string}`> extends never ? false : true;
|
|
44
|
+
|
|
45
|
+
type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string>
|
|
46
|
+
? HasNestedPath<K> extends true
|
|
47
|
+
? any
|
|
48
|
+
: Pick<T, Extract<K[number], keyof T>>
|
|
49
|
+
: T;
|
|
50
|
+
|
|
51
|
+
type InferPick<Options> = Options extends { pick: infer K extends ReadonlyArray<string> }
|
|
52
|
+
? K
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
type InferData<T, Options> = Options extends { transform: (...args: any) => infer R }
|
|
56
|
+
? R
|
|
57
|
+
: PickedData<T, InferPick<Options>>;
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
* Options for useAsyncData API requests with lifecycle callbacks.
|
|
42
61
|
* Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
|
|
43
62
|
* Native options like baseURL, method, body, headers, query, lazy, server, immediate, dedupe, etc. are all available.
|
|
44
63
|
* watch: boolean (true = auto-watch reactive params, false = disable auto-refresh)
|
|
45
64
|
*/
|
|
46
|
-
export type ApiAsyncDataOptions<
|
|
47
|
-
|
|
65
|
+
export type ApiAsyncDataOptions<
|
|
66
|
+
T,
|
|
67
|
+
DataT = T,
|
|
68
|
+
PickT extends PickInput = undefined,
|
|
69
|
+
> = Omit<BaseApiRequestOptions<T>, 'transform' | 'pick'> &
|
|
70
|
+
Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick' | 'watch'> & {
|
|
71
|
+
pick?: PickT;
|
|
72
|
+
transform?: (data: PickedData<T, PickT>) => DataT;
|
|
48
73
|
/**
|
|
49
74
|
* Enable automatic refresh when reactive params/url change (default: true).
|
|
50
75
|
* Set to false to disable auto-refresh entirely.
|
|
@@ -61,10 +86,13 @@ export type ApiAsyncDataOptions<T> = BaseApiRequestOptions<T> &
|
|
|
61
86
|
* - Global headers from useApiHeaders or $getApiHeaders
|
|
62
87
|
* - Watch pattern for reactive parameters
|
|
63
88
|
*/
|
|
64
|
-
export function useApiAsyncData<
|
|
89
|
+
export function useApiAsyncData<
|
|
90
|
+
T,
|
|
91
|
+
Options extends ApiAsyncDataOptions<T, any, any> = ApiAsyncDataOptions<T>,
|
|
92
|
+
>(
|
|
65
93
|
key: string,
|
|
66
94
|
url: string | (() => string),
|
|
67
|
-
options?:
|
|
95
|
+
options?: Options
|
|
68
96
|
) {
|
|
69
97
|
const {
|
|
70
98
|
method = 'GET',
|
|
@@ -295,7 +323,7 @@ export function useApiAsyncData<T>(
|
|
|
295
323
|
};
|
|
296
324
|
|
|
297
325
|
// Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
|
|
298
|
-
const result = useAsyncData<
|
|
326
|
+
const result = useAsyncData<InferData<T, Options>>(computedKey, fetchFn, {
|
|
299
327
|
immediate,
|
|
300
328
|
lazy,
|
|
301
329
|
server,
|
|
@@ -48,18 +48,35 @@ type MaybeTransformedRaw<T, Options> = Options extends { transform: (...args: an
|
|
|
48
48
|
? RawResponse<any> // With nested paths, type inference is complex
|
|
49
49
|
: RawResponse<T>;
|
|
50
50
|
|
|
51
|
+
type PickInput = ReadonlyArray<string> | undefined;
|
|
52
|
+
|
|
53
|
+
type HasNestedPath<K extends ReadonlyArray<string>> =
|
|
54
|
+
Extract<K[number], `${string}.${string}`> extends never ? false : true;
|
|
55
|
+
|
|
56
|
+
type PickedData<T, K extends PickInput> = K extends ReadonlyArray<string>
|
|
57
|
+
? HasNestedPath<K> extends true
|
|
58
|
+
? any
|
|
59
|
+
: Pick<T, Extract<K[number], keyof T>>
|
|
60
|
+
: T;
|
|
61
|
+
|
|
51
62
|
/**
|
|
52
63
|
* Options for useAsyncData Raw API requests.
|
|
53
64
|
* Extends all native Nuxt useFetch options plus our custom callbacks, transform, and pick.
|
|
54
65
|
* onSuccess receives data AND the full response (headers, status, statusText).
|
|
55
66
|
*/
|
|
56
|
-
export type ApiAsyncDataRawOptions<
|
|
57
|
-
|
|
67
|
+
export type ApiAsyncDataRawOptions<
|
|
68
|
+
T,
|
|
69
|
+
DataT = T,
|
|
70
|
+
PickT extends PickInput = undefined,
|
|
71
|
+
> = Omit<BaseApiRequestOptions<T>, 'onSuccess' | 'transform' | 'pick'> &
|
|
72
|
+
Omit<UseFetchOptions<T, DataT>, 'transform' | 'pick' | 'onSuccess'> & {
|
|
73
|
+
pick?: PickT;
|
|
74
|
+
transform?: (data: PickedData<T, PickT>) => DataT;
|
|
58
75
|
/**
|
|
59
76
|
* Called when the request succeeds — receives both data and the full response object.
|
|
60
77
|
*/
|
|
61
78
|
onSuccess?: (
|
|
62
|
-
data:
|
|
79
|
+
data: DataT,
|
|
63
80
|
response: { headers: Headers; status: number; statusText: string; url: string }
|
|
64
81
|
) => void | Promise<void>;
|
|
65
82
|
};
|
|
@@ -76,10 +93,15 @@ export type ApiAsyncDataRawOptions<T> = Omit<BaseApiRequestOptions<T>, 'onSucces
|
|
|
76
93
|
* - Global headers from useApiHeaders or $getApiHeaders
|
|
77
94
|
* - Watch pattern for reactive parameters
|
|
78
95
|
*/
|
|
79
|
-
export function useApiAsyncDataRaw<
|
|
96
|
+
export function useApiAsyncDataRaw<
|
|
97
|
+
T,
|
|
98
|
+
DataT = T,
|
|
99
|
+
PickT extends PickInput = undefined,
|
|
100
|
+
Options extends ApiAsyncDataRawOptions<T, DataT, PickT> = ApiAsyncDataRawOptions<T, DataT, PickT>,
|
|
101
|
+
>(
|
|
80
102
|
key: string,
|
|
81
103
|
url: string | (() => string),
|
|
82
|
-
options?:
|
|
104
|
+
options?: Options
|
|
83
105
|
) {
|
|
84
106
|
const {
|
|
85
107
|
method = 'GET',
|
|
@@ -151,7 +173,7 @@ export function useApiAsyncDataRaw<T>(
|
|
|
151
173
|
};
|
|
152
174
|
|
|
153
175
|
// Fetch function for useAsyncData
|
|
154
|
-
const fetchFn = async (): Promise<RawResponse<
|
|
176
|
+
const fetchFn = async (): Promise<RawResponse<DataT>> => {
|
|
155
177
|
// Get URL value for merging callbacks
|
|
156
178
|
const finalUrl = typeof url === 'function' ? url() : url;
|
|
157
179
|
|
|
@@ -253,7 +275,7 @@ export function useApiAsyncDataRaw<T>(
|
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
// Construct the raw response object
|
|
256
|
-
const rawResponse: RawResponse<
|
|
278
|
+
const rawResponse: RawResponse<DataT> = {
|
|
257
279
|
data,
|
|
258
280
|
headers: response.headers,
|
|
259
281
|
status: response.status,
|
|
@@ -290,7 +312,7 @@ export function useApiAsyncDataRaw<T>(
|
|
|
290
312
|
};
|
|
291
313
|
|
|
292
314
|
// Use Nuxt's useAsyncData with a computed key for proper cache isolation per params
|
|
293
|
-
const result = useAsyncData<MaybeTransformedRaw<T,
|
|
315
|
+
const result = useAsyncData<MaybeTransformedRaw<T, Options>>(computedKey, fetchFn, {
|
|
294
316
|
immediate,
|
|
295
317
|
lazy,
|
|
296
318
|
server,
|