nuxt-feathers-zod 0.2.0 → 0.2.1
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 +51 -0
- package/bin/nuxt-feathers-zod +0 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +13 -1
- package/dist/runtime/options/index.d.ts +3 -0
- package/dist/runtime/options/index.js +4 -1
- package/dist/runtime/options/swagger.d.ts +34 -0
- package/dist/runtime/options/swagger.js +16 -0
- package/dist/runtime/templates/server/plugin.js +45 -0
- package/package.json +1 -1
- package/src/cli/index.ts +115 -18
package/README.md
CHANGED
|
@@ -364,8 +364,59 @@ Template basé sur le pattern existant `playground/server/feathers/dummy.ts` :
|
|
|
364
364
|
* `--servicesDir <dir>`
|
|
365
365
|
* `--target nitro|feathers`
|
|
366
366
|
|
|
367
|
+
|
|
368
|
+
## Changements du 25 janvier 2026
|
|
369
|
+
|
|
370
|
+
### 1) `add service` : nouvelles options ROI
|
|
371
|
+
|
|
372
|
+
La commande supporte désormais :
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
bunx nuxt-feathers-zod add service <serviceName> \
|
|
376
|
+
[--idField id|_id] \
|
|
377
|
+
[--path <customPath>] \
|
|
378
|
+
[--docs] \
|
|
379
|
+
[--adapter mongodb|memory] \
|
|
380
|
+
[--auth] \
|
|
381
|
+
[--servicesDir <dir>] \
|
|
382
|
+
[--dry] \
|
|
383
|
+
[--force]
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### `--idField id|_id`
|
|
387
|
+
|
|
388
|
+
* Détermine le champ d’identifiant dans le schéma Zod généré.
|
|
389
|
+
* Compatible avec `mongodb` (ObjectId en string 24 hex) et `memory` (number int).
|
|
390
|
+
* Défauts :
|
|
391
|
+
|
|
392
|
+
* `mongodb` → `_id`
|
|
393
|
+
* `memory` → `id`
|
|
394
|
+
|
|
395
|
+
#### `--path <customPath>`
|
|
396
|
+
|
|
397
|
+
* Découple le **dossier service** du **path Feathers exposé**.
|
|
398
|
+
* Normalise automatiquement les `/` de tête/fin (ex: `/accounts/` → `accounts`).
|
|
399
|
+
* Impacte : `*.shared.ts` (path const), et donc tous les usages de `service(path)`.
|
|
400
|
+
|
|
401
|
+
#### `--docs`
|
|
402
|
+
|
|
403
|
+
* Injecte un bloc `docs:` (Swagger legacy) dans le `app.use(...)`.
|
|
404
|
+
* Ajoute `securities: ['jwt']` si `--auth` est activé.
|
|
405
|
+
|
|
406
|
+
### 2) Tests smoke mis à jour
|
|
407
|
+
|
|
408
|
+
* Ajout d’un test couvrant `--path`, `--idField`, `--docs`.
|
|
409
|
+
|
|
367
410
|
---
|
|
368
411
|
|
|
412
|
+
## Exemple rapide (valide)
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
bunx nuxt-feathers-zod add service users --adapter mongodb --auth --idField id --path accounts --docs
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Génère `services/users/*` mais expose le service sur `accounts` avec schéma `id: objectIdSchema()` + bloc `docs:`.
|
|
419
|
+
|
|
369
420
|
|
|
370
421
|
|
|
371
422
|
## License
|
package/bin/nuxt-feathers-zod
CHANGED
|
File without changes
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addImportsDir, addTemplate, addServerPlugin, addImports, addPlugin, hasNuxtModule, installModule } from '@nuxt/kit';
|
|
2
2
|
import { consola } from 'consola';
|
|
3
3
|
import defu from 'defu';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
4
5
|
import { resolveOptions, resolveRuntimeConfig, resolvePublicRuntimeConfig } from '../dist/runtime/options/index.js';
|
|
5
6
|
import { serverDefaults } from '../dist/runtime/options/server.js';
|
|
6
7
|
import { getServicesImports, addServicesImports } from '../dist/runtime/services.js';
|
|
@@ -64,11 +65,22 @@ const module$1 = defineNuxtModule({
|
|
|
64
65
|
extendDefaults: true
|
|
65
66
|
},
|
|
66
67
|
loadFeathersConfig: false,
|
|
67
|
-
auth: true
|
|
68
|
+
auth: true,
|
|
69
|
+
swagger: false
|
|
68
70
|
},
|
|
69
71
|
async setup(options, nuxt) {
|
|
70
72
|
const resolver = createResolver(import.meta.url);
|
|
71
73
|
const resolvedOptions = await resolveOptions(options, nuxt);
|
|
74
|
+
if (resolvedOptions.swagger) {
|
|
75
|
+
const require = createRequire(import.meta.url);
|
|
76
|
+
try {
|
|
77
|
+
require.resolve("feathers-swagger", { paths: [nuxt.options.rootDir] });
|
|
78
|
+
} catch {
|
|
79
|
+
consola.warn(
|
|
80
|
+
"feathers.swagger is enabled but 'feathers-swagger' could not be resolved from this Nuxt project. Install it in your app (root) dependencies: bun add feathers-swagger swagger-ui-dist"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
72
84
|
nuxt.options.runtimeConfig._feathers = resolveRuntimeConfig(resolvedOptions);
|
|
73
85
|
nuxt.options.runtimeConfig.public._feathers = resolvePublicRuntimeConfig(resolvedOptions);
|
|
74
86
|
const servicesImports = await getServicesImports(resolvedOptions.servicesDirs);
|
|
@@ -7,6 +7,7 @@ import type { ResolvedServerOptions, ServerOptions } from './server.js';
|
|
|
7
7
|
import type { ServicesDir, ServicesDirs } from './services.js';
|
|
8
8
|
import type { ResolvedTransportsOptions, TransportsOptions } from './transports/index.js';
|
|
9
9
|
import type { ResolvedValidatorOptions, ValidatorOptions } from './validator.js';
|
|
10
|
+
import type { ResolvedSwaggerOptionsOrDisabled, SwaggerOptionsOrDisabled } from './swagger.js';
|
|
10
11
|
export interface ModuleOptions {
|
|
11
12
|
transports: TransportsOptions;
|
|
12
13
|
database: DataBaseOptions;
|
|
@@ -16,6 +17,7 @@ export interface ModuleOptions {
|
|
|
16
17
|
client: ClientOptions | boolean;
|
|
17
18
|
validator: ValidatorOptions;
|
|
18
19
|
loadFeathersConfig: boolean;
|
|
20
|
+
swagger?: SwaggerOptionsOrDisabled;
|
|
19
21
|
}
|
|
20
22
|
export interface ResolvedOptions {
|
|
21
23
|
templateDir: string;
|
|
@@ -27,6 +29,7 @@ export interface ResolvedOptions {
|
|
|
27
29
|
client: ResolvedClientOptionsOrDisabled;
|
|
28
30
|
validator: ResolvedValidatorOptions;
|
|
29
31
|
loadFeathersConfig: boolean;
|
|
32
|
+
swagger?: ResolvedSwaggerOptionsOrDisabled;
|
|
30
33
|
}
|
|
31
34
|
export interface FeathersRuntimeConfig {
|
|
32
35
|
auth?: ResolvedAuthOptions;
|
|
@@ -7,6 +7,7 @@ import { resolveServerOptions } from "./server.js";
|
|
|
7
7
|
import { resolveServicesDirs } from "./services.js";
|
|
8
8
|
import { resolveTransportsOptions } from "./transports/index.js";
|
|
9
9
|
import { resolveValidatorOptions } from "./validator.js";
|
|
10
|
+
import { resolveSwaggerOptions } from "./swagger.js";
|
|
10
11
|
export async function resolveOptions(options, nuxt) {
|
|
11
12
|
const { rootDir, srcDir, serverDir, appDir, buildDir, ssr } = nuxt.options;
|
|
12
13
|
const resolver = createResolver(import.meta.url);
|
|
@@ -17,6 +18,7 @@ export async function resolveOptions(options, nuxt) {
|
|
|
17
18
|
const server = await resolveServerOptions(options.server, rootDir, serverDir);
|
|
18
19
|
const client = await resolveClientOptions(options.client, !!database.mongo, rootDir, srcDir);
|
|
19
20
|
const validator = resolveValidatorOptions(options.validator);
|
|
21
|
+
const swagger = resolveSwaggerOptions(options.swagger, transports);
|
|
20
22
|
const servicesImports = await getServicesImports(servicesDirs);
|
|
21
23
|
const auth = resolveAuthOptions(options.auth, !!client, servicesImports, appDir);
|
|
22
24
|
const loadFeathersConfig = options.loadFeathersConfig;
|
|
@@ -29,7 +31,8 @@ export async function resolveOptions(options, nuxt) {
|
|
|
29
31
|
client,
|
|
30
32
|
validator,
|
|
31
33
|
auth,
|
|
32
|
-
loadFeathersConfig
|
|
34
|
+
loadFeathersConfig,
|
|
35
|
+
swagger
|
|
33
36
|
};
|
|
34
37
|
console.dir(resolvedOptions, { depth: null });
|
|
35
38
|
return resolvedOptions;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResolvedTransportsOptions } from './transports/index.js';
|
|
2
|
+
export interface SwaggerOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Enable Swagger/OpenAPI documentation generation via feathers-swagger (legacy).
|
|
5
|
+
* When `true`, defaults are applied.
|
|
6
|
+
*/
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/** Swagger UI base path (e.g. /docs) */
|
|
9
|
+
docsPath?: string;
|
|
10
|
+
/** OpenAPI JSON path (e.g. /docs.json) */
|
|
11
|
+
docsJsonPath?: string;
|
|
12
|
+
/** OpenAPI version (2 or 3). Default: 3 */
|
|
13
|
+
openApiVersion?: 2 | 3;
|
|
14
|
+
/** OpenAPI info */
|
|
15
|
+
info?: {
|
|
16
|
+
title: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
version: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export type SwaggerOptionsOrDisabled = SwaggerOptions | boolean;
|
|
22
|
+
export interface ResolvedSwaggerOptions {
|
|
23
|
+
enabled: true;
|
|
24
|
+
docsPath: string;
|
|
25
|
+
docsJsonPath: string;
|
|
26
|
+
openApiVersion: 2 | 3;
|
|
27
|
+
info: {
|
|
28
|
+
title: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
version: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export type ResolvedSwaggerOptionsOrDisabled = ResolvedSwaggerOptions | false;
|
|
34
|
+
export declare function resolveSwaggerOptions(options: SwaggerOptionsOrDisabled | undefined, transports: ResolvedTransportsOptions): ResolvedSwaggerOptionsOrDisabled;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function resolveSwaggerOptions(options, transports) {
|
|
2
|
+
if (options == null || options === false)
|
|
3
|
+
return false;
|
|
4
|
+
if (options === true)
|
|
5
|
+
options = {};
|
|
6
|
+
const enabled = options.enabled ?? true;
|
|
7
|
+
if (!enabled)
|
|
8
|
+
return false;
|
|
9
|
+
return {
|
|
10
|
+
enabled: true,
|
|
11
|
+
docsPath: options.docsPath || "/docs",
|
|
12
|
+
docsJsonPath: options.docsJsonPath || "/docs.json",
|
|
13
|
+
openApiVersion: options.openApiVersion || 3,
|
|
14
|
+
info: options.info || { title: "API Docs", version: "1.0.0" }
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -26,6 +26,46 @@ export function getServerPluginContents(options) {
|
|
|
26
26
|
const authService = options?.auth?.service;
|
|
27
27
|
const restPath = transports?.rest?.path;
|
|
28
28
|
const websocketPath = transports?.websocket?.path;
|
|
29
|
+
const swaggerEnabled = !!options.swagger;
|
|
30
|
+
const swagger = options.swagger;
|
|
31
|
+
const normalizePath = (p) => {
|
|
32
|
+
if (!p)
|
|
33
|
+
return "";
|
|
34
|
+
return p.startsWith("/") ? p : `/${p}`;
|
|
35
|
+
};
|
|
36
|
+
const trimTrailingSlash = (p) => {
|
|
37
|
+
if (!p)
|
|
38
|
+
return "";
|
|
39
|
+
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
40
|
+
};
|
|
41
|
+
const docsPath = trimTrailingSlash(normalizePath(swagger?.docsPath ?? "/docs"));
|
|
42
|
+
const docsPathSlash = `${docsPath}/`;
|
|
43
|
+
const docsJsonPath = trimTrailingSlash(normalizePath(swagger?.docsJsonPath ?? "/docs.json"));
|
|
44
|
+
const swaggerInitBlock = swaggerEnabled ? ` // Init Swagger (feathers-swagger legacy)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
app.configure(swagger.customMethodsHandler)
|
|
48
|
+
app.configure(swagger({
|
|
49
|
+
docsPath: '${docsPath}',
|
|
50
|
+
docsJsonPath: '/swagger.json',
|
|
51
|
+
specs: {
|
|
52
|
+
info: ${JSON.stringify(swagger?.info ?? { title: "API Docs", description: "Feathers API", version: "1.0.0" })},
|
|
53
|
+
components: {
|
|
54
|
+
securitySchemes: {
|
|
55
|
+
BearerAuth: {
|
|
56
|
+
type: 'http',
|
|
57
|
+
scheme: 'bearer',
|
|
58
|
+
bearerFormat: 'JWT',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
security: [
|
|
63
|
+
{ BearerAuth: [] },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
ui: swagger.swaggerUI({ docsPath: '${docsPath}' }),
|
|
67
|
+
}))
|
|
68
|
+
` : "";
|
|
29
69
|
return `// ! Generated by nuxt-feathers-zod - do not change manually
|
|
30
70
|
import type { NitroApp } from 'nitropack'
|
|
31
71
|
import type { Application } from './server.js'
|
|
@@ -36,6 +76,7 @@ ${puts([
|
|
|
36
76
|
[exp, `import feathersExpress, { json, rest, urlencoded } from '@feathersjs/express'`],
|
|
37
77
|
[sio, `import socketio from '@feathersjs/socketio'`]
|
|
38
78
|
])}
|
|
79
|
+
${put(swaggerEnabled, `import swagger from 'feathers-swagger'`)}
|
|
39
80
|
${put(rest, `import { ${framework}ErrorHandler } from '@gabortorma/feathers-nitro-adapter/handlers'`)}
|
|
40
81
|
${put(auth, `import authentication from './authentication.js'`)}
|
|
41
82
|
import { ${routers.join(", ")} } from '@gabortorma/feathers-nitro-adapter/routers'
|
|
@@ -52,6 +93,7 @@ export default defineNitroPlugin((nitroApp: NitroApp) => {
|
|
|
52
93
|
${put(options.loadFeathersConfig, `
|
|
53
94
|
app.configure(configuration())
|
|
54
95
|
`)}
|
|
96
|
+
${put(swaggerEnabled, swaggerInitBlock)}
|
|
55
97
|
// Add nitroApp to feathers app
|
|
56
98
|
app.nitroApp = nitroApp;
|
|
57
99
|
${put(rest, `${put(koa, `
|
|
@@ -70,6 +112,7 @@ ${put(exp, ` // Set up Express middleware
|
|
|
70
112
|
},
|
|
71
113
|
}`)}))
|
|
72
114
|
`)}`)}
|
|
115
|
+
|
|
73
116
|
// Init socket.io server for real-time functionality
|
|
74
117
|
app.set('websocket', ${!!sio})${put(sio, `
|
|
75
118
|
app.configure(socketio({
|
|
@@ -83,11 +126,13 @@ ${put(exp, ` // Set up Express middleware
|
|
|
83
126
|
${put(mongo, `// Init mongodb
|
|
84
127
|
app.configure(mongodb)
|
|
85
128
|
`)}
|
|
129
|
+
|
|
86
130
|
// Init services
|
|
87
131
|
${services.map((service) => `app.configure(${service.meta.importId})`).join("\n ")}
|
|
88
132
|
|
|
89
133
|
// Init plugins
|
|
90
134
|
${plugins.map((plugin) => `app.configure(${plugin.meta.importId})`).join("\n ")}
|
|
135
|
+
|
|
91
136
|
${put(exp, `
|
|
92
137
|
// Set up Express middleware for 404s and the error handler
|
|
93
138
|
app.configure(expressErrorHandler)
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import consola from 'consola'
|
|
|
7
7
|
|
|
8
8
|
type Adapter = 'mongodb' | 'memory'
|
|
9
9
|
type MiddlewareTarget = 'nitro' | 'feathers'
|
|
10
|
+
type IdField = 'id' | '_id'
|
|
11
|
+
type CollectionName = string
|
|
10
12
|
|
|
11
13
|
export type RunCliOptions = {
|
|
12
14
|
cwd: string
|
|
@@ -48,6 +50,10 @@ export async function runCli(argv: string[], opts: RunCliOptions) {
|
|
|
48
50
|
if (subcmd === 'service') {
|
|
49
51
|
const adapter = (flags.adapter as Adapter | undefined) ?? 'mongodb'
|
|
50
52
|
const auth = Boolean(flags.auth)
|
|
53
|
+
const idField = (flags.idField as IdField | undefined) ?? (adapter === 'mongodb' ? '_id' : 'id')
|
|
54
|
+
const servicePath = typeof flags.path === 'string' ? String(flags.path) : undefined
|
|
55
|
+
const collectionName = typeof flags.collection === 'string' ? String(flags.collection) : undefined
|
|
56
|
+
const docs = Boolean(flags.docs)
|
|
51
57
|
const dry = Boolean(flags.dry)
|
|
52
58
|
const force = Boolean(flags.force)
|
|
53
59
|
|
|
@@ -60,6 +66,10 @@ export async function runCli(argv: string[], opts: RunCliOptions) {
|
|
|
60
66
|
name,
|
|
61
67
|
adapter,
|
|
62
68
|
auth,
|
|
69
|
+
idField,
|
|
70
|
+
servicePath,
|
|
71
|
+
collectionName,
|
|
72
|
+
docs,
|
|
63
73
|
dry,
|
|
64
74
|
force,
|
|
65
75
|
})
|
|
@@ -86,7 +96,7 @@ export async function runCli(argv: string[], opts: RunCliOptions) {
|
|
|
86
96
|
function printHelp() {
|
|
87
97
|
// Keep output short; this is a CLI entrypoint.
|
|
88
98
|
// eslint-disable-next-line no-console
|
|
89
|
-
console.log(`\nnuxt-feathers-zod CLI\n\nUsage:\n nuxt-feathers-zod add service <serviceName> [--adapter mongodb|memory] [--auth] [--servicesDir <dir>] [--dry] [--force]\n nuxt-feathers-zod add middleware <name> [--target nitro|feathers] [--dry] [--force]\n\nExamples:\n bunx nuxt-feathers-zod add service posts --adapter mongodb --auth\n bunx nuxt-feathers-zod add middleware session\n bunx nuxt-feathers-zod add middleware dummy --target feathers\n`)
|
|
99
|
+
console.log(`\nnuxt-feathers-zod CLI\n\nUsage:\n nuxt-feathers-zod add service <serviceName> [--adapter mongodb|memory] [--auth] [--idField id|_id] [--path <customPath>] [--collection <mongoCollection>] [--docs] [--servicesDir <dir>] [--dry] [--force]\n nuxt-feathers-zod add middleware <name> [--target nitro|feathers] [--dry] [--force]\n\nExamples:\n bunx nuxt-feathers-zod add service posts --adapter mongodb --auth\n bunx nuxt-feathers-zod add service users --adapter mongodb --idField _id --path accounts --docs\n bunx nuxt-feathers-zod add service haproxy-domains --path haproxy/domains --collection haproxy-domains --auth --docs\n bunx nuxt-feathers-zod add middleware session\n bunx nuxt-feathers-zod add middleware dummy --target feathers\n`)
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
function parseFlags(argv: string[]) {
|
|
@@ -132,6 +142,28 @@ function normalizeServiceName(raw: string) {
|
|
|
132
142
|
return kebabCase(raw)
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
function normalizeServicePath(raw: string) {
|
|
146
|
+
// Feathers service paths are usually kebab-case and can include slashes.
|
|
147
|
+
// We keep user intent, but normalize leading/trailing slashes.
|
|
148
|
+
const cleaned = String(raw).trim().replace(/^\/+/, '').replace(/\/+$/, '')
|
|
149
|
+
if (!cleaned) throw new Error('Invalid --path: path cannot be empty')
|
|
150
|
+
return cleaned
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeCollectionName(raw: string) {
|
|
154
|
+
// MongoDB collection names are strings, but in practice should not include path separators.
|
|
155
|
+
// We keep it permissive while preventing common foot-guns.
|
|
156
|
+
const cleaned = String(raw).trim()
|
|
157
|
+
if (!cleaned) throw new Error('Invalid --collection: collection name cannot be empty')
|
|
158
|
+
if (cleaned.includes('/') || cleaned.includes('\\')) {
|
|
159
|
+
throw new Error('Invalid --collection: collection name must not include \/ or \\')
|
|
160
|
+
}
|
|
161
|
+
if (cleaned.includes('\u0000')) {
|
|
162
|
+
throw new Error('Invalid --collection: collection name must not include null characters')
|
|
163
|
+
}
|
|
164
|
+
return cleaned
|
|
165
|
+
}
|
|
166
|
+
|
|
135
167
|
function createServiceIds(serviceNameKebab: string) {
|
|
136
168
|
const baseKebab = singularize(serviceNameKebab)
|
|
137
169
|
const basePascal = pascalCase(baseKebab)
|
|
@@ -151,6 +183,10 @@ type GenerateServiceOptions = {
|
|
|
151
183
|
name: string
|
|
152
184
|
adapter: Adapter
|
|
153
185
|
auth: boolean
|
|
186
|
+
idField: IdField
|
|
187
|
+
servicePath?: string
|
|
188
|
+
collectionName?: CollectionName
|
|
189
|
+
docs: boolean
|
|
154
190
|
dry: boolean
|
|
155
191
|
force: boolean
|
|
156
192
|
}
|
|
@@ -159,6 +195,12 @@ export async function generateService(opts: GenerateServiceOptions) {
|
|
|
159
195
|
const serviceNameKebab = normalizeServiceName(opts.name)
|
|
160
196
|
const ids = createServiceIds(serviceNameKebab)
|
|
161
197
|
|
|
198
|
+
const servicePath = normalizeServicePath(opts.servicePath ?? serviceNameKebab)
|
|
199
|
+
const collectionName = normalizeCollectionName(
|
|
200
|
+
opts.collectionName
|
|
201
|
+
?? (servicePath.includes('/') ? serviceNameKebab : servicePath),
|
|
202
|
+
)
|
|
203
|
+
|
|
162
204
|
const dir = join(opts.servicesDir, serviceNameKebab)
|
|
163
205
|
const schemaFile = join(dir, `${serviceNameKebab}.schema.ts`)
|
|
164
206
|
const classFile = join(dir, `${serviceNameKebab}.class.ts`)
|
|
@@ -166,10 +208,10 @@ export async function generateService(opts: GenerateServiceOptions) {
|
|
|
166
208
|
const serviceFile = join(dir, `${serviceNameKebab}.ts`)
|
|
167
209
|
|
|
168
210
|
const files: Array<{ path: string; content: string }> = [
|
|
169
|
-
{ path: schemaFile, content: renderSchema(ids, opts.adapter) },
|
|
170
|
-
{ path: classFile, content: renderClass(ids, opts.adapter) },
|
|
171
|
-
{ path: sharedFile, content: renderShared(ids) },
|
|
172
|
-
{ path: serviceFile, content: renderService(ids, opts.auth) },
|
|
211
|
+
{ path: schemaFile, content: renderSchema(ids, opts.adapter, opts.idField) },
|
|
212
|
+
{ path: classFile, content: renderClass(ids, opts.adapter, collectionName) },
|
|
213
|
+
{ path: sharedFile, content: renderShared(ids, servicePath) },
|
|
214
|
+
{ path: serviceFile, content: renderService(ids, opts.auth, opts.docs) },
|
|
173
215
|
]
|
|
174
216
|
|
|
175
217
|
await ensureDir(dir, opts.dry)
|
|
@@ -178,11 +220,41 @@ export async function generateService(opts: GenerateServiceOptions) {
|
|
|
178
220
|
await writeFileSafe(f.path, f.content, { dry: opts.dry, force: opts.force })
|
|
179
221
|
}
|
|
180
222
|
|
|
223
|
+
if (opts.docs) {
|
|
224
|
+
await ensureFeathersSwaggerSupport(opts.projectRoot, { dry: opts.dry, force: opts.force })
|
|
225
|
+
}
|
|
226
|
+
|
|
181
227
|
if (!opts.dry) {
|
|
182
228
|
consola.success(`Generated service '${serviceNameKebab}' in ${relativeToCwd(dir)}`)
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
|
|
232
|
+
async function ensureFeathersSwaggerSupport(projectRoot: string, io: { dry: boolean; force: boolean }) {
|
|
233
|
+
// 1) Ensure TS sees `ServiceOptions.docs` (required for feathers-swagger in TS projects)
|
|
234
|
+
const typesDir = join(projectRoot, 'types')
|
|
235
|
+
const typesFile = join(typesDir, 'feathers-swagger.d.ts')
|
|
236
|
+
const typesContent = `// Auto-generated by nuxt-feathers-zod CLI (required when using feathers-swagger in TypeScript)\n\nimport type { ServiceSwaggerOptions } from 'feathers-swagger'\n\ndeclare module '@feathersjs/feathers' {\n interface ServiceOptions {\n docs?: ServiceSwaggerOptions\n }\n}\n`
|
|
237
|
+
|
|
238
|
+
await ensureDir(typesDir, io.dry)
|
|
239
|
+
await writeFileSafe(typesFile, typesContent, { dry: io.dry, force: io.force })
|
|
240
|
+
|
|
241
|
+
// 2) Best-effort dependency hint (we do not auto-install dependencies)
|
|
242
|
+
try {
|
|
243
|
+
const pkgPath = join(projectRoot, 'package.json')
|
|
244
|
+
if (!existsSync(pkgPath)) return
|
|
245
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8')) as any
|
|
246
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
|
|
247
|
+
if (!deps['feathers-swagger']) {
|
|
248
|
+
consola.warn(
|
|
249
|
+
`--docs was used but 'feathers-swagger' is not listed in package.json. Install it (and swagger UI deps if needed): bun add feathers-swagger swagger-ui-dist`,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// ignore
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
186
258
|
type GenerateMiddlewareOptions = {
|
|
187
259
|
projectRoot: string
|
|
188
260
|
name: string
|
|
@@ -238,12 +310,12 @@ function relativeToCwd(p: string) {
|
|
|
238
310
|
}
|
|
239
311
|
}
|
|
240
312
|
|
|
241
|
-
function renderSchema(ids: ReturnType<typeof createServiceIds>, adapter: Adapter) {
|
|
313
|
+
function renderSchema(ids: ReturnType<typeof createServiceIds>, adapter: Adapter, idField: IdField) {
|
|
242
314
|
const base = ids.baseCamel
|
|
243
315
|
const Base = ids.basePascal
|
|
244
316
|
const serviceClass = `${Base}Service`
|
|
245
317
|
|
|
246
|
-
const
|
|
318
|
+
const idSchemaField = idField
|
|
247
319
|
const idSchema = adapter === 'mongodb'
|
|
248
320
|
? `
|
|
249
321
|
const objectIdRegex = /^[0-9a-f]{24}$/i
|
|
@@ -253,11 +325,11 @@ export const objectIdSchema = () => z.string().regex(objectIdRegex, 'Invalid Obj
|
|
|
253
325
|
|
|
254
326
|
const mainSchema = adapter === 'mongodb'
|
|
255
327
|
? `export const ${base}Schema = z.object({
|
|
256
|
-
${
|
|
328
|
+
${idSchemaField}: objectIdSchema(),
|
|
257
329
|
text: z.string(),
|
|
258
330
|
})`
|
|
259
331
|
: `export const ${base}Schema = z.object({
|
|
260
|
-
${
|
|
332
|
+
${idSchemaField}: z.number().int(),
|
|
261
333
|
text: z.string(),
|
|
262
334
|
})`
|
|
263
335
|
|
|
@@ -303,8 +375,7 @@ export const ${base}QueryResolver = resolve<${Base}Query, HookContext<${serviceC
|
|
|
303
375
|
`
|
|
304
376
|
}
|
|
305
377
|
|
|
306
|
-
function renderClass(ids: ReturnType<typeof createServiceIds>, adapter: Adapter) {
|
|
307
|
-
const base = ids.baseCamel
|
|
378
|
+
function renderClass(ids: ReturnType<typeof createServiceIds>, adapter: Adapter, collectionName: string) {
|
|
308
379
|
const Base = ids.basePascal
|
|
309
380
|
const serviceName = ids.serviceNameKebab
|
|
310
381
|
const serviceClass = `${Base}Service`
|
|
@@ -364,13 +435,13 @@ export function getOptions(app: Application): MongoDBAdapterOptions {
|
|
|
364
435
|
max: 100,
|
|
365
436
|
},
|
|
366
437
|
multi: true,
|
|
367
|
-
Model: mongoClient.then(db => db.collection('${
|
|
438
|
+
Model: mongoClient.then(db => db.collection('${collectionName}')),
|
|
368
439
|
}
|
|
369
440
|
}
|
|
370
441
|
`
|
|
371
442
|
}
|
|
372
443
|
|
|
373
|
-
function renderShared(ids: ReturnType<typeof createServiceIds
|
|
444
|
+
function renderShared(ids: ReturnType<typeof createServiceIds>, servicePath: string) {
|
|
374
445
|
const base = ids.baseCamel
|
|
375
446
|
const Base = ids.basePascal
|
|
376
447
|
const serviceName = ids.serviceNameKebab
|
|
@@ -390,7 +461,7 @@ export type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query }
|
|
|
390
461
|
|
|
391
462
|
export type ${clientServiceType} = Pick<${serviceClass}<Params<${Base}Query>>, (typeof ${methodsConst})[number]>
|
|
392
463
|
|
|
393
|
-
export const ${pathConst} = '${
|
|
464
|
+
export const ${pathConst} = '${servicePath}'
|
|
394
465
|
|
|
395
466
|
export const ${methodsConst}: Array<keyof ${serviceClass}> = ['find', 'get', 'create', 'patch', 'remove']
|
|
396
467
|
|
|
@@ -410,13 +481,39 @@ declare module 'nuxt-feathers-zod/client' {
|
|
|
410
481
|
`
|
|
411
482
|
}
|
|
412
483
|
|
|
413
|
-
function renderService(ids: ReturnType<typeof createServiceIds>, auth: boolean) {
|
|
484
|
+
function renderService(ids: ReturnType<typeof createServiceIds>, auth: boolean, docs: boolean) {
|
|
414
485
|
const base = ids.baseCamel
|
|
415
486
|
const Base = ids.basePascal
|
|
416
487
|
const serviceName = ids.serviceNameKebab
|
|
417
488
|
const serviceClass = `${Base}Service`
|
|
418
489
|
const authImports = auth ? "import { authenticate } from '@feathersjs/authentication'\n" : ''
|
|
419
490
|
|
|
491
|
+
const swaggerImports = ''
|
|
492
|
+
|
|
493
|
+
const swaggerSchemaImports = ''
|
|
494
|
+
const docsBlock = docs
|
|
495
|
+
? `
|
|
496
|
+
docs: {
|
|
497
|
+
description: '${Base} service',
|
|
498
|
+
idType: 'string',
|
|
499
|
+
${auth ? ` securities: ${base}Methods,
|
|
500
|
+
` : ''} definitions: {
|
|
501
|
+
${base}: { type: 'object', properties: {} },
|
|
502
|
+
${base}Data: { type: 'object', properties: {} },
|
|
503
|
+
${base}Patch: { type: 'object', properties: {} },
|
|
504
|
+
${base}Query: {
|
|
505
|
+
type: 'object',
|
|
506
|
+
properties: {
|
|
507
|
+
$limit: { type: 'number' },
|
|
508
|
+
$skip: { type: 'number' },
|
|
509
|
+
$sort: { type: 'object', additionalProperties: { type: 'number' } },
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
`
|
|
515
|
+
: ''
|
|
516
|
+
|
|
420
517
|
const authAround = auth
|
|
421
518
|
? `
|
|
422
519
|
find: [authenticate('jwt')],
|
|
@@ -436,10 +533,10 @@ function renderService(ids: ReturnType<typeof createServiceIds>, auth: boolean)
|
|
|
436
533
|
return `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.html
|
|
437
534
|
|
|
438
535
|
import type { Application } from 'nuxt-feathers-zod/server'
|
|
439
|
-
${authImports}import { hooks as schemaHooks } from '@feathersjs/schema'
|
|
536
|
+
${authImports}${swaggerImports}import { hooks as schemaHooks } from '@feathersjs/schema'
|
|
440
537
|
import { getOptions, ${serviceClass} } from './${serviceName}.class'
|
|
441
538
|
import {
|
|
442
|
-
${base}DataResolver,
|
|
539
|
+
${swaggerSchemaImports} ${base}DataResolver,
|
|
443
540
|
${base}DataValidator,
|
|
444
541
|
${base}ExternalResolver,
|
|
445
542
|
${base}PatchResolver,
|
|
@@ -456,7 +553,7 @@ export * from './${serviceName}.schema'
|
|
|
456
553
|
export function ${base}(app: Application) {
|
|
457
554
|
app.use(${base}Path, new ${serviceClass}(getOptions(app)), {
|
|
458
555
|
methods: ${base}Methods,
|
|
459
|
-
events: []
|
|
556
|
+
events: [],${docsBlock}
|
|
460
557
|
})
|
|
461
558
|
|
|
462
559
|
app.service(${base}Path).hooks({
|