@tantainnovative/create-ndpr 0.1.0 → 0.4.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 +28 -5
- package/bin/index.mjs +146 -11
- package/package.json +14 -4
- package/templates/drizzle-schema.ts +92 -7
- package/templates/express-consent-route.ts +111 -5
- package/templates/express-cross-border-route.ts +317 -0
- package/templates/express-dpia-route.ts +287 -0
- package/templates/express-lawful-basis-route.ts +292 -0
- package/templates/express-setup.ts +262 -3
- package/templates/github-ndpr-audit.yml +31 -0
- package/templates/ndpr-audit.json +14 -0
- package/templates/nextjs-breach-route.ts +133 -1
- package/templates/nextjs-consent-route.ts +93 -28
- package/templates/nextjs-cross-border-route.ts +325 -0
- package/templates/nextjs-dpia-route.ts +290 -0
- package/templates/nextjs-dsr-route.ts +84 -3
- package/templates/nextjs-lawful-basis-route.ts +297 -0
- package/templates/nextjs-layout.tsx +12 -3
- package/templates/prisma-schema.prisma +63 -5
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express — Cross-Border Transfer Router
|
|
3
|
+
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
+
* DPO: {{DPO_EMAIL}}
|
|
5
|
+
*
|
|
6
|
+
* NDPA §41–43 — transfers of personal data outside Nigeria require
|
|
7
|
+
* adequate safeguards and, in some cases, NDPC approval.
|
|
8
|
+
*
|
|
9
|
+
* Routes:
|
|
10
|
+
* GET /cross-border — List transfers (optional ?riskLevel= or ?destinationCountry= filter)
|
|
11
|
+
* GET /cross-border/:id — Get a single transfer record
|
|
12
|
+
* POST /cross-border — Register a new cross-border transfer
|
|
13
|
+
* PUT /cross-border/:id — Update an existing transfer record
|
|
14
|
+
* DELETE /cross-border/:id — Delete a transfer record
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { crossBorderRouter } from './routes/cross-border';
|
|
18
|
+
* app.use('/api/cross-border', crossBorderRouter);
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Router } from 'express';
|
|
22
|
+
// {{#if ORM=prisma}}
|
|
23
|
+
import { PrismaClient } from '@prisma/client';
|
|
24
|
+
|
|
25
|
+
const prisma = new PrismaClient();
|
|
26
|
+
// {{/if}}
|
|
27
|
+
// {{#if ORM=drizzle}}
|
|
28
|
+
import { db } from '@/drizzle';
|
|
29
|
+
import { crossBorderTransferRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
|
|
30
|
+
import { eq, and, desc, type SQL } from 'drizzle-orm';
|
|
31
|
+
// {{/if}}
|
|
32
|
+
// {{#if ORM=none}}
|
|
33
|
+
// TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
|
|
34
|
+
// in-memory map below is enough to develop against locally but DOES NOT
|
|
35
|
+
// satisfy NDPA Section 44 (record-keeping) or the cross-border register
|
|
36
|
+
// NDPC inspects under Sections 41–43. Replace `crossBorderStore`/`auditLog`
|
|
37
|
+
// with your DB / KV / API.
|
|
38
|
+
interface CrossBorderTransferRecord {
|
|
39
|
+
id: string;
|
|
40
|
+
destinationCountry: string;
|
|
41
|
+
recipientName: string;
|
|
42
|
+
transferMechanism: string;
|
|
43
|
+
safeguards: string;
|
|
44
|
+
dataCategories: string[];
|
|
45
|
+
adequacyStatus: string;
|
|
46
|
+
ndpcApprovalRequired: boolean;
|
|
47
|
+
ndpcApprovalReference: string | null;
|
|
48
|
+
riskLevel: string;
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
updatedAt: Date;
|
|
51
|
+
}
|
|
52
|
+
const crossBorderStore = new Map<string, CrossBorderTransferRecord>();
|
|
53
|
+
const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
|
|
54
|
+
function newId() { return crypto.randomUUID(); }
|
|
55
|
+
// {{/if}}
|
|
56
|
+
|
|
57
|
+
export const crossBorderRouter = Router();
|
|
58
|
+
|
|
59
|
+
// GET /cross-border?riskLevel=high&destinationCountry=US
|
|
60
|
+
crossBorderRouter.get('/', async (req, res) => {
|
|
61
|
+
const { riskLevel, destinationCountry } = req.query;
|
|
62
|
+
const riskLevelFilter = typeof riskLevel === 'string' ? riskLevel : undefined;
|
|
63
|
+
const destinationCountryFilter = typeof destinationCountry === 'string' ? destinationCountry : undefined;
|
|
64
|
+
|
|
65
|
+
// {{#if ORM=prisma}}
|
|
66
|
+
const where: Record<string, string> = {};
|
|
67
|
+
if (riskLevelFilter) where.riskLevel = riskLevelFilter;
|
|
68
|
+
if (destinationCountryFilter) where.destinationCountry = destinationCountryFilter;
|
|
69
|
+
|
|
70
|
+
const records = await prisma.crossBorderTransferRecord.findMany({
|
|
71
|
+
where: Object.keys(where).length > 0 ? where : undefined,
|
|
72
|
+
orderBy: { createdAt: 'desc' },
|
|
73
|
+
});
|
|
74
|
+
// {{/if}}
|
|
75
|
+
// {{#if ORM=drizzle}}
|
|
76
|
+
const filters: SQL[] = [];
|
|
77
|
+
if (riskLevelFilter) filters.push(eq(crossBorderTransferRecords.riskLevel, riskLevelFilter));
|
|
78
|
+
if (destinationCountryFilter) filters.push(eq(crossBorderTransferRecords.destinationCountry, destinationCountryFilter));
|
|
79
|
+
|
|
80
|
+
const records = filters.length > 0
|
|
81
|
+
? await db.select().from(crossBorderTransferRecords).where(and(...filters)).orderBy(desc(crossBorderTransferRecords.createdAt))
|
|
82
|
+
: await db.select().from(crossBorderTransferRecords).orderBy(desc(crossBorderTransferRecords.createdAt));
|
|
83
|
+
// {{/if}}
|
|
84
|
+
// {{#if ORM=none}}
|
|
85
|
+
const records = [...crossBorderStore.values()]
|
|
86
|
+
.filter((r) => (riskLevelFilter ? r.riskLevel === riskLevelFilter : true))
|
|
87
|
+
.filter((r) => (destinationCountryFilter ? r.destinationCountry === destinationCountryFilter : true))
|
|
88
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
89
|
+
// {{/if}}
|
|
90
|
+
|
|
91
|
+
return res.json(records);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// GET /cross-border/:id
|
|
95
|
+
crossBorderRouter.get('/:id', async (req, res) => {
|
|
96
|
+
// {{#if ORM=prisma}}
|
|
97
|
+
const record = await prisma.crossBorderTransferRecord.findUnique({ where: { id: req.params.id } });
|
|
98
|
+
// {{/if}}
|
|
99
|
+
// {{#if ORM=drizzle}}
|
|
100
|
+
const [record] = await db.select().from(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, req.params.id)).limit(1);
|
|
101
|
+
// {{/if}}
|
|
102
|
+
// {{#if ORM=none}}
|
|
103
|
+
const record = crossBorderStore.get(req.params.id) ?? null;
|
|
104
|
+
// {{/if}}
|
|
105
|
+
|
|
106
|
+
if (!record) {
|
|
107
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return res.json(record);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// POST /cross-border
|
|
114
|
+
crossBorderRouter.post('/', async (req, res) => {
|
|
115
|
+
const {
|
|
116
|
+
destinationCountry,
|
|
117
|
+
recipientName,
|
|
118
|
+
transferMechanism,
|
|
119
|
+
safeguards,
|
|
120
|
+
dataCategories,
|
|
121
|
+
adequacyStatus,
|
|
122
|
+
ndpcApprovalRequired,
|
|
123
|
+
ndpcApprovalReference,
|
|
124
|
+
riskLevel,
|
|
125
|
+
} = req.body;
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
!destinationCountry ||
|
|
129
|
+
!recipientName ||
|
|
130
|
+
!transferMechanism ||
|
|
131
|
+
!safeguards ||
|
|
132
|
+
!dataCategories ||
|
|
133
|
+
!adequacyStatus ||
|
|
134
|
+
!riskLevel
|
|
135
|
+
) {
|
|
136
|
+
return res.status(400).json({
|
|
137
|
+
error:
|
|
138
|
+
'destinationCountry, recipientName, transferMechanism, safeguards, dataCategories, adequacyStatus, and riskLevel are required',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!Array.isArray(dataCategories)) {
|
|
143
|
+
return res.status(400).json({ error: 'dataCategories must be an array' });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// {{#if ORM=prisma}}
|
|
147
|
+
const record = await prisma.crossBorderTransferRecord.create({
|
|
148
|
+
data: {
|
|
149
|
+
destinationCountry,
|
|
150
|
+
recipientName,
|
|
151
|
+
transferMechanism,
|
|
152
|
+
safeguards,
|
|
153
|
+
dataCategories,
|
|
154
|
+
adequacyStatus,
|
|
155
|
+
ndpcApprovalRequired: ndpcApprovalRequired ?? false,
|
|
156
|
+
ndpcApprovalReference: ndpcApprovalReference ?? null,
|
|
157
|
+
riskLevel,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await prisma.complianceAuditLog.create({
|
|
162
|
+
data: {
|
|
163
|
+
module: 'cross-border',
|
|
164
|
+
action: 'created',
|
|
165
|
+
entityId: record.id,
|
|
166
|
+
entityType: 'CrossBorderTransferRecord',
|
|
167
|
+
changes: { destinationCountry, recipientName, riskLevel },
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
// {{/if}}
|
|
171
|
+
// {{#if ORM=drizzle}}
|
|
172
|
+
const [record] = await db.insert(crossBorderTransferRecords).values({
|
|
173
|
+
destinationCountry,
|
|
174
|
+
recipientName,
|
|
175
|
+
transferMechanism,
|
|
176
|
+
safeguards,
|
|
177
|
+
dataCategories,
|
|
178
|
+
adequacyStatus,
|
|
179
|
+
ndpcApprovalRequired: ndpcApprovalRequired ?? false,
|
|
180
|
+
ndpcApprovalReference: ndpcApprovalReference ?? null,
|
|
181
|
+
riskLevel,
|
|
182
|
+
}).returning();
|
|
183
|
+
|
|
184
|
+
await db.insert(complianceAuditLog).values({
|
|
185
|
+
module: 'cross-border',
|
|
186
|
+
action: 'created',
|
|
187
|
+
entityId: record.id,
|
|
188
|
+
entityType: 'CrossBorderTransferRecord',
|
|
189
|
+
changes: { destinationCountry, recipientName, riskLevel },
|
|
190
|
+
});
|
|
191
|
+
// {{/if}}
|
|
192
|
+
// {{#if ORM=none}}
|
|
193
|
+
const now = new Date();
|
|
194
|
+
const record: CrossBorderTransferRecord = {
|
|
195
|
+
id: newId(),
|
|
196
|
+
destinationCountry,
|
|
197
|
+
recipientName,
|
|
198
|
+
transferMechanism,
|
|
199
|
+
safeguards,
|
|
200
|
+
dataCategories,
|
|
201
|
+
adequacyStatus,
|
|
202
|
+
ndpcApprovalRequired: ndpcApprovalRequired ?? false,
|
|
203
|
+
ndpcApprovalReference: ndpcApprovalReference ?? null,
|
|
204
|
+
riskLevel,
|
|
205
|
+
createdAt: now,
|
|
206
|
+
updatedAt: now,
|
|
207
|
+
};
|
|
208
|
+
crossBorderStore.set(record.id, record);
|
|
209
|
+
auditLog.push({ id: newId(), module: 'cross-border', action: 'created', entityId: record.id, at: new Date() });
|
|
210
|
+
// {{/if}}
|
|
211
|
+
|
|
212
|
+
return res.status(201).json(record);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// PUT /cross-border/:id
|
|
216
|
+
crossBorderRouter.put('/:id', async (req, res) => {
|
|
217
|
+
const { id } = req.params;
|
|
218
|
+
|
|
219
|
+
// {{#if ORM=prisma}}
|
|
220
|
+
const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
|
|
221
|
+
if (!existing) {
|
|
222
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const record = await prisma.crossBorderTransferRecord.update({
|
|
226
|
+
where: { id },
|
|
227
|
+
data: req.body,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await prisma.complianceAuditLog.create({
|
|
231
|
+
data: {
|
|
232
|
+
module: 'cross-border',
|
|
233
|
+
action: 'updated',
|
|
234
|
+
entityId: record.id,
|
|
235
|
+
entityType: 'CrossBorderTransferRecord',
|
|
236
|
+
changes: req.body,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
// {{/if}}
|
|
240
|
+
// {{#if ORM=drizzle}}
|
|
241
|
+
const [existing] = await db.select().from(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id)).limit(1);
|
|
242
|
+
if (!existing) {
|
|
243
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const [record] = await db.update(crossBorderTransferRecords).set(req.body).where(eq(crossBorderTransferRecords.id, id)).returning();
|
|
247
|
+
|
|
248
|
+
await db.insert(complianceAuditLog).values({
|
|
249
|
+
module: 'cross-border',
|
|
250
|
+
action: 'updated',
|
|
251
|
+
entityId: record.id,
|
|
252
|
+
entityType: 'CrossBorderTransferRecord',
|
|
253
|
+
changes: req.body,
|
|
254
|
+
});
|
|
255
|
+
// {{/if}}
|
|
256
|
+
// {{#if ORM=none}}
|
|
257
|
+
const existing = crossBorderStore.get(id);
|
|
258
|
+
if (!existing) {
|
|
259
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
260
|
+
}
|
|
261
|
+
const record: CrossBorderTransferRecord = { ...existing, ...req.body, id, updatedAt: new Date() };
|
|
262
|
+
crossBorderStore.set(id, record);
|
|
263
|
+
auditLog.push({ id: newId(), module: 'cross-border', action: 'updated', entityId: record.id, at: new Date() });
|
|
264
|
+
// {{/if}}
|
|
265
|
+
|
|
266
|
+
return res.json(record);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// DELETE /cross-border/:id
|
|
270
|
+
crossBorderRouter.delete('/:id', async (req, res) => {
|
|
271
|
+
const { id } = req.params;
|
|
272
|
+
|
|
273
|
+
// {{#if ORM=prisma}}
|
|
274
|
+
const existing = await prisma.crossBorderTransferRecord.findUnique({ where: { id } });
|
|
275
|
+
if (!existing) {
|
|
276
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await prisma.crossBorderTransferRecord.delete({ where: { id } });
|
|
280
|
+
|
|
281
|
+
await prisma.complianceAuditLog.create({
|
|
282
|
+
data: {
|
|
283
|
+
module: 'cross-border',
|
|
284
|
+
action: 'deleted',
|
|
285
|
+
entityId: id,
|
|
286
|
+
entityType: 'CrossBorderTransferRecord',
|
|
287
|
+
changes: { destinationCountry: existing.destinationCountry, recipientName: existing.recipientName },
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
// {{/if}}
|
|
291
|
+
// {{#if ORM=drizzle}}
|
|
292
|
+
const [existing] = await db.select().from(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id)).limit(1);
|
|
293
|
+
if (!existing) {
|
|
294
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await db.delete(crossBorderTransferRecords).where(eq(crossBorderTransferRecords.id, id));
|
|
298
|
+
|
|
299
|
+
await db.insert(complianceAuditLog).values({
|
|
300
|
+
module: 'cross-border',
|
|
301
|
+
action: 'deleted',
|
|
302
|
+
entityId: id,
|
|
303
|
+
entityType: 'CrossBorderTransferRecord',
|
|
304
|
+
changes: { destinationCountry: existing.destinationCountry, recipientName: existing.recipientName },
|
|
305
|
+
});
|
|
306
|
+
// {{/if}}
|
|
307
|
+
// {{#if ORM=none}}
|
|
308
|
+
const existing = crossBorderStore.get(id);
|
|
309
|
+
if (!existing) {
|
|
310
|
+
return res.status(404).json({ error: 'Cross-border transfer record not found' });
|
|
311
|
+
}
|
|
312
|
+
crossBorderStore.delete(id);
|
|
313
|
+
auditLog.push({ id: newId(), module: 'cross-border', action: 'deleted', entityId: id, at: new Date() });
|
|
314
|
+
// {{/if}}
|
|
315
|
+
|
|
316
|
+
return res.json({ success: true });
|
|
317
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express — DPIA (Data Protection Impact Assessment) Router
|
|
3
|
+
* Generated by create-ndpr for: {{ORG_NAME}}
|
|
4
|
+
* DPO: {{DPO_EMAIL}}
|
|
5
|
+
*
|
|
6
|
+
* NDPA §28 — a DPIA must be conducted where processing is likely
|
|
7
|
+
* to result in a high risk to the rights and freedoms of data subjects.
|
|
8
|
+
*
|
|
9
|
+
* Routes:
|
|
10
|
+
* GET /dpia — List DPIA records (optional ?status= filter)
|
|
11
|
+
* GET /dpia/:id — Get a single DPIA record
|
|
12
|
+
* POST /dpia — Create a new DPIA record
|
|
13
|
+
* PUT /dpia/:id — Update an existing DPIA record
|
|
14
|
+
* DELETE /dpia/:id — Delete a DPIA record
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* import { dpiaRouter } from './routes/dpia';
|
|
18
|
+
* app.use('/api/dpia', dpiaRouter);
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Router } from 'express';
|
|
22
|
+
// {{#if ORM=prisma}}
|
|
23
|
+
import { PrismaClient } from '@prisma/client';
|
|
24
|
+
|
|
25
|
+
const prisma = new PrismaClient();
|
|
26
|
+
// {{/if}}
|
|
27
|
+
// {{#if ORM=drizzle}}
|
|
28
|
+
import { db } from '@/drizzle';
|
|
29
|
+
import { dpiaRecords, complianceAuditLog } from '@/drizzle/ndpr-schema';
|
|
30
|
+
import { eq, desc } from 'drizzle-orm';
|
|
31
|
+
// {{/if}}
|
|
32
|
+
// {{#if ORM=none}}
|
|
33
|
+
// TODO ({{ORG_NAME}}): wire this to your persistent store of choice. The
|
|
34
|
+
// in-memory map below is enough to develop against locally but DOES NOT
|
|
35
|
+
// satisfy NDPA Section 44 (record-keeping). Replace `dpiaStore`/`auditLog`
|
|
36
|
+
// with your DB / KV / API.
|
|
37
|
+
interface DPIARecord {
|
|
38
|
+
id: string;
|
|
39
|
+
projectName: string;
|
|
40
|
+
description: string;
|
|
41
|
+
dpiaData: unknown;
|
|
42
|
+
overallRisk: string;
|
|
43
|
+
score: number;
|
|
44
|
+
status: string;
|
|
45
|
+
conductedBy: string;
|
|
46
|
+
approvedBy: string | null;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
updatedAt: Date;
|
|
49
|
+
}
|
|
50
|
+
const dpiaStore = new Map<string, DPIARecord>();
|
|
51
|
+
const auditLog: Array<{ id: string; module: string; action: string; entityId: string; at: Date }> = [];
|
|
52
|
+
function newId() { return crypto.randomUUID(); }
|
|
53
|
+
// {{/if}}
|
|
54
|
+
|
|
55
|
+
export const dpiaRouter = Router();
|
|
56
|
+
|
|
57
|
+
// GET /dpia?status=draft
|
|
58
|
+
dpiaRouter.get('/', async (req, res) => {
|
|
59
|
+
const { status } = req.query;
|
|
60
|
+
const statusFilter = typeof status === 'string' ? status : undefined;
|
|
61
|
+
|
|
62
|
+
// {{#if ORM=prisma}}
|
|
63
|
+
const records = await prisma.dPIARecord.findMany({
|
|
64
|
+
where: statusFilter ? { status: statusFilter } : undefined,
|
|
65
|
+
orderBy: { createdAt: 'desc' },
|
|
66
|
+
});
|
|
67
|
+
// {{/if}}
|
|
68
|
+
// {{#if ORM=drizzle}}
|
|
69
|
+
const records = statusFilter
|
|
70
|
+
? await db.select().from(dpiaRecords).where(eq(dpiaRecords.status, statusFilter)).orderBy(desc(dpiaRecords.createdAt))
|
|
71
|
+
: await db.select().from(dpiaRecords).orderBy(desc(dpiaRecords.createdAt));
|
|
72
|
+
// {{/if}}
|
|
73
|
+
// {{#if ORM=none}}
|
|
74
|
+
const records = [...dpiaStore.values()]
|
|
75
|
+
.filter((r) => (statusFilter ? r.status === statusFilter : true))
|
|
76
|
+
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
77
|
+
// {{/if}}
|
|
78
|
+
|
|
79
|
+
return res.json(records);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// GET /dpia/:id
|
|
83
|
+
dpiaRouter.get('/:id', async (req, res) => {
|
|
84
|
+
// {{#if ORM=prisma}}
|
|
85
|
+
const record = await prisma.dPIARecord.findUnique({ where: { id: req.params.id } });
|
|
86
|
+
// {{/if}}
|
|
87
|
+
// {{#if ORM=drizzle}}
|
|
88
|
+
const [record] = await db.select().from(dpiaRecords).where(eq(dpiaRecords.id, req.params.id)).limit(1);
|
|
89
|
+
// {{/if}}
|
|
90
|
+
// {{#if ORM=none}}
|
|
91
|
+
const record = dpiaStore.get(req.params.id) ?? null;
|
|
92
|
+
// {{/if}}
|
|
93
|
+
|
|
94
|
+
if (!record) {
|
|
95
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return res.json(record);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// POST /dpia
|
|
102
|
+
dpiaRouter.post('/', async (req, res) => {
|
|
103
|
+
const {
|
|
104
|
+
projectName,
|
|
105
|
+
description,
|
|
106
|
+
dpiaData,
|
|
107
|
+
overallRisk,
|
|
108
|
+
score,
|
|
109
|
+
conductedBy,
|
|
110
|
+
approvedBy,
|
|
111
|
+
} = req.body;
|
|
112
|
+
|
|
113
|
+
if (!projectName || !description || !dpiaData || !overallRisk || score == null || !conductedBy) {
|
|
114
|
+
return res.status(400).json({
|
|
115
|
+
error: 'projectName, description, dpiaData, overallRisk, score, and conductedBy are required',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// {{#if ORM=prisma}}
|
|
120
|
+
const record = await prisma.dPIARecord.create({
|
|
121
|
+
data: {
|
|
122
|
+
projectName,
|
|
123
|
+
description,
|
|
124
|
+
dpiaData,
|
|
125
|
+
overallRisk,
|
|
126
|
+
score,
|
|
127
|
+
status: 'draft',
|
|
128
|
+
conductedBy,
|
|
129
|
+
approvedBy: approvedBy ?? null,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await prisma.complianceAuditLog.create({
|
|
134
|
+
data: {
|
|
135
|
+
module: 'dpia',
|
|
136
|
+
action: 'created',
|
|
137
|
+
entityId: record.id,
|
|
138
|
+
entityType: 'DPIARecord',
|
|
139
|
+
changes: { projectName, overallRisk, score, status: 'draft' },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
// {{/if}}
|
|
143
|
+
// {{#if ORM=drizzle}}
|
|
144
|
+
const [record] = await db.insert(dpiaRecords).values({
|
|
145
|
+
projectName,
|
|
146
|
+
description,
|
|
147
|
+
dpiaData,
|
|
148
|
+
overallRisk,
|
|
149
|
+
score,
|
|
150
|
+
status: 'draft',
|
|
151
|
+
conductedBy,
|
|
152
|
+
approvedBy: approvedBy ?? null,
|
|
153
|
+
}).returning();
|
|
154
|
+
|
|
155
|
+
await db.insert(complianceAuditLog).values({
|
|
156
|
+
module: 'dpia',
|
|
157
|
+
action: 'created',
|
|
158
|
+
entityId: record.id,
|
|
159
|
+
entityType: 'DPIARecord',
|
|
160
|
+
changes: { projectName, overallRisk, score, status: 'draft' },
|
|
161
|
+
});
|
|
162
|
+
// {{/if}}
|
|
163
|
+
// {{#if ORM=none}}
|
|
164
|
+
const now = new Date();
|
|
165
|
+
const record: DPIARecord = {
|
|
166
|
+
id: newId(),
|
|
167
|
+
projectName,
|
|
168
|
+
description,
|
|
169
|
+
dpiaData,
|
|
170
|
+
overallRisk,
|
|
171
|
+
score,
|
|
172
|
+
status: 'draft',
|
|
173
|
+
conductedBy,
|
|
174
|
+
approvedBy: approvedBy ?? null,
|
|
175
|
+
createdAt: now,
|
|
176
|
+
updatedAt: now,
|
|
177
|
+
};
|
|
178
|
+
dpiaStore.set(record.id, record);
|
|
179
|
+
auditLog.push({ id: newId(), module: 'dpia', action: 'created', entityId: record.id, at: new Date() });
|
|
180
|
+
// {{/if}}
|
|
181
|
+
|
|
182
|
+
return res.status(201).json(record);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// PUT /dpia/:id
|
|
186
|
+
dpiaRouter.put('/:id', async (req, res) => {
|
|
187
|
+
const { id } = req.params;
|
|
188
|
+
|
|
189
|
+
// {{#if ORM=prisma}}
|
|
190
|
+
const existing = await prisma.dPIARecord.findUnique({ where: { id } });
|
|
191
|
+
if (!existing) {
|
|
192
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const record = await prisma.dPIARecord.update({
|
|
196
|
+
where: { id },
|
|
197
|
+
data: req.body,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await prisma.complianceAuditLog.create({
|
|
201
|
+
data: {
|
|
202
|
+
module: 'dpia',
|
|
203
|
+
action: 'updated',
|
|
204
|
+
entityId: record.id,
|
|
205
|
+
entityType: 'DPIARecord',
|
|
206
|
+
changes: req.body,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
// {{/if}}
|
|
210
|
+
// {{#if ORM=drizzle}}
|
|
211
|
+
const [existing] = await db.select().from(dpiaRecords).where(eq(dpiaRecords.id, id)).limit(1);
|
|
212
|
+
if (!existing) {
|
|
213
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const [record] = await db.update(dpiaRecords).set(req.body).where(eq(dpiaRecords.id, id)).returning();
|
|
217
|
+
|
|
218
|
+
await db.insert(complianceAuditLog).values({
|
|
219
|
+
module: 'dpia',
|
|
220
|
+
action: 'updated',
|
|
221
|
+
entityId: record.id,
|
|
222
|
+
entityType: 'DPIARecord',
|
|
223
|
+
changes: req.body,
|
|
224
|
+
});
|
|
225
|
+
// {{/if}}
|
|
226
|
+
// {{#if ORM=none}}
|
|
227
|
+
const existing = dpiaStore.get(id);
|
|
228
|
+
if (!existing) {
|
|
229
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
230
|
+
}
|
|
231
|
+
const record: DPIARecord = { ...existing, ...req.body, id, updatedAt: new Date() };
|
|
232
|
+
dpiaStore.set(id, record);
|
|
233
|
+
auditLog.push({ id: newId(), module: 'dpia', action: 'updated', entityId: record.id, at: new Date() });
|
|
234
|
+
// {{/if}}
|
|
235
|
+
|
|
236
|
+
return res.json(record);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// DELETE /dpia/:id
|
|
240
|
+
dpiaRouter.delete('/:id', async (req, res) => {
|
|
241
|
+
const { id } = req.params;
|
|
242
|
+
|
|
243
|
+
// {{#if ORM=prisma}}
|
|
244
|
+
const existing = await prisma.dPIARecord.findUnique({ where: { id } });
|
|
245
|
+
if (!existing) {
|
|
246
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await prisma.dPIARecord.delete({ where: { id } });
|
|
250
|
+
|
|
251
|
+
await prisma.complianceAuditLog.create({
|
|
252
|
+
data: {
|
|
253
|
+
module: 'dpia',
|
|
254
|
+
action: 'deleted',
|
|
255
|
+
entityId: id,
|
|
256
|
+
entityType: 'DPIARecord',
|
|
257
|
+
changes: { projectName: existing.projectName },
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
// {{/if}}
|
|
261
|
+
// {{#if ORM=drizzle}}
|
|
262
|
+
const [existing] = await db.select().from(dpiaRecords).where(eq(dpiaRecords.id, id)).limit(1);
|
|
263
|
+
if (!existing) {
|
|
264
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await db.delete(dpiaRecords).where(eq(dpiaRecords.id, id));
|
|
268
|
+
|
|
269
|
+
await db.insert(complianceAuditLog).values({
|
|
270
|
+
module: 'dpia',
|
|
271
|
+
action: 'deleted',
|
|
272
|
+
entityId: id,
|
|
273
|
+
entityType: 'DPIARecord',
|
|
274
|
+
changes: { projectName: existing.projectName },
|
|
275
|
+
});
|
|
276
|
+
// {{/if}}
|
|
277
|
+
// {{#if ORM=none}}
|
|
278
|
+
const existing = dpiaStore.get(id);
|
|
279
|
+
if (!existing) {
|
|
280
|
+
return res.status(404).json({ error: 'DPIA record not found' });
|
|
281
|
+
}
|
|
282
|
+
dpiaStore.delete(id);
|
|
283
|
+
auditLog.push({ id: newId(), module: 'dpia', action: 'deleted', entityId: id, at: new Date() });
|
|
284
|
+
// {{/if}}
|
|
285
|
+
|
|
286
|
+
return res.json({ success: true });
|
|
287
|
+
});
|