@idevconn/create-icore 0.10.1 → 0.11.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 (27) hide show
  1. package/dist/cli.js +140 -9
  2. package/dist/index.cjs +140 -9
  3. package/dist/index.d.cts +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.js +140 -9
  6. package/package.json +1 -1
  7. package/templates/apps/templates/client-antd/src/components/layout/LayoutHeader.tsx +2 -2
  8. package/templates/apps/templates/client-mui/src/components/layout/LayoutHeader.tsx +1 -1
  9. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutHeader.tsx +2 -2
  10. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +7 -3
  11. package/templates/apps/templates/client-shadcn/src/routes/index.tsx +2 -2
  12. package/templates/apps/templates/client-shadcn/vite.config.mts +18 -1
  13. package/templates/docker-compose.yml +14 -0
  14. package/templates/libs/db-strategies/postgres/eslint.config.mjs +30 -0
  15. package/templates/libs/db-strategies/postgres/package.json +19 -0
  16. package/templates/libs/db-strategies/postgres/project.json +19 -0
  17. package/templates/libs/db-strategies/postgres/src/index.ts +3 -0
  18. package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.contract.unit.test.ts +4 -0
  19. package/templates/libs/db-strategies/postgres/src/lib/__tests__/postgres-db.module.unit.test.ts +37 -0
  20. package/templates/libs/db-strategies/postgres/src/lib/postgres-db.module.ts +33 -0
  21. package/templates/libs/db-strategies/postgres/src/lib/postgres-db.strategy.ts +139 -0
  22. package/templates/libs/db-strategies/postgres/src/lib/testing/mock-postgres.ts +94 -0
  23. package/templates/libs/db-strategies/postgres/tsconfig.json +19 -0
  24. package/templates/libs/db-strategies/postgres/tsconfig.lib.json +24 -0
  25. package/templates/libs/db-strategies/postgres/tsconfig.spec.json +22 -0
  26. package/templates/libs/db-strategies/postgres/vitest.config.mts +22 -0
  27. package/templates/tsconfig.base.json +1 -0
package/dist/index.js CHANGED
@@ -78,6 +78,7 @@ async function rewriteRootPackageJson(targetDir, opts) {
78
78
  const pkg = JSON.parse(raw);
79
79
  pkg["name"] = opts.projectName;
80
80
  pkg["version"] = "0.0.1";
81
+ pkg["icoreVersion"] = typeof ICORE_OWN_VERSION !== "undefined" ? ICORE_OWN_VERSION : "unknown";
81
82
  pkg["private"] = true;
82
83
  delete pkg.description;
83
84
  const transportDeps = TRANSPORT_DEPS[opts.transport];
@@ -345,6 +346,7 @@ var AUTH_ONLY_PATHS = [
345
346
  "apps/api/src/app/abilities",
346
347
  "libs/shared/src/abilities",
347
348
  "apps/client/src/components/auth",
349
+ "apps/client/src/components/AccessDeniedPage.tsx",
348
350
  "apps/client/src/routes/login.tsx",
349
351
  "apps/client/src/routes/auth.callback.tsx",
350
352
  "apps/client/src/routes/auth.oauth.callback.tsx",
@@ -481,6 +483,121 @@ export * from './lib/draft/index.js';
481
483
  export * from './lib/landing/LandingPage.js';
482
484
  export * from './lib/stores/theme.store.js';
483
485
  `;
486
+ var SHADCN_PAGE_LAYOUT_TSX = `import type { ReactNode } from 'react';
487
+ import { useTranslation } from 'react-i18next';
488
+ import { useDraft, useLoading } from '@icore/template-shared';
489
+
490
+ interface PageLayoutProps {
491
+ title: string;
492
+ description?: string;
493
+ actions?: ReactNode;
494
+ children: ReactNode;
495
+ }
496
+
497
+ export function PageLayout({ title, description, actions, children }: PageLayoutProps) {
498
+ const { t } = useTranslation();
499
+ const isLoading = useLoading();
500
+
501
+ useDraft(false);
502
+
503
+ return (
504
+ <div className="p-4 md:p-6 space-y-4">
505
+ <div className="flex items-start justify-between gap-3">
506
+ <div>
507
+ <h1 className="text-xl font-semibold text-foreground">{title}</h1>
508
+ {description && <p className="text-sm text-muted-foreground mt-1">{description}</p>}
509
+ </div>
510
+ {actions && <div>{actions}</div>}
511
+ </div>
512
+
513
+ {isLoading && (
514
+ <div
515
+ role="status"
516
+ aria-label={t('common.loading')}
517
+ className="fixed inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-sm"
518
+ >
519
+ <div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
520
+ </div>
521
+ )}
522
+
523
+ {children}
524
+ </div>
525
+ );
526
+ }
527
+ `;
528
+ var ANTD_PAGE_LAYOUT_TSX = `import type { ReactNode } from 'react';
529
+ import { Descriptions, Spin } from 'antd';
530
+ import { useDraft, useLoading } from '@icore/template-shared';
531
+
532
+ export interface PageLayoutProps {
533
+ title: ReactNode;
534
+ description?: ReactNode;
535
+ extra?: ReactNode;
536
+ children?: ReactNode;
537
+ }
538
+
539
+ export function PageLayout({ title, description, extra, children }: PageLayoutProps) {
540
+ useDraft(false);
541
+ const loading = useLoading();
542
+
543
+ return (
544
+ <div style={{ padding: 24 }}>
545
+ <Descriptions title={title} extra={extra} style={{ marginBottom: 16 }}>
546
+ {description ? <Descriptions.Item>{description}</Descriptions.Item> : null}
547
+ </Descriptions>
548
+ <Spin spinning={loading}>
549
+ <div>{children}</div>
550
+ </Spin>
551
+ </div>
552
+ );
553
+ }
554
+ `;
555
+ var MUI_PAGE_LAYOUT_TSX = `import type { ReactNode } from 'react';
556
+ import { Box, LinearProgress, Stack, Typography } from '@mui/material';
557
+ import { useDraft, useLoading } from '@icore/template-shared';
558
+
559
+ export interface PageLayoutProps {
560
+ title: ReactNode;
561
+ description?: ReactNode;
562
+ extra?: ReactNode;
563
+ children?: ReactNode;
564
+ }
565
+
566
+ export function PageLayout({ title, description, extra, children }: PageLayoutProps) {
567
+ useDraft(false);
568
+ const loading = useLoading();
569
+
570
+ return (
571
+ <Box sx={{ p: 3 }}>
572
+ <Stack
573
+ direction="row"
574
+ justifyContent="space-between"
575
+ alignItems="flex-start"
576
+ spacing={2}
577
+ mb={3}
578
+ >
579
+ <Box>
580
+ <Typography variant="h4" component="h1">
581
+ {title}
582
+ </Typography>
583
+ {description ? (
584
+ <Typography variant="body2" color="text.secondary" mt={0.5}>
585
+ {description}
586
+ </Typography>
587
+ ) : null}
588
+ </Box>
589
+ {extra ? (
590
+ <Stack direction="row" spacing={1}>
591
+ {extra}
592
+ </Stack>
593
+ ) : null}
594
+ </Stack>
595
+ {loading ? <LinearProgress sx={{ mb: 2 }} /> : null}
596
+ <Box>{children}</Box>
597
+ </Box>
598
+ );
599
+ }
600
+ `;
484
601
  var SHADCN_MAIN_TSX = `import './globals.css';
485
602
  import { StrictMode } from 'react';
486
603
  import { createRoot } from 'react-dom/client';
@@ -581,7 +698,7 @@ export const Route = createFileRoute('/')({
581
698
  ),
582
699
  });
583
700
  `;
584
- var SHADCN_LAYOUT_HEADER_TSX = `import { setStoredLocale, type IcoreLocale } from '@icore/template-shared';
701
+ var SHADCN_LAYOUT_HEADER_TSX = `import { setStoredLocale, type IcoreLocale, i18next } from '@icore/template-shared';
585
702
  import { ThemeToggle } from '../ThemeToggle';
586
703
 
587
704
  const LOCALES: { code: IcoreLocale; label: string }[] = [
@@ -593,7 +710,7 @@ const LOCALES: { code: IcoreLocale; label: string }[] = [
593
710
  export function LayoutHeader() {
594
711
  function handleLocale(code: IcoreLocale) {
595
712
  setStoredLocale(code);
596
- window.location.reload();
713
+ void i18next.changeLanguage(code);
597
714
  }
598
715
 
599
716
  return (
@@ -727,7 +844,7 @@ export const Route = createFileRoute('/')({
727
844
  });
728
845
  `;
729
846
  var ANTD_LAYOUT_HEADER_TSX = `import { Button, Layout, Space } from 'antd';
730
- import { setStoredLocale, type IcoreLocale } from '@icore/template-shared';
847
+ import { setStoredLocale, type IcoreLocale, i18next } from '@icore/template-shared';
731
848
  import { ThemeToggle } from '../ThemeToggle';
732
849
 
733
850
  const APP_VERSION = (import.meta.env.VITE_APP_VERSION as string | undefined) ?? '0.0.0-dev';
@@ -741,7 +858,7 @@ const LOCALES: { code: IcoreLocale; label: string }[] = [
741
858
  export function LayoutHeader() {
742
859
  function handleLocale(code: IcoreLocale) {
743
860
  setStoredLocale(code);
744
- window.location.reload();
861
+ void i18next.changeLanguage(code);
745
862
  }
746
863
 
747
864
  return (
@@ -913,7 +1030,7 @@ export function LayoutHeader() {
913
1030
 
914
1031
  function handleLocale(code: IcoreLocale) {
915
1032
  setStoredLocale(code);
916
- window.location.reload();
1033
+ void i18n.changeLanguage(code);
917
1034
  }
918
1035
 
919
1036
  return (
@@ -957,19 +1074,22 @@ var UI_VARIANTS = {
957
1074
  "apps/client/src/main.tsx": SHADCN_MAIN_TSX,
958
1075
  "apps/client/src/routes/_dashboard.tsx": SHADCN_DASHBOARD_TSX,
959
1076
  "apps/client/src/routes/index.tsx": SHADCN_INDEX_TSX,
960
- "apps/client/src/components/layout/LayoutHeader.tsx": SHADCN_LAYOUT_HEADER_TSX
1077
+ "apps/client/src/components/layout/LayoutHeader.tsx": SHADCN_LAYOUT_HEADER_TSX,
1078
+ "apps/client/src/components/PageLayout.tsx": SHADCN_PAGE_LAYOUT_TSX
961
1079
  },
962
1080
  antd: {
963
1081
  "apps/client/src/main.tsx": ANTD_MAIN_TSX,
964
1082
  "apps/client/src/routes/_dashboard.tsx": ANTD_DASHBOARD_TSX,
965
1083
  "apps/client/src/routes/index.tsx": ANTD_INDEX_TSX,
966
- "apps/client/src/components/layout/LayoutHeader.tsx": ANTD_LAYOUT_HEADER_TSX
1084
+ "apps/client/src/components/layout/LayoutHeader.tsx": ANTD_LAYOUT_HEADER_TSX,
1085
+ "apps/client/src/components/PageLayout.tsx": ANTD_PAGE_LAYOUT_TSX
967
1086
  },
968
1087
  mui: {
969
1088
  "apps/client/src/main.tsx": MUI_MAIN_TSX,
970
1089
  "apps/client/src/routes/_dashboard.tsx": MUI_DASHBOARD_TSX,
971
1090
  "apps/client/src/routes/index.tsx": MUI_INDEX_TSX,
972
- "apps/client/src/components/layout/LayoutHeader.tsx": MUI_LAYOUT_HEADER_TSX
1091
+ "apps/client/src/components/layout/LayoutHeader.tsx": MUI_LAYOUT_HEADER_TSX,
1092
+ "apps/client/src/components/PageLayout.tsx": MUI_PAGE_LAYOUT_TSX
973
1093
  }
974
1094
  };
975
1095
  async function applyAuthNoneVariants(targetDir, ui) {
@@ -1086,6 +1206,16 @@ var MANIFEST = {
1086
1206
  deps: { mongoose: "^9.6.3" },
1087
1207
  tsPaths: { "@icore/db-mongodb": ["libs/db-strategies/mongodb/src/index.ts"] },
1088
1208
  nestModule: { importFrom: "@icore/db-mongodb", symbol: "MongoDbDbModule", into: "notes" }
1209
+ },
1210
+ postgres: {
1211
+ libDirs: ["libs/db-strategies/postgres"],
1212
+ deps: { postgres: "^3" },
1213
+ tsPaths: { "@icore/db-postgres": ["libs/db-strategies/postgres/src/index.ts"] },
1214
+ nestModule: {
1215
+ importFrom: "@icore/db-postgres",
1216
+ symbol: "PostgresDbModule",
1217
+ into: "notes"
1218
+ }
1089
1219
  }
1090
1220
  },
1091
1221
  feature: {
@@ -2084,7 +2214,8 @@ Re-run with @latest to refresh:
2084
2214
  options: [
2085
2215
  { value: "supabase", label: "Supabase Postgres" },
2086
2216
  { value: "firebase", label: "Firestore" },
2087
- { value: "mongodb", label: "MongoDB" }
2217
+ { value: "mongodb", label: "MongoDB" },
2218
+ { value: "postgres", label: "PostgreSQL (direct, postgres.js)" }
2088
2219
  ],
2089
2220
  initialValue: authProvider
2090
2221
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idevconn/create-icore",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "iDEVconn",
@@ -13,13 +13,13 @@ const LOCALES: { code: IcoreLocale; label: string }[] = [
13
13
  ];
14
14
 
15
15
  export function LayoutHeader() {
16
- const { t } = useTranslation();
16
+ const { t, i18n } = useTranslation();
17
17
  const navigate = useNavigate();
18
18
  const user = useAuthStore((s) => s.user);
19
19
 
20
20
  function handleLocale(code: IcoreLocale) {
21
21
  setStoredLocale(code);
22
- window.location.reload();
22
+ void i18n.changeLanguage(code);
23
23
  }
24
24
 
25
25
  function handleLogout() {
@@ -32,7 +32,7 @@ export function LayoutHeader() {
32
32
 
33
33
  function handleLocale(code: IcoreLocale) {
34
34
  setStoredLocale(code);
35
- window.location.reload();
35
+ void i18n.changeLanguage(code);
36
36
  }
37
37
 
38
38
  function handleMenuOpen(event: React.MouseEvent<HTMLElement>) {
@@ -12,14 +12,14 @@ const LOCALES: { code: IcoreLocale; label: string }[] = [
12
12
  ];
13
13
 
14
14
  export function LayoutHeader() {
15
- const { t } = useTranslation();
15
+ const { t, i18n } = useTranslation();
16
16
  const navigate = useNavigate();
17
17
  const user = useAuthStore((s) => s.user);
18
18
  const logout = useAuthStore((s) => s.logout);
19
19
 
20
20
  function handleLocale(code: IcoreLocale) {
21
21
  setStoredLocale(code);
22
- window.location.reload();
22
+ void i18n.changeLanguage(code);
23
23
  }
24
24
 
25
25
  function handleLogout() {
@@ -16,7 +16,7 @@ export function LayoutSider() {
16
16
 
17
17
  return (
18
18
  <aside
19
- className={`relative flex flex-col border-r border-[--color-border] bg-[--color-card] transition-all duration-200 ${
19
+ className={`relative flex flex-col border-e border-[--color-border] bg-[--color-card] transition-all duration-200 ${
20
20
  collapsed ? 'w-14' : 'w-52'
21
21
  }`}
22
22
  >
@@ -41,9 +41,13 @@ export function LayoutSider() {
41
41
  type="button"
42
42
  onClick={() => setCollapsed((c) => !c)}
43
43
  aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
44
- className="absolute -right-3 top-5 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-[--color-border] bg-[--color-card] text-[--color-muted-foreground] shadow-sm hover:text-[--color-foreground] transition-colors"
44
+ className="absolute -end-3 top-5 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-[--color-border] bg-[--color-card] text-[--color-muted-foreground] shadow-sm hover:text-[--color-foreground] transition-colors"
45
45
  >
46
- {collapsed ? <ChevronRight size={12} /> : <ChevronLeft size={12} />}
46
+ {collapsed ? (
47
+ <ChevronRight size={12} className="rtl:scale-x-[-1]" />
48
+ ) : (
49
+ <ChevronLeft size={12} className="rtl:scale-x-[-1]" />
50
+ )}
47
51
  </button>
48
52
  </aside>
49
53
  );
@@ -4,12 +4,12 @@ import { LandingPage } from '@icore/template-shared';
4
4
  // All version strings are injected at build time by vite.config.mts
5
5
  // (reads root package.json via fs.readFileSync so they stay accurate
6
6
  // even when workspace packages are bumped independently).
7
- const APP_VERSION = (import.meta.env.VITE_APP_VERSION as string | undefined) ?? '0.0.0-dev';
7
+ const ICORE_VERSION = (import.meta.env.VITE_ICORE_VERSION as string | undefined) ?? '0.0.0-dev';
8
8
 
9
9
  export const Route = createFileRoute('/')({
10
10
  component: () => (
11
11
  <LandingPage
12
- coreVersion={APP_VERSION}
12
+ coreVersion={ICORE_VERSION}
13
13
  uiLibrary="shadcn"
14
14
  deps={[
15
15
  { name: 'react', version: (import.meta.env.VITE_DEP_REACT as string) ?? '?' },
@@ -19,12 +19,26 @@ import {
19
19
  const rootPackageJsonPath = new URL('../../../package.json', import.meta.url);
20
20
  const rootPackageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')) as {
21
21
  version: string;
22
+ icoreVersion?: string;
23
+ dependencies?: Record<string, string>;
24
+ devDependencies?: Record<string, string>;
25
+ };
26
+
27
+ const selfPackageJson = JSON.parse(
28
+ fs.readFileSync(new URL('./package.json', import.meta.url), 'utf-8'),
29
+ ) as {
22
30
  dependencies?: Record<string, string>;
23
31
  devDependencies?: Record<string, string>;
24
32
  };
25
33
 
26
34
  function depVersion(name: string): string {
27
- return rootPackageJson.dependencies?.[name] ?? rootPackageJson.devDependencies?.[name] ?? '?';
35
+ return (
36
+ rootPackageJson.dependencies?.[name] ??
37
+ rootPackageJson.devDependencies?.[name] ??
38
+ selfPackageJson.dependencies?.[name] ??
39
+ selfPackageJson.devDependencies?.[name] ??
40
+ '?'
41
+ );
28
42
  }
29
43
 
30
44
  export default defineConfig(() => ({
@@ -37,6 +51,9 @@ export default defineConfig(() => ({
37
51
  },
38
52
  define: {
39
53
  ...commonDefines(rootPackageJson),
54
+ 'import.meta.env.VITE_ICORE_VERSION': JSON.stringify(
55
+ rootPackageJson.icoreVersion ?? rootPackageJson.version,
56
+ ),
40
57
  'import.meta.env.VITE_DEP_TAILWINDCSS': JSON.stringify(depVersion('tailwindcss')),
41
58
  },
42
59
  plugins: [
@@ -1,4 +1,18 @@
1
1
  services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: ${POSTGRES_USER:-icore}
6
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-icore}
7
+ POSTGRES_DB: ${POSTGRES_DB:-icore}
8
+ ports:
9
+ - '5432:5432'
10
+ healthcheck:
11
+ test: ['CMD-SHELL', 'pg_isready -U icore']
12
+ interval: 5s
13
+ retries: 10
14
+ networks: [icore]
15
+
2
16
  redis:
3
17
  image: redis:7-alpine
4
18
  healthcheck:
@@ -0,0 +1,30 @@
1
+ import baseConfig from '../../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
+ '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
+ ],
15
+ ignoredDependencies: [
16
+ '@icore/shared',
17
+ 'postgres',
18
+ '@nestjs/common',
19
+ '@nestjs/config',
20
+ '@nestjs/testing',
21
+ 'vitest',
22
+ ],
23
+ },
24
+ ],
25
+ },
26
+ languageOptions: {
27
+ parser: await import('jsonc-eslint-parser'),
28
+ },
29
+ },
30
+ ];
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@icore/db-postgres",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.ts",
8
+ "dependencies": {
9
+ "@icore/shared": "*",
10
+ "@nestjs/common": "^11.1.27",
11
+ "@nestjs/config": "^4.0.4",
12
+ "postgres": "^3.4.5",
13
+ "tslib": "^2.8.1"
14
+ },
15
+ "devDependencies": {
16
+ "@nestjs/testing": "^11.1.27",
17
+ "vitest": "^4.1.9"
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "db-postgres",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/db-strategies/postgres/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/js:tsc",
10
+ "outputs": ["{options.outputPath}"],
11
+ "options": {
12
+ "outputPath": "dist/libs/db-strategies/postgres",
13
+ "main": "libs/db-strategies/postgres/src/index.ts",
14
+ "tsConfig": "libs/db-strategies/postgres/tsconfig.lib.json",
15
+ "assets": ["libs/db-strategies/postgres/*.md"]
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ export * from './lib/postgres-db.strategy';
2
+ export * from './lib/testing/mock-postgres';
3
+ export * from './lib/postgres-db.module';
@@ -0,0 +1,4 @@
1
+ import { runDBContract } from '@icore/shared/testing';
2
+ import { createMockPostgresDB } from '../testing/mock-postgres.js';
3
+
4
+ runDBContract('PostgresDBStrategy', () => createMockPostgresDB());
@@ -0,0 +1,37 @@
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 { PostgresDbModule, POSTGRES_DB_REQUIRED_ENV } from '../postgres-db.module.js';
6
+ import { PostgresDBStrategy } from '../postgres-db.strategy.js';
7
+
8
+ let ENV: Record<string, string | undefined> = {};
9
+
10
+ @Global()
11
+ @Module({
12
+ providers: [
13
+ {
14
+ provide: ConfigService,
15
+ useValue: {
16
+ get: (k: string) => ENV[k],
17
+ getOrThrow: (k: string) => ENV[k],
18
+ },
19
+ },
20
+ ],
21
+ exports: [ConfigService],
22
+ })
23
+ class StubConfigModule {}
24
+
25
+ describe('PostgresDbModule', () => {
26
+ it('declares its required env', () => {
27
+ expect(POSTGRES_DB_REQUIRED_ENV).toEqual(['POSTGRES_URL']);
28
+ });
29
+
30
+ it('provides a real PostgresDBStrategy under DBStrategy when env present', async () => {
31
+ ENV = { POSTGRES_URL: 'postgresql://user:pass@localhost:5432/test' };
32
+ const ref = await Test.createTestingModule({
33
+ imports: [StubConfigModule, PostgresDbModule.forRoot('.env')],
34
+ }).compile();
35
+ expect(ref.get('DBStrategy')).toBeInstanceOf(PostgresDBStrategy);
36
+ });
37
+ });
@@ -0,0 +1,33 @@
1
+ import { Module, DynamicModule } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { buildStrategyWithFallback, FakeDBStrategy } from '@icore/shared';
4
+ import type { DBStrategy } from '@icore/shared';
5
+ import { PostgresDBStrategy } from './postgres-db.strategy';
6
+
7
+ export const POSTGRES_DB_REQUIRED_ENV = ['POSTGRES_URL'];
8
+
9
+ @Module({})
10
+ export class PostgresDbModule {
11
+ static forRoot(envPath: string): DynamicModule {
12
+ return {
13
+ module: PostgresDbModule,
14
+ providers: [
15
+ {
16
+ provide: 'DBStrategy',
17
+ useFactory: (cfg: ConfigService): DBStrategy =>
18
+ buildStrategyWithFallback<DBStrategy>({
19
+ service: 'notes MS',
20
+ provider: 'postgres',
21
+ requiredEnv: POSTGRES_DB_REQUIRED_ENV,
22
+ cfg,
23
+ envPath,
24
+ build: () => new PostgresDBStrategy(cfg.getOrThrow<string>('POSTGRES_URL')),
25
+ fake: () => new FakeDBStrategy(),
26
+ }),
27
+ inject: [ConfigService],
28
+ },
29
+ ],
30
+ exports: ['DBStrategy'],
31
+ };
32
+ }
33
+ }