@idevconn/create-icore 0.5.0 → 0.5.2

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 (59) hide show
  1. package/dist/cli.js +84 -25
  2. package/dist/index.cjs +81 -24
  3. package/dist/index.d.cts +7 -1
  4. package/dist/index.d.ts +7 -1
  5. package/dist/index.js +80 -24
  6. package/package.json +3 -1
  7. package/templates/apps/api/.env.example +14 -0
  8. package/templates/apps/microservices/auth/package.json +1 -1
  9. package/templates/apps/microservices/auth/src/app/app.module.ts +17 -30
  10. package/templates/apps/microservices/auth/src/main.ts +6 -23
  11. package/templates/apps/microservices/jobs/src/app/redis-connection.ts +35 -0
  12. package/templates/apps/microservices/jobs/src/app/workers/cleanup.worker.ts +2 -1
  13. package/templates/apps/microservices/jobs/src/app/workers/email.worker.ts +2 -1
  14. package/templates/apps/microservices/jobs/src/app/workers/image-process.worker.ts +2 -1
  15. package/templates/apps/microservices/notes/src/app/app.module.ts +22 -27
  16. package/templates/apps/microservices/notes/src/main.ts +6 -23
  17. package/templates/apps/microservices/payment/src/app/app.module.ts +6 -4
  18. package/templates/apps/microservices/payment/src/main.ts +6 -23
  19. package/templates/apps/microservices/upload/package.json +1 -1
  20. package/templates/apps/microservices/upload/src/app/app.module.ts +18 -30
  21. package/templates/apps/microservices/upload/src/main.ts +6 -23
  22. package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.contract.unit.test.ts +1 -1
  23. package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.contract.unit.test.ts +1 -1
  24. package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.contract.unit.test.ts +1 -1
  25. package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.contract.unit.test.ts +1 -1
  26. package/templates/libs/firebase-admin/README.md +11 -0
  27. package/templates/libs/firebase-admin/eslint.config.mjs +24 -0
  28. package/templates/libs/firebase-admin/package.json +12 -0
  29. package/templates/libs/firebase-admin/project.json +19 -0
  30. package/templates/libs/firebase-admin/src/index.ts +1 -0
  31. package/templates/libs/firebase-admin/src/lib/__tests__/firebase-admin.unit.test.ts +105 -0
  32. package/templates/libs/firebase-admin/src/lib/firebase-admin.ts +70 -0
  33. package/templates/libs/firebase-admin/tsconfig.json +24 -0
  34. package/templates/libs/firebase-admin/tsconfig.lib.json +23 -0
  35. package/templates/libs/firebase-admin/tsconfig.spec.json +22 -0
  36. package/templates/libs/firebase-admin/vitest.config.mts +21 -0
  37. package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +14 -2
  38. package/templates/libs/jobs-client/tsconfig.json +2 -1
  39. package/templates/libs/notes-client/tsconfig.json +2 -1
  40. package/templates/libs/payment-client/tsconfig.json +2 -1
  41. package/templates/libs/shared/src/__tests__/bootstrap.unit.test.ts +92 -0
  42. package/templates/libs/shared/src/__tests__/transport.unit.test.ts +14 -2
  43. package/templates/libs/shared/src/bootstrap.ts +79 -0
  44. package/templates/libs/shared/src/index.ts +1 -0
  45. package/templates/libs/shared/src/strategies/__tests__/fake-auth.contract.unit.test.ts +1 -1
  46. package/templates/libs/shared/src/strategies/__tests__/fake-db.contract.unit.test.ts +1 -1
  47. package/templates/libs/shared/src/strategies/__tests__/fake-storage.contract.unit.test.ts +1 -1
  48. package/templates/libs/shared/src/strategies/index.ts +3 -3
  49. package/templates/libs/shared/src/testing.ts +14 -0
  50. package/templates/libs/shared/src/transport.ts +25 -3
  51. package/templates/libs/shared/tsconfig.lib.json +3 -1
  52. package/templates/libs/shared/vitest.config.mts +11 -1
  53. package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.contract.unit.test.ts +1 -1
  54. package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.contract.unit.test.ts +1 -1
  55. package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.contract.unit.test.ts +1 -1
  56. package/templates/tsconfig.base.json +3 -1
  57. /package/templates/libs/shared/src/strategies/{contract/auth-contract.ts → __tests__/auth.contract.unit.test.ts} +0 -0
  58. /package/templates/libs/shared/src/strategies/{contract/db-contract.ts → __tests__/db.contract.unit.test.ts} +0 -0
  59. /package/templates/libs/shared/src/strategies/{contract/storage-contract.ts → __tests__/storage.contract.unit.test.ts} +0 -0
@@ -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",
@@ -1,27 +1,21 @@
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';
9
+ import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
10
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
 
15
14
  const ENV_PATH = 'apps/microservices/upload/.env';
16
15
 
17
16
  const REQUIRED_ENV: Record<string, string[]> = {
18
17
  supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_STORAGE_BUCKET'],
19
- firebase: [
20
- 'FIREBASE_STORAGE_BUCKET',
21
- 'FB_ADMIN_PROJECT_ID',
22
- 'FB_ADMIN_CLIENT_EMAIL',
23
- 'FB_ADMIN_PRIVATE_KEY',
24
- ],
18
+ firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_STORAGE_BUCKET'],
25
19
  cloudinary: ['CLOUDINARY_CLOUD_NAME', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET'],
26
20
  };
27
21
 
@@ -29,19 +23,23 @@ function requireEnv(cfg: ConfigService, key: string): string {
29
23
  return cfg.getOrThrow<string>(key);
30
24
  }
31
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
+ });
36
+ }
37
+
32
38
  function makeFirebaseStorage(cfg: ConfigService): StorageStrategy {
33
39
  const bucketName = requireEnv(cfg, 'FIREBASE_STORAGE_BUCKET');
34
- if (admin.apps.length === 0) {
35
- admin.initializeApp({
36
- credential: admin.credential.cert({
37
- projectId: requireEnv(cfg, 'FB_ADMIN_PROJECT_ID'),
38
- clientEmail: requireEnv(cfg, 'FB_ADMIN_CLIENT_EMAIL'),
39
- privateKey: requireEnv(cfg, 'FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
40
- }),
41
- });
42
- }
40
+ const app = getFirebaseAdmin(cfg);
43
41
  return new FirebaseStorageStrategy({
44
- bucket: admin
42
+ bucket: app
45
43
  .storage()
46
44
  .bucket(bucketName) as unknown as import('@icore/storage-firebase').FirebaseStorageBucketLike,
47
45
  });
@@ -129,17 +127,7 @@ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
129
127
  if (!keys || missing.length > 0) return fallback();
130
128
 
131
129
  try {
132
- if (provider === 'supabase') {
133
- const client = createClient(
134
- requireEnv(cfg, 'SUPABASE_URL'),
135
- requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
136
- { auth: { autoRefreshToken: false, persistSession: false } },
137
- );
138
- return new SupabaseStorageStrategy({
139
- client,
140
- bucket: requireEnv(cfg, 'SUPABASE_STORAGE_BUCKET'),
141
- });
142
- }
130
+ if (provider === 'supabase') return makeSupabaseStorage(cfg);
143
131
  if (provider === 'firebase') return makeFirebaseStorage(cfg);
144
132
  return makeCloudinaryStorage(cfg);
145
133
  } catch (err) {
@@ -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
+ );
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { runAuthContract } from '@icore/shared';
2
+ import { runAuthContract } from '@icore/shared/testing';
3
3
  import { FirebaseAuthStrategy } from '../firebase-auth.strategy';
4
4
  import { createMockIdentityToolkit, type MockHandle } from '../testing/mock-identity-toolkit';
5
5
  import { createMockAdminAuth } from '../testing/mock-admin-auth';
@@ -1,4 +1,4 @@
1
- import { runAuthContract } from '@icore/shared';
1
+ import { runAuthContract } from '@icore/shared/testing';
2
2
  import { SupabaseAuthStrategy } from '../supabase-auth.strategy';
3
3
  import { createMockSupabaseClient, type MockSupabaseClient } from '../testing/mock-supabase';
4
4
 
@@ -1,4 +1,4 @@
1
- import { runDBContract } from '@icore/shared';
1
+ import { runDBContract } from '@icore/shared/testing';
2
2
  import { FirestoreDBStrategy } from '../firestore-db.strategy';
3
3
  import { createMockFirestore } from '../testing/mock-firestore';
4
4
 
@@ -1,4 +1,4 @@
1
- import { runDBContract } from '@icore/shared';
1
+ import { runDBContract } from '@icore/shared/testing';
2
2
  import { SupabaseDBStrategy } from '../supabase-db.strategy';
3
3
  import { createMockSupabaseDB } from '../testing/mock-supabase-postgres';
4
4
 
@@ -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,24 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "importHelpers": true,
9
+ "noImplicitOverride": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "noPropertyAccessFromIndexSignature": true
13
+ },
14
+ "files": [],
15
+ "include": [],
16
+ "references": [
17
+ {
18
+ "path": "./tsconfig.lib.json"
19
+ },
20
+ {
21
+ "path": "./tsconfig.spec.json"
22
+ }
23
+ ]
24
+ }
@@ -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,4 +1,4 @@
1
- import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
1
+ import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
2
2
  import { Queue, type JobsOptions } from 'bullmq';
3
3
  import IORedis from 'ioredis';
4
4
  import { ICORE_QUEUES, type JobsMap } from '@icore/shared';
@@ -6,11 +6,23 @@ import { JOBS_REDIS_URL } from './jobs-client.tokens';
6
6
 
7
7
  @Injectable()
8
8
  export class JobsClientService implements OnModuleDestroy {
9
+ private readonly logger = new Logger(JobsClientService.name);
9
10
  private readonly connection: IORedis;
10
11
  private readonly queues = new Map<string, Queue>();
11
12
 
12
13
  constructor(@Inject(JOBS_REDIS_URL) redisUrl: string) {
13
- this.connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
14
+ this.connection = new IORedis(redisUrl, {
15
+ maxRetriesPerRequest: null,
16
+ retryStrategy: (times) => Math.min(times * 200, 5000),
17
+ });
18
+ // Without an 'error' handler ioredis throws an unhandled 'error' event and
19
+ // crashes the host process when Redis is down. Log once, keep retrying.
20
+ let warned = false;
21
+ this.connection.on('error', (err: Error) => {
22
+ if (warned) return;
23
+ warned = true;
24
+ this.logger.warn(`Redis unreachable at ${redisUrl}: ${err.message}. enqueue() will retry.`);
25
+ });
14
26
  }
15
27
 
16
28
  async enqueue<K extends keyof JobsMap>(
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
- "module": "commonjs",
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
5
6
  "forceConsistentCasingInFileNames": true,
6
7
  "strict": true,
7
8
  "importHelpers": true,
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
- "module": "commonjs",
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
5
6
  "forceConsistentCasingInFileNames": true,
6
7
  "strict": true,
7
8
  "importHelpers": true,
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
- "module": "commonjs",
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
5
6
  "forceConsistentCasingInFileNames": true,
6
7
  "strict": true,
7
8
  "importHelpers": true,