@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.
- package/dist/cli.js +84 -25
- package/dist/index.cjs +81 -24
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +80 -24
- package/package.json +3 -1
- package/templates/apps/api/.env.example +14 -0
- package/templates/apps/microservices/auth/package.json +1 -1
- package/templates/apps/microservices/auth/src/app/app.module.ts +17 -30
- package/templates/apps/microservices/auth/src/main.ts +6 -23
- package/templates/apps/microservices/jobs/src/app/redis-connection.ts +35 -0
- package/templates/apps/microservices/jobs/src/app/workers/cleanup.worker.ts +2 -1
- package/templates/apps/microservices/jobs/src/app/workers/email.worker.ts +2 -1
- package/templates/apps/microservices/jobs/src/app/workers/image-process.worker.ts +2 -1
- package/templates/apps/microservices/notes/src/app/app.module.ts +22 -27
- package/templates/apps/microservices/notes/src/main.ts +6 -23
- package/templates/apps/microservices/payment/src/app/app.module.ts +6 -4
- package/templates/apps/microservices/payment/src/main.ts +6 -23
- package/templates/apps/microservices/upload/package.json +1 -1
- package/templates/apps/microservices/upload/src/app/app.module.ts +18 -30
- package/templates/apps/microservices/upload/src/main.ts +6 -23
- package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.contract.unit.test.ts +1 -1
- package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.contract.unit.test.ts +1 -1
- package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.contract.unit.test.ts +1 -1
- package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.contract.unit.test.ts +1 -1
- package/templates/libs/firebase-admin/README.md +11 -0
- package/templates/libs/firebase-admin/eslint.config.mjs +24 -0
- package/templates/libs/firebase-admin/package.json +12 -0
- package/templates/libs/firebase-admin/project.json +19 -0
- package/templates/libs/firebase-admin/src/index.ts +1 -0
- package/templates/libs/firebase-admin/src/lib/__tests__/firebase-admin.unit.test.ts +105 -0
- package/templates/libs/firebase-admin/src/lib/firebase-admin.ts +70 -0
- package/templates/libs/firebase-admin/tsconfig.json +24 -0
- package/templates/libs/firebase-admin/tsconfig.lib.json +23 -0
- package/templates/libs/firebase-admin/tsconfig.spec.json +22 -0
- package/templates/libs/firebase-admin/vitest.config.mts +21 -0
- package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +14 -2
- package/templates/libs/jobs-client/tsconfig.json +2 -1
- package/templates/libs/notes-client/tsconfig.json +2 -1
- package/templates/libs/payment-client/tsconfig.json +2 -1
- package/templates/libs/shared/src/__tests__/bootstrap.unit.test.ts +92 -0
- package/templates/libs/shared/src/__tests__/transport.unit.test.ts +14 -2
- package/templates/libs/shared/src/bootstrap.ts +79 -0
- package/templates/libs/shared/src/index.ts +1 -0
- package/templates/libs/shared/src/strategies/__tests__/fake-auth.contract.unit.test.ts +1 -1
- package/templates/libs/shared/src/strategies/__tests__/fake-db.contract.unit.test.ts +1 -1
- package/templates/libs/shared/src/strategies/__tests__/fake-storage.contract.unit.test.ts +1 -1
- package/templates/libs/shared/src/strategies/index.ts +3 -3
- package/templates/libs/shared/src/testing.ts +14 -0
- package/templates/libs/shared/src/transport.ts +25 -3
- package/templates/libs/shared/tsconfig.lib.json +3 -1
- package/templates/libs/shared/vitest.config.mts +11 -1
- package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.contract.unit.test.ts +1 -1
- package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.contract.unit.test.ts +1 -1
- package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.contract.unit.test.ts +1 -1
- package/templates/tsconfig.base.json +3 -1
- /package/templates/libs/shared/src/strategies/{contract/auth-contract.ts → __tests__/auth.contract.unit.test.ts} +0 -0
- /package/templates/libs/shared/src/strategies/{contract/db-contract.ts → __tests__/db.contract.unit.test.ts} +0 -0
- /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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
|
@@ -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,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, {
|
|
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>(
|