@nestledjs/api 0.0.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/CHANGELOG.md +5 -0
- package/README.md +11 -0
- package/eslint.config.cjs +28 -0
- package/generators.json +69 -0
- package/package.json +21 -0
- package/project.json +47 -0
- package/src/account/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/account/files/data-access/src/lib/api-account-data-access.module.ts__tmpl__ +10 -0
- package/src/account/files/data-access/src/lib/api-account-data-access.service.ts__tmpl__ +152 -0
- package/src/account/files/data-access/src/lib/dto/account-create-email.input.ts__tmpl__ +9 -0
- package/src/account/files/data-access/src/lib/dto/account-update-password.input.ts__tmpl__ +16 -0
- package/src/account/files/data-access/src/lib/dto/account-update-profile.input.ts__tmpl__ +25 -0
- package/src/account/files/feature/src/index.ts__tmpl__ +1 -0
- package/src/account/files/feature/src/lib/api-account-feature.module.ts__tmpl__ +9 -0
- package/src/account/files/feature/src/lib/api-account-feature.resolver.ts__tmpl__ +83 -0
- package/src/account/generator.spec.ts +71 -0
- package/src/account/generator.ts +20 -0
- package/src/account/schema.d.ts +3 -0
- package/src/account/schema.json +13 -0
- package/src/app/files/src/app.config.ts__tmpl__ +66 -0
- package/src/app/files/src/app.module.ts__tmpl__ +43 -0
- package/src/app/files/src/applogger.middleware.ts__tmpl__ +21 -0
- package/src/app/files/src/main.ts__tmpl__ +33 -0
- package/src/app/files/webpack.config.js__tmpl__ +54 -0
- package/src/app/generator.spec.ts +112 -0
- package/src/app/generator.ts +105 -0
- package/src/app/schema.d.ts +1 -0
- package/src/app/schema.json +9 -0
- package/src/config/files/src/index.ts__tmpl__ +3 -0
- package/src/config/files/src/lib/config.service.ts__tmpl__ +51 -0
- package/src/config/files/src/lib/configuration.ts__tmpl__ +32 -0
- package/src/config/files/src/lib/validation.ts__tmpl__ +21 -0
- package/src/config/generator.spec.ts +47 -0
- package/src/config/generator.ts +16 -0
- package/src/config/schema.d.ts +3 -0
- package/src/config/schema.json +13 -0
- package/src/core/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/core/files/data-access/src/lib/api-core-data-access.module.ts__tmpl__ +9 -0
- package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +97 -0
- package/src/core/files/data-access/src/lib/api-core-pub-sub.ts__tmpl__ +37 -0
- package/src/core/files/data-access/src/lib/dto/core-paging.input.ts__tmpl__ +26 -0
- package/src/core/files/data-access/src/lib/dto/multi-select-input.ts__tmpl__ +7 -0
- package/src/core/files/data-access/src/lib/models/core-paging.ts__tmpl__ +19 -0
- package/src/core/files/feature/src/index.ts__tmpl__ +2 -0
- package/src/core/files/feature/src/lib/api-core-feature.controller.ts__tmpl__ +12 -0
- package/src/core/files/feature/src/lib/api-core-feature.module.ts__tmpl__ +86 -0
- package/src/core/files/feature/src/lib/api-core-feature.resolver.ts__tmpl__ +12 -0
- package/src/core/files/feature/src/lib/api-core-feature.service.ts__tmpl__ +55 -0
- package/src/core/files/feature/src/lib/config/configuration.ts__tmpl__ +32 -0
- package/src/core/files/feature/src/lib/config/validation.ts__tmpl__ +25 -0
- package/src/core/files/feature/src/lib/plugins/complexity.plugin.ts__tmpl__ +51 -0
- package/src/core/files/feature/src/lib/plugins/logging.plugin.ts__tmpl__ +17 -0
- package/src/core/files/models/src/index.ts__tmpl__ +1 -0
- package/src/core/files/models/src/lib/generate-models.ts__tmpl__ +294 -0
- package/src/core/files/models/src/lib/models/core-paging.model.ts__tmpl__ +25 -0
- package/src/core/generator.spec.ts +85 -0
- package/src/core/generator.ts +35 -0
- package/src/core/schema.d.ts +3 -0
- package/src/core/schema.json +13 -0
- package/src/custom/generator.spec.ts +75 -0
- package/src/custom/generator.ts +239 -0
- package/src/custom/schema.json +21 -0
- package/src/custom/schema.ts +5 -0
- package/src/extended/generator.spec.ts +95 -0
- package/src/extended/generator.ts +161 -0
- package/src/extended/index.ts +1 -0
- package/src/extended/schema.json +12 -0
- package/src/extended/schema.ts +3 -0
- package/src/generate-crud/files/data-access/src/index.ts__tmpl__ +3 -0
- package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.module.ts__tmpl__ +11 -0
- package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.service.ts__tmpl__ +72 -0
- package/src/generate-crud/files/data-access/src/lib/dto/index.ts__tmpl__ +224 -0
- package/src/generate-crud/files/feature/.gitkeep +0 -0
- package/src/generate-crud/generator.spec.ts +84 -0
- package/src/generate-crud/generator.ts +354 -0
- package/src/generate-crud/schema.json +32 -0
- package/src/generate-crud/schema.ts +8 -0
- package/src/index.ts +13 -0
- package/src/plugin/generator.spec.ts +18 -0
- package/src/plugin/generator.ts +74 -0
- package/src/plugin/schema.json +14 -0
- package/src/plugin/schema.ts +4 -0
- package/src/prisma/files/src/index.ts__tmpl__ +1 -0
- package/src/prisma/files/src/lib/.gitkeep +1 -0
- package/src/prisma/files/src/lib/schemas/schema.prisma__tmpl__ +402 -0
- package/src/prisma/files/src/lib/seed/seed-data/iso-3166-countries.ts__tmpl__ +3239 -0
- package/src/prisma/files/src/lib/seed/seed-data/seed-users.ts__tmpl__ +32 -0
- package/src/prisma/files/src/lib/seed/seed.ts__tmpl__ +64 -0
- package/src/prisma/generator.spec.ts +60 -0
- package/src/prisma/generator.ts +61 -0
- package/src/prisma/schema.d.ts +3 -0
- package/src/prisma/schema.json +13 -0
- package/src/setup/generator.md +49 -0
- package/src/setup/generator.spec.ts +18 -0
- package/src/setup/generator.ts +106 -0
- package/src/setup/schema.json +8 -0
- package/src/smtp-mailer/files/data-access/src/index.ts__tmpl__ +2 -0
- package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.module.ts__tmpl__ +10 -0
- package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.service.ts__tmpl__ +61 -0
- package/src/smtp-mailer/generator.spec.ts +41 -0
- package/src/smtp-mailer/generator.ts +14 -0
- package/src/smtp-mailer/schema.d.ts +0 -0
- package/src/smtp-mailer/schema.json +7 -0
- package/src/user/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/user/files/data-access/src/lib/api-user-data-access.module.ts__tmpl__ +10 -0
- package/src/user/files/data-access/src/lib/api-user-data-access.service.ts__tmpl__ +119 -0
- package/src/user/files/data-access/src/lib/dto/admin-create-user.input.ts__tmpl__ +20 -0
- package/src/user/files/data-access/src/lib/dto/admin-update-user.input.ts__tmpl__ +29 -0
- package/src/user/files/feature/src/index.ts__tmpl__ +1 -0
- package/src/user/files/feature/src/lib/api-user-feature-admin.resolver.ts__tmpl__ +57 -0
- package/src/user/files/feature/src/lib/api-user-feature.module.ts__tmpl__ +10 -0
- package/src/user/files/feature/src/lib/api-user-feature.resolver.ts__tmpl__ +17 -0
- package/src/user/generator.spec.ts +41 -0
- package/src/user/generator.ts +15 -0
- package/src/user/schema.d.ts +0 -0
- package/src/user/schema.json +7 -0
- package/src/utils/files/src/index.ts__tmpl__ +3 -0
- package/src/utils/files/src/lib/decorators/ctx-user.decorator.ts__tmpl__ +6 -0
- package/src/utils/files/src/lib/guards/gql-auth-admin.guard.ts__tmpl__ +39 -0
- package/src/utils/files/src/lib/guards/gql-auth.guard.ts__tmpl__ +11 -0
- package/src/utils/generator.ts +14 -0
- package/src/utils/schema.json +8 -0
- package/src/workspace-setup/generator.md +39 -0
- package/src/workspace-setup/generator.spec.ts +82 -0
- package/src/workspace-setup/generator.ts +49 -0
- package/src/workspace-setup/lib/helpers.ts +142 -0
- package/src/workspace-setup/schema.d.ts +3 -0
- package/src/workspace-setup/schema.json +7 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +23 -0
- package/tsconfig.spec.json +22 -0
- package/vite.config.mts +37 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { registerAs } from '@nestjs/config'
|
|
2
|
+
import Joi from 'joi'
|
|
3
|
+
|
|
4
|
+
export const appConfig = registerAs('app', () => ({
|
|
5
|
+
prefix: 'api',
|
|
6
|
+
environment: process.env.NODE_ENV,
|
|
7
|
+
host: process.env.HOST,
|
|
8
|
+
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
|
|
9
|
+
apiUrl: process.env.API_URL,
|
|
10
|
+
webPort: process.env.WEB_PORT ? parseInt(process.env.WEB_PORT, 10) : 4200,
|
|
11
|
+
api: {
|
|
12
|
+
cookie: {
|
|
13
|
+
name: process.env.API_COOKIE_NAME,
|
|
14
|
+
secret: process.env.API_COOKIE_SECRET,
|
|
15
|
+
options: {
|
|
16
|
+
domain: process.env.API_COOKIE_DOMAIN,
|
|
17
|
+
httpOnly: true,
|
|
18
|
+
secure: true,
|
|
19
|
+
sameSite: 'none',
|
|
20
|
+
path: '/',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
cors: {
|
|
24
|
+
origin: [process.env.SITE_URL],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
siteUrl: process.env.SITE_URL || (process.env.API_URL ? process.env.API_URL.replace('/api', '') : ''),
|
|
28
|
+
app: {
|
|
29
|
+
email: process.env.APP_EMAIL,
|
|
30
|
+
supportEmail: process.env.APP_SUPPORT_EMAIL,
|
|
31
|
+
adminEmails: process.env.APP_ADMIN_EMAILS,
|
|
32
|
+
name: process.env.APP_NAME,
|
|
33
|
+
},
|
|
34
|
+
smtp: {
|
|
35
|
+
host: process.env.SMTP_HOST,
|
|
36
|
+
port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
|
|
37
|
+
user: process.env.SMTP_USER,
|
|
38
|
+
pass: process.env.SMTP_PASS,
|
|
39
|
+
ignoreTLS: true,
|
|
40
|
+
},
|
|
41
|
+
twilio: {
|
|
42
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
43
|
+
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
44
|
+
fromNumber: process.env.TWILIO_FROM_NUMBER,
|
|
45
|
+
},
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
export const validationSchema = Joi.object({
|
|
49
|
+
NODE_ENV: Joi.string().valid('development', 'production', 'test'),
|
|
50
|
+
HOST: Joi.string().alphanum().default('localhost'),
|
|
51
|
+
PORT: Joi.number().default(3000),
|
|
52
|
+
WEB_PORT: Joi.number().default(4200),
|
|
53
|
+
WEB_URL: Joi.string().default(`http://${process.env.HOST || 'localhost'}:${process.env.WEB_PORT}`),
|
|
54
|
+
API_COOKIE_DOMAIN: Joi.string().default('localhost'),
|
|
55
|
+
API_COOKIE_NAME: Joi.string().default('__session'),
|
|
56
|
+
API_URL: Joi.string().default(`http://${process.env.HOST || 'localhost'}:${process.env.PORT}/api`),
|
|
57
|
+
APP_NAME: Joi.string().required(),
|
|
58
|
+
APP_EMAIL: Joi.string().email().required(),
|
|
59
|
+
APP_SUPPORT_EMAIL: Joi.string().email().required(),
|
|
60
|
+
APP_ADMIN_EMAILS: Joi.string().required(),
|
|
61
|
+
SITE_URL: Joi.string().uri().required(),
|
|
62
|
+
SMTP_HOST: Joi.string().required(),
|
|
63
|
+
SMTP_PORT: Joi.string().required(),
|
|
64
|
+
SMTP_USER: Joi.string().required(),
|
|
65
|
+
SMTP_PASS: Joi.string().required(),
|
|
66
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'
|
|
2
|
+
import { LoggerMiddleware } from './applogger.middleware'
|
|
3
|
+
import { ConfigModule } from '@nestjs/config'
|
|
4
|
+
import { appConfig, validationSchema } from './app.config'
|
|
5
|
+
|
|
6
|
+
// Auto-generated modules with special functions
|
|
7
|
+
export const coreModules = [
|
|
8
|
+
// Auto-generated modules with special functions
|
|
9
|
+
]
|
|
10
|
+
// Auto-generated modules for each data type/model
|
|
11
|
+
export const defaultModules = [
|
|
12
|
+
// Auto-generated modules for each data type/model
|
|
13
|
+
];
|
|
14
|
+
// Manually maintained plugin modules (never overwritten by generator)
|
|
15
|
+
export const pluginModules = [
|
|
16
|
+
// Manually maintained plugin modules (never overwritten by generator)
|
|
17
|
+
];
|
|
18
|
+
// Combined modules used in the app
|
|
19
|
+
export const appModules = [
|
|
20
|
+
...coreModules,
|
|
21
|
+
...defaultModules,
|
|
22
|
+
...pluginModules,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
@Module({
|
|
26
|
+
imports: [
|
|
27
|
+
ConfigModule.forRoot({
|
|
28
|
+
load: [appConfig],
|
|
29
|
+
validationSchema: validationSchema,
|
|
30
|
+
validationOptions: {
|
|
31
|
+
allowUnknown: true,
|
|
32
|
+
abortEarly: false,
|
|
33
|
+
},
|
|
34
|
+
isGlobal: true,
|
|
35
|
+
}),
|
|
36
|
+
...appModules,
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
export class AppModule implements NestModule {
|
|
40
|
+
configure(consumer: MiddlewareConsumer): void {
|
|
41
|
+
consumer.apply(LoggerMiddleware) .forRoutes({ path: '*path', method: RequestMethod.ALL })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from 'express'
|
|
2
|
+
import { Injectable, Logger, NestMiddleware } from '@nestjs/common'
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class LoggerMiddleware implements NestMiddleware {
|
|
6
|
+
private readonly logger = new Logger('HTTP')
|
|
7
|
+
|
|
8
|
+
use(request: Request, response: Response, next: NextFunction): void {
|
|
9
|
+
const { ip, method, originalUrl } = request
|
|
10
|
+
const userAgent = request.get('user-agent') || ''
|
|
11
|
+
|
|
12
|
+
response.on('finish', () => {
|
|
13
|
+
const { statusCode } = response
|
|
14
|
+
const contentLength = response.get('content-length')
|
|
15
|
+
|
|
16
|
+
this.logger.log(`${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
next()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common'
|
|
2
|
+
import { NestFactory } from '@nestjs/core'
|
|
3
|
+
import { ConfigService } from '@nestjs/config'
|
|
4
|
+
import cookieParser from 'cookie-parser'
|
|
5
|
+
|
|
6
|
+
import { AppModule } from './app.module'
|
|
7
|
+
|
|
8
|
+
async function bootstrap() {
|
|
9
|
+
const app = await NestFactory.create(AppModule)
|
|
10
|
+
const configService = app.get(ConfigService)
|
|
11
|
+
|
|
12
|
+
// Get individual properties with fallbacks
|
|
13
|
+
const prefix = configService.get('app.prefix') || 'api';
|
|
14
|
+
const port = configService.get('app.port') || 3000;
|
|
15
|
+
const host = configService.get('app.host') || 'localhost';
|
|
16
|
+
|
|
17
|
+
app.setGlobalPrefix(prefix)
|
|
18
|
+
app.enableCors({
|
|
19
|
+
credentials: true,
|
|
20
|
+
origin: configService.get('app.api.cors.origin') || '*',
|
|
21
|
+
})
|
|
22
|
+
app.use(cookieParser(configService.get('app.api.cookie.secret') || 'secret'));
|
|
23
|
+
|
|
24
|
+
await app.listen(port, host, () => {
|
|
25
|
+
Logger.log(`Listening at http://${host}:${port}/${prefix}`)
|
|
26
|
+
Logger.log(`Listening at http://${host}:${port}/graphql`)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
bootstrap().catch((error) => {
|
|
31
|
+
Logger.error('Failed to start the application', error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
|
3
|
+
const nodeExternals = require('webpack-node-externals');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
target: 'node',
|
|
7
|
+
mode: 'development',
|
|
8
|
+
entry: path.resolve(__dirname, 'src/main.ts'),
|
|
9
|
+
output: {
|
|
10
|
+
path: path.resolve(__dirname, '../../dist/apps/api'),
|
|
11
|
+
filename: 'main.js',
|
|
12
|
+
},
|
|
13
|
+
resolve: {
|
|
14
|
+
extensions: ['.ts', '.js'],
|
|
15
|
+
plugins: [
|
|
16
|
+
new TsconfigPathsPlugin({
|
|
17
|
+
configFile: path.resolve(__dirname, '../../tsconfig.base.json'),
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
module: {
|
|
22
|
+
rules: [
|
|
23
|
+
{
|
|
24
|
+
test: /\.ts$/,
|
|
25
|
+
use: 'ts-loader',
|
|
26
|
+
exclude: [
|
|
27
|
+
/node_modules/,
|
|
28
|
+
path.resolve(__dirname, '../../libs/api/prisma/src/lib/prisma-generated')
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
externals: [
|
|
35
|
+
nodeExternals({
|
|
36
|
+
allowlist: [
|
|
37
|
+
/^@<%= npmScope %>\/api/, // allow your own libs
|
|
38
|
+
],
|
|
39
|
+
// Manually exclude Prisma internals if needed
|
|
40
|
+
function ({ request }, callback) {
|
|
41
|
+
if (request && request.includes('@prisma/client')) {
|
|
42
|
+
return callback(null, 'commonjs @prisma/client')
|
|
43
|
+
}
|
|
44
|
+
if (request && request.includes('.prisma/client')) {
|
|
45
|
+
return callback(null, 'commonjs .prisma/client')
|
|
46
|
+
}
|
|
47
|
+
if (request && request === '@<%= npmScope %>/api/prisma') {
|
|
48
|
+
return callback(null, 'commonjs @<%= npmScope %>/api/prisma')
|
|
49
|
+
}
|
|
50
|
+
callback()
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
]
|
|
54
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
|
|
2
|
+
import { Tree } from '@nx/devkit'
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import generator from './generator'
|
|
6
|
+
|
|
7
|
+
vi.mock('child_process', async (importOriginal) => {
|
|
8
|
+
const originalModule = await importOriginal()
|
|
9
|
+
return {
|
|
10
|
+
...(originalModule as object),
|
|
11
|
+
execSync: vi.fn(),
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('api generator', () => {
|
|
16
|
+
let tree: Tree
|
|
17
|
+
const mockedExecSync = execSync as ReturnType<(typeof vi)['fn']>
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tree = createTreeWithEmptyWorkspace()
|
|
21
|
+
tree.write('package.json', JSON.stringify({ name: 'test-repo', scripts: {} }))
|
|
22
|
+
|
|
23
|
+
mockedExecSync.mockImplementation((command: string) => {
|
|
24
|
+
if (command.startsWith('nx g @nx/nest:application')) {
|
|
25
|
+
tree.write(
|
|
26
|
+
'apps/api/webpack.config.js',
|
|
27
|
+
`
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { composePlugins, withNx } = require('@nx/webpack');
|
|
30
|
+
|
|
31
|
+
module.exports = composePlugins(withNx(), (config) => {
|
|
32
|
+
return {
|
|
33
|
+
...config,
|
|
34
|
+
output: {
|
|
35
|
+
...config.output,
|
|
36
|
+
devtoolModuleFilenameTemplate: (info) => {
|
|
37
|
+
const rel = path.relative(process.cwd(), info.absoluteResourcePath);
|
|
38
|
+
return \`webpack://\${rel}\`;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
assets: ['./src/assets'],
|
|
42
|
+
generatePackageJson: true,
|
|
43
|
+
};
|
|
44
|
+
});`
|
|
45
|
+
)
|
|
46
|
+
// Add a mock tsconfig.app.json file with the compiler options we want to remove
|
|
47
|
+
tree.write('apps/api/tsconfig.app.json', JSON.stringify({
|
|
48
|
+
extends: '../../tsconfig.base.json',
|
|
49
|
+
compilerOptions: {
|
|
50
|
+
module: 'commonjs',
|
|
51
|
+
moduleResolution: 'node',
|
|
52
|
+
emitDecoratorMetadata: true,
|
|
53
|
+
experimentalDecorators: true,
|
|
54
|
+
outDir: '../../dist/out-tsc',
|
|
55
|
+
declaration: true,
|
|
56
|
+
types: ['node']
|
|
57
|
+
},
|
|
58
|
+
exclude: ['jest.config.ts', '**/*.spec.ts', '**/*.test.ts'],
|
|
59
|
+
include: ['**/*.ts']
|
|
60
|
+
}, null, 2))
|
|
61
|
+
tree.write('apps/api/src/app/.gitkeep', '')
|
|
62
|
+
tree.write('apps/api/src/assets/.gitkeep', '')
|
|
63
|
+
}
|
|
64
|
+
return ''
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
vi.spyOn(console, 'log').mockImplementation(() => { /* empty */ })
|
|
68
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { /* empty */ })
|
|
69
|
+
vi.spyOn(console, 'error').mockImplementation(() => { /* empty */ })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
vi.restoreAllMocks()
|
|
74
|
+
mockedExecSync.mockClear()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should run successfully', async () => {
|
|
78
|
+
await generator(tree, {})
|
|
79
|
+
|
|
80
|
+
expect(mockedExecSync).toHaveBeenCalledWith(
|
|
81
|
+
'nx g @nx/nest:application --name api --directory apps/api --no-interactive',
|
|
82
|
+
{
|
|
83
|
+
stdio: 'inherit',
|
|
84
|
+
cwd: tree.root,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const webpackConfig = tree.read('apps/api/webpack.config.js', 'utf-8')
|
|
89
|
+
expect(webpackConfig).not.toContain("assets: ['./src/assets']")
|
|
90
|
+
|
|
91
|
+
const packageJson = JSON.parse(tree.read('package.json', 'utf-8'))
|
|
92
|
+
expect(packageJson.scripts['dev:api']).toBe('nx serve api --skip-nx-cache')
|
|
93
|
+
|
|
94
|
+
expect(tree.exists('apps/api/src/app')).toBe(false)
|
|
95
|
+
expect(tree.exists('apps/api/src/assets')).toBe(false)
|
|
96
|
+
|
|
97
|
+
expect(tree.exists('apps/api/src/app.config.ts')).toBe(true)
|
|
98
|
+
expect(tree.exists('apps/api/src/app.module.ts')).toBe(true)
|
|
99
|
+
expect(tree.exists('apps/api/src/applogger.middleware.ts')).toBe(true)
|
|
100
|
+
expect(tree.exists('apps/api/src/main.ts')).toBe(true)
|
|
101
|
+
|
|
102
|
+
// Verify that the compiler options are removed from tsconfig.app.json
|
|
103
|
+
const tsConfig = JSON.parse(tree.read('apps/api/tsconfig.app.json', 'utf-8'))
|
|
104
|
+
expect(tsConfig.compilerOptions.module).toBeUndefined()
|
|
105
|
+
expect(tsConfig.compilerOptions.moduleResolution).toBeUndefined()
|
|
106
|
+
expect(tsConfig.compilerOptions.emitDecoratorMetadata).toBeUndefined()
|
|
107
|
+
expect(tsConfig.compilerOptions.experimentalDecorators).toBeUndefined()
|
|
108
|
+
// Verify that other compiler options are preserved
|
|
109
|
+
expect(tsConfig.compilerOptions.outDir).toBe('../../dist/out-tsc')
|
|
110
|
+
expect(tsConfig.compilerOptions.declaration).toBe(true)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { generateFiles, joinPathFragments, Tree, updateJson } from '@nx/devkit'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Removes specific compiler options from tsconfig.app.json to use the settings from tsconfig.base.json
|
|
8
|
+
* @param tree The file system tree
|
|
9
|
+
*/
|
|
10
|
+
function updateAppTsConfig(tree: Tree): void {
|
|
11
|
+
const tsConfigPath = 'apps/api/tsconfig.app.json'
|
|
12
|
+
if (tree.exists(tsConfigPath)) {
|
|
13
|
+
updateJson(tree, tsConfigPath, (json) => {
|
|
14
|
+
if (json.compilerOptions) {
|
|
15
|
+
// Remove specific options to use the ones from tsconfig.base.json
|
|
16
|
+
delete json.compilerOptions.module
|
|
17
|
+
delete json.compilerOptions.moduleResolution
|
|
18
|
+
delete json.compilerOptions.emitDecoratorMetadata
|
|
19
|
+
delete json.compilerOptions.experimentalDecorators
|
|
20
|
+
}
|
|
21
|
+
return json
|
|
22
|
+
})
|
|
23
|
+
} else {
|
|
24
|
+
console.warn(`tsconfig.app.json not found at: ${tsConfigPath}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Schema {
|
|
29
|
+
[key: string]: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default async function (tree: Tree, schema: Schema) {
|
|
33
|
+
try {
|
|
34
|
+
// Get the workspace root directory
|
|
35
|
+
const workspaceRoot = tree.root
|
|
36
|
+
|
|
37
|
+
// Create apps directory if it doesn't exist
|
|
38
|
+
if (!tree.exists('apps')) {
|
|
39
|
+
tree.write('apps/.gitkeep', '')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Run the Nx generator command directly from the workspace root with proper workspace layout
|
|
43
|
+
execSync('nx g @nx/nest:application --name api --directory apps/api --no-interactive', {
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
cwd: workspaceRoot,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Wait a bit for files to be created
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
50
|
+
|
|
51
|
+
// Update tsconfig.app.json to remove specific compiler options
|
|
52
|
+
updateAppTsConfig(tree)
|
|
53
|
+
|
|
54
|
+
// Generate all files according to the template folder structure
|
|
55
|
+
generateFiles(
|
|
56
|
+
tree,
|
|
57
|
+
joinPathFragments(__dirname, './files'),
|
|
58
|
+
path.join('apps', 'api'),
|
|
59
|
+
{ ...schema, tmpl: '', npmScope: getNpmScope(tree) }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Add dev:api script to package.json
|
|
63
|
+
updateJson(tree, 'package.json', (json) => {
|
|
64
|
+
if (!json.scripts) {
|
|
65
|
+
json.scripts = {}
|
|
66
|
+
}
|
|
67
|
+
json.scripts['dev:api'] = 'nx serve api --skip-nx-cache'
|
|
68
|
+
return json
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Update the build target in apps/api/project.json to use custom webpack command
|
|
72
|
+
const projectJsonPath = path.join('apps', 'api', 'project.json')
|
|
73
|
+
if (tree.exists(projectJsonPath)) {
|
|
74
|
+
updateJson(tree, projectJsonPath, (json) => {
|
|
75
|
+
json.targets = json.targets || {}
|
|
76
|
+
json.targets.build = {
|
|
77
|
+
executor: 'nx:run-commands',
|
|
78
|
+
options: {
|
|
79
|
+
command: 'NODE_ENV=production webpack-cli --config apps/api/webpack.config.js',
|
|
80
|
+
},
|
|
81
|
+
configurations: {
|
|
82
|
+
development: {
|
|
83
|
+
command: 'NODE_ENV=development webpack-cli --config apps/api/webpack.config.js',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
return json
|
|
88
|
+
})
|
|
89
|
+
} else {
|
|
90
|
+
console.warn(`project.json not found at: ${projectJsonPath}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Optionally, delete the unused default app files if they exist
|
|
94
|
+
const targetPath = path.join('apps', 'api', 'src')
|
|
95
|
+
const filesToDelete = [path.join(targetPath, 'assets'), path.join(targetPath, 'app')]
|
|
96
|
+
filesToDelete.forEach((filePath) => {
|
|
97
|
+
if (tree.exists(filePath)) {
|
|
98
|
+
tree.delete(filePath)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Error generating API app:', error)
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type AppSetupGeneratorSchema = Record<string, unknown>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common'
|
|
2
|
+
import { ConfigService } from '@nestjs/config'
|
|
3
|
+
import { CookieOptions } from 'express'
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class ApiConfigService {
|
|
7
|
+
constructor(public readonly config: ConfigService) {}
|
|
8
|
+
|
|
9
|
+
get apiUrl(): string {
|
|
10
|
+
return this.config.getOrThrow<string>('apiUrl')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get apiCorsOrigins(): string[] {
|
|
14
|
+
return this.config.get<string[]>('api.cors.origin') ?? []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get cookie(): { name: string; options: CookieOptions } {
|
|
18
|
+
return this.config.getOrThrow<{ name: string; options: CookieOptions }>('api.cookie')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get appEmail(): string {
|
|
22
|
+
return this.config.getOrThrow<string>('app.email')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get appSupportEmail(): string {
|
|
26
|
+
return this.config.getOrThrow<string>('app.supportEmail')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get appAdminEmails(): string {
|
|
30
|
+
return this.config.getOrThrow<string>('app.adminEmails')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get appName(): string {
|
|
34
|
+
return this.config.getOrThrow<string>('app.name')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get siteUrl(): string {
|
|
38
|
+
return this.config.getOrThrow<string>('siteUrl')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get mailerConfig() {
|
|
42
|
+
return {
|
|
43
|
+
host: this.config.getOrThrow<string>('smtp.host'),
|
|
44
|
+
port: this.config.getOrThrow<string>('smtp.port'),
|
|
45
|
+
auth: {
|
|
46
|
+
user: this.config.getOrThrow<string>('smtp.user'),
|
|
47
|
+
pass: this.config.getOrThrow<string>('smtp.pass'),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const configuration = () => ({
|
|
2
|
+
prefix: 'api',
|
|
3
|
+
environment: process.env['NODE_ENV'],
|
|
4
|
+
host: process.env['HOST'],
|
|
5
|
+
port: parseInt(process.env['PORT'] ?? '3000', 10),
|
|
6
|
+
apiUrl: process.env['API_URL'],
|
|
7
|
+
api: {
|
|
8
|
+
cookie: {
|
|
9
|
+
name: process.env['API_COOKIE_NAME'],
|
|
10
|
+
options: {
|
|
11
|
+
domain: process.env['API_COOKIE_DOMAIN'],
|
|
12
|
+
httpOnly: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
cors: {
|
|
16
|
+
origin: [process.env['WEB_URL']],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
siteUrl: process.env['SITE_URL'] || process.env['API_URL']?.replace('/api', ''),
|
|
20
|
+
app: {
|
|
21
|
+
email: process.env['APP_EMAIL'],
|
|
22
|
+
supportEmail: process.env['APP_SUPPORT_EMAIL'],
|
|
23
|
+
adminEmails: process.env['APP_ADMIN_EMAILS'],
|
|
24
|
+
name: process.env['APP_NAME'],
|
|
25
|
+
},
|
|
26
|
+
smtp: {
|
|
27
|
+
host: process.env['SMTP_HOST'],
|
|
28
|
+
port: process.env['SMTP_PORT'],
|
|
29
|
+
user: process.env['SMTP_USER'],
|
|
30
|
+
pass: process.env['SMTP_PASS'],
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Joi from 'joi'
|
|
2
|
+
|
|
3
|
+
export const validationSchema = Joi.object({
|
|
4
|
+
NODE_ENV: Joi.string().valid('development', 'production', 'test'),
|
|
5
|
+
HOST: Joi.string().alphanum().default('localhost'),
|
|
6
|
+
PORT: Joi.number().default(3000),
|
|
7
|
+
WEB_PORT: Joi.number().default(4200),
|
|
8
|
+
WEB_URL: Joi.string().default(`http://${process.env['HOST'] || 'localhost'}:${process.env['WEB_PORT']}`),
|
|
9
|
+
API_COOKIE_DOMAIN: Joi.string().default('localhost'),
|
|
10
|
+
API_COOKIE_NAME: Joi.string().default('__session'),
|
|
11
|
+
API_URL: Joi.string().default(`http://${process.env['HOST'] || 'localhost'}:${process.env['PORT']}/api`),
|
|
12
|
+
APP_NAME: Joi.string().required(),
|
|
13
|
+
APP_EMAIL: Joi.string().email().required(),
|
|
14
|
+
APP_SUPPORT_EMAIL: Joi.string().email().required(),
|
|
15
|
+
APP_ADMIN_EMAILS: Joi.string().required(),
|
|
16
|
+
SITE_URL: Joi.string().uri().required(),
|
|
17
|
+
SMTP_HOST: Joi.string().required(),
|
|
18
|
+
SMTP_PORT: Joi.string().required(),
|
|
19
|
+
SMTP_USER: Joi.string().required(),
|
|
20
|
+
SMTP_PASS: Joi.string().required(),
|
|
21
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'
|
|
2
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { apiLibraryGenerator } from '@nestledjs/utils'
|
|
5
|
+
|
|
6
|
+
import generator from './generator'
|
|
7
|
+
|
|
8
|
+
vi.mock('@nestledjs/utils', async () => {
|
|
9
|
+
const actual = await vi.importActual('@nestledjs/utils')
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
apiLibraryGenerator: vi.fn(),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
vi.mock('@nx/devkit', async () => {
|
|
17
|
+
const actual = await vi.importActual('@nx/devkit')
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
formatFiles: vi.fn(),
|
|
21
|
+
installPackagesTask: vi.fn(),
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('config generator', () => {
|
|
26
|
+
let tree: Tree
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
tree = createTreeWithEmptyWorkspace()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should run successfully', async () => {
|
|
33
|
+
const callback = await generator(tree)
|
|
34
|
+
callback()
|
|
35
|
+
|
|
36
|
+
expect(apiLibraryGenerator).toHaveBeenCalledWith(tree, { name: 'config', overwrite: false }, expect.any(String))
|
|
37
|
+
expect(formatFiles).toHaveBeenCalledWith(tree)
|
|
38
|
+
expect(installPackagesTask).toHaveBeenCalledWith(tree)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should pass overwrite flag to apiLibraryGenerator when overwrite is true', async () => {
|
|
42
|
+
const callback = await generator(tree, { overwrite: true })
|
|
43
|
+
callback()
|
|
44
|
+
|
|
45
|
+
expect(apiLibraryGenerator).toHaveBeenCalledWith(tree, { name: 'config', overwrite: true }, expect.any(String))
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { formatFiles, installPackagesTask, joinPathFragments, Tree } from '@nx/devkit'
|
|
2
|
+
import { apiLibraryGenerator } from '@nestledjs/utils'
|
|
3
|
+
import { ApiConfigGeneratorSchema } from './schema'
|
|
4
|
+
|
|
5
|
+
export default async function generateLibraries(tree: Tree, options: ApiConfigGeneratorSchema = {}) {
|
|
6
|
+
const templateRootPath = joinPathFragments(__dirname, './files')
|
|
7
|
+
const overwrite = options.overwrite === true
|
|
8
|
+
|
|
9
|
+
await apiLibraryGenerator(tree, { name: 'config', overwrite }, templateRootPath)
|
|
10
|
+
|
|
11
|
+
await formatFiles(tree)
|
|
12
|
+
|
|
13
|
+
return () => {
|
|
14
|
+
installPackagesTask(tree)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "ApiConfig",
|
|
4
|
+
"title": "Create the API Config library",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"overwrite": {
|
|
8
|
+
"type": "boolean",
|
|
9
|
+
"description": "Whether to overwrite the existing config library if it exists.",
|
|
10
|
+
"default": false
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|