@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,45 @@
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 { v2 as cloudinary } from 'cloudinary';
7
6
  import { SupabaseStorageStrategy } from '@icore/storage-supabase';
8
7
  import { FirebaseStorageStrategy } from '@icore/storage-firebase';
9
8
  import { CloudinaryStorageStrategy, type CloudinaryApiLike } from '@icore/storage-cloudinary';
10
- import { FakeStorageStrategy } from '@icore/shared';
9
+ import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
10
+ import { FakeStorageStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
11
11
  import type { StorageStrategy } from '@icore/shared';
12
- import { Logger } from '@nestjs/common';
13
12
  import { StorageController } from './storage.controller';
14
13
 
14
+ const ENV_PATH = 'apps/microservices/upload/.env';
15
+
16
+ const REQUIRED_ENV: Record<string, string[]> = {
17
+ supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_STORAGE_BUCKET'],
18
+ firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_STORAGE_BUCKET'],
19
+ cloudinary: ['CLOUDINARY_CLOUD_NAME', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET'],
20
+ };
21
+
15
22
  function requireEnv(cfg: ConfigService, key: string): string {
16
- const val = cfg.getOrThrow<string>(key);
17
- if (!val) throw new Error(`${key} is not set — check apps/microservices/upload/.env`);
18
- return val;
23
+ return cfg.getOrThrow<string>(key);
24
+ }
25
+
26
+ function makeSupabaseStorage(cfg: ConfigService): StorageStrategy {
27
+ const client = createClient(
28
+ requireEnv(cfg, 'SUPABASE_URL'),
29
+ requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
30
+ { auth: { autoRefreshToken: false, persistSession: false } },
31
+ );
32
+ return new SupabaseStorageStrategy({
33
+ client,
34
+ bucket: requireEnv(cfg, 'SUPABASE_STORAGE_BUCKET'),
35
+ });
19
36
  }
20
37
 
21
38
  function makeFirebaseStorage(cfg: ConfigService): StorageStrategy {
22
39
  const bucketName = requireEnv(cfg, 'FIREBASE_STORAGE_BUCKET');
23
- if (admin.apps.length === 0) {
24
- admin.initializeApp({
25
- credential: admin.credential.cert({
26
- projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
27
- clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
28
- privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
29
- }),
30
- });
31
- }
40
+ const app = getFirebaseAdmin(cfg);
32
41
  return new FirebaseStorageStrategy({
33
- bucket: admin
42
+ bucket: app
34
43
  .storage()
35
44
  .bucket(bucketName) as unknown as import('@icore/storage-firebase').FirebaseStorageBucketLike,
36
45
  });
@@ -97,33 +106,32 @@ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
97
106
  {
98
107
  provide: 'StorageStrategy',
99
108
  useFactory: (cfg: ConfigService): StorageStrategy => {
109
+ const logger = new Logger('StorageStrategy');
110
+ const provider = cfg.get<string>('STORAGE_PROVIDER')?.trim();
111
+ const keys = provider ? REQUIRED_ENV[provider] : undefined;
112
+ const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
113
+
114
+ const fallback = (reason?: string): StorageStrategy => {
115
+ const banner = formatEnvBanner({
116
+ service: 'upload MS',
117
+ provider,
118
+ missing,
119
+ envPath: ENV_PATH,
120
+ reason,
121
+ });
122
+ if (process.env.NODE_ENV === 'production') throw new Error(banner);
123
+ logger.warn(banner);
124
+ return new FakeStorageStrategy();
125
+ };
126
+
127
+ if (!keys || missing.length > 0) return fallback();
128
+
100
129
  try {
101
- const provider = requireEnv(cfg, 'STORAGE_PROVIDER');
102
- switch (provider) {
103
- case 'supabase': {
104
- const client = createClient(
105
- requireEnv(cfg, 'SUPABASE_URL'),
106
- requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
107
- { auth: { autoRefreshToken: false, persistSession: false } },
108
- );
109
- return new SupabaseStorageStrategy({
110
- client,
111
- bucket: requireEnv(cfg, 'SUPABASE_STORAGE_BUCKET'),
112
- });
113
- }
114
- case 'firebase':
115
- return makeFirebaseStorage(cfg);
116
- case 'cloudinary':
117
- return makeCloudinaryStorage(cfg);
118
- default:
119
- throw new Error(`Unsupported STORAGE_PROVIDER: ${provider}`);
120
- }
130
+ if (provider === 'supabase') return makeSupabaseStorage(cfg);
131
+ if (provider === 'firebase') return makeFirebaseStorage(cfg);
132
+ return makeCloudinaryStorage(cfg);
121
133
  } catch (err) {
122
- new Logger('StorageStrategy').warn(
123
- `Not configured: ${err instanceof Error ? err.message : String(err)}. ` +
124
- `Requests will fail until apps/microservices/upload/.env is set.`,
125
- );
126
- return new FakeStorageStrategy();
134
+ return fallback(err instanceof Error ? err.message : String(err));
127
135
  }
128
136
  },
129
137
  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('UPLOAD'),
11
- );
12
- await app.listen();
13
- }
14
-
15
- bootstrap()
16
- .then(() => {
17
- const logger = new Logger('Upload-Bootstrap');
18
- logger.log(
19
- `Upload MS Bootstrap completed: transport=${process.env.UPLOAD_TRANSPORT ?? 'tcp'} host=${process.env.UPLOAD_HOST ?? '127.0.0.1'} port=${process.env.UPLOAD_PORT ?? '4002'}`,
20
- );
21
- })
22
- .catch((err) => {
23
- new Logger('Upload-Bootstrap').error(
24
- 'Upload MS bootstrap failed',
25
- err instanceof Error ? err.stack : err,
26
- );
27
- process.exit(1);
28
- });
7
+ void bootstrapMicroservice(
8
+ 'UPLOAD',
9
+ () => NestFactory.createMicroservice<MicroserviceOptions>(AppModule, buildTransportMS('UPLOAD')),
10
+ new Logger('Upload-Bootstrap'),
11
+ );
@@ -0,0 +1,7 @@
1
+ # ─── Client environment variables ────────────────────────────────────────
2
+ # API gateway base URL.
3
+ # /api — default; dev-server proxies it to http://localhost:3001 (see vite.config.mts)
4
+ # http://localhost:3001/api — set explicitly if you want to bypass the proxy
5
+ # https://your-domain.com/api — production override when building a standalone SPA
6
+ VITE_API_URL=/api
7
+
@@ -6,8 +6,10 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite';
6
6
  import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
7
7
  import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
8
8
  import {
9
+ apiInfoPlugin,
9
10
  commonDefines,
10
11
  commonManualChunks,
12
+ commonServer,
11
13
  commonTestConfig,
12
14
  injectAppVersionPlugin,
13
15
  noServerModulesPlugin,
@@ -27,10 +29,7 @@ function depVersion(name: string): string {
27
29
  export default defineConfig(() => ({
28
30
  root: import.meta.dirname,
29
31
  cacheDir: '../../../node_modules/.vite/apps/templates/client-antd',
30
- server: {
31
- port: 4201,
32
- host: 'localhost',
33
- },
32
+ server: commonServer(4201),
34
33
  preview: {
35
34
  port: 4201,
36
35
  host: 'localhost',
@@ -49,6 +48,7 @@ export default defineConfig(() => ({
49
48
  nxViteTsPaths(),
50
49
  nxCopyAssetsPlugin(['*.md']),
51
50
  noServerModulesPlugin(),
51
+ apiInfoPlugin(),
52
52
  injectAppVersionPlugin(rootPackageJson),
53
53
  ],
54
54
  // Uncomment this if you are using workers.
@@ -0,0 +1,7 @@
1
+ # ─── Client environment variables ────────────────────────────────────────
2
+ # API gateway base URL.
3
+ # /api — default; dev-server proxies it to http://localhost:3001 (see vite.config.mts)
4
+ # http://localhost:3001/api — set explicitly if you want to bypass the proxy
5
+ # https://your-domain.com/api — production override when building a standalone SPA
6
+ VITE_API_URL=/api
7
+
@@ -6,8 +6,10 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite';
6
6
  import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
7
7
  import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
8
8
  import {
9
+ apiInfoPlugin,
9
10
  commonDefines,
10
11
  commonManualChunks,
12
+ commonServer,
11
13
  commonTestConfig,
12
14
  injectAppVersionPlugin,
13
15
  noServerModulesPlugin,
@@ -27,10 +29,7 @@ function depVersion(name: string): string {
27
29
  export default defineConfig(() => ({
28
30
  root: import.meta.dirname,
29
31
  cacheDir: '../../../node_modules/.vite/client-mui',
30
- server: {
31
- port: 4202,
32
- host: 'localhost',
33
- },
32
+ server: commonServer(4202),
34
33
  preview: {
35
34
  port: 4202,
36
35
  host: 'localhost',
@@ -49,6 +48,7 @@ export default defineConfig(() => ({
49
48
  nxViteTsPaths(),
50
49
  nxCopyAssetsPlugin(['*.md']),
51
50
  noServerModulesPlugin(),
51
+ apiInfoPlugin(),
52
52
  injectAppVersionPlugin(rootPackageJson),
53
53
  ],
54
54
  // Uncomment this if you are using workers.
@@ -1,2 +1,7 @@
1
- # API base URL dev server reverse-proxies /api to the gateway
1
+ # ─── Client environment variables ────────────────────────────────────────
2
+ # API gateway base URL.
3
+ # /api — default; dev-server proxies it to http://localhost:3001 (see vite.config.mts)
4
+ # http://localhost:3001/api — set explicitly if you want to bypass the proxy
5
+ # https://your-domain.com/api — production override when building a standalone SPA
2
6
  VITE_API_URL=/api
7
+
@@ -7,8 +7,10 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite';
7
7
  import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
8
8
  import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
9
9
  import {
10
+ apiInfoPlugin,
10
11
  commonDefines,
11
12
  commonManualChunks,
13
+ commonServer,
12
14
  commonTestConfig,
13
15
  injectAppVersionPlugin,
14
16
  noServerModulesPlugin,
@@ -28,10 +30,7 @@ function depVersion(name: string): string {
28
30
  export default defineConfig(() => ({
29
31
  root: import.meta.dirname,
30
32
  cacheDir: '../../../node_modules/.vite/apps/templates/client-shadcn',
31
- server: {
32
- port: 4200,
33
- host: 'localhost',
34
- },
33
+ server: commonServer(4200),
35
34
  preview: {
36
35
  port: 4200,
37
36
  host: 'localhost',
@@ -51,6 +50,7 @@ export default defineConfig(() => ({
51
50
  nxViteTsPaths(),
52
51
  nxCopyAssetsPlugin(['*.md']),
53
52
  noServerModulesPlugin(),
53
+ apiInfoPlugin(),
54
54
  injectAppVersionPlugin(rootPackageJson),
55
55
  ],
56
56
  // Uncomment this if you are using workers.
@@ -1,2 +1,3 @@
1
+ export * from './lib/auth-client.tokens';
1
2
  export * from './lib/auth-client.module';
2
3
  export * from './lib/auth-client.service';
@@ -3,7 +3,7 @@ import { ClientsModule } from '@nestjs/microservices';
3
3
  import { buildTransport } from '@icore/shared';
4
4
  import { AuthClientService } from './auth-client.service';
5
5
 
6
- export const AUTH_CLIENT = 'AUTH_CLIENT';
6
+ import { AUTH_CLIENT } from './auth-client.tokens';
7
7
 
8
8
  @Module({})
9
9
  export class AuthClientModule {
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
2
2
  import { ClientProxy } from '@nestjs/microservices';
3
3
  import { firstValueFrom } from 'rxjs';
4
4
  import type { AuthSession, OAuthProvider, OAuthStartResult, VerifiedToken } from '@icore/shared';
5
- import { AUTH_CLIENT } from './auth-client.module';
5
+ import { AUTH_CLIENT } from './auth-client.tokens';
6
6
 
7
7
  @Injectable()
8
8
  export class AuthClientService {
@@ -0,0 +1,4 @@
1
+ // Injection token — kept in its own file so module and service can both
2
+ // import it without creating a circular dependency (which breaks DI in
3
+ // webpack-bundled NestJS apps).
4
+ export const AUTH_CLIENT = 'AUTH_CLIENT';
@@ -0,0 +1,11 @@
1
+ # firebase-admin
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build firebase-admin` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test firebase-admin` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,24 @@
1
+ import baseConfig from '../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
+ '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
+ '{projectRoot}/src/**/*.{spec,test}.{js,ts,jsx,tsx}',
15
+ '{projectRoot}/src/**/__tests__/**/*.{js,ts,jsx,tsx}',
16
+ ],
17
+ },
18
+ ],
19
+ },
20
+ languageOptions: {
21
+ parser: await import('jsonc-eslint-parser'),
22
+ },
23
+ },
24
+ ];
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@icore/firebase-admin",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "dependencies": {
9
+ "firebase-admin": "^13.10.0",
10
+ "tslib": "^2.3.0"
11
+ }
12
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "firebase-admin",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/firebase-admin/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/js:tsc",
10
+ "outputs": ["{options.outputPath}"],
11
+ "options": {
12
+ "outputPath": "dist/libs/firebase-admin",
13
+ "main": "libs/firebase-admin/src/index.ts",
14
+ "tsConfig": "libs/firebase-admin/tsconfig.lib.json",
15
+ "assets": ["libs/firebase-admin/*.md"]
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export * from './lib/firebase-admin';
@@ -0,0 +1,105 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { apps, cert, initializeApp, app } = vi.hoisted(() => {
4
+ const apps: unknown[] = [];
5
+ return {
6
+ apps,
7
+ cert: vi.fn((sa: unknown) => ({ __cert: sa })),
8
+ initializeApp: vi.fn((opts: unknown) => {
9
+ const created = { __app: opts };
10
+ apps.push(created);
11
+ return created;
12
+ }),
13
+ app: vi.fn(() => apps[0]),
14
+ };
15
+ });
16
+
17
+ vi.mock('firebase-admin', () => ({
18
+ get apps() {
19
+ return apps;
20
+ },
21
+ app,
22
+ initializeApp,
23
+ credential: { cert },
24
+ }));
25
+
26
+ import { FIREBASE_ADMIN_REQUIRED_ENV, getFirebaseAdmin } from '../firebase-admin';
27
+
28
+ type Cfg = {
29
+ getOrThrow: (k: string) => string;
30
+ get: (k: string) => string | undefined;
31
+ };
32
+
33
+ function makeCfg(extra: Record<string, string> = {}): Cfg {
34
+ const env: Record<string, string> = {
35
+ FB_ADMIN_TYPE: 'service_account',
36
+ FB_ADMIN_PROJECT_ID: 'proj',
37
+ FB_ADMIN_PRIVATE_KEY_ID: 'kid',
38
+ FB_ADMIN_PRIVATE_KEY: '-----BEGIN-----\\nABC\\n-----END-----',
39
+ FB_ADMIN_CLIENT_EMAIL: 'svc@proj.iam.gserviceaccount.com',
40
+ FB_ADMIN_CLIENT_ID: 'cid',
41
+ FB_ADMIN_AUTH_URI: 'https://accounts.google.com/o/oauth2/auth',
42
+ FB_ADMIN_TOKEN_URI: 'https://oauth2.googleapis.com/token',
43
+ FB_ADMIN_AUTH_PROVIDER_X509_CERT_URL: 'https://www.googleapis.com/oauth2/v1/certs',
44
+ FB_ADMIN_CLIENT_X509_CERT_URL: 'https://www.googleapis.com/robot/v1/metadata/x509/svc',
45
+ FB_ADMIN_UNIVERSE_DOMAIN: 'googleapis.com',
46
+ ...extra,
47
+ };
48
+ return {
49
+ getOrThrow: (k) => {
50
+ if (!(k in env)) throw new Error(`missing ${k}`);
51
+ return env[k];
52
+ },
53
+ get: (k) => env[k],
54
+ };
55
+ }
56
+
57
+ describe('getFirebaseAdmin', () => {
58
+ beforeEach(() => {
59
+ apps.length = 0;
60
+ cert.mockClear();
61
+ initializeApp.mockClear();
62
+ app.mockClear();
63
+ });
64
+
65
+ afterEach(() => vi.restoreAllMocks());
66
+
67
+ it('requires the full Firebase service-account env contract (all 11 keys)', () => {
68
+ expect([...FIREBASE_ADMIN_REQUIRED_ENV]).toEqual([
69
+ 'FB_ADMIN_TYPE',
70
+ 'FB_ADMIN_PROJECT_ID',
71
+ 'FB_ADMIN_PRIVATE_KEY_ID',
72
+ 'FB_ADMIN_PRIVATE_KEY',
73
+ 'FB_ADMIN_CLIENT_EMAIL',
74
+ 'FB_ADMIN_CLIENT_ID',
75
+ 'FB_ADMIN_AUTH_URI',
76
+ 'FB_ADMIN_TOKEN_URI',
77
+ 'FB_ADMIN_AUTH_PROVIDER_X509_CERT_URL',
78
+ 'FB_ADMIN_CLIENT_X509_CERT_URL',
79
+ 'FB_ADMIN_UNIVERSE_DOMAIN',
80
+ ]);
81
+ });
82
+
83
+ it('passes the complete service account to cert() with escaped newlines fixed', () => {
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ getFirebaseAdmin(makeCfg() as any);
86
+ expect(initializeApp).toHaveBeenCalledTimes(1);
87
+ const sa = cert.mock.calls[0][0] as Record<string, string>;
88
+ expect(sa['project_id']).toBe('proj');
89
+ expect(sa['client_email']).toBe('svc@proj.iam.gserviceaccount.com');
90
+ expect(sa['private_key_id']).toBe('kid');
91
+ expect(sa['universe_domain']).toBe('googleapis.com');
92
+ // \n escapes in the env value are turned into real newlines
93
+ expect(sa['private_key']).toContain('\n');
94
+ expect(sa['private_key']).not.toContain('\\n');
95
+ });
96
+
97
+ it('initializes the default app only once (guards on admin.apps)', () => {
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ const cfg = makeCfg() as any;
100
+ getFirebaseAdmin(cfg);
101
+ getFirebaseAdmin(cfg);
102
+ expect(initializeApp).toHaveBeenCalledTimes(1);
103
+ expect(app).toHaveBeenCalledTimes(1); // second call returns the existing app
104
+ });
105
+ });
@@ -0,0 +1,70 @@
1
+ import * as admin from 'firebase-admin';
2
+
3
+ /**
4
+ * Minimal structural view of NestJS's `ConfigService` — just the two readers
5
+ * this helper needs. Declaring it structurally keeps the lib free of a
6
+ * `@nestjs/config` dependency while still accepting a real `ConfigService` at
7
+ * every call site.
8
+ */
9
+ export interface FirebaseConfigReader {
10
+ getOrThrow<T = string>(key: string): T;
11
+ get<T = string>(key: string): T | undefined;
12
+ }
13
+
14
+ /**
15
+ * The full Firebase service-account env contract.
16
+ *
17
+ * These are exactly the keys Firebase emits in the service-account JSON you
18
+ * download from Project Settings → Service accounts. Every microservice that
19
+ * talks to Firebase (auth, storage, Firestore) requires all of them, so each
20
+ * MS spreads this into its own REQUIRED_ENV map — keeping the contract in one
21
+ * place instead of three drifting copies.
22
+ */
23
+ export const FIREBASE_ADMIN_REQUIRED_ENV = [
24
+ 'FB_ADMIN_TYPE',
25
+ 'FB_ADMIN_PROJECT_ID',
26
+ 'FB_ADMIN_PRIVATE_KEY_ID',
27
+ 'FB_ADMIN_PRIVATE_KEY',
28
+ 'FB_ADMIN_CLIENT_EMAIL',
29
+ 'FB_ADMIN_CLIENT_ID',
30
+ 'FB_ADMIN_AUTH_URI',
31
+ 'FB_ADMIN_TOKEN_URI',
32
+ 'FB_ADMIN_AUTH_PROVIDER_X509_CERT_URL',
33
+ 'FB_ADMIN_CLIENT_X509_CERT_URL',
34
+ 'FB_ADMIN_UNIVERSE_DOMAIN',
35
+ ] as const;
36
+
37
+ /**
38
+ * Initialises the default Firebase Admin app exactly once and returns it.
39
+ *
40
+ * The Admin SDK throws "default app already exists" if `initializeApp` runs
41
+ * twice in a process, so this guards on `admin.apps.length` and is safe to call
42
+ * from every Firebase consumer (auth / storage / notes). Callers then use the
43
+ * returned app: `getFirebaseAdmin(cfg).auth()`, `.firestore()`, `.storage()`.
44
+ *
45
+ * The full service-account object is passed to `cert()` — Firebase only mints
46
+ * tokens from project_id/client_email/private_key, but feeding the complete
47
+ * downloaded JSON keeps the code aligned with the documented FB_ADMIN_* env.
48
+ */
49
+ export function getFirebaseAdmin(cfg: FirebaseConfigReader): admin.app.App {
50
+ if (admin.apps.length > 0) return admin.app();
51
+
52
+ return admin.initializeApp({
53
+ credential: admin.credential.cert({
54
+ type: cfg.getOrThrow<string>('FB_ADMIN_TYPE'),
55
+ project_id: cfg.getOrThrow<string>('FB_ADMIN_PROJECT_ID'),
56
+ private_key_id: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY_ID'),
57
+ private_key: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
58
+ client_email: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_EMAIL'),
59
+ client_id: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_ID'),
60
+ auth_uri: cfg.getOrThrow<string>('FB_ADMIN_AUTH_URI'),
61
+ token_uri: cfg.getOrThrow<string>('FB_ADMIN_TOKEN_URI'),
62
+ auth_provider_x509_cert_url: cfg.getOrThrow<string>('FB_ADMIN_AUTH_PROVIDER_X509_CERT_URL'),
63
+ client_x509_cert_url: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_X509_CERT_URL'),
64
+ universe_domain: cfg.getOrThrow<string>('FB_ADMIN_UNIVERSE_DOMAIN'),
65
+ } as unknown as admin.ServiceAccount),
66
+ // Optional: the storage MS also passes the bucket name explicitly to
67
+ // .bucket(); set here too so admin.storage().bucket() (no arg) works.
68
+ storageBucket: cfg.get<string>('FIREBASE_STORAGE_BUCKET'),
69
+ });
70
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "forceConsistentCasingInFileNames": true,
6
+ "strict": true,
7
+ "importHelpers": true,
8
+ "noImplicitOverride": true,
9
+ "noImplicitReturns": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "noPropertyAccessFromIndexSignature": true
12
+ },
13
+ "files": [],
14
+ "include": [],
15
+ "references": [
16
+ {
17
+ "path": "./tsconfig.lib.json"
18
+ },
19
+ {
20
+ "path": "./tsconfig.spec.json"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": [
10
+ "vite.config.ts",
11
+ "vite.config.mts",
12
+ "vitest.config.ts",
13
+ "vitest.config.mts",
14
+ "src/**/*.test.ts",
15
+ "src/**/*.spec.ts",
16
+ "src/**/*.test.tsx",
17
+ "src/**/*.spec.tsx",
18
+ "src/**/*.test.js",
19
+ "src/**/*.spec.js",
20
+ "src/**/*.test.jsx",
21
+ "src/**/*.spec.jsx"
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
6
+ },
7
+ "include": [
8
+ "vite.config.ts",
9
+ "vite.config.mts",
10
+ "vitest.config.ts",
11
+ "vitest.config.mts",
12
+ "src/**/*.test.ts",
13
+ "src/**/*.spec.ts",
14
+ "src/**/*.test.tsx",
15
+ "src/**/*.spec.tsx",
16
+ "src/**/*.test.js",
17
+ "src/**/*.spec.js",
18
+ "src/**/*.test.jsx",
19
+ "src/**/*.spec.jsx",
20
+ "src/**/*.d.ts"
21
+ ]
22
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
3
+ import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
4
+
5
+ export default defineConfig(() => ({
6
+ root: __dirname,
7
+ cacheDir: '../../node_modules/.vite/libs/firebase-admin',
8
+ plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
9
+ test: {
10
+ name: 'firebase-admin',
11
+ watch: false,
12
+ globals: true,
13
+ environment: 'node',
14
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
15
+ reporters: ['default'],
16
+ coverage: {
17
+ reportsDirectory: '../../coverage/libs/firebase-admin',
18
+ provider: 'v8' as const,
19
+ },
20
+ },
21
+ }));
@@ -1,2 +1,3 @@
1
+ export * from './lib/jobs-client.tokens';
1
2
  export * from './lib/jobs-client.module';
2
3
  export * from './lib/jobs-client.service';