@dismissible/nestjs-postgres-storage 0.0.2-canary.8976e84.0 → 0.0.2-canary.b0d8bfe.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 +19 -23
- package/prisma/generated/prisma/commonInputTypes.ts +0 -75
- package/prisma/generated/prisma/internal/class.ts +4 -4
- package/prisma/generated/prisma/internal/prismaNamespace.ts +5 -37
- 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 +33 -0
- package/src/index.ts +1 -0
- package/src/postgres-storage.adapter.spec.ts +0 -88
- 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 +125 -0
- package/src/prisma.service.ts +11 -1
- package/src/schema-path.spec.ts +22 -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,12 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "dismissible_items" (
|
|
3
|
+
"id" TEXT NOT NULL,
|
|
4
|
+
"user_id" TEXT NOT NULL,
|
|
5
|
+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
6
|
+
"dismissed_at" TIMESTAMP(3),
|
|
7
|
+
|
|
8
|
+
CONSTRAINT "dismissible_items_pkey" PRIMARY KEY ("user_id","id")
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- CreateIndex
|
|
12
|
+
CREATE INDEX "dismissible_items_user_id_idx" ON "dismissible_items"("user_id");
|
package/prisma/schema.prisma
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
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 ?? process.env.DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING ?? '',
|
|
32
|
+
},
|
|
33
|
+
});
|
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';
|
|
@@ -19,17 +18,14 @@ describe('PostgresStorageAdapter', () => {
|
|
|
19
18
|
beforeEach(() => {
|
|
20
19
|
mockLogger = mock<IDismissibleLogger>({ failIfMockNotProvided: false });
|
|
21
20
|
|
|
22
|
-
// Use real DismissibleItemFactory since it's a simple factory
|
|
23
21
|
mockItemFactory = new DismissibleItemFactory();
|
|
24
22
|
|
|
25
|
-
// Create a mock for the dismissibleItem delegate
|
|
26
23
|
mockDismissibleItem = {
|
|
27
24
|
findUnique: jest.fn(),
|
|
28
25
|
create: jest.fn(),
|
|
29
26
|
update: jest.fn(),
|
|
30
27
|
};
|
|
31
28
|
|
|
32
|
-
// Create a partial mock of PrismaService with the dismissibleItem property
|
|
33
29
|
mockPrismaService = {
|
|
34
30
|
dismissibleItem: mockDismissibleItem,
|
|
35
31
|
} as unknown as PrismaService;
|
|
@@ -64,7 +60,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
64
60
|
userId: 'user-123',
|
|
65
61
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
66
62
|
dismissedAt: null,
|
|
67
|
-
metadata: null,
|
|
68
63
|
};
|
|
69
64
|
mockDismissibleItem.findUnique.mockResolvedValue(dbItem);
|
|
70
65
|
|
|
@@ -75,34 +70,12 @@ describe('PostgresStorageAdapter', () => {
|
|
|
75
70
|
userId: 'user-123',
|
|
76
71
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
77
72
|
dismissedAt: undefined,
|
|
78
|
-
metadata: undefined,
|
|
79
73
|
});
|
|
80
74
|
expect(mockLogger.debug).toHaveBeenCalledWith('PostgreSQL storage hit', {
|
|
81
75
|
userId: 'user-123',
|
|
82
76
|
itemId: 'item-456',
|
|
83
77
|
});
|
|
84
78
|
});
|
|
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
79
|
});
|
|
107
80
|
|
|
108
81
|
describe('create', () => {
|
|
@@ -117,7 +90,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
117
90
|
userId: 'user-123',
|
|
118
91
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
119
92
|
dismissedAt: null,
|
|
120
|
-
metadata: null,
|
|
121
93
|
};
|
|
122
94
|
mockDismissibleItem.create.mockResolvedValue(dbItem);
|
|
123
95
|
|
|
@@ -128,7 +100,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
128
100
|
userId: 'user-123',
|
|
129
101
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
130
102
|
dismissedAt: undefined,
|
|
131
|
-
metadata: undefined,
|
|
132
103
|
});
|
|
133
104
|
expect(mockDismissibleItem.create).toHaveBeenCalledWith({
|
|
134
105
|
data: {
|
|
@@ -136,7 +107,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
136
107
|
userId: 'user-123',
|
|
137
108
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
138
109
|
dismissedAt: null,
|
|
139
|
-
metadata: Prisma.JsonNull,
|
|
140
110
|
},
|
|
141
111
|
});
|
|
142
112
|
expect(mockLogger.debug).toHaveBeenCalledWith('PostgreSQL storage create', {
|
|
@@ -144,33 +114,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
144
114
|
itemId: 'item-456',
|
|
145
115
|
});
|
|
146
116
|
});
|
|
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
117
|
});
|
|
175
118
|
|
|
176
119
|
describe('update', () => {
|
|
@@ -186,7 +129,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
186
129
|
userId: 'user-123',
|
|
187
130
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
188
131
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
189
|
-
metadata: null,
|
|
190
132
|
};
|
|
191
133
|
mockDismissibleItem.update.mockResolvedValue(dbItem);
|
|
192
134
|
|
|
@@ -197,7 +139,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
197
139
|
userId: 'user-123',
|
|
198
140
|
createdAt: new Date('2024-01-15T10:30:00.000Z'),
|
|
199
141
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
200
|
-
metadata: undefined,
|
|
201
142
|
});
|
|
202
143
|
expect(mockDismissibleItem.update).toHaveBeenCalledWith({
|
|
203
144
|
where: {
|
|
@@ -208,7 +149,6 @@ describe('PostgresStorageAdapter', () => {
|
|
|
208
149
|
},
|
|
209
150
|
data: {
|
|
210
151
|
dismissedAt: new Date('2024-01-15T12:00:00.000Z'),
|
|
211
|
-
metadata: Prisma.JsonNull,
|
|
212
152
|
},
|
|
213
153
|
});
|
|
214
154
|
expect(mockLogger.debug).toHaveBeenCalledWith('PostgreSQL storage update', {
|
|
@@ -216,33 +156,5 @@ describe('PostgresStorageAdapter', () => {
|
|
|
216
156
|
itemId: 'item-456',
|
|
217
157
|
});
|
|
218
158
|
});
|
|
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
159
|
});
|
|
248
160
|
});
|
|
@@ -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,125 @@
|
|
|
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
|
+
const mockPoolInstance = {};
|
|
7
|
+
const mockPrismaPgInstance = {};
|
|
8
|
+
|
|
9
|
+
jest.mock('pg', () => ({
|
|
10
|
+
Pool: jest.fn().mockImplementation(() => mockPoolInstance),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('@prisma/adapter-pg', () => ({
|
|
14
|
+
PrismaPg: jest.fn().mockImplementation(() => mockPrismaPgInstance),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const mockPrismaClientMethods = {
|
|
18
|
+
$connect: jest.fn(),
|
|
19
|
+
$disconnect: jest.fn(),
|
|
20
|
+
$queryRaw: jest.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
jest.mock('../prisma/generated/prisma/client', () => {
|
|
24
|
+
class MockPrismaClient {
|
|
25
|
+
$connect = jest.fn();
|
|
26
|
+
$disconnect = jest.fn();
|
|
27
|
+
$queryRaw = jest.fn();
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
PrismaClient: MockPrismaClient,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('PrismaService', () => {
|
|
35
|
+
let service: PrismaService;
|
|
36
|
+
let mockConfig: PostgresStorageConfig;
|
|
37
|
+
let mockLogger: jest.Mocked<IDismissibleLogger>;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockConfig = {
|
|
41
|
+
connectionString: 'postgresql://user:password@localhost:5432/testdb',
|
|
42
|
+
};
|
|
43
|
+
mockLogger = mock<IDismissibleLogger>({ failIfMockNotProvided: false });
|
|
44
|
+
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
mockPrismaClientMethods.$connect.mockClear();
|
|
47
|
+
mockPrismaClientMethods.$disconnect.mockClear();
|
|
48
|
+
mockPrismaClientMethods.$queryRaw.mockClear();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('constructor', () => {
|
|
52
|
+
it('should create PrismaService with pool and adapter', () => {
|
|
53
|
+
const { Pool } = require('pg');
|
|
54
|
+
const { PrismaPg } = require('@prisma/adapter-pg');
|
|
55
|
+
|
|
56
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
57
|
+
|
|
58
|
+
expect(service).toBeDefined();
|
|
59
|
+
expect(Pool).toHaveBeenCalledWith({
|
|
60
|
+
connectionString: mockConfig.connectionString,
|
|
61
|
+
});
|
|
62
|
+
expect(PrismaPg).toHaveBeenCalledWith(mockPoolInstance);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('onModuleInit', () => {
|
|
67
|
+
it('should connect to database and verify connection', async () => {
|
|
68
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
69
|
+
(service as any).$connect.mockResolvedValue(undefined);
|
|
70
|
+
(service as any).$queryRaw.mockResolvedValue([{ '?column?': 1 }]);
|
|
71
|
+
|
|
72
|
+
await service.onModuleInit();
|
|
73
|
+
|
|
74
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Connecting to PostgreSQL database');
|
|
75
|
+
expect((service as any).$connect).toHaveBeenCalled();
|
|
76
|
+
expect((service as any).$queryRaw).toHaveBeenCalled();
|
|
77
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Connected to PostgreSQL database');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw error when connection fails', async () => {
|
|
81
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
82
|
+
const connectError = new Error('Connection refused');
|
|
83
|
+
(service as any).$connect = jest.fn().mockRejectedValue(connectError);
|
|
84
|
+
|
|
85
|
+
await expect(service.onModuleInit()).rejects.toThrow('Connection refused');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw error when query verification fails', async () => {
|
|
89
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
90
|
+
(service as any).$connect.mockResolvedValue(undefined);
|
|
91
|
+
(service as any).$queryRaw.mockRejectedValue(new Error('Query failed'));
|
|
92
|
+
|
|
93
|
+
await expect(service.onModuleInit()).rejects.toThrow(
|
|
94
|
+
'Database connection failed: Query failed. Ensure PostgreSQL is running and DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING is configured correctly.',
|
|
95
|
+
);
|
|
96
|
+
expect(mockLogger.error).toHaveBeenCalledWith(
|
|
97
|
+
'Failed to connect to PostgreSQL database',
|
|
98
|
+
expect.any(Error),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle non-Error objects in catch block', async () => {
|
|
103
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
104
|
+
(service as any).$connect.mockResolvedValue(undefined);
|
|
105
|
+
(service as any).$queryRaw.mockRejectedValue('String error');
|
|
106
|
+
|
|
107
|
+
await expect(service.onModuleInit()).rejects.toThrow(
|
|
108
|
+
'Database connection failed: Unknown error. Ensure PostgreSQL is running and DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING is configured correctly.',
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('onModuleDestroy', () => {
|
|
114
|
+
it('should disconnect from database', async () => {
|
|
115
|
+
service = new PrismaService(mockConfig, mockLogger);
|
|
116
|
+
(service as any).$disconnect.mockResolvedValue(undefined);
|
|
117
|
+
|
|
118
|
+
await service.onModuleDestroy();
|
|
119
|
+
|
|
120
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Disconnecting from PostgreSQL database');
|
|
121
|
+
expect((service as any).$disconnect).toHaveBeenCalled();
|
|
122
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('Disconnected from PostgreSQL database');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/prisma.service.ts
CHANGED
|
@@ -23,7 +23,17 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
|
|
23
23
|
async onModuleInit(): Promise<void> {
|
|
24
24
|
this.logger.debug('Connecting to PostgreSQL database');
|
|
25
25
|
await this.$connect();
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await this.$queryRaw`SELECT 1`;
|
|
29
|
+
this.logger.debug('Connected to PostgreSQL database');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.logger.error('Failed to connect to PostgreSQL database', error);
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Database connection failed: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
|
|
34
|
+
'Ensure PostgreSQL is running and DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING is configured correctly.',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
async onModuleDestroy(): Promise<void> {
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
expect(schemaPath).toBe(join(__dirname, '..', 'prisma', 'schema.prisma'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return a valid path structure', () => {
|
|
17
|
+
const schemaPath = getPrismaSchemaPath();
|
|
18
|
+
|
|
19
|
+
expect(schemaPath).toMatch(/^(\/|[A-Z]:\\)/);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|