nuxt-feathers-zod 0.1.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 CHANGED
@@ -278,6 +278,147 @@ Notes:
278
278
 
279
279
  The runtime auth bootstrap (`src/runtime/plugins/feathers-auth.ts`) re-authenticates **client-side only** to avoid SSR startup failures when the Feathers client is not yet injected.
280
280
 
281
+
282
+ C’est appliqué au dépôt (zip monté), en respectant exactement l’ordre que vous avez demandé.
283
+
284
+ Voici ce qui a été ajouté / modifié dans **nuxt-feathers-zod-main** :
285
+
286
+ ## 1) CLI Bun: `bunx nuxt-feathers-zod …`
287
+
288
+ ### Ajouts
289
+
290
+ * `bin/nuxt-feathers-zod` (exécutable, shebang Bun)
291
+ * `src/cli/index.ts` (implémentation complète de la CLI)
292
+
293
+ ### package.json
294
+
295
+ * Ajout de :
296
+
297
+ * `"bin": { "nuxt-feathers-zod": "bin/nuxt-feathers-zod" }`
298
+ * `"files": ["dist", "bin", "src/cli"]` pour que la CLI soit bien publiée
299
+ * `"test": "vitest run"` (smoke tests)
300
+
301
+ ## 2) Commande `add service` (4 fichiers) + smoke tests
302
+
303
+ ### Commande
304
+
305
+ ```bash
306
+ bunx nuxt-feathers-zod add service <serviceName> [--adapter mongodb|memory] [--auth] [--servicesDir <dir>] [--dry] [--force]
307
+ ```
308
+
309
+ ### Génération
310
+
311
+ Crée automatiquement :
312
+
313
+ * `services/<name>/<name>.schema.ts`
314
+ * `services/<name>/<name>.class.ts`
315
+ * `services/<name>/<name>.shared.ts`
316
+ * `services/<name>/<name>.ts`
317
+
318
+ Aligné sur le style déjà présent dans le repo :
319
+
320
+ * Zod + `getZodValidator(...)`
321
+ * `zodQuerySyntax(schema)`
322
+ * `schemaHooks.validateQuery/validateData + resolveQuery/resolveData`
323
+ * `--auth` ajoute `authenticate('jwt')` sur `find/get/patch/remove` (create reste ouvert)
324
+
325
+ `--adapter mongodb` génère un `_id` ObjectId (regex 24 hex) comme le service `mongos` existant.
326
+ `--adapter memory` génère un `id: z.number().int()` comme `messages`.
327
+
328
+ Smoke test ajouté : `test/cli.spec.ts` (vérifie service + middleware).
329
+
330
+ ## 3) Commande `add middleware` (Nitro)
331
+
332
+ ### Commande
333
+
334
+ ```bash
335
+ bunx nuxt-feathers-zod add middleware <name> [--target nitro|feathers] [--dry] [--force]
336
+ ```
337
+
338
+ Par défaut (`--target nitro`) :
339
+
340
+ * crée `server/middleware/<name>.ts` avec un template `defineEventHandler(...)`.
341
+
342
+ ## 4) `--target feathers` (plugin Feathers côté serveur)
343
+
344
+ Si vous faites :
345
+
346
+ ```bash
347
+ bunx nuxt-feathers-zod add middleware dummy --target feathers
348
+ ```
349
+
350
+ Génère :
351
+
352
+ * `server/feathers/dummy.ts`
353
+
354
+ Template basé sur le pattern existant `playground/server/feathers/dummy.ts` :
355
+
356
+ * `defineFeathersServerPlugin((app) => app.hooks({ setup: [...] }))`
357
+
358
+ ## 5) Options ajoutées
359
+
360
+ * `--adapter mongodb|memory`
361
+ * `--auth`
362
+ * `--dry`
363
+ * `--force`
364
+ * `--servicesDir <dir>`
365
+ * `--target nitro|feathers`
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
+
410
+ ---
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
+
420
+
421
+
281
422
  ## License
282
423
 
283
424
  MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { runCli } from '../src/cli/index.ts'
4
+
5
+ await runCli(process.argv.slice(2), { cwd: process.cwd() })
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^4.0.0"
6
6
  },
7
- "version": "0.1.0",
7
+ "version": "0.2.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-feathers-zod",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.1",
5
5
  "packageManager": "bun@1.3.6",
6
6
  "description": "Feathers API integration for Nuxt",
7
7
  "author": "Herve de CHAVIGNY",
@@ -21,15 +21,28 @@
21
21
  "./query": "./dist/runtime/zod/query.js",
22
22
  "./format": "./dist/runtime/zod/format.js"
23
23
  },
24
+ "bin": {
25
+ "nuxt-feathers-zod": "bin/nuxt-feathers-zod"
26
+ },
24
27
  "main": "./dist/module.mjs",
25
28
  "typesVersions": {
26
29
  "*": {
27
- ".": ["./dist/types.d.mts"],
28
- "options": ["./dist/runtime/options/index.d.mts"],
29
- "ofetch-adapter": ["./dist/runtime/adapters/ofetch.d.mts"]
30
+ ".": [
31
+ "./dist/types.d.mts"
32
+ ],
33
+ "options": [
34
+ "./dist/runtime/options/index.d.mts"
35
+ ],
36
+ "ofetch-adapter": [
37
+ "./dist/runtime/adapters/ofetch.d.mts"
38
+ ]
30
39
  }
31
40
  },
32
- "files": ["dist"],
41
+ "files": [
42
+ "dist",
43
+ "bin",
44
+ "src/cli"
45
+ ],
33
46
  "scripts": {
34
47
  "prepack": "nuxt-module-build build",
35
48
  "dev": "nuxi dev playground",
@@ -48,7 +61,7 @@
48
61
  },
49
62
  "dependencies": {
50
63
  "@feathersjs/adapter-commons": "latest",
51
- "@feathersjs/authentication": "latest",
64
+ "@feathersjs/authentication": "5.0.37",
52
65
  "@feathersjs/authentication-client": "latest",
53
66
  "@feathersjs/authentication-local": "latest",
54
67
  "@feathersjs/configuration": "latest",
@@ -96,16 +109,5 @@
96
109
  "vitest-mongodb": "latest",
97
110
  "vue": "latest",
98
111
  "vue-router": "latest"
99
- },
100
- "overrides": {
101
- "@feathersjs/authentication": "5.0.37",
102
- "@feathersjs/authentication-client": "5.0.37",
103
- "@feathersjs/authentication-local": "5.0.37",
104
- "@feathersjs/errors": "5.0.37",
105
- "@feathersjs/feathers": "5.0.37",
106
- "@feathersjs/koa": "5.0.37",
107
- "@feathersjs/rest-client": "5.0.37",
108
- "@feathersjs/schema": "5.0.37",
109
- "@feathersjs/socketio-client": "5.0.37"
110
112
  }
111
113
  }
@@ -0,0 +1,619 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ import { kebabCase, pascalCase } from 'change-case'
6
+ import consola from 'consola'
7
+
8
+ type Adapter = 'mongodb' | 'memory'
9
+ type MiddlewareTarget = 'nitro' | 'feathers'
10
+ type IdField = 'id' | '_id'
11
+ type CollectionName = string
12
+
13
+ export type RunCliOptions = {
14
+ cwd: string
15
+ }
16
+
17
+ export async function runCli(argv: string[], opts: RunCliOptions) {
18
+ const cwd = resolve(opts.cwd)
19
+
20
+ const [cmd, subcmd, name, ...rest] = argv
21
+
22
+ if (!cmd || cmd === '-h' || cmd === '--help') {
23
+ printHelp()
24
+ process.exit(0)
25
+ }
26
+
27
+ // Expected:
28
+ // nuxt-feathers-zod add service <name> [...]
29
+ // nuxt-feathers-zod add middleware <name> [...]
30
+ if (cmd !== 'add') {
31
+ consola.error(`Unknown command: ${cmd}`)
32
+ printHelp()
33
+ process.exit(1)
34
+ }
35
+
36
+ if (subcmd !== 'service' && subcmd !== 'middleware') {
37
+ consola.error(`Unknown add target: ${subcmd ?? '(missing)'}`)
38
+ printHelp()
39
+ process.exit(1)
40
+ }
41
+
42
+ if (!name) {
43
+ consola.error('Missing <name>.')
44
+ printHelp()
45
+ process.exit(1)
46
+ }
47
+
48
+ const flags = parseFlags(rest)
49
+
50
+ if (subcmd === 'service') {
51
+ const adapter = (flags.adapter as Adapter | undefined) ?? 'mongodb'
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)
57
+ const dry = Boolean(flags.dry)
58
+ const force = Boolean(flags.force)
59
+
60
+ const projectRoot = await findProjectRoot(cwd)
61
+ const servicesDir = resolve(projectRoot, flags.servicesDir ?? 'services')
62
+
63
+ await generateService({
64
+ projectRoot,
65
+ servicesDir,
66
+ name,
67
+ adapter,
68
+ auth,
69
+ idField,
70
+ servicePath,
71
+ collectionName,
72
+ docs,
73
+ dry,
74
+ force,
75
+ })
76
+ return
77
+ }
78
+
79
+ if (subcmd === 'middleware') {
80
+ const target = (flags.target as MiddlewareTarget | undefined) ?? 'nitro'
81
+ const dry = Boolean(flags.dry)
82
+ const force = Boolean(flags.force)
83
+
84
+ const projectRoot = await findProjectRoot(cwd)
85
+
86
+ await generateMiddleware({
87
+ projectRoot,
88
+ name,
89
+ target,
90
+ dry,
91
+ force,
92
+ })
93
+ }
94
+ }
95
+
96
+ function printHelp() {
97
+ // Keep output short; this is a CLI entrypoint.
98
+ // eslint-disable-next-line no-console
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`)
100
+ }
101
+
102
+ function parseFlags(argv: string[]) {
103
+ const out: Record<string, string | boolean> = {}
104
+ for (let i = 0; i < argv.length; i++) {
105
+ const a = argv[i]
106
+ if (!a) continue
107
+ if (!a.startsWith('--')) continue
108
+ const key = a.slice(2)
109
+ const next = argv[i + 1]
110
+ if (!next || next.startsWith('--')) {
111
+ out[key] = true
112
+ continue
113
+ }
114
+ out[key] = next
115
+ i++
116
+ }
117
+ return out
118
+ }
119
+
120
+ async function findProjectRoot(start: string) {
121
+ // Walk up until we find a package.json.
122
+ let dir = resolve(start)
123
+ for (let i = 0; i < 20; i++) {
124
+ if (existsSync(join(dir, 'package.json'))) return dir
125
+ const parent = resolve(dir, '..')
126
+ if (parent === dir) break
127
+ dir = parent
128
+ }
129
+ throw new Error(`Could not find project root from ${start}`)
130
+ }
131
+
132
+ function singularize(input: string) {
133
+ // Minimal heuristic (good enough for a DX helper; users can rename if needed)
134
+ if (input.endsWith('ies')) return `${input.slice(0, -3)}y`
135
+ if (input.endsWith('ses')) return input.slice(0, -2)
136
+ if (input.endsWith('s') && input.length > 1) return input.slice(0, -1)
137
+ return input
138
+ }
139
+
140
+ function normalizeServiceName(raw: string) {
141
+ // Allow "posts", "haproxy-domains", "traefik_stacks" etc.
142
+ return kebabCase(raw)
143
+ }
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
+
167
+ function createServiceIds(serviceNameKebab: string) {
168
+ const baseKebab = singularize(serviceNameKebab)
169
+ const basePascal = pascalCase(baseKebab)
170
+ const baseCamel = basePascal.charAt(0).toLowerCase() + basePascal.slice(1)
171
+
172
+ return {
173
+ serviceNameKebab,
174
+ baseKebab,
175
+ basePascal,
176
+ baseCamel,
177
+ }
178
+ }
179
+
180
+ type GenerateServiceOptions = {
181
+ projectRoot: string
182
+ servicesDir: string
183
+ name: string
184
+ adapter: Adapter
185
+ auth: boolean
186
+ idField: IdField
187
+ servicePath?: string
188
+ collectionName?: CollectionName
189
+ docs: boolean
190
+ dry: boolean
191
+ force: boolean
192
+ }
193
+
194
+ export async function generateService(opts: GenerateServiceOptions) {
195
+ const serviceNameKebab = normalizeServiceName(opts.name)
196
+ const ids = createServiceIds(serviceNameKebab)
197
+
198
+ const servicePath = normalizeServicePath(opts.servicePath ?? serviceNameKebab)
199
+ const collectionName = normalizeCollectionName(
200
+ opts.collectionName
201
+ ?? (servicePath.includes('/') ? serviceNameKebab : servicePath),
202
+ )
203
+
204
+ const dir = join(opts.servicesDir, serviceNameKebab)
205
+ const schemaFile = join(dir, `${serviceNameKebab}.schema.ts`)
206
+ const classFile = join(dir, `${serviceNameKebab}.class.ts`)
207
+ const sharedFile = join(dir, `${serviceNameKebab}.shared.ts`)
208
+ const serviceFile = join(dir, `${serviceNameKebab}.ts`)
209
+
210
+ const files: Array<{ path: string; content: string }> = [
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) },
215
+ ]
216
+
217
+ await ensureDir(dir, opts.dry)
218
+
219
+ for (const f of files) {
220
+ await writeFileSafe(f.path, f.content, { dry: opts.dry, force: opts.force })
221
+ }
222
+
223
+ if (opts.docs) {
224
+ await ensureFeathersSwaggerSupport(opts.projectRoot, { dry: opts.dry, force: opts.force })
225
+ }
226
+
227
+ if (!opts.dry) {
228
+ consola.success(`Generated service '${serviceNameKebab}' in ${relativeToCwd(dir)}`)
229
+ }
230
+ }
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
+
258
+ type GenerateMiddlewareOptions = {
259
+ projectRoot: string
260
+ name: string
261
+ target: MiddlewareTarget
262
+ dry: boolean
263
+ force: boolean
264
+ }
265
+
266
+ export async function generateMiddleware(opts: GenerateMiddlewareOptions) {
267
+ const fileBase = kebabCase(opts.name)
268
+
269
+ if (opts.target === 'nitro') {
270
+ const dir = join(opts.projectRoot, 'server', 'middleware')
271
+ const file = join(dir, `${fileBase}.ts`)
272
+ await ensureDir(dir, opts.dry)
273
+ await writeFileSafe(file, renderNitroMiddleware(fileBase), { dry: opts.dry, force: opts.force })
274
+ if (!opts.dry) consola.success(`Generated Nitro middleware '${fileBase}' in ${relativeToCwd(file)}`)
275
+ return
276
+ }
277
+
278
+ // feathers target: generate a server plugin under server/feathers
279
+ const dir = join(opts.projectRoot, 'server', 'feathers')
280
+ const file = join(dir, `${fileBase}.ts`)
281
+ await ensureDir(dir, opts.dry)
282
+ await writeFileSafe(file, renderFeathersPlugin(fileBase), { dry: opts.dry, force: opts.force })
283
+ if (!opts.dry) consola.success(`Generated Feathers server plugin '${fileBase}' in ${relativeToCwd(file)}`)
284
+ }
285
+
286
+ async function ensureDir(dir: string, dry: boolean) {
287
+ if (dry) return
288
+ await mkdir(dir, { recursive: true })
289
+ }
290
+
291
+ async function writeFileSafe(path: string, content: string, opts: { dry: boolean; force: boolean }) {
292
+ if (!opts.force && existsSync(path)) {
293
+ throw new Error(`File already exists: ${path} (use --force to overwrite)`)
294
+ }
295
+
296
+ if (opts.dry) {
297
+ consola.info(`[dry] write ${relativeToCwd(path)}`)
298
+ return
299
+ }
300
+
301
+ await writeFile(path, content, 'utf8')
302
+ }
303
+
304
+ function relativeToCwd(p: string) {
305
+ try {
306
+ return p.replace(resolve(process.cwd()) + '/', '')
307
+ }
308
+ catch {
309
+ return p
310
+ }
311
+ }
312
+
313
+ function renderSchema(ids: ReturnType<typeof createServiceIds>, adapter: Adapter, idField: IdField) {
314
+ const base = ids.baseCamel
315
+ const Base = ids.basePascal
316
+ const serviceClass = `${Base}Service`
317
+
318
+ const idSchemaField = idField
319
+ const idSchema = adapter === 'mongodb'
320
+ ? `
321
+ const objectIdRegex = /^[0-9a-f]{24}$/i
322
+ export const objectIdSchema = () => z.string().regex(objectIdRegex, 'Invalid ObjectId')
323
+ `
324
+ : ''
325
+
326
+ const mainSchema = adapter === 'mongodb'
327
+ ? `export const ${base}Schema = z.object({
328
+ ${idSchemaField}: objectIdSchema(),
329
+ text: z.string(),
330
+ })`
331
+ : `export const ${base}Schema = z.object({
332
+ ${idSchemaField}: z.number().int(),
333
+ text: z.string(),
334
+ })`
335
+
336
+ const pickCreate = adapter === 'mongodb'
337
+ ? `{ text: true }`
338
+ : `{ text: true }`
339
+
340
+ return `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
341
+
342
+ import type { HookContext } from 'nuxt-feathers-zod/server'
343
+ import type { ${serviceClass} } from './${ids.serviceNameKebab}.class'
344
+ import { resolve } from '@feathersjs/schema'
345
+ import { zodQuerySyntax } from 'nuxt-feathers-zod/query'
346
+ import { getZodValidator } from 'nuxt-feathers-zod/validators'
347
+ import { z } from 'zod'
348
+ ${idSchema}
349
+
350
+ // Main data model schema
351
+ ${mainSchema}
352
+ export type ${Base} = z.infer<typeof ${base}Schema>
353
+ export const ${base}Validator = getZodValidator(${base}Schema, { kind: 'data' })
354
+ export const ${base}Resolver = resolve<${Base}, HookContext<${serviceClass}>>({})
355
+
356
+ export const ${base}ExternalResolver = resolve<${Base}, HookContext<${serviceClass}>>({})
357
+
358
+ // Schema for creating new entries
359
+ export const ${base}DataSchema = ${base}Schema.pick(${pickCreate})
360
+ export type ${Base}Data = z.infer<typeof ${base}DataSchema>
361
+ export const ${base}DataValidator = getZodValidator(${base}DataSchema, { kind: 'data' })
362
+ export const ${base}DataResolver = resolve<${Base}, HookContext<${serviceClass}>>({})
363
+
364
+ // Schema for updating existing entries
365
+ export const ${base}PatchSchema = ${base}Schema.partial()
366
+ export type ${Base}Patch = z.infer<typeof ${base}PatchSchema>
367
+ export const ${base}PatchValidator = getZodValidator(${base}PatchSchema, { kind: 'data' })
368
+ export const ${base}PatchResolver = resolve<${Base}, HookContext<${serviceClass}>>({})
369
+
370
+ // Schema for allowed query properties
371
+ export const ${base}QuerySchema = zodQuerySyntax(${base}Schema)
372
+ export type ${Base}Query = z.infer<typeof ${base}QuerySchema>
373
+ export const ${base}QueryValidator = getZodValidator(${base}QuerySchema, { kind: 'query' })
374
+ export const ${base}QueryResolver = resolve<${Base}Query, HookContext<${serviceClass}>>({})
375
+ `
376
+ }
377
+
378
+ function renderClass(ids: ReturnType<typeof createServiceIds>, adapter: Adapter, collectionName: string) {
379
+ const Base = ids.basePascal
380
+ const serviceName = ids.serviceNameKebab
381
+ const serviceClass = `${Base}Service`
382
+ const paramsName = `${Base}Params`
383
+
384
+ if (adapter === 'memory') {
385
+ return `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.class.html#custom-services
386
+
387
+ import type { Params } from '@feathersjs/feathers'
388
+ import type { MemoryServiceOptions } from '@feathersjs/memory'
389
+ import type { Application } from 'nuxt-feathers-zod/server'
390
+ import type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query } from './${serviceName}.schema'
391
+ import { MemoryService } from '@feathersjs/memory'
392
+
393
+ export type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query }
394
+
395
+ export interface ${paramsName} extends Params<${Base}Query> {}
396
+
397
+ export class ${serviceClass}<ServiceParams extends Params = ${paramsName}> extends MemoryService<
398
+ ${Base},
399
+ ${Base}Data
400
+ > {}
401
+
402
+ export function getOptions(app: Application): MemoryServiceOptions<${Base}> {
403
+ return {
404
+ multi: true,
405
+ }
406
+ }
407
+ `
408
+ }
409
+
410
+ // mongodb
411
+ return `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.class.html#database-services
412
+
413
+ import type { Params } from '@feathersjs/feathers'
414
+ import type { MongoDBAdapterOptions, MongoDBAdapterParams } from '@feathersjs/mongodb'
415
+ import type { Application } from 'nuxt-feathers-zod/server'
416
+ import type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query } from './${serviceName}.schema'
417
+ import { MongoDBService } from '@feathersjs/mongodb'
418
+
419
+ export type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query }
420
+
421
+ export interface ${paramsName} extends MongoDBAdapterParams<${Base}Query> {}
422
+
423
+ export class ${serviceClass}<ServiceParams extends Params = ${paramsName}> extends MongoDBService<
424
+ ${Base},
425
+ ${Base}Data,
426
+ ${paramsName},
427
+ ${Base}Patch
428
+ > {}
429
+
430
+ export function getOptions(app: Application): MongoDBAdapterOptions {
431
+ const mongoClient = app.get('mongodbClient')
432
+ return {
433
+ paginate: {
434
+ default: 10,
435
+ max: 100,
436
+ },
437
+ multi: true,
438
+ Model: mongoClient.then(db => db.collection('${collectionName}')),
439
+ }
440
+ }
441
+ `
442
+ }
443
+
444
+ function renderShared(ids: ReturnType<typeof createServiceIds>, servicePath: string) {
445
+ const base = ids.baseCamel
446
+ const Base = ids.basePascal
447
+ const serviceName = ids.serviceNameKebab
448
+ const serviceClass = `${Base}Service`
449
+ const methodsConst = `${base}Methods`
450
+ const pathConst = `${base}Path`
451
+ const clientFn = `${base}Client`
452
+ const clientServiceType = `${Base}ClientService`
453
+
454
+ return `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.shared.html
455
+
456
+ import type { Params } from '@feathersjs/feathers'
457
+ import type { ClientApplication } from 'nuxt-feathers-zod/client'
458
+ import type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query, ${serviceClass} } from './${serviceName}.class'
459
+
460
+ export type { ${Base}, ${Base}Data, ${Base}Patch, ${Base}Query }
461
+
462
+ export type ${clientServiceType} = Pick<${serviceClass}<Params<${Base}Query>>, (typeof ${methodsConst})[number]>
463
+
464
+ export const ${pathConst} = '${servicePath}'
465
+
466
+ export const ${methodsConst}: Array<keyof ${serviceClass}> = ['find', 'get', 'create', 'patch', 'remove']
467
+
468
+ export function ${clientFn}(client: ClientApplication) {
469
+ const connection = client.get('connection')
470
+
471
+ client.use(${pathConst}, connection.service(${pathConst}), {
472
+ methods: ${methodsConst},
473
+ })
474
+ }
475
+
476
+ declare module 'nuxt-feathers-zod/client' {
477
+ interface ServiceTypes {
478
+ [${pathConst}]: ${clientServiceType}
479
+ }
480
+ }
481
+ `
482
+ }
483
+
484
+ function renderService(ids: ReturnType<typeof createServiceIds>, auth: boolean, docs: boolean) {
485
+ const base = ids.baseCamel
486
+ const Base = ids.basePascal
487
+ const serviceName = ids.serviceNameKebab
488
+ const serviceClass = `${Base}Service`
489
+ const authImports = auth ? "import { authenticate } from '@feathersjs/authentication'\n" : ''
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
+
517
+ const authAround = auth
518
+ ? `
519
+ find: [authenticate('jwt')],
520
+ get: [authenticate('jwt')],
521
+ create: [],
522
+ patch: [authenticate('jwt')],
523
+ remove: [authenticate('jwt')],
524
+ `
525
+ : `
526
+ find: [],
527
+ get: [],
528
+ create: [],
529
+ patch: [],
530
+ remove: [],
531
+ `
532
+
533
+ return `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.html
534
+
535
+ import type { Application } from 'nuxt-feathers-zod/server'
536
+ ${authImports}${swaggerImports}import { hooks as schemaHooks } from '@feathersjs/schema'
537
+ import { getOptions, ${serviceClass} } from './${serviceName}.class'
538
+ import {
539
+ ${swaggerSchemaImports} ${base}DataResolver,
540
+ ${base}DataValidator,
541
+ ${base}ExternalResolver,
542
+ ${base}PatchResolver,
543
+ ${base}PatchValidator,
544
+ ${base}QueryResolver,
545
+ ${base}QueryValidator,
546
+ ${base}Resolver,
547
+ } from './${serviceName}.schema'
548
+ import { ${base}Methods, ${base}Path } from './${serviceName}.shared'
549
+
550
+ export * from './${serviceName}.class'
551
+ export * from './${serviceName}.schema'
552
+
553
+ export function ${base}(app: Application) {
554
+ app.use(${base}Path, new ${serviceClass}(getOptions(app)), {
555
+ methods: ${base}Methods,
556
+ events: [],${docsBlock}
557
+ })
558
+
559
+ app.service(${base}Path).hooks({
560
+ around: {
561
+ all: [schemaHooks.resolveExternal(${base}ExternalResolver), schemaHooks.resolveResult(${base}Resolver)],
562
+ ${authAround} },
563
+ before: {
564
+ all: [schemaHooks.validateQuery(${base}QueryValidator), schemaHooks.resolveQuery(${base}QueryResolver)],
565
+ find: [],
566
+ get: [],
567
+ create: [schemaHooks.validateData(${base}DataValidator), schemaHooks.resolveData(${base}DataResolver)],
568
+ patch: [schemaHooks.validateData(${base}PatchValidator), schemaHooks.resolveData(${base}PatchResolver)],
569
+ remove: [],
570
+ },
571
+ after: {
572
+ all: [],
573
+ },
574
+ error: {
575
+ all: [],
576
+ },
577
+ })
578
+ }
579
+
580
+ declare module 'nuxt-feathers-zod/server' {
581
+ interface ServiceTypes {
582
+ [${base}Path]: ${serviceClass}
583
+ }
584
+ }
585
+ `
586
+ }
587
+
588
+ function renderNitroMiddleware(name: string) {
589
+ const nice = name.replace(/-/g, ' ')
590
+ return `// Nitro middleware: ${nice}
591
+ // Runs on every request (or conditionally based on route rules).
592
+
593
+ export default defineEventHandler(async (event) => {
594
+ // Example: attach a request id
595
+ // event.context.requestId = crypto.randomUUID()
596
+ })
597
+ `
598
+ }
599
+
600
+ function renderFeathersPlugin(name: string) {
601
+ const nice = name.replace(/-/g, ' ')
602
+ return `// Feathers server plugin: ${nice}
603
+ // Loaded by Nuxt Nitro server (see playground/server/feathers/*.ts for examples)
604
+
605
+ import type { HookContext, NextFunction } from 'nuxt-feathers-zod/server'
606
+ import { defineFeathersServerPlugin } from 'nuxt-feathers-zod/server'
607
+
608
+ export default defineFeathersServerPlugin((app) => {
609
+ app.hooks({
610
+ setup: [
611
+ async (context: HookContext, next: NextFunction) => {
612
+ // Place initialization logic here
613
+ await next()
614
+ },
615
+ ],
616
+ })
617
+ })
618
+ `
619
+ }