@skyf0xx/hedgehog-core-full-stack-app 1.0.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 (137) hide show
  1. package/CLAUDE.core.md +147 -0
  2. package/LICENSE +21 -0
  3. package/README.md +41 -0
  4. package/agents/backend-eng.md +189 -0
  5. package/agents/front-end-eng.md +176 -0
  6. package/agents/ux-planner.md +174 -0
  7. package/hedgehog-core.yaml +20 -0
  8. package/package.json +41 -0
  9. package/skills/hedgehog-bootstrap/SKILL.md +324 -0
  10. package/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +411 -0
  11. package/skills/hedgehog-loop/SKILL.md +643 -0
  12. package/skills/link-workspace-packages/SKILL.md +41 -0
  13. package/skills/nx-generate/SKILL.md +165 -0
  14. package/skills/nx-run-tasks/SKILL.md +52 -0
  15. package/skills/nx-workspace/SKILL.md +261 -0
  16. package/skills/nx-workspace/references/AFFECTED.md +27 -0
  17. package/vendor-skills/GSAP/ATTRIBUTION.md +30 -0
  18. package/vendor-skills/GSAP/LICENSE +21 -0
  19. package/vendor-skills/GSAP/gsap-core/SKILL.md +254 -0
  20. package/vendor-skills/GSAP/gsap-frameworks/SKILL.md +266 -0
  21. package/vendor-skills/GSAP/gsap-performance/SKILL.md +79 -0
  22. package/vendor-skills/GSAP/gsap-plugins/SKILL.md +433 -0
  23. package/vendor-skills/GSAP/gsap-react/SKILL.md +136 -0
  24. package/vendor-skills/GSAP/gsap-scrolltrigger/SKILL.md +296 -0
  25. package/vendor-skills/GSAP/gsap-timeline/SKILL.md +107 -0
  26. package/vendor-skills/GSAP/gsap-utils/SKILL.md +284 -0
  27. package/vendor-skills/GSAP/llms.txt +39 -0
  28. package/workspace/.env.example +5 -0
  29. package/workspace/.github/workflows/phase-gate.yml +17 -0
  30. package/workspace/.prettierignore +5 -0
  31. package/workspace/.prettierrc +3 -0
  32. package/workspace/.vscode/extensions.json +3 -0
  33. package/workspace/apps/api/eslint.config.mjs +3 -0
  34. package/workspace/apps/api/package.json +117 -0
  35. package/workspace/apps/api/src/app/app.module.spec.ts +15 -0
  36. package/workspace/apps/api/src/app/app.module.ts +15 -0
  37. package/workspace/apps/api/src/app/feature-modules.ts +8 -0
  38. package/workspace/apps/api/src/app/health.controller.ts +9 -0
  39. package/workspace/apps/api/src/assets/.gitkeep +0 -0
  40. package/workspace/apps/api/src/main.ts +32 -0
  41. package/workspace/apps/api/tsconfig.app.json +22 -0
  42. package/workspace/apps/api/tsconfig.json +13 -0
  43. package/workspace/apps/api/tsconfig.spec.json +36 -0
  44. package/workspace/apps/api/vitest.config.mts +18 -0
  45. package/workspace/apps/api/webpack.config.cjs +25 -0
  46. package/workspace/apps/api-e2e/eslint.config.mjs +3 -0
  47. package/workspace/apps/api-e2e/package.json +28 -0
  48. package/workspace/apps/api-e2e/src/api/api.spec.ts +11 -0
  49. package/workspace/apps/api-e2e/src/support/global-setup.ts +17 -0
  50. package/workspace/apps/api-e2e/src/support/test-setup.ts +6 -0
  51. package/workspace/apps/api-e2e/tsconfig.json +10 -0
  52. package/workspace/apps/api-e2e/tsconfig.spec.json +28 -0
  53. package/workspace/apps/api-e2e/vitest.config.mts +20 -0
  54. package/workspace/apps/web/.env.example +12 -0
  55. package/workspace/apps/web/.prettierrc.js +10 -0
  56. package/workspace/apps/web/.swcrc +30 -0
  57. package/workspace/apps/web/components.json +21 -0
  58. package/workspace/apps/web/eslint.config.mjs +12 -0
  59. package/workspace/apps/web/index.d.ts +7 -0
  60. package/workspace/apps/web/next-env.d.ts +7 -0
  61. package/workspace/apps/web/next.config.js +9 -0
  62. package/workspace/apps/web/package.json +38 -0
  63. package/workspace/apps/web/postcss.config.mjs +7 -0
  64. package/workspace/apps/web/public/.gitkeep +0 -0
  65. package/workspace/apps/web/public/favicon.ico +0 -0
  66. package/workspace/apps/web/src/app/global.css +83 -0
  67. package/workspace/apps/web/src/app/layout.tsx +34 -0
  68. package/workspace/apps/web/src/app/module-routes.ts +13 -0
  69. package/workspace/apps/web/src/app/page.tsx +34 -0
  70. package/workspace/apps/web/src/app/providers.tsx +12 -0
  71. package/workspace/apps/web/src/components/theme-toggle.spec.tsx +20 -0
  72. package/workspace/apps/web/src/components/theme-toggle.tsx +32 -0
  73. package/workspace/apps/web/src/components/ui/button.tsx +60 -0
  74. package/workspace/apps/web/src/lib/utils.ts +6 -0
  75. package/workspace/apps/web/src/test-setup.ts +1 -0
  76. package/workspace/apps/web/tsconfig.json +62 -0
  77. package/workspace/apps/web/tsconfig.spec.json +37 -0
  78. package/workspace/apps/web/vitest.config.mts +27 -0
  79. package/workspace/apps/web-e2e/eslint.config.mjs +12 -0
  80. package/workspace/apps/web-e2e/package.json +10 -0
  81. package/workspace/apps/web-e2e/playwright.config.mts +75 -0
  82. package/workspace/apps/web-e2e/src/example.spec.ts +7 -0
  83. package/workspace/apps/web-e2e/tsconfig.json +26 -0
  84. package/workspace/commitlint.config.cjs +3 -0
  85. package/workspace/core.yaml +68 -0
  86. package/workspace/docker-compose.yml +14 -0
  87. package/workspace/eslint.config.mjs +15 -0
  88. package/workspace/gitignore.template +24 -0
  89. package/workspace/lefthook.yml +52 -0
  90. package/workspace/nx.json +90 -0
  91. package/workspace/package.json +99 -0
  92. package/workspace/packages/config/eslint-base.js +205 -0
  93. package/workspace/packages/config/package.json +28 -0
  94. package/workspace/packages/config/prettier.js +13 -0
  95. package/workspace/packages/config/src/env.schema.spec.ts +38 -0
  96. package/workspace/packages/config/src/env.schema.ts +18 -0
  97. package/workspace/packages/config/src/index.ts +1 -0
  98. package/workspace/packages/config/tsconfig.json +13 -0
  99. package/workspace/packages/config/tsconfig.lib.json +27 -0
  100. package/workspace/packages/config/tsconfig.spec.json +34 -0
  101. package/workspace/packages/config/vitest.config.mts +18 -0
  102. package/workspace/packages/db/package.json +27 -0
  103. package/workspace/packages/db/src/index.ts +2 -0
  104. package/workspace/packages/db/src/lib/db.spec.ts +48 -0
  105. package/workspace/packages/db/src/lib/db.ts +54 -0
  106. package/workspace/packages/db/src/schema/index.ts +4 -0
  107. package/workspace/packages/db/tsconfig.json +13 -0
  108. package/workspace/packages/db/tsconfig.lib.json +27 -0
  109. package/workspace/packages/db/tsconfig.spec.json +34 -0
  110. package/workspace/packages/db/vitest.config.mts +18 -0
  111. package/workspace/pnpm-lock.yaml +23719 -0
  112. package/workspace/pnpm-workspace.yaml +7 -0
  113. package/workspace/tools/generate-feature-modules.cjs +104 -0
  114. package/workspace/tools/generate-module-routes.cjs +89 -0
  115. package/workspace/tools/generators/contract/generator.ts +336 -0
  116. package/workspace/tools/generators/contract/schema.json +24 -0
  117. package/workspace/tools/generators/controller/generator.ts +390 -0
  118. package/workspace/tools/generators/controller/schema.json +24 -0
  119. package/workspace/tools/generators/fields.ts +160 -0
  120. package/workspace/tools/generators/generators.json +42 -0
  121. package/workspace/tools/generators/hook/generator.ts +291 -0
  122. package/workspace/tools/generators/hook/schema.json +19 -0
  123. package/workspace/tools/generators/lib-shell.ts +124 -0
  124. package/workspace/tools/generators/naming.ts +84 -0
  125. package/workspace/tools/generators/package.json +6 -0
  126. package/workspace/tools/generators/repository/generator.ts +298 -0
  127. package/workspace/tools/generators/repository/schema.json +15 -0
  128. package/workspace/tools/generators/schema/generator.ts +169 -0
  129. package/workspace/tools/generators/schema/schema.json +20 -0
  130. package/workspace/tools/generators/screen/generator.ts +218 -0
  131. package/workspace/tools/generators/screen/schema.json +15 -0
  132. package/workspace/tools/generators/service/generator.ts +270 -0
  133. package/workspace/tools/generators/service/schema.json +19 -0
  134. package/workspace/tools/phase-gate.cjs +77 -0
  135. package/workspace/tsconfig.base.json +21 -0
  136. package/workspace/tsconfig.json +25 -0
  137. package/workspace/vitest.workspace.ts +4 -0
@@ -0,0 +1,205 @@
1
+ import nx from '@nx/eslint-plugin';
2
+
3
+ /**
4
+ * Locked, shared ESLint flat config — extended by every app/lib in the
5
+ * workspace (via root `eslint.config.mjs`). Encodes Nx module boundaries
6
+ * (tags + depConstraints) as a build-time (`nx lint`) failure. See
7
+ * `.claude/skills/hedgehog-bootstrap` and `.claude/skills/hedgehog-loop`
8
+ * for the discipline this enforces.
9
+ *
10
+ * The constraints below describe the project shape `core.yaml`'s compiled
11
+ * layer sequence actually produces. One layer, `repository`, lands a
12
+ * module's port interface AND its Drizzle adapter in the same lib
13
+ * (`libs/<module>/repository`, tagged `type:adapter`) — there is no
14
+ * separate `type:port` project, so a tag constraint alone cannot say
15
+ * "import the interface, not the concrete class." The
16
+ * `no-restricted-imports` block below carries that half of port
17
+ * discipline; the tag graph carries the rest.
18
+ *
19
+ * Tag reference:
20
+ * apps/api -> scope:api
21
+ * apps/worker -> scope:worker
22
+ * apps/web -> scope:web
23
+ * apps/mobile -> scope:mobile
24
+ * packages/db -> scope:db, type:adapter
25
+ * packages/contracts -> scope:contracts, type:contract
26
+ * packages/hooks -> scope:hooks, type:hook
27
+ * packages/auth -> scope:auth, type:adapter
28
+ * packages/config -> scope:config, type:util
29
+ * packages/shared -> scope:shared, type:util
30
+ * libs/<module>/repository -> scope:<module>, type:adapter
31
+ * libs/<module>/service -> scope:<module>, type:service
32
+ */
33
+ export default [
34
+ ...nx.configs['flat/base'],
35
+ ...nx.configs['flat/typescript'],
36
+ ...nx.configs['flat/javascript'],
37
+ {
38
+ ignores: ['**/dist', '**/out-tsc'],
39
+ },
40
+ {
41
+ files: [
42
+ '**/*.ts',
43
+ '**/*.tsx',
44
+ '**/*.js',
45
+ '**/*.jsx',
46
+ '**/*.cjs',
47
+ '**/*.mjs',
48
+ ],
49
+ rules: {
50
+ '@nx/enforce-module-boundaries': [
51
+ 'error',
52
+ {
53
+ // `false`, not `true`: `packages/contracts` and `packages/hooks`
54
+ // are source-only by this core's design — they have no `build`
55
+ // target and are consumed as TypeScript sources. With this on,
56
+ // `apps/web` (a real Next build target) importing either one
57
+ // fails lint purely for that structural choice, not for any
58
+ // dependency that's actually wrong.
59
+ enforceBuildableLibDependency: false,
60
+ allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
61
+ depConstraints: [
62
+ // domain services reach storage through their own module's
63
+ // repository lib, which holds the port interface alongside the
64
+ // Drizzle adapter (`type:adapter`). Which symbol a service may
65
+ // import out of that lib is the no-restricted-imports rule
66
+ // below, not something a tag can express.
67
+ {
68
+ sourceTag: 'type:service',
69
+ onlyDependOnLibsWithTags: [
70
+ 'type:adapter',
71
+ 'type:contract',
72
+ 'type:util',
73
+ ],
74
+ },
75
+ // adapters (repositories, db client, auth config) depend on
76
+ // packages/db and shared utils — never reach across into
77
+ // another module's service
78
+ {
79
+ sourceTag: 'type:adapter',
80
+ onlyDependOnLibsWithTags: [
81
+ 'type:adapter',
82
+ 'type:contract',
83
+ 'type:util',
84
+ ],
85
+ },
86
+ // the ts-rest/Zod contract is derived from the Drizzle schema
87
+ // (`drizzle-zod`), so it reaches packages/db and nothing else
88
+ {
89
+ sourceTag: 'type:contract',
90
+ onlyDependOnLibsWithTags: ['type:adapter', 'type:util'],
91
+ },
92
+ // TanStack Query hooks are typed by the contract they call
93
+ {
94
+ sourceTag: 'type:hook',
95
+ onlyDependOnLibsWithTags: ['type:contract', 'type:util'],
96
+ },
97
+ // shared utils (packages/config, packages/shared) depend on
98
+ // nothing else in the workspace — they're the floor
99
+ {
100
+ sourceTag: 'type:util',
101
+ onlyDependOnLibsWithTags: ['type:util'],
102
+ },
103
+ // web/mobile never import db or api internals — only contracts +
104
+ // hooks (+ shared util packages like packages/config, same as
105
+ // every other app in the workspace)
106
+ {
107
+ sourceTag: 'scope:web',
108
+ onlyDependOnLibsWithTags: [
109
+ 'scope:contracts',
110
+ 'scope:hooks',
111
+ 'scope:shared',
112
+ 'type:util',
113
+ ],
114
+ },
115
+ {
116
+ sourceTag: 'scope:mobile',
117
+ onlyDependOnLibsWithTags: [
118
+ 'scope:contracts',
119
+ 'scope:hooks',
120
+ 'scope:shared',
121
+ 'type:util',
122
+ ],
123
+ },
124
+ // worker reaches domain logic the same way api does
125
+ {
126
+ sourceTag: 'scope:worker',
127
+ onlyDependOnLibsWithTags: [
128
+ 'type:adapter',
129
+ 'type:service',
130
+ 'type:contract',
131
+ 'type:util',
132
+ 'scope:shared',
133
+ ],
134
+ },
135
+ // api is the composition root: its NestJS module is the only
136
+ // place that constructs a module's concrete adapter and binds
137
+ // it to the port's DI token, so it reaches `type:adapter` by
138
+ // necessity. It also serves the ts-rest contract, hence
139
+ // `type:contract`. What it may NOT do is inject an adapter
140
+ // into a controller — that's the no-restricted-imports rule.
141
+ {
142
+ sourceTag: 'scope:api',
143
+ onlyDependOnLibsWithTags: [
144
+ 'type:adapter',
145
+ 'type:service',
146
+ 'type:contract',
147
+ 'type:util',
148
+ ],
149
+ },
150
+ ],
151
+ },
152
+ ],
153
+ },
154
+ },
155
+ {
156
+ // Port discipline, the half tags can't express. A module's port
157
+ // interface and its Drizzle adapter live in the same lib, so
158
+ // `type:service -> type:adapter` has to be allowed above; this is what
159
+ // stops a service from importing the concrete adapter through it.
160
+ // Services see the port only. Deep paths are named rather than the
161
+ // package entry point, because the entry point is also how the port
162
+ // itself is imported.
163
+ files: ['libs/*/service/**/*.ts'],
164
+ rules: {
165
+ 'no-restricted-imports': [
166
+ 'error',
167
+ {
168
+ patterns: [
169
+ {
170
+ group: ['**/*.adapter', '**/*.adapter.js', '**/*.adapter.ts'],
171
+ message:
172
+ 'A service depends on its module\'s port interface, never the concrete adapter. Import the port from the repository lib\'s entry point and let apps/api bind the adapter to its DI token.',
173
+ },
174
+ {
175
+ group: ['@app/db', '@app/db/*', 'db', 'db/*', 'drizzle-orm', 'drizzle-orm/*'],
176
+ message:
177
+ 'A service never talks to the database directly — that is the repository adapter\'s job, reached through the port.',
178
+ },
179
+ ],
180
+ },
181
+ ],
182
+ },
183
+ },
184
+ {
185
+ // The same rule one layer up, minus the composition root. A NestJS
186
+ // module file (`*.module.ts`) is where the adapter is constructed and
187
+ // bound, so it is exempt; a controller injecting an adapter is not.
188
+ files: ['apps/api/**/*.ts'],
189
+ ignores: ['apps/api/**/*.module.ts'],
190
+ rules: {
191
+ 'no-restricted-imports': [
192
+ 'error',
193
+ {
194
+ patterns: [
195
+ {
196
+ group: ['**/*.adapter', '**/*.adapter.js', '**/*.adapter.ts'],
197
+ message:
198
+ 'Only a NestJS module (*.module.ts) constructs a concrete adapter. A controller depends on the service, or on the port token the module bound.',
199
+ },
200
+ ],
201
+ },
202
+ ],
203
+ },
204
+ },
205
+ ];
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "config",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./prettier": "./prettier.js",
15
+ "./eslint-base": "./eslint-base.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "dependencies": {
19
+ "zod": "^4.0.0",
20
+ "@ts-rest/core": "3.53.0-rc.1"
21
+ },
22
+ "nx": {
23
+ "tags": [
24
+ "scope:config",
25
+ "type:util"
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Locked, shared Prettier config — extended by every app/lib.
3
+ *
4
+ * Deliberately does NOT include `prettier-plugin-tailwindcss`: the plugin
5
+ * parses every file Prettier touches (not just files with Tailwind
6
+ * classes) and throws on any file when no Tailwind config is resolvable
7
+ * yet — true for every project from this step through `apps/web`'s own
8
+ * setup. Add the plugin to `apps/web`'s own Prettier config (extending
9
+ * this base) once Tailwind exists there, not here.
10
+ */
11
+ export default {
12
+ singleQuote: true,
13
+ };
@@ -0,0 +1,38 @@
1
+ import { envSchema } from './env.schema';
2
+
3
+ describe('envSchema', () => {
4
+ const valid = {
5
+ DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/app',
6
+ NODE_ENV: 'development',
7
+ };
8
+
9
+ it('accepts a fully valid env', () => {
10
+ const result = envSchema.safeParse(valid);
11
+ expect(result.success).toBe(true);
12
+ });
13
+
14
+ it('rejects a missing DATABASE_URL', () => {
15
+ const rest: Record<string, string> = { ...valid };
16
+ delete rest['DATABASE_URL'];
17
+ const result = envSchema.safeParse(rest);
18
+ expect(result.success).toBe(false);
19
+ });
20
+
21
+ it('rejects an invalid NODE_ENV value', () => {
22
+ const result = envSchema.safeParse({ ...valid, NODE_ENV: 'staging' });
23
+ expect(result.success).toBe(false);
24
+ });
25
+
26
+ it('defaults WEB_ORIGIN when absent', () => {
27
+ const result = envSchema.safeParse(valid);
28
+ expect(result.success).toBe(true);
29
+ if (result.success) {
30
+ expect(result.data.WEB_ORIGIN).toBe('http://localhost:3000');
31
+ }
32
+ });
33
+
34
+ it('rejects a non-URL WEB_ORIGIN', () => {
35
+ const result = envSchema.safeParse({ ...valid, WEB_ORIGIN: 'not-a-url' });
36
+ expect(result.success).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+
3
+ export const envSchema = z.object({
4
+ DATABASE_URL: z.string().url(),
5
+ NODE_ENV: z.enum(['development', 'test', 'production']),
6
+ WEB_ORIGIN: z.string().url().default('http://localhost:3000'),
7
+ });
8
+
9
+ export type Env = z.infer<typeof envSchema>;
10
+
11
+ export function loadEnv(): Env {
12
+ const parsed = envSchema.safeParse(process.env);
13
+ if (!parsed.success) {
14
+ console.error(parsed.error.format());
15
+ process.exit(1);
16
+ }
17
+ return parsed.data;
18
+ }
@@ -0,0 +1 @@
1
+ export * from './env.schema';
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.lib.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
7
+ "emitDeclarationOnly": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "types": ["node"]
10
+ },
11
+ "include": ["src/**/*.ts"],
12
+ "references": [],
13
+ "exclude": [
14
+ "vite.config.ts",
15
+ "vite.config.mts",
16
+ "vitest.config.ts",
17
+ "vitest.config.mts",
18
+ "src/**/*.test.ts",
19
+ "src/**/*.spec.ts",
20
+ "src/**/*.test.tsx",
21
+ "src/**/*.spec.tsx",
22
+ "src/**/*.test.js",
23
+ "src/**/*.spec.js",
24
+ "src/**/*.test.jsx",
25
+ "src/**/*.spec.jsx"
26
+ ]
27
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/vitest",
5
+ "types": [
6
+ "vitest/globals",
7
+ "vitest/importMeta",
8
+ "vite/client",
9
+ "node",
10
+ "vitest"
11
+ ],
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": [
15
+ "vite.config.ts",
16
+ "vite.config.mts",
17
+ "vitest.config.ts",
18
+ "vitest.config.mts",
19
+ "src/**/*.test.ts",
20
+ "src/**/*.spec.ts",
21
+ "src/**/*.test.tsx",
22
+ "src/**/*.spec.tsx",
23
+ "src/**/*.test.js",
24
+ "src/**/*.spec.js",
25
+ "src/**/*.test.jsx",
26
+ "src/**/*.spec.jsx",
27
+ "src/**/*.d.ts"
28
+ ],
29
+ "references": [
30
+ {
31
+ "path": "./tsconfig.lib.json"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig(() => ({
4
+ root: __dirname,
5
+ cacheDir: '../../node_modules/.vite/packages/config',
6
+ test: {
7
+ name: 'config',
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ reporters: ['default'],
13
+ coverage: {
14
+ reportsDirectory: './test-output/vitest/coverage',
15
+ provider: 'v8' as const,
16
+ },
17
+ },
18
+ }));
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "db",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dependencies": {
17
+ "drizzle-orm": "^0.45.2",
18
+ "pg": "^8.22.0",
19
+ "config": "workspace:*"
20
+ },
21
+ "nx": {
22
+ "tags": [
23
+ "scope:db",
24
+ "type:adapter"
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,2 @@
1
+ export * from './lib/db';
2
+ export * from './schema/index';
@@ -0,0 +1,48 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ describe('db', () => {
4
+ const originalEnv = { ...process.env };
5
+
6
+ beforeEach(() => {
7
+ vi.resetModules();
8
+ process.env.DATABASE_URL =
9
+ 'postgres://postgres:postgres@localhost:5432/app';
10
+ process.env.NODE_ENV = 'test';
11
+ });
12
+
13
+ afterEach(() => {
14
+ process.env = { ...originalEnv };
15
+ });
16
+
17
+ it('lazily constructs a single shared pool', async () => {
18
+ const { getPool, closeDb } = await import('./db');
19
+ const first = getPool();
20
+ const second = getPool();
21
+ expect(first).toBe(second);
22
+ await closeDb();
23
+ });
24
+
25
+ it('returns a drizzle client bound to the shared pool', async () => {
26
+ const { getDb, closeDb } = await import('./db');
27
+ const db = getDb();
28
+ expect(db).toBeDefined();
29
+ await closeDb();
30
+ });
31
+
32
+ it('exits fast when DATABASE_URL is missing or malformed', async () => {
33
+ process.env.DATABASE_URL = 'not-a-url';
34
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
35
+ throw new Error('process.exit called');
36
+ });
37
+ const errorSpy = vi
38
+ .spyOn(console, 'error')
39
+ .mockImplementation(() => undefined);
40
+
41
+ const { getPool } = await import('./db');
42
+ expect(() => getPool()).toThrow('process.exit called');
43
+ expect(exitSpy).toHaveBeenCalledWith(1);
44
+
45
+ exitSpy.mockRestore();
46
+ errorSpy.mockRestore();
47
+ });
48
+ });
@@ -0,0 +1,54 @@
1
+ import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
2
+ import { Pool } from 'pg';
3
+ import { loadEnv } from 'config';
4
+
5
+ /**
6
+ * Shared Drizzle client (Postgres via `pg`). Reads `DATABASE_URL` through
7
+ * `loadEnv()` — boot fails fast on a missing/malformed connection string
8
+ * rather than surfacing as a runtime connection error.
9
+ *
10
+ * No domain schema is registered here — each module's own schema lives in
11
+ * `libs/<module>/repository` (Phase A) and is passed in via `withSchema`
12
+ * (or attached at the call site), keeping this package schema-agnostic
13
+ * infra rather than a place domain modules reach into directly.
14
+ */
15
+
16
+ let pool: Pool | undefined;
17
+ let client: NodePgDatabase | undefined;
18
+
19
+ export function getPool(): Pool {
20
+ if (!pool) {
21
+ const env = loadEnv();
22
+ pool = new Pool({
23
+ connectionString: env.DATABASE_URL,
24
+ // A wedged connection surfaces as a query error rather than a hang.
25
+ connectionTimeoutMillis: 10_000,
26
+ idleTimeoutMillis: 30_000,
27
+ });
28
+ // `pg` emits 'error' on the pool when an *idle* client's connection dies
29
+ // (Postgres restart, failover, laptop sleep). Node terminates the process
30
+ // on an unhandled 'error' event, so without this listener a routine
31
+ // database blip crashes the API. `pg` has already discarded the bad
32
+ // client by the time this fires: the next query opens a fresh connection,
33
+ // so logging and swallowing is the recovery.
34
+ pool.on('error', (err) => {
35
+ console.error('idle postgres client error', err);
36
+ });
37
+ }
38
+ return pool;
39
+ }
40
+
41
+ export function getDb(): NodePgDatabase {
42
+ if (!client) {
43
+ client = drizzle(getPool());
44
+ }
45
+ return client;
46
+ }
47
+
48
+ export async function closeDb(): Promise<void> {
49
+ if (pool) {
50
+ await pool.end();
51
+ pool = undefined;
52
+ client = undefined;
53
+ }
54
+ }
@@ -0,0 +1,4 @@
1
+ // Barrel-of-barrels: one re-export line per module, added by that module's
2
+ // own schema layer (in scope — see core.yaml's schema layer). Keeps
3
+ // packages/db/src/index.ts itself static after bootstrap.
4
+ export {};
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.lib.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
7
+ "emitDeclarationOnly": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "types": ["node"]
10
+ },
11
+ "include": ["src/**/*.ts"],
12
+ "references": [{ "path": "../config/tsconfig.lib.json" }],
13
+ "exclude": [
14
+ "vite.config.ts",
15
+ "vite.config.mts",
16
+ "vitest.config.ts",
17
+ "vitest.config.mts",
18
+ "src/**/*.test.ts",
19
+ "src/**/*.spec.ts",
20
+ "src/**/*.test.tsx",
21
+ "src/**/*.spec.tsx",
22
+ "src/**/*.test.js",
23
+ "src/**/*.spec.js",
24
+ "src/**/*.test.jsx",
25
+ "src/**/*.spec.jsx"
26
+ ]
27
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out-tsc/vitest",
5
+ "types": [
6
+ "vitest/globals",
7
+ "vitest/importMeta",
8
+ "vite/client",
9
+ "node",
10
+ "vitest"
11
+ ],
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": [
15
+ "vite.config.ts",
16
+ "vite.config.mts",
17
+ "vitest.config.ts",
18
+ "vitest.config.mts",
19
+ "src/**/*.test.ts",
20
+ "src/**/*.spec.ts",
21
+ "src/**/*.test.tsx",
22
+ "src/**/*.spec.tsx",
23
+ "src/**/*.test.js",
24
+ "src/**/*.spec.js",
25
+ "src/**/*.test.jsx",
26
+ "src/**/*.spec.jsx",
27
+ "src/**/*.d.ts"
28
+ ],
29
+ "references": [
30
+ {
31
+ "path": "./tsconfig.lib.json"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig(() => ({
4
+ root: __dirname,
5
+ cacheDir: '../../node_modules/.vite/packages/db',
6
+ test: {
7
+ name: 'db',
8
+ watch: false,
9
+ globals: true,
10
+ environment: 'node',
11
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
12
+ reporters: ['default'],
13
+ coverage: {
14
+ reportsDirectory: './test-output/vitest/coverage',
15
+ provider: 'v8' as const,
16
+ },
17
+ },
18
+ }));