@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
@@ -4,9 +4,17 @@ import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
4
4
  import NoteOutlinedIcon from '@mui/icons-material/NoteOutlined';
5
5
  import { Link, useRouterState } from '@tanstack/react-router';
6
6
  import { useTranslation } from 'react-i18next';
7
+ import type { ReactNode } from 'react';
8
+ import { NAV_CONFIG, type NavItem } from '@/nav.config';
7
9
 
8
10
  const DRAWER_WIDTH = 220;
9
11
 
12
+ const ICONS: Record<NavItem['iconName'], ReactNode> = {
13
+ dashboard: <DashboardOutlinedIcon />,
14
+ notes: <NoteOutlinedIcon />,
15
+ profile: <PersonOutlineIcon />,
16
+ };
17
+
10
18
  export function LayoutSider() {
11
19
  const { t } = useTranslation();
12
20
  const pathname = useRouterState({ select: (s) => s.location.pathname });
@@ -25,26 +33,17 @@ export function LayoutSider() {
25
33
  }}
26
34
  >
27
35
  <List>
28
- <ListItemButton component={Link} to="/dashboard" selected={pathname === '/dashboard'}>
29
- <ListItemIcon>
30
- <DashboardOutlinedIcon />
31
- </ListItemIcon>
32
- <ListItemText primary={t('nav.dashboard')} />
33
- </ListItemButton>
34
-
35
- <ListItemButton component={Link} to="/notes" selected={pathname.includes('/notes')}>
36
- <ListItemIcon>
37
- <NoteOutlinedIcon />
38
- </ListItemIcon>
39
- <ListItemText primary={t('notes.title')} />
40
- </ListItemButton>
41
-
42
- <ListItemButton component={Link} to="/profile" selected={pathname === '/profile'}>
43
- <ListItemIcon>
44
- <PersonOutlineIcon />
45
- </ListItemIcon>
46
- <ListItemText primary={t('nav.profile')} />
47
- </ListItemButton>
36
+ {NAV_CONFIG.map(({ to, iconName, labelKey, exact }) => (
37
+ <ListItemButton
38
+ key={to}
39
+ component={Link}
40
+ to={to}
41
+ selected={exact ? pathname === to : pathname.includes(to)}
42
+ >
43
+ <ListItemIcon>{ICONS[iconName]}</ListItemIcon>
44
+ <ListItemText primary={t(labelKey)} />
45
+ </ListItemButton>
46
+ ))}
48
47
  </List>
49
48
  </Drawer>
50
49
  );
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Sidebar navigation. UI-agnostic: each LayoutSider maps `iconName` to its own
3
+ * icon library. Generated by create-icore from a base (dashboard + profile) plus
4
+ * the chosen features' contributions (e.g. notes when example=notes).
5
+ */
6
+ export interface NavItem {
7
+ to: string;
8
+ labelKey: string;
9
+ iconName: 'dashboard' | 'notes' | 'profile';
10
+ exact?: boolean;
11
+ }
12
+
13
+ export const NAV_CONFIG: NavItem[] = [
14
+ { to: '/dashboard', labelKey: 'nav.dashboard', iconName: 'dashboard', exact: true },
15
+ { to: '/notes', labelKey: 'nav.notes', iconName: 'notes' },
16
+ { to: '/profile', labelKey: 'nav.profile', iconName: 'profile' },
17
+ ];
@@ -2,12 +2,13 @@ import { useState } from 'react';
2
2
  import { Link } from '@tanstack/react-router';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { ChevronLeft, ChevronRight, LayoutDashboard, StickyNote, User } from 'lucide-react';
5
+ import { NAV_CONFIG, type NavItem } from '@/nav.config';
5
6
 
6
- const NAV_ITEMS = [
7
- { to: '/dashboard' as const, icon: LayoutDashboard, labelKey: 'nav.dashboard', exact: true },
8
- { to: '/notes' as const, icon: StickyNote, labelKey: 'nav.notes', exact: false },
9
- { to: '/profile' as const, icon: User, labelKey: 'nav.profile', exact: false },
10
- ];
7
+ const ICONS: Record<NavItem['iconName'], typeof LayoutDashboard> = {
8
+ dashboard: LayoutDashboard,
9
+ notes: StickyNote,
10
+ profile: User,
11
+ };
11
12
 
12
13
  export function LayoutSider() {
13
14
  const { t } = useTranslation();
@@ -20,17 +21,20 @@ export function LayoutSider() {
20
21
  }`}
21
22
  >
22
23
  <nav className="flex flex-col gap-0.5 p-2 flex-1 pt-3">
23
- {NAV_ITEMS.map(({ to, icon: Icon, labelKey, exact }) => (
24
- <Link
25
- key={to}
26
- to={to}
27
- activeOptions={{ exact }}
28
- className="group flex items-center gap-3 rounded-md px-2.5 py-2 text-sm text-[--color-muted-foreground] transition-colors hover:bg-[--color-muted] hover:text-[--color-foreground] [&.active]:bg-[--color-primary]/10 [&.active]:text-[--color-primary] [&.active]:font-medium cursor-pointer"
29
- >
30
- <Icon size={16} className="shrink-0" />
31
- {!collapsed && <span className="truncate">{t(labelKey)}</span>}
32
- </Link>
33
- ))}
24
+ {NAV_CONFIG.map(({ to, iconName, labelKey, exact }) => {
25
+ const Icon = ICONS[iconName];
26
+ return (
27
+ <Link
28
+ key={to}
29
+ to={to}
30
+ activeOptions={{ exact: exact ?? false }}
31
+ className="group flex items-center gap-3 rounded-md px-2.5 py-2 text-sm text-[--color-muted-foreground] transition-colors hover:bg-[--color-muted] hover:text-[--color-foreground] [&.active]:bg-[--color-primary]/10 [&.active]:text-[--color-primary] [&.active]:font-medium cursor-pointer"
32
+ >
33
+ <Icon size={16} className="shrink-0" />
34
+ {!collapsed && <span className="truncate">{t(labelKey)}</span>}
35
+ </Link>
36
+ );
37
+ })}
34
38
  </nav>
35
39
 
36
40
  <button
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Sidebar navigation. UI-agnostic: each LayoutSider maps `iconName` to its own
3
+ * icon library. Generated by create-icore from a base (dashboard + profile) plus
4
+ * the chosen features' contributions (e.g. notes when example=notes).
5
+ */
6
+ export interface NavItem {
7
+ to: string;
8
+ labelKey: string;
9
+ iconName: 'dashboard' | 'notes' | 'profile';
10
+ exact?: boolean;
11
+ }
12
+
13
+ export const NAV_CONFIG: NavItem[] = [
14
+ { to: '/dashboard', labelKey: 'nav.dashboard', iconName: 'dashboard', exact: true },
15
+ { to: '/notes', labelKey: 'nav.notes', iconName: 'notes' },
16
+ { to: '/profile', labelKey: 'nav.profile', iconName: 'profile' },
17
+ ];
@@ -12,6 +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: ['@nestjs/testing', 'vitest'],
15
16
  },
16
17
  ],
17
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,4 +1,5 @@
1
1
  export * from './lib/firebase-auth.strategy';
2
+ export * from './lib/firebase-auth.module';
2
3
  export * from './lib/identity-toolkit.client';
3
4
  export * from './lib/testing/mock-identity-toolkit';
4
5
  export * from './lib/testing/mock-admin-auth';
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Test } from '@nestjs/testing';
3
+ import { Global, Module } from '@nestjs/common';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import { FirebaseAuthModule, FIREBASE_AUTH_REQUIRED_ENV } from '../firebase-auth.module.js';
6
+
7
+ // Mirrors the production setup where the auth MS registers a global
8
+ // ConfigModule, so the dynamic module's useFactory can inject ConfigService.
9
+ function globalConfig(get: (k: string) => string | undefined) {
10
+ @Global()
11
+ @Module({
12
+ providers: [
13
+ {
14
+ provide: ConfigService,
15
+ useValue: {
16
+ get,
17
+ getOrThrow: (k: string) => {
18
+ const v = get(k);
19
+ if (v === undefined) throw new Error(`missing ${k}`);
20
+ return v;
21
+ },
22
+ },
23
+ },
24
+ ],
25
+ exports: [ConfigService],
26
+ })
27
+ class StubConfigModule {}
28
+ return StubConfigModule;
29
+ }
30
+
31
+ describe('FirebaseAuthModule', () => {
32
+ it('requires the firebase-admin env plus the web api key', () => {
33
+ expect(FIREBASE_AUTH_REQUIRED_ENV).toContain('FIREBASE_WEB_API_KEY');
34
+ expect(FIREBASE_AUTH_REQUIRED_ENV).toContain('FB_ADMIN_PROJECT_ID');
35
+ });
36
+
37
+ it('falls back to the fake (dev) when env is missing, without touching firebase-admin', async () => {
38
+ process.env['NODE_ENV'] = 'development';
39
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
40
+ const ref = await Test.createTestingModule({
41
+ imports: [globalConfig(() => undefined), FirebaseAuthModule.forRoot('.env')],
42
+ }).compile();
43
+ // Fake strategy still satisfies the AuthStrategy contract (has verifyToken).
44
+ expect(typeof (ref.get('AuthStrategy') as { verifyToken: unknown }).verifyToken).toBe(
45
+ 'function',
46
+ );
47
+ delete process.env['NODE_ENV'];
48
+ });
49
+ });
@@ -0,0 +1,41 @@
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, FakeAuthStrategy } from '@icore/shared';
5
+ import type { AuthStrategy } from '@icore/shared';
6
+ import { FirebaseAuthStrategy } from './firebase-auth.strategy';
7
+ import { HttpIdentityToolkitClient } from './identity-toolkit.client';
8
+
9
+ export const FIREBASE_AUTH_REQUIRED_ENV = [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_WEB_API_KEY'];
10
+
11
+ @Module({})
12
+ export class FirebaseAuthModule {
13
+ static forRoot(envPath: string): DynamicModule {
14
+ return {
15
+ module: FirebaseAuthModule,
16
+ providers: [
17
+ {
18
+ provide: 'AuthStrategy',
19
+ useFactory: (cfg: ConfigService): AuthStrategy =>
20
+ buildStrategyWithFallback<AuthStrategy>({
21
+ service: 'auth MS',
22
+ provider: 'firebase',
23
+ requiredEnv: FIREBASE_AUTH_REQUIRED_ENV,
24
+ cfg,
25
+ envPath,
26
+ build: () => {
27
+ const app = getFirebaseAdmin(cfg);
28
+ const identityToolkit = new HttpIdentityToolkitClient(
29
+ cfg.getOrThrow<string>('FIREBASE_WEB_API_KEY'),
30
+ );
31
+ return new FirebaseAuthStrategy({ identityToolkit, adminAuth: app.auth() });
32
+ },
33
+ fake: () => new FakeAuthStrategy(),
34
+ }),
35
+ inject: [ConfigService],
36
+ },
37
+ ],
38
+ exports: ['AuthStrategy'],
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,
@@ -5,8 +5,15 @@
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
8
+ "devDependencies": {
9
+ "@types/bcrypt": "^6.0.0",
10
+ "@types/jsonwebtoken": "^9.0.10"
11
+ },
8
12
  "dependencies": {
9
- "@icore/shared": "workspace:*",
13
+ "@icore/shared": "*",
14
+ "@nestjs/common": "^11.1.24",
15
+ "@nestjs/config": "^4.0.4",
16
+ "@nestjs/mongoose": "^11.0.4",
10
17
  "bcrypt": "^6.0.0",
11
18
  "jsonwebtoken": "^9.0.2",
12
19
  "mongodb-memory-server": "^11.2.0",
@@ -1 +1,2 @@
1
1
  export * from './lib/mongodb-auth.strategy';
2
+ export * from './lib/mongodb-auth.module';
@@ -0,0 +1,16 @@
1
+ import { MongoDbAuthModule, MONGODB_AUTH_REQUIRED_ENV } from '../mongodb-auth.module';
2
+
3
+ describe('MongoDbAuthModule', () => {
4
+ it('requires the mongo uri and jwt secret', () => {
5
+ expect(MONGODB_AUTH_REQUIRED_ENV).toEqual(['MONGODB_URI', 'JWT_SECRET']);
6
+ });
7
+
8
+ it('forRoot returns a DynamicModule that imports Mongoose and exports the AuthStrategy token', () => {
9
+ const dm = MongoDbAuthModule.forRoot('.env');
10
+ expect(dm.module).toBe(MongoDbAuthModule);
11
+ expect(dm.exports).toContain('AuthStrategy');
12
+ // Mongoose connection wiring is the module's own concern.
13
+ expect(Array.isArray(dm.imports)).toBe(true);
14
+ expect((dm.imports ?? []).length).toBeGreaterThan(0);
15
+ });
16
+ });
@@ -0,0 +1,45 @@
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, FakeAuthStrategy } from '@icore/shared';
6
+ import type { AuthStrategy } from '@icore/shared';
7
+ import { MongoDbAuthStrategy } from './mongodb-auth.strategy';
8
+
9
+ export const MONGODB_AUTH_REQUIRED_ENV = ['MONGODB_URI', 'JWT_SECRET'];
10
+
11
+ @Module({})
12
+ export class MongoDbAuthModule {
13
+ static forRoot(envPath: string): DynamicModule {
14
+ return {
15
+ module: MongoDbAuthModule,
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: 'AuthStrategy',
25
+ useFactory: (cfg: ConfigService, connection: Connection): AuthStrategy =>
26
+ buildStrategyWithFallback<AuthStrategy>({
27
+ service: 'auth MS',
28
+ provider: 'mongodb',
29
+ requiredEnv: MONGODB_AUTH_REQUIRED_ENV,
30
+ cfg,
31
+ envPath,
32
+ build: () =>
33
+ new MongoDbAuthStrategy({
34
+ connection,
35
+ jwtSecret: cfg.getOrThrow<string>('JWT_SECRET'),
36
+ }),
37
+ fake: () => new FakeAuthStrategy(),
38
+ }),
39
+ inject: [ConfigService, getConnectionToken()],
40
+ },
41
+ ],
42
+ exports: ['AuthStrategy'],
43
+ };
44
+ }
45
+ }
@@ -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,6 +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: ['@nestjs/testing', 'vitest'],
15
16
  },
16
17
  ],
17
18
  },
@@ -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-auth.strategy';
2
+ export * from './lib/supabase-auth.module';
2
3
  export * from './lib/testing/mock-supabase';
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Test } from '@nestjs/testing';
3
+ import { Global, Module } from '@nestjs/common';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import { SupabaseAuthModule, SUPABASE_AUTH_REQUIRED_ENV } from '../supabase-auth.module.js';
6
+ import { SupabaseAuthStrategy } from '../supabase-auth.strategy.js';
7
+
8
+ // Mirrors the production setup where the auth MS registers a global
9
+ // ConfigModule, so the dynamic module's useFactory can inject ConfigService.
10
+ function globalConfig(env: Record<string, string | undefined>) {
11
+ @Global()
12
+ @Module({
13
+ providers: [
14
+ {
15
+ provide: ConfigService,
16
+ useValue: { get: (k: string) => env[k], getOrThrow: (k: string) => env[k] },
17
+ },
18
+ ],
19
+ exports: [ConfigService],
20
+ })
21
+ class StubConfigModule {}
22
+ return StubConfigModule;
23
+ }
24
+
25
+ function moduleWith(env: Record<string, string | undefined>) {
26
+ return Test.createTestingModule({
27
+ imports: [globalConfig(env), SupabaseAuthModule.forRoot('.env')],
28
+ }).compile();
29
+ }
30
+
31
+ describe('SupabaseAuthModule', () => {
32
+ it('exposes its required env', () => {
33
+ expect(SUPABASE_AUTH_REQUIRED_ENV).toEqual(['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY']);
34
+ });
35
+
36
+ it('provides a real SupabaseAuthStrategy under the AuthStrategy token when env is present', async () => {
37
+ const ref = await moduleWith({
38
+ SUPABASE_URL: 'https://x.supabase.co',
39
+ SUPABASE_SERVICE_ROLE_KEY: 'svc',
40
+ });
41
+ expect(ref.get('AuthStrategy')).toBeInstanceOf(SupabaseAuthStrategy);
42
+ });
43
+ });
@@ -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, FakeAuthStrategy } from '@icore/shared';
5
+ import type { AuthStrategy } from '@icore/shared';
6
+ import { SupabaseAuthStrategy } from './supabase-auth.strategy';
7
+
8
+ export const SUPABASE_AUTH_REQUIRED_ENV = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'];
9
+
10
+ @Module({})
11
+ export class SupabaseAuthModule {
12
+ static forRoot(envPath: string): DynamicModule {
13
+ return {
14
+ module: SupabaseAuthModule,
15
+ providers: [
16
+ {
17
+ provide: 'AuthStrategy',
18
+ useFactory: (cfg: ConfigService): AuthStrategy =>
19
+ buildStrategyWithFallback<AuthStrategy>({
20
+ service: 'auth MS',
21
+ provider: 'supabase',
22
+ requiredEnv: SUPABASE_AUTH_REQUIRED_ENV,
23
+ cfg,
24
+ envPath,
25
+ build: () =>
26
+ new SupabaseAuthStrategy({
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 FakeAuthStrategy(),
34
+ }),
35
+ inject: [ConfigService],
36
+ },
37
+ ],
38
+ exports: ['AuthStrategy'],
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,
@@ -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/firestore-db.strategy';
2
2
  export * from './lib/testing/mock-firestore';
3
+ export * from './lib/firestore-db.module';
@@ -0,0 +1,37 @@
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 { FirestoreDbModule, FIRESTORE_DB_REQUIRED_ENV } from '../firestore-db.module.js';
6
+
7
+ @Global()
8
+ @Module({
9
+ providers: [
10
+ {
11
+ provide: ConfigService,
12
+ useValue: {
13
+ get: () => undefined,
14
+ getOrThrow: () => {
15
+ throw new Error('missing');
16
+ },
17
+ },
18
+ },
19
+ ],
20
+ exports: [ConfigService],
21
+ })
22
+ class StubConfigModule {}
23
+
24
+ describe('FirestoreDbModule', () => {
25
+ it('requires the firebase-admin env', () => {
26
+ expect(FIRESTORE_DB_REQUIRED_ENV).toContain('FB_ADMIN_PROJECT_ID');
27
+ });
28
+ it('falls back to the fake (dev) when env missing, without touching firebase-admin', async () => {
29
+ process.env.NODE_ENV = 'development';
30
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
31
+ const ref = await Test.createTestingModule({
32
+ imports: [StubConfigModule, FirestoreDbModule.forRoot('.env')],
33
+ }).compile();
34
+ expect(typeof (ref.get('DBStrategy') as { get: unknown }).get).toBe('function');
35
+ delete process.env.NODE_ENV;
36
+ });
37
+ });
@@ -0,0 +1,41 @@
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, FakeDBStrategy } from '@icore/shared';
5
+ import type { DBStrategy } from '@icore/shared';
6
+ import { FirestoreDBStrategy } from './firestore-db.strategy';
7
+
8
+ export const FIRESTORE_DB_REQUIRED_ENV = [...FIREBASE_ADMIN_REQUIRED_ENV];
9
+
10
+ @Module({})
11
+ export class FirestoreDbModule {
12
+ static forRoot(envPath: string): DynamicModule {
13
+ return {
14
+ module: FirestoreDbModule,
15
+ providers: [
16
+ {
17
+ provide: 'DBStrategy',
18
+ useFactory: (cfg: ConfigService): DBStrategy =>
19
+ buildStrategyWithFallback<DBStrategy>({
20
+ service: 'notes MS',
21
+ provider: 'firestore',
22
+ requiredEnv: FIRESTORE_DB_REQUIRED_ENV,
23
+ cfg,
24
+ envPath,
25
+ build: () => {
26
+ const app = getFirebaseAdmin(cfg);
27
+ return new FirestoreDBStrategy({
28
+ db: app.firestore() as unknown as ConstructorParameters<
29
+ typeof FirestoreDBStrategy
30
+ >[0]['db'],
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,
@@ -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-db.strategy';
2
+ export * from './lib/mongodb-db.module';
@@ -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
+ });