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 +141 -0
- package/bin/nuxt-feathers-zod +5 -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 +19 -17
- package/src/cli/index.ts +619 -0
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
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-feathers-zod",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1
|
|
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
|
-
".": [
|
|
28
|
-
|
|
29
|
-
|
|
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": [
|
|
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": "
|
|
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
|
}
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|