@dismissible/nestjs-postgres-storage 0.0.2-canary.c91edbc.0 → 0.0.2-canary.d2f56d7.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/README.md +108 -10
- package/bin/dismissible-prisma.js +4 -2
- package/package.json +13 -5
- package/prisma/generated/prisma/commonInputTypes.ts +0 -75
- package/prisma/generated/prisma/internal/class.ts +2 -2
- package/prisma/generated/prisma/internal/prismaNamespace.ts +1 -33
- package/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +1 -19
- package/prisma/generated/prisma/models/DismissibleItem.ts +1 -23
- package/prisma/migrations/20251219094936_init/migration.sql +12 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +0 -1
- package/prisma.config.mjs +35 -0
- package/src/index.ts +1 -0
- package/src/postgres-storage.adapter.spec.ts +0 -85
- package/src/postgres-storage.adapter.ts +6 -17
- package/src/postgres-storage.module.ts +2 -2
- package/src/prisma-config.spec.ts +93 -0
- package/src/prisma-config.ts +63 -0
- package/src/prisma.service.spec.ts +131 -0
- package/src/schema-path.spec.ts +24 -0
- package/src/schema-path.ts +3 -3
- package/jest.config.ts +0 -10
- package/project.json +0 -73
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -14
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { defineConfig } from 'prisma/config';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const prismaDir = join(__dirname, 'prisma');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prisma configuration for @dismissible/nestjs-postgres-storage.
|
|
10
|
+
*
|
|
11
|
+
* This config is used by the dismissible-prisma CLI and can be used directly
|
|
12
|
+
* by consumers who need to extend or customize the configuration.
|
|
13
|
+
*
|
|
14
|
+
* For consumers who want to use the base config in their own prisma.config.mjs:
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```javascript
|
|
18
|
+
* import { defineConfig } from 'prisma/config';
|
|
19
|
+
* import { basePrismaConfig } from '@dismissible/nestjs-postgres-storage';
|
|
20
|
+
*
|
|
21
|
+
* export default defineConfig(basePrismaConfig);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
schema: join(prismaDir, 'schema.prisma'),
|
|
26
|
+
migrations: {
|
|
27
|
+
path: join(prismaDir, 'migrations'),
|
|
28
|
+
},
|
|
29
|
+
datasource: {
|
|
30
|
+
url:
|
|
31
|
+
process.env.DATABASE_URL ??
|
|
32
|
+
process.env.DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING ??
|
|
33
|
+
'',
|
|
34
|
+
},
|
|
35
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { mock } from 'ts-jest-mocker';
|
|
2
|
-
import { Prisma } from '../prisma/generated/prisma/client';
|
|
3
2
|
import { PostgresStorageAdapter } from './postgres-storage.adapter';
|
|
4
3
|
import { PrismaService } from './prisma.service';
|
|
5
4
|
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
@@ -64,7 +63,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
64
63
|
userId: 'user-123',
|
|
65
64
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
66
65
|
dismissedAt: null,
|
|
67
|
-
metadata: null,
|
|
68
66
|
};
|
|
69
67
|
mockDismissibleItem.findUnique.mockResolvedValue(dbItem);
|
|
70
68
|
|
|
@@ -75,34 +73,12 @@ describe('PostgresStorageAdapter', () => {
|
|
|
75
73
|
userId: 'user-123',
|
|
76
74
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
77
75
|
dismissedAt: undefined,
|
|
78
|
-
metadata: undefined,
|
|
79
76
|
});
|
|
80
77
|
expect(mockLogger.debug).toHaveBeenCalledWith('PostgreSQL storage hit', {
|
|
81
78
|
userId: 'user-123',
|
|
82
79
|
itemId: 'item-456',
|
|
83
80
|
});
|
|
84
81
|
});
|
|
85
|
-
|
|
86
|
-
it('should return item with metadata when present', async () => {
|
|
87
|
-
const dbItem = {
|
|
88
|
-
id: 'item-456',
|
|
89
|
-
userId: 'user-123',
|
|
90
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
91
|
-
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
92
|
-
metadata: { version: 2, category: 'promotional' },
|
|
93
|
-
};
|
|
94
|
-
mockDismissibleItem.findUnique.mockResolvedValue(dbItem);
|
|
95
|
-
|
|
96
|
-
const result = await adapter.get('user-123', 'item-456');
|
|
97
|
-
|
|
98
|
-
expect(result).toEqual({
|
|
99
|
-
id: 'item-456',
|
|
100
|
-
userId: 'user-123',
|
|
101
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
102
|
-
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
103
|
-
metadata: { version: 2, category: 'promotional' },
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
82
|
});
|
|
107
83
|
|
|
108
84
|
describe('create', () => {
|
|
@@ -117,7 +93,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
117
93
|
userId: 'user-123',
|
|
118
94
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
119
95
|
dismissedAt: null,
|
|
120
|
-
metadata: null,
|
|
121
96
|
};
|
|
122
97
|
mockDismissibleItem.create.mockResolvedValue(dbItem);
|
|
123
98
|
|
|
@@ -128,7 +103,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
128
103
|
userId: 'user-123',
|
|
129
104
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
130
105
|
dismissedAt: undefined,
|
|
131
|
-
metadata: undefined,
|
|
132
106
|
});
|
|
133
107
|
expect(mockDismissibleItem.create).toHaveBeenCalledWith({
|
|
134
108
|
data: {
|
|
@@ -136,7 +110,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
136
110
|
userId: 'user-123',
|
|
137
111
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
138
112
|
dismissedAt: null,
|
|
139
|
-
metadata: Prisma.JsonNull,
|
|
140
113
|
},
|
|
141
114
|
});
|
|
142
115
|
expect(mockLogger.debug).toHaveBeenCalledWith('PostgreSQL storage create', {
|
|
@@ -144,33 +117,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
144
117
|
itemId: 'item-456',
|
|
145
118
|
});
|
|
146
119
|
});
|
|
147
|
-
|
|
148
|
-
it('should create an item with metadata', async () => {
|
|
149
|
-
const item: DismissibleItemDto = {
|
|
150
|
-
id: 'item-456',
|
|
151
|
-
userId: 'user-123',
|
|
152
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
153
|
-
metadata: { version: 2 },
|
|
154
|
-
};
|
|
155
|
-
const dbItem = {
|
|
156
|
-
id: 'item-456',
|
|
157
|
-
userId: 'user-123',
|
|
158
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
159
|
-
dismissedAt: null,
|
|
160
|
-
metadata: { version: 2 },
|
|
161
|
-
};
|
|
162
|
-
mockDismissibleItem.create.mockResolvedValue(dbItem);
|
|
163
|
-
|
|
164
|
-
const result = await adapter.create(item);
|
|
165
|
-
|
|
166
|
-
expect(result).toEqual({
|
|
167
|
-
id: 'item-456',
|
|
168
|
-
userId: 'user-123',
|
|
169
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
170
|
-
dismissedAt: undefined,
|
|
171
|
-
metadata: { version: 2 },
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
120
|
});
|
|
175
121
|
|
|
176
122
|
describe('update', () => {
|
|
@@ -186,7 +132,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
186
132
|
userId: 'user-123',
|
|
187
133
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
188
134
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
189
|
-
metadata: null,
|
|
190
135
|
};
|
|
191
136
|
mockDismissibleItem.update.mockResolvedValue(dbItem);
|
|
192
137
|
|
|
@@ -197,7 +142,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
197
142
|
userId: 'user-123',
|
|
198
143
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
199
144
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
200
|
-
metadata: undefined,
|
|
201
145
|
});
|
|
202
146
|
expect(mockDismissibleItem.update).toHaveBeenCalledWith({
|
|
203
147
|
where: {
|
|
@@ -208,7 +152,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
208
152
|
},
|
|
209
153
|
data: {
|
|
210
154
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
211
|
-
metadata: Prisma.JsonNull,
|
|
212
155
|
},
|
|
213
156
|
});
|
|
214
157
|
expect(mockLogger.debug).toHaveBeenCalledWith('PostgreSQL storage update', {
|
|
@@ -216,33 +159,5 @@ describe('PostgresStorageAdapter', () => {
|
|
|
216
159
|
itemId: 'item-456',
|
|
217
160
|
});
|
|
218
161
|
});
|
|
219
|
-
|
|
220
|
-
it('should update an item with metadata', async () => {
|
|
221
|
-
const item: DismissibleItemDto = {
|
|
222
|
-
id: 'item-456',
|
|
223
|
-
userId: 'user-123',
|
|
224
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
225
|
-
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
226
|
-
metadata: { version: 3, updated: true },
|
|
227
|
-
};
|
|
228
|
-
const dbItem = {
|
|
229
|
-
id: 'item-456',
|
|
230
|
-
userId: 'user-123',
|
|
231
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
232
|
-
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
233
|
-
metadata: { version: 3, updated: true },
|
|
234
|
-
};
|
|
235
|
-
mockDismissibleItem.update.mockResolvedValue(dbItem);
|
|
236
|
-
|
|
237
|
-
const result = await adapter.update(item);
|
|
238
|
-
|
|
239
|
-
expect(result).toEqual({
|
|
240
|
-
id: 'item-456',
|
|
241
|
-
userId: 'user-123',
|
|
242
|
-
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
243
|
-
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
244
|
-
metadata: { version: 3, updated: true },
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
162
|
});
|
|
248
163
|
});
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { Injectable, Inject } from '@nestjs/common';
|
|
2
2
|
import { DISMISSIBLE_LOGGER, IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
3
3
|
import { IDismissibleStorage } from '@dismissible/nestjs-storage';
|
|
4
|
-
import {
|
|
5
|
-
BaseMetadata,
|
|
6
|
-
DismissibleItemDto,
|
|
7
|
-
DismissibleItemFactory,
|
|
8
|
-
} from '@dismissible/nestjs-dismissible-item';
|
|
9
|
-
import { Prisma } from '../prisma/generated/prisma/client';
|
|
4
|
+
import { DismissibleItemDto, DismissibleItemFactory } from '@dismissible/nestjs-dismissible-item';
|
|
10
5
|
import { PrismaService } from './prisma.service';
|
|
11
6
|
|
|
12
7
|
/**
|
|
@@ -14,16 +9,14 @@ import { PrismaService } from './prisma.service';
|
|
|
14
9
|
* Implements IDismissibleStorage for persistent database storage.
|
|
15
10
|
*/
|
|
16
11
|
@Injectable()
|
|
17
|
-
export class PostgresStorageAdapter
|
|
18
|
-
TMetadata extends BaseMetadata = BaseMetadata,
|
|
19
|
-
> implements IDismissibleStorage<TMetadata> {
|
|
12
|
+
export class PostgresStorageAdapter implements IDismissibleStorage {
|
|
20
13
|
constructor(
|
|
21
14
|
private readonly prisma: PrismaService,
|
|
22
15
|
@Inject(DISMISSIBLE_LOGGER) private readonly logger: IDismissibleLogger,
|
|
23
16
|
private readonly itemFactory: DismissibleItemFactory,
|
|
24
17
|
) {}
|
|
25
18
|
|
|
26
|
-
async get(userId: string, itemId: string): Promise<DismissibleItemDto
|
|
19
|
+
async get(userId: string, itemId: string): Promise<DismissibleItemDto | null> {
|
|
27
20
|
this.logger.debug('PostgreSQL storage get', { userId, itemId });
|
|
28
21
|
|
|
29
22
|
const item = await this.prisma.dismissibleItem.findUnique({
|
|
@@ -45,7 +38,7 @@ export class PostgresStorageAdapter<
|
|
|
45
38
|
return this.mapToDto(item);
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
async create(item: DismissibleItemDto
|
|
41
|
+
async create(item: DismissibleItemDto): Promise<DismissibleItemDto> {
|
|
49
42
|
this.logger.debug('PostgreSQL storage create', { userId: item.userId, itemId: item.id });
|
|
50
43
|
|
|
51
44
|
const created = await this.prisma.dismissibleItem.create({
|
|
@@ -54,14 +47,13 @@ export class PostgresStorageAdapter<
|
|
|
54
47
|
userId: item.userId,
|
|
55
48
|
createdAt: item.createdAt,
|
|
56
49
|
dismissedAt: item.dismissedAt ?? null,
|
|
57
|
-
metadata: (item.metadata as Prisma.InputJsonValue) ?? Prisma.JsonNull,
|
|
58
50
|
},
|
|
59
51
|
});
|
|
60
52
|
|
|
61
53
|
return this.mapToDto(created);
|
|
62
54
|
}
|
|
63
55
|
|
|
64
|
-
async update(item: DismissibleItemDto
|
|
56
|
+
async update(item: DismissibleItemDto): Promise<DismissibleItemDto> {
|
|
65
57
|
this.logger.debug('PostgreSQL storage update', { userId: item.userId, itemId: item.id });
|
|
66
58
|
|
|
67
59
|
const updated = await this.prisma.dismissibleItem.update({
|
|
@@ -73,7 +65,6 @@ export class PostgresStorageAdapter<
|
|
|
73
65
|
},
|
|
74
66
|
data: {
|
|
75
67
|
dismissedAt: item.dismissedAt ?? null,
|
|
76
|
-
metadata: (item.metadata as Prisma.InputJsonValue) ?? Prisma.JsonNull,
|
|
77
68
|
},
|
|
78
69
|
});
|
|
79
70
|
|
|
@@ -88,14 +79,12 @@ export class PostgresStorageAdapter<
|
|
|
88
79
|
userId: string;
|
|
89
80
|
createdAt: Date;
|
|
90
81
|
dismissedAt: Date | null;
|
|
91
|
-
|
|
92
|
-
}): DismissibleItemDto<TMetadata> {
|
|
82
|
+
}): DismissibleItemDto {
|
|
93
83
|
return this.itemFactory.create({
|
|
94
84
|
id: item.id,
|
|
95
85
|
userId: item.userId,
|
|
96
86
|
createdAt: item.createdAt,
|
|
97
87
|
dismissedAt: item.dismissedAt ?? undefined,
|
|
98
|
-
metadata: (item.metadata as TMetadata) ?? undefined,
|
|
99
88
|
});
|
|
100
89
|
}
|
|
101
90
|
}
|
|
@@ -46,7 +46,7 @@ export class PostgresStorageModule {
|
|
|
46
46
|
useExisting: PostgresStorageAdapter,
|
|
47
47
|
},
|
|
48
48
|
],
|
|
49
|
-
exports: [DISMISSIBLE_STORAGE_ADAPTER],
|
|
49
|
+
exports: [DISMISSIBLE_STORAGE_ADAPTER, PrismaService],
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -73,7 +73,7 @@ export class PostgresStorageModule {
|
|
|
73
73
|
useExisting: PostgresStorageAdapter,
|
|
74
74
|
},
|
|
75
75
|
],
|
|
76
|
-
exports: [DISMISSIBLE_STORAGE_ADAPTER],
|
|
76
|
+
exports: [DISMISSIBLE_STORAGE_ADAPTER, PrismaService],
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
|
|
3
|
+
describe('prisma-config', () => {
|
|
4
|
+
const originalEnv = process.env;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
jest.resetModules();
|
|
8
|
+
process.env = { ...originalEnv };
|
|
9
|
+
delete process.env.DATABASE_URL;
|
|
10
|
+
delete process.env.DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
process.env = originalEnv;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('createPrismaConfig', () => {
|
|
18
|
+
it('should return config with schema path relative to package', async () => {
|
|
19
|
+
const { createPrismaConfig } = await import('./prisma-config');
|
|
20
|
+
const config = createPrismaConfig();
|
|
21
|
+
|
|
22
|
+
expect(config.schema).toContain('schema.prisma');
|
|
23
|
+
expect(config.schema).toMatch(/postgres-storage.*prisma.*schema\.prisma$/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return config with migrations path relative to package', async () => {
|
|
27
|
+
const { createPrismaConfig } = await import('./prisma-config');
|
|
28
|
+
const config = createPrismaConfig();
|
|
29
|
+
|
|
30
|
+
expect(config.migrations.path).toContain('migrations');
|
|
31
|
+
expect(config.migrations.path).toMatch(/postgres-storage.*prisma.*migrations$/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should use DATABASE_URL when set', async () => {
|
|
35
|
+
process.env.DATABASE_URL = 'postgres://test:test@localhost/test';
|
|
36
|
+
|
|
37
|
+
const { createPrismaConfig } = await import('./prisma-config');
|
|
38
|
+
const config = createPrismaConfig();
|
|
39
|
+
|
|
40
|
+
expect(config.datasource.url).toBe('postgres://test:test@localhost/test');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should fall back to DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING when DATABASE_URL is not set', async () => {
|
|
44
|
+
process.env.DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING =
|
|
45
|
+
'postgres://fallback:fallback@localhost/fallback';
|
|
46
|
+
|
|
47
|
+
const { createPrismaConfig } = await import('./prisma-config');
|
|
48
|
+
const config = createPrismaConfig();
|
|
49
|
+
|
|
50
|
+
expect(config.datasource.url).toBe('postgres://fallback:fallback@localhost/fallback');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should prefer DATABASE_URL over DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING', async () => {
|
|
54
|
+
process.env.DATABASE_URL = 'postgres://primary:primary@localhost/primary';
|
|
55
|
+
process.env.DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING =
|
|
56
|
+
'postgres://fallback:fallback@localhost/fallback';
|
|
57
|
+
|
|
58
|
+
const { createPrismaConfig } = await import('./prisma-config');
|
|
59
|
+
const config = createPrismaConfig();
|
|
60
|
+
|
|
61
|
+
expect(config.datasource.url).toBe('postgres://primary:primary@localhost/primary');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return empty string when no database URL is set', async () => {
|
|
65
|
+
const { createPrismaConfig } = await import('./prisma-config');
|
|
66
|
+
const config = createPrismaConfig();
|
|
67
|
+
|
|
68
|
+
expect(config.datasource.url).toBe('');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('basePrismaConfig', () => {
|
|
73
|
+
it('should be exported and have the expected structure', async () => {
|
|
74
|
+
const { basePrismaConfig } = await import('./prisma-config');
|
|
75
|
+
|
|
76
|
+
expect(basePrismaConfig).toBeDefined();
|
|
77
|
+
expect(basePrismaConfig).toHaveProperty('schema');
|
|
78
|
+
expect(basePrismaConfig).toHaveProperty('migrations');
|
|
79
|
+
expect(basePrismaConfig).toHaveProperty('datasource');
|
|
80
|
+
expect(basePrismaConfig.migrations).toHaveProperty('path');
|
|
81
|
+
expect(basePrismaConfig.datasource).toHaveProperty('url');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should have schema and migrations paths pointing to the same prisma directory', async () => {
|
|
85
|
+
const { basePrismaConfig } = await import('./prisma-config');
|
|
86
|
+
|
|
87
|
+
const schemaDir = join(basePrismaConfig.schema, '..');
|
|
88
|
+
const migrationsDir = join(basePrismaConfig.migrations.path, '..');
|
|
89
|
+
|
|
90
|
+
expect(schemaDir).toBe(migrationsDir);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { getPrismaSchemaPath } from './schema-path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Prisma configuration object with paths resolved relative to
|
|
6
|
+
* the @dismissible/nestjs-postgres-storage package.
|
|
7
|
+
*
|
|
8
|
+
* @returns Prisma configuration object suitable for use with defineConfig()
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // prisma.config.mjs
|
|
13
|
+
* import { defineConfig } from 'prisma/config';
|
|
14
|
+
* import { basePrismaConfig } from '@dismissible/nestjs-postgres-storage';
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig(basePrismaConfig);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function createPrismaConfig() {
|
|
20
|
+
const prismaDir = join(getPrismaSchemaPath(), '..');
|
|
21
|
+
return {
|
|
22
|
+
schema: join(prismaDir, 'schema.prisma'),
|
|
23
|
+
migrations: {
|
|
24
|
+
path: join(prismaDir, 'migrations'),
|
|
25
|
+
},
|
|
26
|
+
datasource: {
|
|
27
|
+
url:
|
|
28
|
+
process.env.DATABASE_URL ??
|
|
29
|
+
process.env.DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING ??
|
|
30
|
+
'',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Base Prisma configuration for @dismissible/nestjs-postgres-storage.
|
|
37
|
+
*
|
|
38
|
+
* Use this with Prisma's defineConfig() to create your prisma.config.mjs:
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```javascript
|
|
42
|
+
* // prisma.config.mjs
|
|
43
|
+
* import { defineConfig } from 'prisma/config';
|
|
44
|
+
* import { basePrismaConfig } from '@dismissible/nestjs-postgres-storage';
|
|
45
|
+
*
|
|
46
|
+
* export default defineConfig(basePrismaConfig);
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example Extending the config
|
|
50
|
+
* ```javascript
|
|
51
|
+
* import { defineConfig } from 'prisma/config';
|
|
52
|
+
* import { basePrismaConfig } from '@dismissible/nestjs-postgres-storage';
|
|
53
|
+
*
|
|
54
|
+
* export default defineConfig({
|
|
55
|
+
* ...basePrismaConfig,
|
|
56
|
+
* migrations: {
|
|
57
|
+
* ...basePrismaConfig.migrations,
|
|
58
|
+
* seed: 'tsx prisma/seed.ts',
|
|
59
|
+
* },
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export const basePrismaConfig = createPrismaConfig();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { mock } from 'ts-jest-mocker';
|
|
2
|
+
import { PrismaService } from './prisma.service';
|
|
3
|
+
import { PostgresStorageConfig } from './postgres-storage.config';
|
|
4
|
+
import { IDismissibleLogger } from '@dismissible/nestjs-logger';
|
|
5
|
+
|
|
6
|
+
// Mock the modules before importing PrismaService
|
|
7
|
+
const mockPoolInstance = {};
|
|
8
|
+
const mockPrismaPgInstance = {};
|
|
9
|
+
|
|
10
|
+
jest.mock('pg', () => ({
|
|
11
|
+
Pool: jest.fn().mockImplementation(() => mockPoolInstance),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('@prisma/adapter-pg', () => ({
|
|
15
|
+
PrismaPg: jest.fn().mockImplementation(() => mockPrismaPgInstance),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock PrismaClient to avoid actual initialization
|
|
19
|
+
const mockPrismaClientMethods = {
|
|
20
|
+
$connect: jest.fn(),
|
|
21
|
+
$disconnect: jest.fn(),
|
|
22
|
+
$queryRaw: jest.fn(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
jest.mock('../prisma/generated/prisma/client', () => {
|
|
26
|
+
// Create a mock PrismaClient class inside the factory
|
|
27
|
+
class MockPrismaClient {
|
|
28
|
+
$connect = jest.fn();
|
|
29
|
+
$disconnect = jest.fn();
|
|
30
|
+
$queryRaw = jest.fn();
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
PrismaClient: MockPrismaClient,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('PrismaService', () => {
|
|
38
|
+
let service: PrismaService;
|
|
39
|
+
let mockConfig: PostgresStorageConfig;
|
|
40
|
+
let mockLogger: jest.Mocked<IDismissibleLogger>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockConfig = {
|
|
44
|
+
connectionString: 'postgresql://user:password@localhost:5432/testdb',
|
|
45
|
+
};
|
|
46
|
+
mockLogger = mock<IDismissibleLogger>({ failIfMockNotProvided: false });
|
|
47
|
+
|
|
48
|
+
// Clear all mocks
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
mockPrismaClientMethods.$connect.mockClear();
|
|
51
|
+
mockPrismaClientMethods.$disconnect.mockClear();
|
|
52
|
+
mockPrismaClientMethods.$queryRaw.mockClear();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('constructor', () => {
|
|
56
|
+
it('should create PrismaService with pool and adapter', () => {
|
|
57
|
+
const { Pool } = require('pg');
|
|
58
|
+
const { PrismaPg } = require('@prisma/adapter-pg');
|
|
59
|
+
|
|
60
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
61
|
+
|
|
62
|
+
expect(service).toBeDefined();
|
|
63
|
+
expect(Pool).toHaveBeenCalledWith({
|
|
64
|
+
connectionString: mockConfig.connectionString,
|
|
65
|
+
});
|
|
66
|
+
expect(PrismaPg).toHaveBeenCalledWith(mockPoolInstance);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('onModuleInit', () => {
|
|
71
|
+
it('should connect to database and verify connection', async () => {
|
|
72
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
73
|
+
(service as any).$connect.mockResolvedValue(undefined);
|
|
74
|
+
(service as any).$queryRaw.mockResolvedValue([{ '?column?': 1 }]);
|
|
75
|
+
|
|
76
|
+
await service.onModuleInit();
|
|
77
|
+
|
|
78
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Connecting to PostgreSQL database');
|
|
79
|
+
expect((service as any).$connect).toHaveBeenCalled();
|
|
80
|
+
expect((service as any).$queryRaw).toHaveBeenCalled();
|
|
81
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Connected to PostgreSQL database');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should throw error when connection fails', async () => {
|
|
85
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
86
|
+
const connectError = new Error('Connection refused');
|
|
87
|
+
(service as any).$connect = jest.fn().mockRejectedValue(connectError);
|
|
88
|
+
|
|
89
|
+
// $connect() is called before the try-catch, so errors propagate directly
|
|
90
|
+
await expect(service.onModuleInit()).rejects.toThrow('Connection refused');
|
|
91
|
+
// Note: $connect errors are not logged because they're outside the try-catch
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw error when query verification fails', async () => {
|
|
95
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
96
|
+
(service as any).$connect.mockResolvedValue(undefined);
|
|
97
|
+
(service as any).$queryRaw.mockRejectedValue(new Error('Query failed'));
|
|
98
|
+
|
|
99
|
+
await expect(service.onModuleInit()).rejects.toThrow(
|
|
100
|
+
'Database connection failed: Query failed. Ensure PostgreSQL is running and DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING is configured correctly.',
|
|
101
|
+
);
|
|
102
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
103
|
+
'Failed to connect to PostgreSQL database',
|
|
104
|
+
expect.any(Error),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle non-Error objects in catch block', async () => {
|
|
109
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
110
|
+
(service as any).$connect.mockResolvedValue(undefined);
|
|
111
|
+
(service as any).$queryRaw.mockRejectedValue('String error');
|
|
112
|
+
|
|
113
|
+
await expect(service.onModuleInit()).rejects.toThrow(
|
|
114
|
+
'Database connection failed: Unknown error. Ensure PostgreSQL is running and DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING is configured correctly.',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('onModuleDestroy', () => {
|
|
120
|
+
it('should disconnect from database', async () => {
|
|
121
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
122
|
+
(service as any).$disconnect.mockResolvedValue(undefined);
|
|
123
|
+
|
|
124
|
+
await service.onModuleDestroy();
|
|
125
|
+
|
|
126
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Disconnecting from PostgreSQL database');
|
|
127
|
+
expect((service as any).$disconnect).toHaveBeenCalled();
|
|
128
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Disconnected from PostgreSQL database');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getPrismaSchemaPath } from './schema-path';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
describe('schema-path', () => {
|
|
5
|
+
describe('getPrismaSchemaPath', () => {
|
|
6
|
+
it('should return the absolute path to the Prisma schema file', () => {
|
|
7
|
+
const schemaPath = getPrismaSchemaPath();
|
|
8
|
+
|
|
9
|
+
expect(schemaPath).toBeDefined();
|
|
10
|
+
expect(typeof schemaPath).toBe('string');
|
|
11
|
+
expect(schemaPath).toContain('prisma');
|
|
12
|
+
expect(schemaPath).toContain('schema.prisma');
|
|
13
|
+
// Verify it uses join to construct the path
|
|
14
|
+
expect(schemaPath).toBe(join(__dirname, '..', 'prisma', 'schema.prisma'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return a valid path structure', () => {
|
|
18
|
+
const schemaPath = getPrismaSchemaPath();
|
|
19
|
+
|
|
20
|
+
// Path should be absolute (starts with / on Unix, or C:\ on Windows)
|
|
21
|
+
expect(schemaPath).toMatch(/^(\/|[A-Z]:\\)/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
package/src/schema-path.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { join } from 'path';
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```typescript
|
|
9
|
-
* import {
|
|
9
|
+
* import { getPrismaSchemaPath } from '@dismissible/nestjs-postgres-storage';
|
|
10
10
|
* import { execSync } from 'child_process';
|
|
11
11
|
*
|
|
12
|
-
* execSync(`npx prisma generate --schema=${
|
|
12
|
+
* execSync(`npx prisma generate --schema=${getPrismaSchemaPath()}`);
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
export function
|
|
15
|
+
export function getPrismaSchemaPath(): string {
|
|
16
16
|
return join(__dirname, '..', 'prisma', 'schema.prisma');
|
|
17
17
|
}
|
package/jest.config.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
displayName: 'postgres-storage',
|
|
3
|
-
preset: '../../jest.preset.js',
|
|
4
|
-
testEnvironment: 'node',
|
|
5
|
-
transform: {
|
|
6
|
-
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
|
|
7
|
-
},
|
|
8
|
-
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
9
|
-
coverageDirectory: '../../coverage/libs/postgres-storage',
|
|
10
|
-
};
|