@idevconn/create-icore 0.4.1 → 0.5.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.
Files changed (76) hide show
  1. package/dist/cli.js +304 -24
  2. package/dist/index.cjs +301 -23
  3. package/dist/index.d.cts +7 -1
  4. package/dist/index.d.ts +7 -1
  5. package/dist/index.js +300 -23
  6. package/package.json +1 -1
  7. package/templates/apps/api/.env.example +14 -0
  8. package/templates/apps/api/src/app/app.module.ts +5 -1
  9. package/templates/apps/api/src/main.ts +12 -6
  10. package/templates/apps/microservices/auth/package.json +1 -1
  11. package/templates/apps/microservices/auth/project.json +2 -1
  12. package/templates/apps/microservices/auth/src/app/app.module.ts +50 -39
  13. package/templates/apps/microservices/auth/src/main.ts +6 -23
  14. package/templates/apps/microservices/jobs/project.json +2 -1
  15. package/templates/apps/microservices/jobs/src/app/redis-connection.ts +35 -0
  16. package/templates/apps/microservices/jobs/src/app/workers/cleanup.worker.ts +2 -1
  17. package/templates/apps/microservices/jobs/src/app/workers/email.worker.ts +2 -1
  18. package/templates/apps/microservices/jobs/src/app/workers/image-process.worker.ts +2 -1
  19. package/templates/apps/microservices/notes/project.json +2 -1
  20. package/templates/apps/microservices/notes/src/app/app.module.ts +52 -38
  21. package/templates/apps/microservices/notes/src/main.ts +6 -23
  22. package/templates/apps/microservices/payment/project.json +2 -1
  23. package/templates/apps/microservices/payment/src/app/app.module.ts +37 -12
  24. package/templates/apps/microservices/payment/src/main.ts +6 -23
  25. package/templates/apps/microservices/upload/package.json +1 -1
  26. package/templates/apps/microservices/upload/project.json +2 -1
  27. package/templates/apps/microservices/upload/src/app/app.module.ts +50 -42
  28. package/templates/apps/microservices/upload/src/main.ts +6 -23
  29. package/templates/apps/templates/client-antd/.env.example +7 -0
  30. package/templates/apps/templates/client-antd/vite.config.mts +4 -4
  31. package/templates/apps/templates/client-mui/.env.example +7 -0
  32. package/templates/apps/templates/client-mui/vite.config.mts +4 -4
  33. package/templates/apps/templates/client-shadcn/.env.example +6 -1
  34. package/templates/apps/templates/client-shadcn/vite.config.mts +4 -4
  35. package/templates/libs/auth-client/src/index.ts +1 -0
  36. package/templates/libs/auth-client/src/lib/auth-client.module.ts +1 -1
  37. package/templates/libs/auth-client/src/lib/auth-client.service.ts +1 -1
  38. package/templates/libs/auth-client/src/lib/auth-client.tokens.ts +4 -0
  39. package/templates/libs/firebase-admin/README.md +11 -0
  40. package/templates/libs/firebase-admin/eslint.config.mjs +24 -0
  41. package/templates/libs/firebase-admin/package.json +12 -0
  42. package/templates/libs/firebase-admin/project.json +19 -0
  43. package/templates/libs/firebase-admin/src/index.ts +1 -0
  44. package/templates/libs/firebase-admin/src/lib/__tests__/firebase-admin.unit.test.ts +105 -0
  45. package/templates/libs/firebase-admin/src/lib/firebase-admin.ts +70 -0
  46. package/templates/libs/firebase-admin/tsconfig.json +23 -0
  47. package/templates/libs/firebase-admin/tsconfig.lib.json +23 -0
  48. package/templates/libs/firebase-admin/tsconfig.spec.json +22 -0
  49. package/templates/libs/firebase-admin/vitest.config.mts +21 -0
  50. package/templates/libs/jobs-client/src/index.ts +1 -0
  51. package/templates/libs/jobs-client/src/lib/jobs-client.module.ts +1 -1
  52. package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +15 -3
  53. package/templates/libs/jobs-client/src/lib/jobs-client.tokens.ts +4 -0
  54. package/templates/libs/notes-client/src/index.ts +1 -0
  55. package/templates/libs/notes-client/src/lib/notes-client.module.ts +1 -1
  56. package/templates/libs/notes-client/src/lib/notes-client.service.ts +1 -1
  57. package/templates/libs/notes-client/src/lib/notes-client.tokens.ts +4 -0
  58. package/templates/libs/payment-client/src/index.ts +1 -0
  59. package/templates/libs/payment-client/src/lib/payment-client.module.ts +1 -1
  60. package/templates/libs/payment-client/src/lib/payment-client.service.ts +1 -1
  61. package/templates/libs/payment-client/src/lib/payment-client.tokens.ts +4 -0
  62. package/templates/libs/shared/src/__tests__/bootstrap.unit.test.ts +92 -0
  63. package/templates/libs/shared/src/__tests__/transport.unit.test.ts +14 -2
  64. package/templates/libs/shared/src/bootstrap.ts +79 -0
  65. package/templates/libs/shared/src/env.ts +88 -0
  66. package/templates/libs/shared/src/index.ts +2 -0
  67. package/templates/libs/shared/src/transport.ts +62 -3
  68. package/templates/libs/upload-client/src/index.ts +1 -0
  69. package/templates/libs/upload-client/src/lib/upload-client.module.ts +1 -1
  70. package/templates/libs/upload-client/src/lib/upload-client.service.ts +1 -1
  71. package/templates/libs/upload-client/src/lib/upload-client.tokens.ts +4 -0
  72. package/templates/libs/vite-plugins/src/index.d.mts +6 -0
  73. package/templates/libs/vite-plugins/src/index.mjs +50 -0
  74. package/templates/package.json +1 -0
  75. package/templates/tools/create-icore/_template-shell/package.json +1 -0
  76. package/templates/tsconfig.base.json +2 -1
@@ -1,36 +1,41 @@
1
1
  import { join } from 'node:path';
2
- import { Module } from '@nestjs/common';
2
+ import { Module, Logger } from '@nestjs/common';
3
3
  import { ConfigModule, ConfigService } from '@nestjs/config';
4
4
  import { createClient } from '@supabase/supabase-js';
5
- import * as admin from 'firebase-admin';
6
5
  import { SupabaseAuthStrategy } from '@icore/auth-supabase';
7
6
  import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
8
- import { FakeAuthStrategy } from '@icore/shared';
7
+ import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
8
+ import { FakeAuthStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
9
9
  import type { AuthStrategy } from '@icore/shared';
10
- import { Logger } from '@nestjs/common';
11
10
  import { AuthController } from './auth.controller';
12
11
 
12
+ const ENV_PATH = 'apps/microservices/auth/.env';
13
+
14
+ // Env vars each provider needs (besides AUTH_PROVIDER itself).
15
+ const REQUIRED_ENV: Record<string, string[]> = {
16
+ supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
17
+ firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_WEB_API_KEY'],
18
+ };
19
+
13
20
  function requireEnv(cfg: ConfigService, key: string): string {
14
- const val = cfg.getOrThrow<string>(key);
15
- if (!val) throw new Error(`${key} is not set — check apps/microservices/auth/.env`);
16
- return val;
21
+ return cfg.getOrThrow<string>(key);
17
22
  }
18
23
 
19
- function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
20
- const projectId = requireEnv(cfg, 'FB_ADMIN_PROJECT_ID');
21
- if (admin.apps.length === 0) {
22
- admin.initializeApp({
23
- credential: admin.credential.cert({
24
- projectId,
25
- clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
26
- privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
27
- }),
28
- });
29
- }
24
+ function makeSupabaseAuth(cfg: ConfigService): AuthStrategy {
25
+ const client = createClient(
26
+ requireEnv(cfg, 'SUPABASE_URL'),
27
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
28
+ { auth: { autoRefreshToken: false, persistSession: false } },
29
+ );
30
+ return new SupabaseAuthStrategy({ client });
31
+ }
32
+
33
+ function makeFirebaseAuth(cfg: ConfigService): AuthStrategy {
34
+ const app = getFirebaseAdmin(cfg);
30
35
  const identityToolkit = new HttpIdentityToolkitClient(requireEnv(cfg, 'FIREBASE_WEB_API_KEY'));
31
36
  return new FirebaseAuthStrategy({
32
37
  identityToolkit,
33
- adminAuth: admin.auth(),
38
+ adminAuth: app.auth(),
34
39
  });
35
40
  }
36
41
 
@@ -49,28 +54,34 @@ function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
49
54
  {
50
55
  provide: 'AuthStrategy',
51
56
  useFactory: (cfg: ConfigService): AuthStrategy => {
57
+ const logger = new Logger('AuthStrategy');
58
+ const provider = cfg.get<string>('AUTH_PROVIDER')?.trim();
59
+ const keys = provider ? REQUIRED_ENV[provider] : undefined;
60
+ const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
61
+
62
+ // Prod: fail fast — never silently run a fake auth strategy.
63
+ // Dev: warn with a boxed banner + fall back to the in-memory fake.
64
+ const fallback = (reason?: string): AuthStrategy => {
65
+ const banner = formatEnvBanner({
66
+ service: 'auth MS',
67
+ provider,
68
+ missing,
69
+ envPath: ENV_PATH,
70
+ reason,
71
+ });
72
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
73
+ logger.warn(banner);
74
+ return new FakeAuthStrategy();
75
+ };
76
+
77
+ if (!keys || missing.length > 0) return fallback();
78
+
52
79
  try {
53
- const provider = requireEnv(cfg, 'AUTH_PROVIDER');
54
- switch (provider) {
55
- case 'supabase': {
56
- const client = createClient(
57
- requireEnv(cfg, 'SUPABASE_URL'),
58
- requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
59
- { auth: { autoRefreshToken: false, persistSession: false } },
60
- );
61
- return new SupabaseAuthStrategy({ client });
62
- }
63
- case 'firebase':
64
- return makeFirebaseStrategy(cfg);
65
- default:
66
- throw new Error(`Unsupported AUTH_PROVIDER: ${provider}`);
67
- }
80
+ if (provider === 'supabase') return makeSupabaseAuth(cfg);
81
+ return makeFirebaseAuth(cfg);
68
82
  } catch (err) {
69
- new Logger('AuthStrategy').warn(
70
- `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
71
- `Requests will fail until apps/microservices/auth/.env is set.`,
72
- );
73
- return new FakeAuthStrategy();
83
+ // Vars present but invalid (e.g. placeholder URL the SDK rejects).
84
+ return fallback(err instanceof Error ? err.message : String(err));
74
85
  }
75
86
  },
76
87
  inject: [ConfigService],
@@ -1,28 +1,11 @@
1
1
  import { Logger } from '@nestjs/common';
2
2
  import { NestFactory } from '@nestjs/core';
3
3
  import { MicroserviceOptions } from '@nestjs/microservices';
4
- import { buildTransportMS } from '@icore/shared';
4
+ import { bootstrapMicroservice, buildTransportMS } from '@icore/shared';
5
5
  import { AppModule } from './app/app.module';
6
6
 
7
- async function bootstrap() {
8
- const app = await NestFactory.createMicroservice<MicroserviceOptions>(
9
- AppModule,
10
- buildTransportMS('AUTH'),
11
- );
12
- await app.listen();
13
- }
14
-
15
- bootstrap()
16
- .then(() => {
17
- const logger = new Logger('Auth-Bootstrap');
18
- logger.log(
19
- `Auth MS Bootstrap completed: transport=${process.env.AUTH_TRANSPORT ?? 'tcp'} host=${process.env.AUTH_HOST ?? '127.0.0.1'} port=${process.env.AUTH_PORT ?? '4001'}`,
20
- );
21
- })
22
- .catch((err) => {
23
- new Logger('Auth-Bootstrap').error(
24
- 'Auth MS bootstrap failed',
25
- err instanceof Error ? err.stack : err,
26
- );
27
- process.exit(1);
28
- });
7
+ void bootstrapMicroservice(
8
+ 'AUTH',
9
+ () => NestFactory.createMicroservice<MicroserviceOptions>(AppModule, buildTransportMS('AUTH')),
10
+ new Logger('Auth-Bootstrap'),
11
+ );
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "jobs:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9234
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -0,0 +1,35 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import IORedis from 'ioredis';
3
+ import { formatEnvBanner } from '@icore/shared';
4
+
5
+ /**
6
+ * Creates an IORedis connection that NEVER crashes the process when Redis is
7
+ * unreachable. Without an 'error' handler, ioredis emits an unhandled 'error'
8
+ * event → Node exits. Here we log one boxed banner and let ioredis keep
9
+ * retrying in the background, so the jobs MS stays up and connects once Redis
10
+ * is available (honours the "never crash on missing infra" rule).
11
+ */
12
+ export function createJobsRedis(url: string, logger: Logger): IORedis {
13
+ const connection = new IORedis(url, {
14
+ maxRetriesPerRequest: null,
15
+ retryStrategy: (times) => Math.min(times * 200, 5000),
16
+ });
17
+
18
+ let warned = false;
19
+ connection.on('error', (err: Error) => {
20
+ if (warned) return;
21
+ warned = true;
22
+ logger.warn(
23
+ formatEnvBanner({
24
+ service: 'jobs MS',
25
+ provider: 'redis',
26
+ missing: [],
27
+ envPath: 'apps/microservices/jobs/.env (JOBS_REDIS_URL)',
28
+ reason: `${err.message} — retrying in the background until Redis is up at ${url}`,
29
+ headline: `⚠ jobs MS — Redis unreachable (workers idle until it's up)`,
30
+ }),
31
+ );
32
+ });
33
+
34
+ return connection;
35
+ }
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@ne
2
2
  import { ConfigService } from '@nestjs/config';
3
3
  import { Worker, type Job } from 'bullmq';
4
4
  import IORedis from 'ioredis';
5
+ import { createJobsRedis } from '../redis-connection';
5
6
  import type { CleanupJob } from '@icore/shared';
6
7
 
7
8
  @Injectable()
@@ -15,7 +16,7 @@ export class CleanupWorker implements OnModuleInit, OnModuleDestroy {
15
16
  onModuleInit(): void {
16
17
  const url = this.cfg.get<string>('JOBS_REDIS_URL') ?? 'redis://localhost:6379';
17
18
  const concurrency = Number(this.cfg.get<string>('JOBS_WORKER_CONCURRENCY') ?? '5');
18
- this.connection = new IORedis(url, { maxRetriesPerRequest: null });
19
+ this.connection = createJobsRedis(url, this.logger);
19
20
  this.worker = new Worker<CleanupJob>(
20
21
  'cleanup',
21
22
  async (job: Job<CleanupJob>) => {
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@ne
2
2
  import { ConfigService } from '@nestjs/config';
3
3
  import { Worker, type Job } from 'bullmq';
4
4
  import IORedis from 'ioredis';
5
+ import { createJobsRedis } from '../redis-connection';
5
6
  import type { EmailJob } from '@icore/shared';
6
7
 
7
8
  @Injectable()
@@ -15,7 +16,7 @@ export class EmailWorker implements OnModuleInit, OnModuleDestroy {
15
16
  onModuleInit(): void {
16
17
  const url = this.cfg.get<string>('JOBS_REDIS_URL') ?? 'redis://localhost:6379';
17
18
  const concurrency = Number(this.cfg.get<string>('JOBS_WORKER_CONCURRENCY') ?? '5');
18
- this.connection = new IORedis(url, { maxRetriesPerRequest: null });
19
+ this.connection = createJobsRedis(url, this.logger);
19
20
  this.worker = new Worker<EmailJob>(
20
21
  'email',
21
22
  async (job: Job<EmailJob>) => {
@@ -2,6 +2,7 @@ import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from '@ne
2
2
  import { ConfigService } from '@nestjs/config';
3
3
  import { Worker, type Job } from 'bullmq';
4
4
  import IORedis from 'ioredis';
5
+ import { createJobsRedis } from '../redis-connection';
5
6
  import type { ImageProcessJob } from '@icore/shared';
6
7
 
7
8
  @Injectable()
@@ -15,7 +16,7 @@ export class ImageProcessWorker implements OnModuleInit, OnModuleDestroy {
15
16
  onModuleInit(): void {
16
17
  const url = this.cfg.get<string>('JOBS_REDIS_URL') ?? 'redis://localhost:6379';
17
18
  const concurrency = Number(this.cfg.get<string>('JOBS_WORKER_CONCURRENCY') ?? '5');
18
- this.connection = new IORedis(url, { maxRetriesPerRequest: null });
19
+ this.connection = createJobsRedis(url, this.logger);
19
20
  this.worker = new Worker<ImageProcessJob>(
20
21
  'image-process',
21
22
  async (job: Job<ImageProcessJob>) => {
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "notes:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9232
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -1,19 +1,41 @@
1
1
  import { join } from 'node:path';
2
- import { Module } from '@nestjs/common';
2
+ import { Module, Logger } from '@nestjs/common';
3
3
  import { ConfigModule, ConfigService } from '@nestjs/config';
4
4
  import { createClient } from '@supabase/supabase-js';
5
- import * as admin from 'firebase-admin';
6
5
  import { SupabaseDBStrategy } from '@icore/db-supabase';
7
6
  import { FirestoreDBStrategy } from '@icore/db-firestore';
8
- import { FakeDBStrategy } from '@icore/shared';
7
+ import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
8
+ import { FakeDBStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
9
9
  import type { DBStrategy } from '@icore/shared';
10
- import { Logger } from '@nestjs/common';
11
10
  import { NotesController } from './notes.controller';
12
11
 
12
+ const ENV_PATH = 'apps/microservices/notes/.env';
13
+
14
+ // DB_PROVIDER accepts supabase | firestore | firebase (latter two are Firestore).
15
+ const REQUIRED_ENV: Record<string, string[]> = {
16
+ supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
17
+ firestore: [...FIREBASE_ADMIN_REQUIRED_ENV],
18
+ firebase: [...FIREBASE_ADMIN_REQUIRED_ENV],
19
+ };
20
+
13
21
  function requireEnv(cfg: ConfigService, key: string): string {
14
- const val = cfg.getOrThrow<string>(key);
15
- if (!val) throw new Error(`${key} is not set — check apps/microservices/notes/.env`);
16
- return val;
22
+ return cfg.getOrThrow<string>(key);
23
+ }
24
+
25
+ function makeSupabaseDB(cfg: ConfigService): DBStrategy {
26
+ const client = createClient(
27
+ requireEnv(cfg, 'SUPABASE_URL'),
28
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
29
+ { auth: { autoRefreshToken: false, persistSession: false } },
30
+ );
31
+ return new SupabaseDBStrategy({ client });
32
+ }
33
+
34
+ function makeFirestoreDB(cfg: ConfigService): DBStrategy {
35
+ const app = getFirebaseAdmin(cfg);
36
+ return new FirestoreDBStrategy({
37
+ db: app.firestore() as unknown as ConstructorParameters<typeof FirestoreDBStrategy>[0]['db'],
38
+ });
17
39
  }
18
40
 
19
41
  @Module({
@@ -31,39 +53,31 @@ function requireEnv(cfg: ConfigService, key: string): string {
31
53
  {
32
54
  provide: 'DBStrategy',
33
55
  useFactory: (cfg: ConfigService): DBStrategy => {
56
+ const logger = new Logger('DBStrategy');
57
+ const provider = cfg.get<string>('DB_PROVIDER')?.trim();
58
+ const keys = provider ? REQUIRED_ENV[provider] : undefined;
59
+ const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
60
+
61
+ const fallback = (reason?: string): DBStrategy => {
62
+ const banner = formatEnvBanner({
63
+ service: 'notes MS',
64
+ provider,
65
+ missing,
66
+ envPath: ENV_PATH,
67
+ reason,
68
+ });
69
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
70
+ logger.warn(banner);
71
+ return new FakeDBStrategy();
72
+ };
73
+
74
+ if (!keys || missing.length > 0) return fallback();
75
+
34
76
  try {
35
- const provider = requireEnv(cfg, 'DB_PROVIDER');
36
- if (provider === 'supabase') {
37
- const client = createClient(
38
- requireEnv(cfg, 'SUPABASE_URL'),
39
- requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
40
- { auth: { autoRefreshToken: false, persistSession: false } },
41
- );
42
- return new SupabaseDBStrategy({ client });
43
- }
44
- if (provider === 'firestore' || provider === 'firebase') {
45
- if (admin.apps.length === 0) {
46
- admin.initializeApp({
47
- credential: admin.credential.cert({
48
- projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
49
- clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
50
- privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
51
- }),
52
- });
53
- }
54
- return new FirestoreDBStrategy({
55
- db: admin.firestore() as unknown as ConstructorParameters<
56
- typeof FirestoreDBStrategy
57
- >[0]['db'],
58
- });
59
- }
60
- throw new Error(`Unsupported DB_PROVIDER: ${provider}`);
77
+ if (provider === 'supabase') return makeSupabaseDB(cfg);
78
+ return makeFirestoreDB(cfg);
61
79
  } catch (err) {
62
- new Logger('DBStrategy').warn(
63
- `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
64
- `Requests will fail until apps/microservices/notes/.env is set.`,
65
- );
66
- return new FakeDBStrategy();
80
+ return fallback(err instanceof Error ? err.message : String(err));
67
81
  }
68
82
  },
69
83
  inject: [ConfigService],
@@ -1,28 +1,11 @@
1
1
  import { Logger } from '@nestjs/common';
2
2
  import { NestFactory } from '@nestjs/core';
3
3
  import { MicroserviceOptions } from '@nestjs/microservices';
4
- import { buildTransportMS } from '@icore/shared';
4
+ import { bootstrapMicroservice, buildTransportMS } from '@icore/shared';
5
5
  import { AppModule } from './app/app.module';
6
6
 
7
- async function bootstrap() {
8
- const app = await NestFactory.createMicroservice<MicroserviceOptions>(
9
- AppModule,
10
- buildTransportMS('NOTES'),
11
- );
12
- await app.listen();
13
- }
14
-
15
- bootstrap()
16
- .then(() => {
17
- const logger = new Logger('Notes-Bootstrap');
18
- logger.log(
19
- `Notes MS Bootstrap completed: transport=${process.env.NOTES_TRANSPORT ?? 'tcp'} host=${process.env.NOTES_HOST ?? '127.0.0.1'} port=${process.env.NOTES_PORT ?? '4004'}`,
20
- );
21
- })
22
- .catch((err) => {
23
- new Logger('Notes-Bootstrap').error(
24
- 'Notes MS bootstrap failed',
25
- err instanceof Error ? err.stack : err,
26
- );
27
- process.exit(1);
28
- });
7
+ void bootstrapMicroservice(
8
+ 'NOTES',
9
+ () => NestFactory.createMicroservice<MicroserviceOptions>(AppModule, buildTransportMS('NOTES')),
10
+ new Logger('Notes-Bootstrap'),
11
+ );
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "payment:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9233
54
55
  },
55
56
  "configurations": {
56
57
  "development": {
@@ -1,9 +1,16 @@
1
1
  import { join } from 'node:path';
2
- import { Module } from '@nestjs/common';
2
+ import { Module, Logger } from '@nestjs/common';
3
3
  import { ConfigModule, ConfigService } from '@nestjs/config';
4
4
  import { PaymentRegistry, PaypalStrategy, createPayment } from '@idevconn/payment';
5
+ import { missingEnv, formatEnvBanner } from '@icore/shared';
5
6
  import { PaymentController } from './payment.controller';
6
7
 
8
+ const ENV_PATH = 'apps/microservices/payment/.env';
9
+
10
+ const REQUIRED_ENV: Record<string, string[]> = {
11
+ paypal: ['PAYPAL_CLIENT_ID', 'PAYPAL_CLIENT_SECRET'],
12
+ };
13
+
7
14
  @Module({
8
15
  imports: [
9
16
  ConfigModule.forRoot({
@@ -19,19 +26,37 @@ import { PaymentController } from './payment.controller';
19
26
  {
20
27
  provide: PaymentRegistry,
21
28
  useFactory: (cfg: ConfigService) => {
22
- const provider = cfg.get<string>('PAYMENT_PROVIDER') ?? 'paypal';
23
- if (provider === 'paypal') {
24
- return createPayment({
25
- strategies: {
26
- paypal: new PaypalStrategy({
27
- clientId: cfg.getOrThrow<string>('PAYPAL_CLIENT_ID'),
28
- secret: cfg.getOrThrow<string>('PAYPAL_CLIENT_SECRET'),
29
- environment: cfg.get<'sandbox' | 'live'>('PAYPAL_ENVIRONMENT') ?? 'sandbox',
30
- }),
31
- },
29
+ const logger = new Logger('PaymentRegistry');
30
+ const provider = (cfg.get<string>('PAYMENT_PROVIDER') ?? 'paypal').trim();
31
+ const keys = REQUIRED_ENV[provider];
32
+ if (!keys) throw new Error(`Unsupported PAYMENT_PROVIDER: ${provider}`);
33
+
34
+ const missing = missingEnv((k) => cfg.get<string>(k), keys);
35
+ if (missing.length > 0) {
36
+ const banner = formatEnvBanner({
37
+ service: 'payment MS',
38
+ provider,
39
+ missing,
40
+ envPath: ENV_PATH,
41
+ headline: `⚠ payment MS — ${provider} credentials missing (payments will fail)`,
32
42
  });
43
+ // Prod: fail fast. Dev: warn + register an EMPTY strategy map so the
44
+ // MS boots (PaypalStrategy's constructor throws on blank creds, so we
45
+ // must not instantiate it) — payment endpoints fail until creds are set.
46
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
47
+ logger.warn(banner);
48
+ return createPayment({ strategies: {} });
33
49
  }
34
- throw new Error(`Unsupported PAYMENT_PROVIDER: ${provider}`);
50
+
51
+ return createPayment({
52
+ strategies: {
53
+ paypal: new PaypalStrategy({
54
+ clientId: cfg.getOrThrow<string>('PAYPAL_CLIENT_ID'),
55
+ secret: cfg.getOrThrow<string>('PAYPAL_CLIENT_SECRET'),
56
+ environment: cfg.get<'sandbox' | 'live'>('PAYPAL_ENVIRONMENT') ?? 'sandbox',
57
+ }),
58
+ },
59
+ });
35
60
  },
36
61
  inject: [ConfigService],
37
62
  },
@@ -1,28 +1,11 @@
1
1
  import { Logger } from '@nestjs/common';
2
2
  import { NestFactory } from '@nestjs/core';
3
3
  import { MicroserviceOptions } from '@nestjs/microservices';
4
- import { buildTransportMS } from '@icore/shared';
4
+ import { bootstrapMicroservice, buildTransportMS } from '@icore/shared';
5
5
  import { AppModule } from './app/app.module';
6
6
 
7
- async function bootstrap() {
8
- const app = await NestFactory.createMicroservice<MicroserviceOptions>(
9
- AppModule,
10
- buildTransportMS('PAYMENT'),
11
- );
12
- await app.listen();
13
- }
14
-
15
- bootstrap()
16
- .then(() => {
17
- const logger = new Logger('Payment-Bootstrap');
18
- logger.log(
19
- `Payment MS Bootstrap completed: transport=${process.env.PAYMENT_TRANSPORT ?? 'tcp'} host=${process.env.PAYMENT_HOST ?? '127.0.0.1'} port=${process.env.PAYMENT_PORT ?? '4003'}`,
20
- );
21
- })
22
- .catch((err) => {
23
- new Logger('Payment-Bootstrap').error(
24
- 'Payment MS bootstrap failed',
25
- err instanceof Error ? err.stack : err,
26
- );
27
- process.exit(1);
28
- });
7
+ void bootstrapMicroservice(
8
+ 'PAYMENT',
9
+ () => NestFactory.createMicroservice<MicroserviceOptions>(AppModule, buildTransportMS('PAYMENT')),
10
+ new Logger('Payment-Bootstrap'),
11
+ );
@@ -3,12 +3,12 @@
3
3
  "version": "0.0.1",
4
4
  "private": true,
5
5
  "dependencies": {
6
+ "@icore/firebase-admin": "*",
6
7
  "@icore/shared": "*",
7
8
  "@icore/storage-cloudinary": "*",
8
9
  "@icore/storage-firebase": "*",
9
10
  "@icore/storage-supabase": "*",
10
11
  "cloudinary": "^2.0.0",
11
- "firebase-admin": "^13.0.0",
12
12
  "@nestjs/common": "^11.1.24",
13
13
  "@nestjs/config": "^4.0.4",
14
14
  "@nestjs/core": "^11.1.24",
@@ -50,7 +50,8 @@
50
50
  "dependsOn": ["build"],
51
51
  "options": {
52
52
  "buildTarget": "upload:build",
53
- "runBuildTargetDependencies": false
53
+ "runBuildTargetDependencies": false,
54
+ "port": 9231
54
55
  },
55
56
  "configurations": {
56
57
  "development": {