@idevconn/create-icore 0.7.2 → 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.
- package/dist/cli.js +509 -353
- package/dist/index.cjs +644 -366
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +635 -358
- package/dist/manifest/audit.js +122 -0
- package/package.json +1 -1
- package/templates/apps/api/src/app/app.module.ts +2 -6
- package/templates/apps/api/src/app/features.module.ts +9 -0
- package/templates/apps/api/src/app/gateway-services.ts +7 -0
- package/templates/apps/api/src/main.ts +1 -5
- package/templates/apps/microservices/auth/src/app/app.module.ts +4 -93
- package/templates/apps/microservices/auth/src/app/auth.provider.ts +9 -0
- package/templates/apps/microservices/notes/src/app/app.module.ts +4 -86
- package/templates/apps/microservices/notes/src/app/db.provider.ts +9 -0
- package/templates/apps/microservices/upload/src/app/app.module.ts +4 -140
- package/templates/apps/microservices/upload/src/app/storage.provider.ts +9 -0
- package/templates/apps/templates/client-antd/src/components/layout/LayoutSider.tsx +15 -23
- package/templates/apps/templates/client-antd/src/nav.config.ts +17 -0
- package/templates/apps/templates/client-mui/src/components/layout/LayoutSider.tsx +19 -20
- package/templates/apps/templates/client-mui/src/nav.config.ts +17 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +20 -16
- package/templates/apps/templates/client-shadcn/src/nav.config.ts +17 -0
- package/templates/libs/auth-strategies/firebase/eslint.config.mjs +1 -0
- package/templates/libs/auth-strategies/firebase/package.json +4 -0
- package/templates/libs/auth-strategies/firebase/src/index.ts +1 -0
- package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.module.unit.test.ts +49 -0
- package/templates/libs/auth-strategies/firebase/src/lib/firebase-auth.module.ts +41 -0
- package/templates/libs/auth-strategies/firebase/tsconfig.json +2 -0
- package/templates/libs/auth-strategies/mongodb/package.json +4 -1
- package/templates/libs/auth-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/__tests__/mongodb-auth.module.unit.test.ts +16 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/mongodb-auth.module.ts +45 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.json +2 -0
- package/templates/libs/auth-strategies/supabase/eslint.config.mjs +1 -0
- package/templates/libs/auth-strategies/supabase/package.json +3 -0
- package/templates/libs/auth-strategies/supabase/src/index.ts +1 -0
- package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.module.unit.test.ts +43 -0
- package/templates/libs/auth-strategies/supabase/src/lib/supabase-auth.module.ts +41 -0
- package/templates/libs/auth-strategies/supabase/tsconfig.json +2 -0
- package/templates/libs/db-strategies/firestore/eslint.config.mjs +1 -1
- package/templates/libs/db-strategies/firestore/package.json +4 -0
- package/templates/libs/db-strategies/firestore/src/index.ts +1 -0
- package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.module.unit.test.ts +37 -0
- package/templates/libs/db-strategies/firestore/src/lib/firestore-db.module.ts +41 -0
- package/templates/libs/db-strategies/firestore/tsconfig.json +2 -0
- package/templates/libs/db-strategies/mongodb/package.json +4 -1
- package/templates/libs/db-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/db-strategies/mongodb/src/lib/__tests__/mongodb-db.module.unit.test.ts +14 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb-db.module.ts +41 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.json +2 -0
- package/templates/libs/db-strategies/supabase/eslint.config.mjs +6 -1
- package/templates/libs/db-strategies/supabase/package.json +3 -0
- package/templates/libs/db-strategies/supabase/src/index.ts +1 -0
- package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.module.unit.test.ts +32 -0
- package/templates/libs/db-strategies/supabase/src/lib/supabase-db.module.ts +41 -0
- package/templates/libs/db-strategies/supabase/tsconfig.json +2 -0
- package/templates/libs/shared/src/strategies/__tests__/provide-strategy.unit.test.ts +73 -0
- package/templates/libs/shared/src/strategies/index.ts +1 -0
- package/templates/libs/shared/src/strategies/provide-strategy.ts +44 -0
- package/templates/libs/storage-strategies/cloudinary/eslint.config.mjs +1 -1
- package/templates/libs/storage-strategies/cloudinary/package.json +4 -0
- package/templates/libs/storage-strategies/cloudinary/src/index.ts +1 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.module.unit.test.ts +40 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/cloudinary-storage.module.ts +85 -0
- package/templates/libs/storage-strategies/cloudinary/tsconfig.json +2 -0
- package/templates/libs/storage-strategies/firebase/eslint.config.mjs +1 -1
- package/templates/libs/storage-strategies/firebase/package.json +4 -0
- package/templates/libs/storage-strategies/firebase/src/index.ts +1 -0
- package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.module.unit.test.ts +42 -0
- package/templates/libs/storage-strategies/firebase/src/lib/firebase-storage.module.ts +46 -0
- package/templates/libs/storage-strategies/firebase/tsconfig.json +2 -0
- package/templates/libs/storage-strategies/mongodb/package.json +4 -1
- package/templates/libs/storage-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/__tests__/mongodb-storage.module.unit.test.ts +14 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/mongodb-storage.module.ts +41 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.json +2 -0
- package/templates/libs/storage-strategies/supabase/eslint.config.mjs +1 -0
- package/templates/libs/storage-strategies/supabase/package.json +3 -0
- package/templates/libs/storage-strategies/supabase/src/index.ts +1 -0
- package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.module.unit.test.ts +46 -0
- package/templates/libs/storage-strategies/supabase/src/lib/supabase-storage.module.ts +46 -0
- package/templates/libs/storage-strategies/supabase/tsconfig.json +2 -0
- package/templates/package.json +1 -1
- package/templates/tools/create-icore/_template-shell/package.json +1 -1
- 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
|
+
}
|
|
@@ -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: [
|
|
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
|
}
|
package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.module.unit.test.ts
ADDED
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@icore/shared": "
|
|
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"
|
|
@@ -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
|
+
});
|