@tantainnovative/ndpr-recipes 0.1.0 → 0.2.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 +57 -6
- package/package.json +12 -3
- package/prisma/schema.prisma +60 -5
- package/src/adapters/drizzle-breach.ts +179 -0
- package/src/adapters/drizzle-cross-border.ts +174 -0
- package/src/adapters/drizzle-dpia.ts +195 -0
- package/src/adapters/drizzle-lawful-basis.ts +166 -0
- package/src/adapters/drizzle-ropa.ts +214 -0
- package/src/adapters/index.ts +7 -0
- package/src/drizzle/schema.ts +229 -2
- package/src/express/index.ts +4 -0
- package/src/express/routes/breach.ts +50 -1
- package/src/express/routes/registration.ts +61 -0
- package/src/nextjs/app-router/api/breach/[id]/route.ts +47 -1
- package/src/nextjs/app-router/api/registration/route.ts +69 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tantainnovative/ndpr-recipes
|
|
2
2
|
|
|
3
|
-
Backend recipes for NDPA compliance with [@tantainnovative/ndpr-toolkit](https://github.com/
|
|
3
|
+
Backend recipes for Nigeria NDPA 2023 / NDPC GAID 2025 compliance with [@tantainnovative/ndpr-toolkit](https://github.com/mr-tanta/ndpr-toolkit).
|
|
4
4
|
|
|
5
5
|
## What is this?
|
|
6
6
|
|
|
@@ -23,9 +23,11 @@ This package is a **reference implementation** — not a library to install. Cop
|
|
|
23
23
|
| DSR request persistence | Prisma adapter, Drizzle adapter |
|
|
24
24
|
| Breach report persistence | Prisma adapter |
|
|
25
25
|
| ROPA persistence | Prisma adapter |
|
|
26
|
-
| Next.js App Router | Consent, DSR, Breach, ROPA, Compliance route handlers |
|
|
27
|
-
| Express | Full NDPR router with consent, DSR, breach, ROPA, compliance routes |
|
|
26
|
+
| Next.js App Router | Consent, DSR, Breach, ROPA, Compliance, Registration route handlers |
|
|
27
|
+
| Express | Full NDPR router with consent, DSR, breach, ROPA, compliance, registration routes |
|
|
28
28
|
| Consent middleware | Next.js edge middleware + Express middleware |
|
|
29
|
+
| GAID 2025 (DCPMI + CAR) | `/registration` route — tier classification + Compliance Audit Return schedule |
|
|
30
|
+
| Breach Article-33 readiness | Breach detail routes return which NDPC notification fields are still missing |
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
@@ -41,19 +43,27 @@ This package is a **reference implementation** — not a library to install. Cop
|
|
|
41
43
|
| `src/adapters/prisma-ropa.ts` | Prisma `StorageAdapter<RecordOfProcessingActivities>` |
|
|
42
44
|
| `src/adapters/drizzle-consent.ts` | Drizzle `StorageAdapter<ConsentSettings>` |
|
|
43
45
|
| `src/adapters/drizzle-dsr.ts` | Drizzle `StorageAdapter<DSRRequest[]>` |
|
|
46
|
+
| `src/adapters/drizzle-breach.ts` | Drizzle `StorageAdapter<BreachState>` |
|
|
47
|
+
| `src/adapters/drizzle-ropa.ts` | Drizzle `StorageAdapter<RecordOfProcessingActivities>` |
|
|
48
|
+
| `src/adapters/drizzle-dpia.ts` | Drizzle `StorageAdapter<DPIAResult[]>` |
|
|
49
|
+
| `src/adapters/drizzle-lawful-basis.ts` | Drizzle `StorageAdapter<ProcessingActivity[]>` |
|
|
50
|
+
| `src/adapters/drizzle-cross-border.ts` | Drizzle `StorageAdapter<CrossBorderTransfer[]>` |
|
|
44
51
|
| `src/nextjs/app-router/api/consent/route.ts` | Next.js consent API route |
|
|
45
52
|
| `src/nextjs/app-router/api/dsr/route.ts` | Next.js DSR API route |
|
|
46
53
|
| `src/nextjs/app-router/api/breach/route.ts` | Next.js breach API route |
|
|
54
|
+
| `src/nextjs/app-router/api/breach/[id]/route.ts` | Next.js breach detail route — returns GAID 2025 Art. 33 readiness |
|
|
47
55
|
| `src/nextjs/app-router/api/ropa/route.ts` | Next.js ROPA API route |
|
|
48
56
|
| `src/nextjs/app-router/api/compliance/route.ts` | Next.js compliance score API route |
|
|
57
|
+
| `src/nextjs/app-router/api/registration/route.ts` | Next.js DCPMI tier + CAR schedule route (GAID 2025) |
|
|
49
58
|
| `src/nextjs/app-router/middleware.ts` | Next.js consent gate middleware |
|
|
50
59
|
| `src/nextjs/app-router/layout-example.tsx` | Full wiring example for App Router |
|
|
51
60
|
| `src/express/index.ts` | Express router factory — mounts all routes |
|
|
52
61
|
| `src/express/routes/consent.ts` | Express consent router |
|
|
53
62
|
| `src/express/routes/dsr.ts` | Express DSR router |
|
|
54
|
-
| `src/express/routes/breach.ts` | Express breach router |
|
|
63
|
+
| `src/express/routes/breach.ts` | Express breach router — `GET /:id` returns GAID 2025 Art. 33 readiness |
|
|
55
64
|
| `src/express/routes/ropa.ts` | Express ROPA router |
|
|
56
65
|
| `src/express/routes/compliance.ts` | Express compliance score router |
|
|
66
|
+
| `src/express/routes/registration.ts` | Express DCPMI tier + CAR schedule router (GAID 2025) |
|
|
57
67
|
| `src/express/middleware/consent-check.ts` | Express consent gate middleware |
|
|
58
68
|
|
|
59
69
|
---
|
|
@@ -307,6 +317,7 @@ This mounts:
|
|
|
307
317
|
| `GET/POST/PATCH /api/ndpr/breach` | Breach notification |
|
|
308
318
|
| `GET/POST/PATCH /api/ndpr/ropa` | Record of Processing Activities |
|
|
309
319
|
| `GET /api/ndpr/compliance` | Compliance score |
|
|
320
|
+
| `GET /api/ndpr/registration` | DCPMI tier + CAR schedule (GAID 2025) |
|
|
310
321
|
|
|
311
322
|
### Consent middleware (route protection)
|
|
312
323
|
|
|
@@ -373,6 +384,46 @@ The `apiAdapter` hits your `/api/consent` route handler (from `src/nextjs/app-ro
|
|
|
373
384
|
|
|
374
385
|
---
|
|
375
386
|
|
|
387
|
+
## GAID 2025 — DCPMI registration & breach readiness
|
|
388
|
+
|
|
389
|
+
The NDPC's General Application and Implementation Directive (GAID) 2025 added
|
|
390
|
+
obligations the original recipes predate. Two recipes cover them, both built on
|
|
391
|
+
the toolkit's React-free `/server` utilities (no extra database tables needed).
|
|
392
|
+
|
|
393
|
+
### DCPMI tier + Compliance Audit Return (`/registration`)
|
|
394
|
+
|
|
395
|
+
`classifyDCPMI` derives your registration tier and annual fee from the number of
|
|
396
|
+
data subjects you process in a six-month window; `generateComplianceAuditReturn`
|
|
397
|
+
derives the filing schedule for those that must file:
|
|
398
|
+
|
|
399
|
+
- **UHL** (> 5,000 subjects) — ₦250,000/yr, files a **CAR annually**
|
|
400
|
+
- **EHL** (1,000–5,000) — ₦100,000/yr, files a **CAR annually**
|
|
401
|
+
- **OHL** (200–999) — ₦10,000/yr, **renews registration** (no CAR)
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
// GET /api/registration?dataSubjects=6200&commencementDate=2025-01-15
|
|
405
|
+
import { classifyDCPMI, generateComplianceAuditReturn } from '@tantainnovative/ndpr-toolkit/server';
|
|
406
|
+
|
|
407
|
+
const classification = classifyDCPMI({ dataSubjectsInSixMonths: 6200 });
|
|
408
|
+
const auditReturn = generateComplianceAuditReturn({
|
|
409
|
+
commencementDate: '2025-01-15',
|
|
410
|
+
tier: classification.tier, // CAR applies to UHL/EHL only
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
> Thresholds, fees, and deadlines follow the NDPC GAID 2025 baseline and can
|
|
415
|
+
> change — verify against current NDPC guidance before relying on them.
|
|
416
|
+
|
|
417
|
+
### Breach Article-33 readiness
|
|
418
|
+
|
|
419
|
+
The breach detail routes (`GET /api/breach/[id]` in Next.js, `GET /breach/:id`
|
|
420
|
+
in Express) now return an `ndpcReadiness` object via `assessBreachNotification` —
|
|
421
|
+
which GAID 2025 Article 33(5) notification fields are still missing and how many
|
|
422
|
+
hours remain on the 72-hour clock — so you know what to collect before filing.
|
|
423
|
+
Set `NDPR_DPO_NAME` / `NDPR_DPO_EMAIL` to record the contact point.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
376
427
|
## Database Schema
|
|
377
428
|
|
|
378
429
|
### Tables
|
|
@@ -380,7 +431,7 @@ The `apiAdapter` hits your `/api/consent` route handler (from `src/nextjs/app-ro
|
|
|
380
431
|
| Table | Description | NDPA reference |
|
|
381
432
|
|---|---|---|
|
|
382
433
|
| `ndpr_consent_records` | Immutable consent audit trail. `revokedAt` marks withdrawal — rows are never deleted. | §25–26 |
|
|
383
|
-
| `ndpr_dsr_requests` | Data subject rights requests. Tracks type, status, and 30-day response deadline. | Part
|
|
434
|
+
| `ndpr_dsr_requests` | Data subject rights requests. Tracks type, status, and 30-day response deadline. | Part VI §34–38 |
|
|
384
435
|
| `ndpr_breach_reports` | Breach incident records with 72-hour NDPC notification tracking. | §40 |
|
|
385
436
|
| `ndpr_processing_records` | Record of Processing Activities (ROPA). | Accountability principle |
|
|
386
437
|
| `ndpr_audit_log` | Append-only compliance event log. | §44 |
|
|
@@ -396,7 +447,7 @@ The consent table follows an immutable-audit pattern: when a subject updates or
|
|
|
396
447
|
| Module | NDPA provision |
|
|
397
448
|
|---|---|
|
|
398
449
|
| Consent | Sections 25–26 (lawful basis, consent withdrawal) |
|
|
399
|
-
| Data Subject Rights | Part
|
|
450
|
+
| Data Subject Rights | Part VI, Sections 34–38 (access, erasure, portability, etc.) |
|
|
400
451
|
| Breach Notification | Section 40 (72-hour notification to NDPC) |
|
|
401
452
|
| ROPA | Accountability principle; Schedule 1, Part 1 |
|
|
402
453
|
| Audit Log | Section 44 (accountability and record-keeping) |
|
package/package.json
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tantainnovative/ndpr-recipes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Backend recipes for @tantainnovative/ndpr-toolkit — Prisma schemas, API routes, and ORM adapters for NDPA compliance",
|
|
5
|
+
"description": "Backend recipes for @tantainnovative/ndpr-toolkit — Prisma schemas, API routes, and ORM adapters for Nigeria NDPA 2023 / NDPC GAID 2025 compliance",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Abraham Esandayinze Tanta",
|
|
9
9
|
"url": "https://linkedin.com/in/mr-tanta"
|
|
10
10
|
},
|
|
11
|
+
"homepage": "https://ndprtoolkit.com.ng",
|
|
11
12
|
"repository": {
|
|
12
13
|
"type": "git",
|
|
13
14
|
"url": "git+https://github.com/mr-tanta/ndpr-toolkit.git",
|
|
14
15
|
"directory": "packages/ndpr-recipes"
|
|
15
16
|
},
|
|
16
|
-
"keywords": ["ndpa", "ndpr", "nigeria", "data-protection", "prisma", "nextjs", "express"],
|
|
17
|
+
"keywords": ["ndpa", "ndpr", "gaid-2025", "dcpmi", "compliance-audit-return", "nigeria", "data-protection", "prisma", "nextjs", "express"],
|
|
17
18
|
"files": [
|
|
18
19
|
"src/**/*",
|
|
19
20
|
"prisma/**/*",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@tantainnovative/ndpr-toolkit": "^5.5.1"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@tantainnovative/ndpr-toolkit": {
|
|
28
|
+
"optional": false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
22
31
|
"publishConfig": {
|
|
23
32
|
"access": "public"
|
|
24
33
|
}
|
package/prisma/schema.prisma
CHANGED
|
@@ -89,16 +89,71 @@ model ProcessingRecord {
|
|
|
89
89
|
@@map("ndpr_processing_records")
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
model DPIARecord {
|
|
93
|
+
id String @id @default(cuid())
|
|
94
|
+
projectName String
|
|
95
|
+
description String
|
|
96
|
+
dpiaData Json
|
|
97
|
+
overallRisk String
|
|
98
|
+
score Int
|
|
99
|
+
status String @default("draft")
|
|
100
|
+
conductedBy String
|
|
101
|
+
approvedBy String?
|
|
102
|
+
createdAt DateTime @default(now())
|
|
103
|
+
updatedAt DateTime @updatedAt
|
|
104
|
+
|
|
105
|
+
@@index([status])
|
|
106
|
+
@@index([conductedBy])
|
|
107
|
+
@@map("ndpr_dpia_records")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
model LawfulBasisRecord {
|
|
111
|
+
id String @id @default(cuid())
|
|
112
|
+
activityName String
|
|
113
|
+
lawfulBasis String
|
|
114
|
+
justification String
|
|
115
|
+
dataCategories Json
|
|
116
|
+
purposes Json
|
|
117
|
+
assessedBy String
|
|
118
|
+
assessedAt DateTime @default(now())
|
|
119
|
+
reviewDate DateTime?
|
|
120
|
+
createdAt DateTime @default(now())
|
|
121
|
+
updatedAt DateTime @updatedAt
|
|
122
|
+
|
|
123
|
+
@@index([lawfulBasis])
|
|
124
|
+
@@index([assessedBy])
|
|
125
|
+
@@map("ndpr_lawful_basis_records")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
model CrossBorderTransferRecord {
|
|
129
|
+
id String @id @default(cuid())
|
|
130
|
+
destinationCountry String
|
|
131
|
+
recipientName String
|
|
132
|
+
transferMechanism String
|
|
133
|
+
safeguards String
|
|
134
|
+
dataCategories Json
|
|
135
|
+
adequacyStatus String
|
|
136
|
+
ndpcApprovalRequired Boolean @default(false)
|
|
137
|
+
ndpcApprovalReference String?
|
|
138
|
+
riskLevel String
|
|
139
|
+
createdAt DateTime @default(now())
|
|
140
|
+
updatedAt DateTime @updatedAt
|
|
141
|
+
|
|
142
|
+
@@index([destinationCountry])
|
|
143
|
+
@@index([riskLevel])
|
|
144
|
+
@@map("ndpr_cross_border_transfer_records")
|
|
145
|
+
}
|
|
146
|
+
|
|
92
147
|
model ComplianceAuditLog {
|
|
93
148
|
id String @id @default(cuid())
|
|
94
|
-
module String
|
|
95
149
|
action String
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
changes Json?
|
|
150
|
+
module String
|
|
151
|
+
details Json?
|
|
99
152
|
performedBy String?
|
|
153
|
+
ipAddress String?
|
|
100
154
|
createdAt DateTime @default(now())
|
|
101
155
|
|
|
102
|
-
@@index([module
|
|
156
|
+
@@index([module])
|
|
157
|
+
@@index([performedBy])
|
|
103
158
|
@@map("ndpr_audit_log")
|
|
104
159
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle adapter for the Breach Notification module.
|
|
3
|
+
*
|
|
4
|
+
* Implements StorageAdapter<BreachState> backed by the `ndpr_breach_reports`
|
|
5
|
+
* Drizzle table, where BreachState is the shape managed by the useBreach() hook:
|
|
6
|
+
*
|
|
7
|
+
* {
|
|
8
|
+
* reports: BreachReport[];
|
|
9
|
+
* assessments: RiskAssessment[];
|
|
10
|
+
* notifications: RegulatoryNotification[];
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* This adapter persists and loads BreachReport records. RiskAssessments and
|
|
14
|
+
* RegulatoryNotifications are stored as JSON on the breach row — extend the
|
|
15
|
+
* schema with additional columns if you need full relational queries.
|
|
16
|
+
*
|
|
17
|
+
* Behaviour
|
|
18
|
+
* ---------
|
|
19
|
+
* - LOAD → loads all breach reports from the database, ordered newest first.
|
|
20
|
+
* - SAVE → upserts each report by ID using Drizzle's `onConflictDoUpdate`.
|
|
21
|
+
* - REMOVE → marks all ongoing reports as 'resolved' (no hard deletes;
|
|
22
|
+
* NDPA Section 40 audit trail is preserved).
|
|
23
|
+
*
|
|
24
|
+
* Usage
|
|
25
|
+
* -----
|
|
26
|
+
* Copy this file into your project alongside your Drizzle client, then wire it
|
|
27
|
+
* into the toolkit hook:
|
|
28
|
+
*
|
|
29
|
+
* import { drizzle } from 'drizzle-orm/node-postgres';
|
|
30
|
+
* import { Pool } from 'pg';
|
|
31
|
+
* import { useBreach } from '@tantainnovative/ndpr-toolkit';
|
|
32
|
+
* import { drizzleBreachAdapter } from './adapters/drizzle-breach';
|
|
33
|
+
* import * as schema from './drizzle/schema';
|
|
34
|
+
*
|
|
35
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
36
|
+
* const db = drizzle(pool, { schema });
|
|
37
|
+
*
|
|
38
|
+
* function BreachPage() {
|
|
39
|
+
* const adapter = drizzleBreachAdapter(db);
|
|
40
|
+
* const { reports, submitReport } = useBreach({ adapter });
|
|
41
|
+
* // ...
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* Prerequisites
|
|
45
|
+
* -------------
|
|
46
|
+
* - The `ndpr_breach_reports` table must exist (run your Drizzle migration).
|
|
47
|
+
* - `drizzle-orm` must be installed in your project.
|
|
48
|
+
* - `@tantainnovative/ndpr-toolkit` must be installed in your project.
|
|
49
|
+
*
|
|
50
|
+
* @module adapters/drizzle-breach
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { eq, desc } from 'drizzle-orm';
|
|
54
|
+
import type { StorageAdapter } from '@tantainnovative/ndpr-toolkit';
|
|
55
|
+
import type { BreachReport, RiskAssessment, RegulatoryNotification } from '@tantainnovative/ndpr-toolkit';
|
|
56
|
+
import { breachReports } from '../drizzle/schema';
|
|
57
|
+
|
|
58
|
+
/** The state shape managed by the useBreach() hook */
|
|
59
|
+
export interface BreachState {
|
|
60
|
+
reports: BreachReport[];
|
|
61
|
+
assessments: RiskAssessment[];
|
|
62
|
+
notifications: RegulatoryNotification[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a Drizzle-backed StorageAdapter for the breach module's state.
|
|
67
|
+
*
|
|
68
|
+
* @param db - Your Drizzle database instance (any driver — pg, neon, libsql, etc.)
|
|
69
|
+
* @returns A StorageAdapter<BreachState> ready to pass to useBreach().
|
|
70
|
+
*/
|
|
71
|
+
export function drizzleBreachAdapter(db: any): StorageAdapter<BreachState> {
|
|
72
|
+
return {
|
|
73
|
+
/**
|
|
74
|
+
* Load all breach reports from the database, ordered newest first.
|
|
75
|
+
* Assessments and notifications are returned as empty arrays — extend
|
|
76
|
+
* the schema if you need to persist them.
|
|
77
|
+
*/
|
|
78
|
+
async load(): Promise<BreachState | null> {
|
|
79
|
+
const rows = await db
|
|
80
|
+
.select()
|
|
81
|
+
.from(breachReports)
|
|
82
|
+
.orderBy(desc(breachReports.reportedAt));
|
|
83
|
+
|
|
84
|
+
if (rows.length === 0) return null;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
reports: rows.map(mapRowToBreachReport),
|
|
88
|
+
assessments: [],
|
|
89
|
+
notifications: [],
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Persist the current breach state.
|
|
95
|
+
* Each report is upserted by ID using Drizzle's `onConflictDoUpdate`.
|
|
96
|
+
* Assessments and notifications are ignored unless you extend the schema.
|
|
97
|
+
*/
|
|
98
|
+
async save(state: BreachState): Promise<void> {
|
|
99
|
+
if (state.reports.length === 0) return;
|
|
100
|
+
|
|
101
|
+
await Promise.all(
|
|
102
|
+
state.reports.map((report) => {
|
|
103
|
+
const row = mapBreachReportToRow(report);
|
|
104
|
+
return db
|
|
105
|
+
.insert(breachReports)
|
|
106
|
+
.values({ id: report.id, ...row })
|
|
107
|
+
.onConflictDoUpdate({
|
|
108
|
+
target: breachReports.id,
|
|
109
|
+
set: row,
|
|
110
|
+
});
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Soft-close all ongoing breach reports by setting status to 'resolved'.
|
|
117
|
+
* Hard deletes are never performed to preserve the NDPA Section 40
|
|
118
|
+
* compliance audit trail.
|
|
119
|
+
*/
|
|
120
|
+
async remove(): Promise<void> {
|
|
121
|
+
await db
|
|
122
|
+
.update(breachReports)
|
|
123
|
+
.set({ status: 'resolved' })
|
|
124
|
+
.where(eq(breachReports.status, 'ongoing'));
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Mapping helpers
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Map a raw Drizzle row (from ndpr_breach_reports) to the toolkit's BreachReport type.
|
|
135
|
+
*/
|
|
136
|
+
function mapRowToBreachReport(row: any): BreachReport {
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
title: row.title,
|
|
140
|
+
description: row.description,
|
|
141
|
+
category: row.category,
|
|
142
|
+
discoveredAt: row.discoveredAt.getTime(),
|
|
143
|
+
occurredAt: row.occurredAt?.getTime(),
|
|
144
|
+
reportedAt: row.reportedAt.getTime(),
|
|
145
|
+
status: row.status as BreachReport['status'],
|
|
146
|
+
reporter: {
|
|
147
|
+
name: row.reporterName,
|
|
148
|
+
email: row.reporterEmail,
|
|
149
|
+
department: row.reporterDepartment ?? '',
|
|
150
|
+
},
|
|
151
|
+
affectedSystems: (row.affectedSystems as string[]) ?? [],
|
|
152
|
+
dataTypes: (row.dataTypes as string[]) ?? [],
|
|
153
|
+
estimatedAffectedSubjects: row.estimatedAffected ?? undefined,
|
|
154
|
+
initialActions: row.initialActions ?? undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Map a toolkit BreachReport to the Drizzle insert/update row shape.
|
|
160
|
+
*/
|
|
161
|
+
function mapBreachReportToRow(report: BreachReport): Record<string, unknown> {
|
|
162
|
+
return {
|
|
163
|
+
title: report.title,
|
|
164
|
+
description: report.description,
|
|
165
|
+
category: report.category,
|
|
166
|
+
severity: 'medium',
|
|
167
|
+
status: report.status,
|
|
168
|
+
discoveredAt: new Date(report.discoveredAt),
|
|
169
|
+
occurredAt: report.occurredAt ? new Date(report.occurredAt) : null,
|
|
170
|
+
reportedAt: new Date(report.reportedAt),
|
|
171
|
+
reporterName: report.reporter.name,
|
|
172
|
+
reporterEmail: report.reporter.email,
|
|
173
|
+
reporterDepartment: report.reporter.department ?? null,
|
|
174
|
+
affectedSystems: report.affectedSystems,
|
|
175
|
+
dataTypes: report.dataTypes,
|
|
176
|
+
estimatedAffected: report.estimatedAffectedSubjects ?? null,
|
|
177
|
+
initialActions: report.initialActions ?? null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle adapter for the Cross-Border Data Transfer module.
|
|
3
|
+
*
|
|
4
|
+
* Implements StorageAdapter<CrossBorderTransfer[]> backed by the
|
|
5
|
+
* `ndpr_cross_border_transfer_records` Drizzle table.
|
|
6
|
+
*
|
|
7
|
+
* Under NDPA Part VIII (Sections 41-43), personal data may only be transferred
|
|
8
|
+
* outside Nigeria under specific conditions. This adapter tracks every
|
|
9
|
+
* cross-border transfer, the mechanism relied upon, and NDPC approval status.
|
|
10
|
+
*
|
|
11
|
+
* Behaviour
|
|
12
|
+
* ---------
|
|
13
|
+
* - LOAD → returns all cross-border transfer records, ordered newest first.
|
|
14
|
+
* - SAVE → upserts each CrossBorderTransfer by ID using Drizzle's
|
|
15
|
+
* `onConflictDoUpdate` pattern.
|
|
16
|
+
* - REMOVE → soft-terminates all active transfers by setting their status
|
|
17
|
+
* columns (no hard deletes; the audit trail is preserved).
|
|
18
|
+
*
|
|
19
|
+
* Usage
|
|
20
|
+
* -----
|
|
21
|
+
* Copy this file into your project alongside your Drizzle client, then wire it
|
|
22
|
+
* into the toolkit:
|
|
23
|
+
*
|
|
24
|
+
* import { drizzle } from 'drizzle-orm/node-postgres';
|
|
25
|
+
* import { Pool } from 'pg';
|
|
26
|
+
* import { drizzleCrossBorderAdapter } from './adapters/drizzle-cross-border';
|
|
27
|
+
* import * as schema from './drizzle/schema';
|
|
28
|
+
*
|
|
29
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
30
|
+
* const db = drizzle(pool, { schema });
|
|
31
|
+
*
|
|
32
|
+
* function CrossBorderPage() {
|
|
33
|
+
* const adapter = drizzleCrossBorderAdapter(db);
|
|
34
|
+
* // pass adapter to your cross-border hook or use it directly
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* Prerequisites
|
|
38
|
+
* -------------
|
|
39
|
+
* - The `ndpr_cross_border_transfer_records` table must exist (run your Drizzle migration).
|
|
40
|
+
* - `drizzle-orm` must be installed in your project.
|
|
41
|
+
* - `@tantainnovative/ndpr-toolkit` must be installed in your project.
|
|
42
|
+
*
|
|
43
|
+
* @module adapters/drizzle-cross-border
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { eq, desc } from 'drizzle-orm';
|
|
47
|
+
import type { StorageAdapter } from '@tantainnovative/ndpr-toolkit';
|
|
48
|
+
import type { CrossBorderTransfer } from '@tantainnovative/ndpr-toolkit';
|
|
49
|
+
import { crossBorderTransferRecords } from '../drizzle/schema';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a Drizzle-backed StorageAdapter for CrossBorderTransfer[].
|
|
53
|
+
*
|
|
54
|
+
* @param db - Your Drizzle database instance (any driver — pg, neon, libsql, etc.)
|
|
55
|
+
* @returns A StorageAdapter<CrossBorderTransfer[]> ready to use with the cross-border module.
|
|
56
|
+
*/
|
|
57
|
+
export function drizzleCrossBorderAdapter(
|
|
58
|
+
db: any,
|
|
59
|
+
): StorageAdapter<CrossBorderTransfer[]> {
|
|
60
|
+
return {
|
|
61
|
+
/**
|
|
62
|
+
* Load all cross-border transfer records, ordered newest first.
|
|
63
|
+
* Returns null if no records exist.
|
|
64
|
+
*/
|
|
65
|
+
async load(): Promise<CrossBorderTransfer[] | null> {
|
|
66
|
+
const rows = await db
|
|
67
|
+
.select()
|
|
68
|
+
.from(crossBorderTransferRecords)
|
|
69
|
+
.orderBy(desc(crossBorderTransferRecords.createdAt));
|
|
70
|
+
|
|
71
|
+
if (rows.length === 0) return null;
|
|
72
|
+
|
|
73
|
+
return rows.map(mapRowToCrossBorderTransfer);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Persist the current list of cross-border transfers.
|
|
78
|
+
* Each transfer is upserted by ID so partial updates work.
|
|
79
|
+
*/
|
|
80
|
+
async save(transfers: CrossBorderTransfer[]): Promise<void> {
|
|
81
|
+
if (transfers.length === 0) return;
|
|
82
|
+
|
|
83
|
+
await Promise.all(
|
|
84
|
+
transfers.map((transfer) => {
|
|
85
|
+
const row = mapCrossBorderTransferToRow(transfer);
|
|
86
|
+
return db
|
|
87
|
+
.insert(crossBorderTransferRecords)
|
|
88
|
+
.values({ id: transfer.id, ...row })
|
|
89
|
+
.onConflictDoUpdate({
|
|
90
|
+
target: crossBorderTransferRecords.id,
|
|
91
|
+
set: row,
|
|
92
|
+
});
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Soft-terminate all active cross-border transfers by updating their
|
|
99
|
+
* timestamp. Transfers are never hard-deleted so the NDPA Part VI
|
|
100
|
+
* compliance audit trail is preserved.
|
|
101
|
+
*
|
|
102
|
+
* Note: This updates all records. If you need to scope removal to a
|
|
103
|
+
* specific status, add a `status` column to the schema and filter on it.
|
|
104
|
+
*/
|
|
105
|
+
async remove(): Promise<void> {
|
|
106
|
+
await db
|
|
107
|
+
.update(crossBorderTransferRecords)
|
|
108
|
+
.set({ updatedAt: new Date() });
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Mapping helpers
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Map a raw Drizzle row (from ndpr_cross_border_transfer_records) to the
|
|
119
|
+
* toolkit's CrossBorderTransfer type. Some rich fields on the toolkit type
|
|
120
|
+
* default to placeholder values — extend the schema if you need full
|
|
121
|
+
* round-trip fidelity.
|
|
122
|
+
*/
|
|
123
|
+
function mapRowToCrossBorderTransfer(row: any): CrossBorderTransfer {
|
|
124
|
+
return {
|
|
125
|
+
id: row.id,
|
|
126
|
+
destinationCountry: row.destinationCountry,
|
|
127
|
+
adequacyStatus: row.adequacyStatus as CrossBorderTransfer['adequacyStatus'],
|
|
128
|
+
transferMechanism: row.transferMechanism as CrossBorderTransfer['transferMechanism'],
|
|
129
|
+
dataCategories: (row.dataCategories as string[]) ?? [],
|
|
130
|
+
includesSensitiveData: false,
|
|
131
|
+
recipientOrganization: row.recipientName,
|
|
132
|
+
recipientContact: {
|
|
133
|
+
name: row.recipientName,
|
|
134
|
+
email: '',
|
|
135
|
+
},
|
|
136
|
+
purpose: '',
|
|
137
|
+
safeguards: row.safeguards ? [row.safeguards] : [],
|
|
138
|
+
riskAssessment: '',
|
|
139
|
+
riskLevel: row.riskLevel as CrossBorderTransfer['riskLevel'],
|
|
140
|
+
ndpcApproval: row.ndpcApprovalRequired
|
|
141
|
+
? {
|
|
142
|
+
required: row.ndpcApprovalRequired,
|
|
143
|
+
applied: !!row.ndpcApprovalReference,
|
|
144
|
+
approved: !!row.ndpcApprovalReference,
|
|
145
|
+
referenceNumber: row.ndpcApprovalReference ?? undefined,
|
|
146
|
+
}
|
|
147
|
+
: undefined,
|
|
148
|
+
tiaCompleted: false,
|
|
149
|
+
frequency: 'continuous',
|
|
150
|
+
startDate: row.createdAt.getTime(),
|
|
151
|
+
status: 'active',
|
|
152
|
+
createdAt: row.createdAt.getTime(),
|
|
153
|
+
updatedAt: row.updatedAt.getTime(),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Map a toolkit CrossBorderTransfer to the Drizzle insert/update row shape.
|
|
159
|
+
* Composite toolkit fields are flattened to match the simplified schema.
|
|
160
|
+
*/
|
|
161
|
+
function mapCrossBorderTransferToRow(transfer: CrossBorderTransfer): Record<string, unknown> {
|
|
162
|
+
return {
|
|
163
|
+
destinationCountry: transfer.destinationCountry,
|
|
164
|
+
recipientName: transfer.recipientOrganization,
|
|
165
|
+
transferMechanism: transfer.transferMechanism,
|
|
166
|
+
safeguards: transfer.safeguards.join('; '),
|
|
167
|
+
dataCategories: transfer.dataCategories,
|
|
168
|
+
adequacyStatus: transfer.adequacyStatus,
|
|
169
|
+
ndpcApprovalRequired: transfer.ndpcApproval?.required ?? false,
|
|
170
|
+
ndpcApprovalReference: transfer.ndpcApproval?.referenceNumber ?? null,
|
|
171
|
+
riskLevel: transfer.riskLevel,
|
|
172
|
+
updatedAt: new Date(),
|
|
173
|
+
};
|
|
174
|
+
}
|