@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.
- package/dist/cli.js +304 -24
- package/dist/index.cjs +301 -23
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +300 -23
- package/package.json +1 -1
- package/templates/apps/api/.env.example +14 -0
- package/templates/apps/api/src/app/app.module.ts +5 -1
- package/templates/apps/api/src/main.ts +12 -6
- package/templates/apps/microservices/auth/package.json +1 -1
- package/templates/apps/microservices/auth/project.json +2 -1
- package/templates/apps/microservices/auth/src/app/app.module.ts +50 -39
- package/templates/apps/microservices/auth/src/main.ts +6 -23
- package/templates/apps/microservices/jobs/project.json +2 -1
- 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/project.json +2 -1
- package/templates/apps/microservices/notes/src/app/app.module.ts +52 -38
- package/templates/apps/microservices/notes/src/main.ts +6 -23
- package/templates/apps/microservices/payment/project.json +2 -1
- package/templates/apps/microservices/payment/src/app/app.module.ts +37 -12
- package/templates/apps/microservices/payment/src/main.ts +6 -23
- package/templates/apps/microservices/upload/package.json +1 -1
- package/templates/apps/microservices/upload/project.json +2 -1
- package/templates/apps/microservices/upload/src/app/app.module.ts +50 -42
- package/templates/apps/microservices/upload/src/main.ts +6 -23
- package/templates/apps/templates/client-antd/.env.example +7 -0
- package/templates/apps/templates/client-antd/vite.config.mts +4 -4
- package/templates/apps/templates/client-mui/.env.example +7 -0
- package/templates/apps/templates/client-mui/vite.config.mts +4 -4
- package/templates/apps/templates/client-shadcn/.env.example +6 -1
- package/templates/apps/templates/client-shadcn/vite.config.mts +4 -4
- package/templates/libs/auth-client/src/index.ts +1 -0
- package/templates/libs/auth-client/src/lib/auth-client.module.ts +1 -1
- package/templates/libs/auth-client/src/lib/auth-client.service.ts +1 -1
- package/templates/libs/auth-client/src/lib/auth-client.tokens.ts +4 -0
- 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 +23 -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/index.ts +1 -0
- package/templates/libs/jobs-client/src/lib/jobs-client.module.ts +1 -1
- package/templates/libs/jobs-client/src/lib/jobs-client.service.ts +15 -3
- package/templates/libs/jobs-client/src/lib/jobs-client.tokens.ts +4 -0
- package/templates/libs/notes-client/src/index.ts +1 -0
- package/templates/libs/notes-client/src/lib/notes-client.module.ts +1 -1
- package/templates/libs/notes-client/src/lib/notes-client.service.ts +1 -1
- package/templates/libs/notes-client/src/lib/notes-client.tokens.ts +4 -0
- package/templates/libs/payment-client/src/index.ts +1 -0
- package/templates/libs/payment-client/src/lib/payment-client.module.ts +1 -1
- package/templates/libs/payment-client/src/lib/payment-client.service.ts +1 -1
- package/templates/libs/payment-client/src/lib/payment-client.tokens.ts +4 -0
- 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/env.ts +88 -0
- package/templates/libs/shared/src/index.ts +2 -0
- package/templates/libs/shared/src/transport.ts +62 -3
- package/templates/libs/upload-client/src/index.ts +1 -0
- package/templates/libs/upload-client/src/lib/upload-client.module.ts +1 -1
- package/templates/libs/upload-client/src/lib/upload-client.service.ts +1 -1
- package/templates/libs/upload-client/src/lib/upload-client.tokens.ts +4 -0
- package/templates/libs/vite-plugins/src/index.d.mts +6 -0
- package/templates/libs/vite-plugins/src/index.mjs +50 -0
- package/templates/package.json +1 -0
- package/templates/tools/create-icore/_template-shell/package.json +1 -0
- 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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
);
|
|
@@ -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
|
-
#
|
|
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.
|
|
@@ -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
|
-
|
|
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.
|
|
5
|
+
import { AUTH_CLIENT } from './auth-client.tokens';
|
|
6
6
|
|
|
7
7
|
@Injectable()
|
|
8
8
|
export class AuthClientService {
|
|
@@ -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,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
|
+
}));
|