@idevconn/create-icore 0.7.1 → 0.8.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/dist/cli.js +509 -353
  2. package/dist/index.cjs +644 -366
  3. package/dist/index.d.cts +9 -1
  4. package/dist/index.d.ts +9 -1
  5. package/dist/index.js +635 -358
  6. package/dist/manifest/audit.js +122 -0
  7. package/package.json +1 -1
  8. package/templates/apps/api/package.json +2 -2
  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 +4 -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,14 @@
1
+ import { MongoDbDbModule, MONGODB_DB_REQUIRED_ENV } from '../mongodb-db.module';
2
+
3
+ describe('MongoDbDbModule', () => {
4
+ it('requires the mongo uri', () => {
5
+ expect(MONGODB_DB_REQUIRED_ENV).toEqual(['MONGODB_URI']);
6
+ });
7
+ it('forRoot returns a DynamicModule importing Mongoose and exporting DBStrategy', () => {
8
+ const dm = MongoDbDbModule.forRoot('.env');
9
+ expect(dm.module).toBe(MongoDbDbModule);
10
+ expect(dm.exports).toContain('DBStrategy');
11
+ expect(Array.isArray(dm.imports)).toBe(true);
12
+ expect((dm.imports ?? []).length).toBeGreaterThan(0);
13
+ });
14
+ });
@@ -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
+ });