@idevconn/create-icore 0.7.2 → 0.9.0

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 (87) hide show
  1. package/README.md +44 -9
  2. package/dist/cli.js +689 -363
  3. package/dist/index.cjs +806 -358
  4. package/dist/index.d.cts +12 -3
  5. package/dist/index.d.ts +12 -3
  6. package/dist/index.js +798 -351
  7. package/dist/manifest/audit.js +122 -0
  8. package/package.json +1 -1
  9. package/templates/apps/api/src/app/app.module.ts +2 -6
  10. package/templates/apps/api/src/app/features.module.ts +9 -0
  11. package/templates/apps/api/src/app/gateway-services.ts +7 -0
  12. package/templates/apps/api/src/main.ts +1 -5
  13. package/templates/apps/microservices/auth/src/app/app.module.ts +4 -93
  14. package/templates/apps/microservices/auth/src/app/auth.provider.ts +9 -0
  15. package/templates/apps/microservices/notes/src/app/app.module.ts +4 -86
  16. package/templates/apps/microservices/notes/src/app/db.provider.ts +9 -0
  17. package/templates/apps/microservices/upload/src/app/app.module.ts +4 -140
  18. package/templates/apps/microservices/upload/src/app/storage.provider.ts +9 -0
  19. package/templates/apps/templates/client-antd/src/components/layout/LayoutSider.tsx +15 -23
  20. package/templates/apps/templates/client-antd/src/nav.config.ts +17 -0
  21. package/templates/apps/templates/client-mui/src/components/layout/LayoutSider.tsx +19 -20
  22. package/templates/apps/templates/client-mui/src/nav.config.ts +17 -0
  23. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +20 -16
  24. package/templates/apps/templates/client-shadcn/src/nav.config.ts +17 -0
  25. package/templates/libs/auth-strategies/firebase/eslint.config.mjs +1 -0
  26. package/templates/libs/auth-strategies/firebase/package.json +4 -0
  27. package/templates/libs/auth-strategies/firebase/src/index.ts +1 -0
  28. package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.module.unit.test.ts +49 -0
  29. package/templates/libs/auth-strategies/firebase/src/lib/firebase-auth.module.ts +41 -0
  30. package/templates/libs/auth-strategies/firebase/tsconfig.json +2 -0
  31. package/templates/libs/auth-strategies/mongodb/package.json +8 -1
  32. package/templates/libs/auth-strategies/mongodb/src/index.ts +1 -0
  33. package/templates/libs/auth-strategies/mongodb/src/lib/__tests__/mongodb-auth.module.unit.test.ts +16 -0
  34. package/templates/libs/auth-strategies/mongodb/src/lib/mongodb-auth.module.ts +45 -0
  35. package/templates/libs/auth-strategies/mongodb/tsconfig.json +2 -0
  36. package/templates/libs/auth-strategies/supabase/eslint.config.mjs +1 -0
  37. package/templates/libs/auth-strategies/supabase/package.json +3 -0
  38. package/templates/libs/auth-strategies/supabase/src/index.ts +1 -0
  39. package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.module.unit.test.ts +43 -0
  40. package/templates/libs/auth-strategies/supabase/src/lib/supabase-auth.module.ts +41 -0
  41. package/templates/libs/auth-strategies/supabase/tsconfig.json +2 -0
  42. package/templates/libs/db-strategies/firestore/eslint.config.mjs +1 -1
  43. package/templates/libs/db-strategies/firestore/package.json +4 -0
  44. package/templates/libs/db-strategies/firestore/src/index.ts +1 -0
  45. package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.module.unit.test.ts +37 -0
  46. package/templates/libs/db-strategies/firestore/src/lib/firestore-db.module.ts +41 -0
  47. package/templates/libs/db-strategies/firestore/tsconfig.json +2 -0
  48. package/templates/libs/db-strategies/mongodb/package.json +4 -1
  49. package/templates/libs/db-strategies/mongodb/src/index.ts +1 -0
  50. package/templates/libs/db-strategies/mongodb/src/lib/__tests__/mongodb-db.module.unit.test.ts +14 -0
  51. package/templates/libs/db-strategies/mongodb/src/lib/mongodb-db.module.ts +41 -0
  52. package/templates/libs/db-strategies/mongodb/tsconfig.json +2 -0
  53. package/templates/libs/db-strategies/supabase/eslint.config.mjs +6 -1
  54. package/templates/libs/db-strategies/supabase/package.json +3 -0
  55. package/templates/libs/db-strategies/supabase/src/index.ts +1 -0
  56. package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.module.unit.test.ts +32 -0
  57. package/templates/libs/db-strategies/supabase/src/lib/supabase-db.module.ts +41 -0
  58. package/templates/libs/db-strategies/supabase/tsconfig.json +2 -0
  59. package/templates/libs/shared/src/strategies/__tests__/provide-strategy.unit.test.ts +73 -0
  60. package/templates/libs/shared/src/strategies/index.ts +1 -0
  61. package/templates/libs/shared/src/strategies/provide-strategy.ts +44 -0
  62. package/templates/libs/storage-strategies/cloudinary/eslint.config.mjs +1 -1
  63. package/templates/libs/storage-strategies/cloudinary/package.json +4 -0
  64. package/templates/libs/storage-strategies/cloudinary/src/index.ts +1 -0
  65. package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.module.unit.test.ts +40 -0
  66. package/templates/libs/storage-strategies/cloudinary/src/lib/cloudinary-storage.module.ts +85 -0
  67. package/templates/libs/storage-strategies/cloudinary/tsconfig.json +2 -0
  68. package/templates/libs/storage-strategies/firebase/eslint.config.mjs +1 -1
  69. package/templates/libs/storage-strategies/firebase/package.json +4 -0
  70. package/templates/libs/storage-strategies/firebase/src/index.ts +1 -0
  71. package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.module.unit.test.ts +42 -0
  72. package/templates/libs/storage-strategies/firebase/src/lib/firebase-storage.module.ts +46 -0
  73. package/templates/libs/storage-strategies/firebase/tsconfig.json +2 -0
  74. package/templates/libs/storage-strategies/mongodb/package.json +4 -1
  75. package/templates/libs/storage-strategies/mongodb/src/index.ts +1 -0
  76. package/templates/libs/storage-strategies/mongodb/src/lib/__tests__/mongodb-storage.module.unit.test.ts +14 -0
  77. package/templates/libs/storage-strategies/mongodb/src/lib/mongodb-storage.module.ts +41 -0
  78. package/templates/libs/storage-strategies/mongodb/tsconfig.json +2 -0
  79. package/templates/libs/storage-strategies/supabase/eslint.config.mjs +1 -0
  80. package/templates/libs/storage-strategies/supabase/package.json +3 -0
  81. package/templates/libs/storage-strategies/supabase/src/index.ts +1 -0
  82. package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.module.unit.test.ts +46 -0
  83. package/templates/libs/storage-strategies/supabase/src/lib/supabase-storage.module.ts +46 -0
  84. package/templates/libs/storage-strategies/supabase/tsconfig.json +2 -0
  85. package/templates/package.json +1 -1
  86. package/templates/tools/create-icore/_template-shell/package.json +1 -1
  87. package/templates/tsconfig.base.json +1 -1
@@ -0,0 +1,41 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
4
+ import { Connection } from 'mongoose';
5
+ import { buildStrategyWithFallback, FakeDBStrategy } from '@icore/shared';
6
+ import type { DBStrategy } from '@icore/shared';
7
+ import { MongoDbDBStrategy } from './mongodb-db.strategy';
8
+
9
+ export const MONGODB_DB_REQUIRED_ENV = ['MONGODB_URI'];
10
+
11
+ @Module({})
12
+ export class MongoDbDbModule {
13
+ static forRoot(envPath: string): DynamicModule {
14
+ return {
15
+ module: MongoDbDbModule,
16
+ imports: [
17
+ MongooseModule.forRootAsync({
18
+ useFactory: (cfg: ConfigService) => ({ uri: cfg.get<string>('MONGODB_URI') }),
19
+ inject: [ConfigService],
20
+ }),
21
+ ],
22
+ providers: [
23
+ {
24
+ provide: 'DBStrategy',
25
+ useFactory: (cfg: ConfigService, connection: Connection): DBStrategy =>
26
+ buildStrategyWithFallback<DBStrategy>({
27
+ service: 'notes MS',
28
+ provider: 'mongodb',
29
+ requiredEnv: MONGODB_DB_REQUIRED_ENV,
30
+ cfg,
31
+ envPath,
32
+ build: () => new MongoDbDBStrategy({ connection }),
33
+ fake: () => new FakeDBStrategy(),
34
+ }),
35
+ inject: [ConfigService, getConnectionToken()],
36
+ },
37
+ ],
38
+ exports: ['DBStrategy'],
39
+ };
40
+ }
41
+ }
@@ -2,6 +2,8 @@
2
2
  "extends": "../../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "module": "commonjs",
5
+ "experimentalDecorators": true,
6
+ "emitDecoratorMetadata": true,
5
7
  "forceConsistentCasingInFileNames": true,
6
8
  "strict": true,
7
9
  "importHelpers": true,
@@ -12,7 +12,12 @@ export default [
12
12
  '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
13
  '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
14
  ],
15
- ignoredDependencies: ['@icore/shared', '@supabase/supabase-js'],
15
+ ignoredDependencies: [
16
+ '@icore/shared',
17
+ '@supabase/supabase-js',
18
+ '@nestjs/testing',
19
+ 'vitest',
20
+ ],
16
21
  },
17
22
  ],
18
23
  },
@@ -7,10 +7,13 @@
7
7
  "types": "./src/index.ts",
8
8
  "dependencies": {
9
9
  "@icore/shared": "*",
10
+ "@nestjs/common": "^11.1.24",
11
+ "@nestjs/config": "^4.0.4",
10
12
  "@supabase/supabase-js": "^2.0.0",
11
13
  "tslib": "^2.3.0"
12
14
  },
13
15
  "devDependencies": {
16
+ "@nestjs/testing": "^11.0.0",
14
17
  "vitest": "^4.0.0"
15
18
  }
16
19
  }
@@ -1,2 +1,3 @@
1
1
  export * from './lib/supabase-db.strategy';
2
2
  export * from './lib/testing/mock-supabase-postgres';
3
+ export * from './lib/supabase-db.module';
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Global, Module } from '@nestjs/common';
3
+ import { Test } from '@nestjs/testing';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import { SupabaseDbModule, SUPABASE_DB_REQUIRED_ENV } from '../supabase-db.module.js';
6
+ import { SupabaseDBStrategy } from '../supabase-db.strategy.js';
7
+
8
+ let ENV: Record<string, string | undefined> = {};
9
+ @Global()
10
+ @Module({
11
+ providers: [
12
+ {
13
+ provide: ConfigService,
14
+ useValue: { get: (k: string) => ENV[k], getOrThrow: (k: string) => ENV[k] },
15
+ },
16
+ ],
17
+ exports: [ConfigService],
18
+ })
19
+ class StubConfigModule {}
20
+
21
+ describe('SupabaseDbModule', () => {
22
+ it('declares its required env', () => {
23
+ expect(SUPABASE_DB_REQUIRED_ENV).toEqual(['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY']);
24
+ });
25
+ it('provides a real SupabaseDBStrategy under DBStrategy when env present', async () => {
26
+ ENV = { SUPABASE_URL: 'https://x.supabase.co', SUPABASE_SERVICE_ROLE_KEY: 'svc' };
27
+ const ref = await Test.createTestingModule({
28
+ imports: [StubConfigModule, SupabaseDbModule.forRoot('.env')],
29
+ }).compile();
30
+ expect(ref.get('DBStrategy')).toBeInstanceOf(SupabaseDBStrategy);
31
+ });
32
+ });
@@ -0,0 +1,41 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { createClient } from '@supabase/supabase-js';
4
+ import { buildStrategyWithFallback, FakeDBStrategy } from '@icore/shared';
5
+ import type { DBStrategy } from '@icore/shared';
6
+ import { SupabaseDBStrategy } from './supabase-db.strategy';
7
+
8
+ export const SUPABASE_DB_REQUIRED_ENV = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'];
9
+
10
+ @Module({})
11
+ export class SupabaseDbModule {
12
+ static forRoot(envPath: string): DynamicModule {
13
+ return {
14
+ module: SupabaseDbModule,
15
+ providers: [
16
+ {
17
+ provide: 'DBStrategy',
18
+ useFactory: (cfg: ConfigService): DBStrategy =>
19
+ buildStrategyWithFallback<DBStrategy>({
20
+ service: 'notes MS',
21
+ provider: 'supabase',
22
+ requiredEnv: SUPABASE_DB_REQUIRED_ENV,
23
+ cfg,
24
+ envPath,
25
+ build: () =>
26
+ new SupabaseDBStrategy({
27
+ client: createClient(
28
+ cfg.getOrThrow<string>('SUPABASE_URL'),
29
+ cfg.getOrThrow<string>('SUPABASE_SERVICE_ROLE_KEY'),
30
+ { auth: { autoRefreshToken: false, persistSession: false } },
31
+ ),
32
+ }),
33
+ fake: () => new FakeDBStrategy(),
34
+ }),
35
+ inject: [ConfigService],
36
+ },
37
+ ],
38
+ exports: ['DBStrategy'],
39
+ };
40
+ }
41
+ }
@@ -3,6 +3,8 @@
3
3
  "compilerOptions": {
4
4
  "module": "node16",
5
5
  "moduleResolution": "node16",
6
+ "experimentalDecorators": true,
7
+ "emitDecoratorMetadata": true,
6
8
  "forceConsistentCasingInFileNames": true,
7
9
  "strict": true,
8
10
  "importHelpers": true,
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { buildStrategyWithFallback } from '../provide-strategy.js';
3
+
4
+ const cfgFrom = (env: Record<string, string | undefined>) => ({
5
+ get: (k: string) => env[k],
6
+ });
7
+
8
+ afterEach(() => {
9
+ delete process.env.NODE_ENV;
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ describe('buildStrategyWithFallback', () => {
14
+ it('returns the built strategy when all required env is present', () => {
15
+ const result = buildStrategyWithFallback({
16
+ service: 'auth MS',
17
+ provider: 'supabase',
18
+ requiredEnv: ['A', 'B'],
19
+ cfg: cfgFrom({ A: '1', B: '2' }),
20
+ envPath: '.env',
21
+ build: () => 'REAL',
22
+ fake: () => 'FAKE',
23
+ });
24
+ expect(result).toBe('REAL');
25
+ });
26
+
27
+ it('returns the fake (dev) when required env is missing', () => {
28
+ process.env.NODE_ENV = 'development';
29
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
30
+ const result = buildStrategyWithFallback({
31
+ service: 'auth MS',
32
+ provider: 'supabase',
33
+ requiredEnv: ['A', 'B'],
34
+ cfg: cfgFrom({ A: '1' }),
35
+ envPath: '.env',
36
+ build: () => 'REAL',
37
+ fake: () => 'FAKE',
38
+ });
39
+ expect(result).toBe('FAKE');
40
+ });
41
+
42
+ it('throws (prod) when required env is missing', () => {
43
+ process.env.NODE_ENV = 'production';
44
+ expect(() =>
45
+ buildStrategyWithFallback({
46
+ service: 'auth MS',
47
+ provider: 'supabase',
48
+ requiredEnv: ['A', 'B'],
49
+ cfg: cfgFrom({}),
50
+ envPath: '.env',
51
+ build: () => 'REAL',
52
+ fake: () => 'FAKE',
53
+ }),
54
+ ).toThrow();
55
+ });
56
+
57
+ it('falls back when build() throws despite present env', () => {
58
+ process.env.NODE_ENV = 'development';
59
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
60
+ const result = buildStrategyWithFallback({
61
+ service: 'auth MS',
62
+ provider: 'supabase',
63
+ requiredEnv: ['A'],
64
+ cfg: cfgFrom({ A: '1' }),
65
+ envPath: '.env',
66
+ build: () => {
67
+ throw new Error('bad url');
68
+ },
69
+ fake: () => 'FAKE',
70
+ });
71
+ expect(result).toBe('FAKE');
72
+ });
73
+ });
@@ -2,6 +2,7 @@ export * from './auth';
2
2
  export * from './storage';
3
3
  export * from './db';
4
4
  export * from './fakes';
5
+ export * from './provide-strategy';
5
6
  // NOTE: the strategy contract harness (runAuthContract / runStorageContract /
6
7
  // runDBContract) is intentionally NOT exported here — it is test-only code and
7
8
  // lives behind the '@icore/shared/testing' entry. See ../testing.ts.
@@ -0,0 +1,44 @@
1
+ import { missingEnv, formatEnvBanner } from '../env';
2
+
3
+ export interface StrategyConfigReader {
4
+ get(key: string): string | undefined;
5
+ }
6
+
7
+ export interface BuildStrategyOpts<T> {
8
+ service: string;
9
+ provider: string;
10
+ requiredEnv: string[];
11
+ cfg: StrategyConfigReader;
12
+ envPath: string;
13
+ build: () => T;
14
+ fake: () => T;
15
+ }
16
+
17
+ /**
18
+ * Build a concrete strategy, or fall back to the in-memory fake. Centralises the
19
+ * dev-warns-and-fakes / prod-fails-fast behavior that used to live inline in each
20
+ * microservice app.module useFactory.
21
+ */
22
+ export function buildStrategyWithFallback<T>(opts: BuildStrategyOpts<T>): T {
23
+ const missing = missingEnv((k) => opts.cfg.get(k), opts.requiredEnv);
24
+
25
+ const fallback = (reason?: string): T => {
26
+ const banner = formatEnvBanner({
27
+ service: opts.service,
28
+ provider: opts.provider,
29
+ missing,
30
+ envPath: opts.envPath,
31
+ reason,
32
+ });
33
+ if (process.env['NODE_ENV'] === 'production') throw new Error(banner);
34
+ console.warn(banner);
35
+ return opts.fake();
36
+ };
37
+
38
+ if (missing.length > 0) return fallback();
39
+ try {
40
+ return opts.build();
41
+ } catch (err) {
42
+ return fallback(err instanceof Error ? err.message : String(err));
43
+ }
44
+ }
@@ -12,7 +12,7 @@ export default [
12
12
  '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
13
  '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
14
  ],
15
- ignoredDependencies: ['@icore/shared'],
15
+ ignoredDependencies: ['@icore/shared', '@nestjs/testing', 'vitest'],
16
16
  },
17
17
  ],
18
18
  },
@@ -7,9 +7,13 @@
7
7
  "types": "./src/index.ts",
8
8
  "dependencies": {
9
9
  "@icore/shared": "*",
10
+ "@nestjs/common": "^11.1.24",
11
+ "@nestjs/config": "^4.0.4",
12
+ "cloudinary": "^2.10.0",
10
13
  "tslib": "^2.3.0"
11
14
  },
12
15
  "devDependencies": {
16
+ "@nestjs/testing": "^11.0.0",
13
17
  "vitest": "^4.0.0"
14
18
  }
15
19
  }
@@ -1,2 +1,3 @@
1
1
  export * from './lib/cloudinary-storage.strategy.js';
2
2
  export * from './lib/testing/mock-cloudinary.js';
3
+ export * from './lib/cloudinary-storage.module.js';
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Global, Module } from '@nestjs/common';
3
+ import { Test } from '@nestjs/testing';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import {
6
+ CloudinaryStorageModule,
7
+ CLOUDINARY_STORAGE_REQUIRED_ENV,
8
+ } from '../cloudinary-storage.module.js';
9
+ import { CloudinaryStorageStrategy } from '../cloudinary-storage.strategy.js';
10
+
11
+ let ENV: Record<string, string | undefined> = {};
12
+ @Global()
13
+ @Module({
14
+ providers: [
15
+ {
16
+ provide: ConfigService,
17
+ useValue: { get: (k: string) => ENV[k], getOrThrow: (k: string) => ENV[k] },
18
+ },
19
+ ],
20
+ exports: [ConfigService],
21
+ })
22
+ class StubConfigModule {}
23
+
24
+ describe('CloudinaryStorageModule', () => {
25
+ it('declares its required env', () => {
26
+ expect(CLOUDINARY_STORAGE_REQUIRED_ENV).toEqual([
27
+ 'CLOUDINARY_CLOUD_NAME',
28
+ 'CLOUDINARY_API_KEY',
29
+ 'CLOUDINARY_API_SECRET',
30
+ ]);
31
+ });
32
+
33
+ it('provides a real CloudinaryStorageStrategy when env present', async () => {
34
+ ENV = { CLOUDINARY_CLOUD_NAME: 'c', CLOUDINARY_API_KEY: 'k', CLOUDINARY_API_SECRET: 's' };
35
+ const ref = await Test.createTestingModule({
36
+ imports: [StubConfigModule, CloudinaryStorageModule.forRoot('.env')],
37
+ }).compile();
38
+ expect(ref.get('StorageStrategy')).toBeInstanceOf(CloudinaryStorageStrategy);
39
+ });
40
+ });
@@ -0,0 +1,85 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { v2 as cloudinary } from 'cloudinary';
4
+ import { buildStrategyWithFallback, FakeStorageStrategy } from '@icore/shared';
5
+ import type { StorageStrategy } from '@icore/shared';
6
+ import { CloudinaryStorageStrategy, type CloudinaryApiLike } from './cloudinary-storage.strategy';
7
+
8
+ export const CLOUDINARY_STORAGE_REQUIRED_ENV = [
9
+ 'CLOUDINARY_CLOUD_NAME',
10
+ 'CLOUDINARY_API_KEY',
11
+ 'CLOUDINARY_API_SECRET',
12
+ ];
13
+
14
+ @Module({})
15
+ export class CloudinaryStorageModule {
16
+ static forRoot(envPath: string): DynamicModule {
17
+ return {
18
+ module: CloudinaryStorageModule,
19
+ providers: [
20
+ {
21
+ provide: 'StorageStrategy',
22
+ useFactory: (cfg: ConfigService): StorageStrategy =>
23
+ buildStrategyWithFallback<StorageStrategy>({
24
+ service: 'upload MS',
25
+ provider: 'cloudinary',
26
+ requiredEnv: CLOUDINARY_STORAGE_REQUIRED_ENV,
27
+ cfg,
28
+ envPath,
29
+ build: () => {
30
+ cloudinary.config({
31
+ cloud_name: cfg.getOrThrow<string>('CLOUDINARY_CLOUD_NAME'),
32
+ api_key: cfg.getOrThrow<string>('CLOUDINARY_API_KEY'),
33
+ api_secret: cfg.getOrThrow<string>('CLOUDINARY_API_SECRET'),
34
+ secure: true,
35
+ });
36
+ const api: CloudinaryApiLike = {
37
+ async upload(buffer, opts) {
38
+ return new Promise((resolve, reject) => {
39
+ const stream = cloudinary.uploader.upload_stream(
40
+ { public_id: opts.public_id, resource_type: opts.resource_type ?? 'raw' },
41
+ (error, result) => {
42
+ if (error || !result) reject(error ?? new Error('upload_failed'));
43
+ else
44
+ resolve({ public_id: result.public_id, secure_url: result.secure_url });
45
+ },
46
+ );
47
+ stream.end(buffer);
48
+ });
49
+ },
50
+ async destroy(publicId) {
51
+ await cloudinary.uploader.destroy(publicId);
52
+ },
53
+ privateDownloadUrl(publicId, format, opts) {
54
+ return cloudinary.utils.private_download_url(
55
+ publicId,
56
+ format ?? '',
57
+ opts ?? {},
58
+ );
59
+ },
60
+ async resources(opts) {
61
+ const res = await cloudinary.api.resources({
62
+ prefix: opts.prefix,
63
+ type: opts.type ?? 'upload',
64
+ });
65
+ return {
66
+ resources: (res.resources ?? []).map((r: { public_id: string }) => ({
67
+ public_id: r.public_id,
68
+ })),
69
+ };
70
+ },
71
+ };
72
+ return new CloudinaryStorageStrategy({
73
+ api,
74
+ bucket: cfg.get<string>('CLOUDINARY_BUCKET_TAG') ?? 'cloudinary',
75
+ });
76
+ },
77
+ fake: () => new FakeStorageStrategy(),
78
+ }),
79
+ inject: [ConfigService],
80
+ },
81
+ ],
82
+ exports: ['StorageStrategy'],
83
+ };
84
+ }
85
+ }
@@ -3,6 +3,8 @@
3
3
  "compilerOptions": {
4
4
  "module": "node16",
5
5
  "moduleResolution": "node16",
6
+ "experimentalDecorators": true,
7
+ "emitDecoratorMetadata": true,
6
8
  "forceConsistentCasingInFileNames": true,
7
9
  "strict": true,
8
10
  "importHelpers": true,
@@ -12,7 +12,7 @@ export default [
12
12
  '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
13
  '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
14
  ],
15
- ignoredDependencies: ['@icore/shared'],
15
+ ignoredDependencies: ['@icore/shared', '@nestjs/testing', 'vitest'],
16
16
  },
17
17
  ],
18
18
  },
@@ -6,10 +6,14 @@
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.ts",
8
8
  "dependencies": {
9
+ "@icore/firebase-admin": "*",
9
10
  "@icore/shared": "*",
11
+ "@nestjs/common": "^11.1.24",
12
+ "@nestjs/config": "^4.0.4",
10
13
  "tslib": "^2.3.0"
11
14
  },
12
15
  "devDependencies": {
16
+ "@nestjs/testing": "^11.0.0",
13
17
  "vitest": "^4.0.0"
14
18
  }
15
19
  }
@@ -1,2 +1,3 @@
1
1
  export * from './lib/firebase-storage.strategy.js';
2
2
  export * from './lib/testing/mock-firebase-storage.js';
3
+ export * from './lib/firebase-storage.module.js';
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Global, Module } from '@nestjs/common';
3
+ import { Test } from '@nestjs/testing';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import {
6
+ FirebaseStorageModule,
7
+ FIREBASE_STORAGE_REQUIRED_ENV,
8
+ } from '../firebase-storage.module.js';
9
+
10
+ @Global()
11
+ @Module({
12
+ providers: [
13
+ {
14
+ provide: ConfigService,
15
+ useValue: {
16
+ get: () => undefined,
17
+ getOrThrow: () => {
18
+ throw new Error('missing');
19
+ },
20
+ },
21
+ },
22
+ ],
23
+ exports: [ConfigService],
24
+ })
25
+ class StubConfigModule {}
26
+
27
+ describe('FirebaseStorageModule', () => {
28
+ it('requires firebase-admin env + the storage bucket', () => {
29
+ expect(FIREBASE_STORAGE_REQUIRED_ENV).toContain('FIREBASE_STORAGE_BUCKET');
30
+ expect(FIREBASE_STORAGE_REQUIRED_ENV).toContain('FB_ADMIN_PROJECT_ID');
31
+ });
32
+
33
+ it('falls back to the fake (dev) when env is missing, without touching firebase-admin', async () => {
34
+ process.env.NODE_ENV = 'development';
35
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
36
+ const ref = await Test.createTestingModule({
37
+ imports: [StubConfigModule, FirebaseStorageModule.forRoot('.env')],
38
+ }).compile();
39
+ expect(typeof (ref.get('StorageStrategy') as { upload: unknown }).upload).toBe('function');
40
+ delete process.env.NODE_ENV;
41
+ });
42
+ });
@@ -0,0 +1,46 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
4
+ import { buildStrategyWithFallback, FakeStorageStrategy } from '@icore/shared';
5
+ import type { StorageStrategy } from '@icore/shared';
6
+ import {
7
+ FirebaseStorageStrategy,
8
+ type FirebaseStorageBucketLike,
9
+ } from './firebase-storage.strategy';
10
+
11
+ export const FIREBASE_STORAGE_REQUIRED_ENV = [
12
+ ...FIREBASE_ADMIN_REQUIRED_ENV,
13
+ 'FIREBASE_STORAGE_BUCKET',
14
+ ];
15
+
16
+ @Module({})
17
+ export class FirebaseStorageModule {
18
+ static forRoot(envPath: string): DynamicModule {
19
+ return {
20
+ module: FirebaseStorageModule,
21
+ providers: [
22
+ {
23
+ provide: 'StorageStrategy',
24
+ useFactory: (cfg: ConfigService): StorageStrategy =>
25
+ buildStrategyWithFallback<StorageStrategy>({
26
+ service: 'upload MS',
27
+ provider: 'firebase',
28
+ requiredEnv: FIREBASE_STORAGE_REQUIRED_ENV,
29
+ cfg,
30
+ envPath,
31
+ build: () => {
32
+ const bucketName = cfg.getOrThrow<string>('FIREBASE_STORAGE_BUCKET');
33
+ const app = getFirebaseAdmin(cfg);
34
+ return new FirebaseStorageStrategy({
35
+ bucket: app.storage().bucket(bucketName) as unknown as FirebaseStorageBucketLike,
36
+ });
37
+ },
38
+ fake: () => new FakeStorageStrategy(),
39
+ }),
40
+ inject: [ConfigService],
41
+ },
42
+ ],
43
+ exports: ['StorageStrategy'],
44
+ };
45
+ }
46
+ }
@@ -3,6 +3,8 @@
3
3
  "compilerOptions": {
4
4
  "module": "node16",
5
5
  "moduleResolution": "node16",
6
+ "experimentalDecorators": true,
7
+ "emitDecoratorMetadata": true,
6
8
  "forceConsistentCasingInFileNames": true,
7
9
  "strict": true,
8
10
  "importHelpers": true,
@@ -6,7 +6,10 @@
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
8
8
  "dependencies": {
9
- "@icore/shared": "workspace:*",
9
+ "@icore/shared": "*",
10
+ "@nestjs/common": "^11.1.24",
11
+ "@nestjs/config": "^4.0.4",
12
+ "@nestjs/mongoose": "^11.0.4",
10
13
  "mongodb-memory-server": "^11.2.0",
11
14
  "mongoose": "^9.6.3",
12
15
  "tslib": "^2.3.0"
@@ -1 +1,2 @@
1
1
  export * from './lib/mongodb-storage.strategy';
2
+ export * from './lib/mongodb-storage.module';
@@ -0,0 +1,14 @@
1
+ import { MongoDbStorageModule, MONGODB_STORAGE_REQUIRED_ENV } from '../mongodb-storage.module';
2
+
3
+ describe('MongoDbStorageModule', () => {
4
+ it('requires the mongo uri', () => {
5
+ expect(MONGODB_STORAGE_REQUIRED_ENV).toEqual(['MONGODB_URI']);
6
+ });
7
+ it('forRoot returns a DynamicModule importing Mongoose and exporting StorageStrategy', () => {
8
+ const dm = MongoDbStorageModule.forRoot('.env');
9
+ expect(dm.module).toBe(MongoDbStorageModule);
10
+ expect(dm.exports).toContain('StorageStrategy');
11
+ expect(Array.isArray(dm.imports)).toBe(true);
12
+ expect((dm.imports ?? []).length).toBeGreaterThan(0);
13
+ });
14
+ });