@tantainnovative/create-ndpr 0.4.0 → 0.5.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 +64 -68
- package/bin/index.mjs +366 -526
- package/package.json +1 -1
- package/templates/drizzle-client.ts +44 -0
- package/templates/drizzle-schema.ts +200 -178
- package/templates/env-example +6 -3
- package/templates/express-breach-route.ts +98 -0
- package/templates/express-consent-route.ts +304 -153
- package/templates/express-cross-border-route.ts +94 -259
- package/templates/express-dpia-route.ts +267 -222
- package/templates/express-dsr-route.ts +76 -0
- package/templates/express-lawful-basis-route.ts +88 -234
- package/templates/express-request-context.ts +130 -0
- package/templates/github-ndpr-audit.yml +3 -3
- package/templates/nextjs-breach-route.ts +117 -183
- package/templates/nextjs-consent-route.ts +312 -104
- package/templates/nextjs-cross-border-route.ts +239 -250
- package/templates/nextjs-dpia-route.ts +434 -214
- package/templates/nextjs-dsr-route.ts +70 -116
- package/templates/nextjs-lawful-basis-route.ts +207 -221
- package/templates/nextjs-layout.tsx +120 -57
- package/templates/nextjs-request-context.ts +137 -0
- package/templates/prisma-schema.prisma +89 -77
- package/templates/express-setup.ts +0 -509
package/bin/index.mjs
CHANGED
|
@@ -1,638 +1,478 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createInterface } from 'readline';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdirSync,
|
|
7
|
-
writeFileSync,
|
|
8
7
|
readFileSync,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from 'fs';
|
|
12
|
-
import {
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
8
|
+
realpathSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
14
13
|
|
|
15
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
const TEMPLATES_DIR = join(__dirname, '..', 'templates');
|
|
17
16
|
const CWD = process.cwd();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
17
|
+
// Keep this exact pin synchronized with the repository root package version.
|
|
18
|
+
const TOOLKIT_VERSION = '6.0.0';
|
|
19
|
+
const FORCE = process.argv.slice(2).includes('--force');
|
|
22
20
|
|
|
23
21
|
const c = {
|
|
24
|
-
reset: '\x1b[0m',
|
|
25
|
-
|
|
26
|
-
dim: '\x1b[2m',
|
|
27
|
-
green: '\x1b[32m',
|
|
28
|
-
cyan: '\x1b[36m',
|
|
29
|
-
yellow: '\x1b[33m',
|
|
30
|
-
blue: '\x1b[34m',
|
|
31
|
-
magenta: '\x1b[35m',
|
|
32
|
-
red: '\x1b[31m',
|
|
33
|
-
white: '\x1b[37m',
|
|
22
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', green: '\x1b[32m',
|
|
23
|
+
cyan: '\x1b[36m', yellow: '\x1b[33m', red: '\x1b[31m',
|
|
34
24
|
};
|
|
35
|
-
|
|
36
|
-
const bold = (
|
|
37
|
-
const green = (
|
|
38
|
-
const cyan = (
|
|
39
|
-
const yellow = (
|
|
40
|
-
const dim = (
|
|
41
|
-
const red = (
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Banner
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
25
|
+
const colour = (code, value) => `${code}${value}${c.reset}`;
|
|
26
|
+
const bold = (value) => colour(c.bold, value);
|
|
27
|
+
const green = (value) => colour(c.green, value);
|
|
28
|
+
const cyan = (value) => colour(c.cyan, value);
|
|
29
|
+
const yellow = (value) => colour(c.yellow, value);
|
|
30
|
+
const dim = (value) => colour(c.dim, value);
|
|
31
|
+
const red = (value) => colour(c.red, value);
|
|
46
32
|
|
|
47
33
|
function printBanner() {
|
|
48
34
|
console.log();
|
|
49
|
-
console.log(bold(cyan('
|
|
50
|
-
console.log(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log(bold(cyan(' ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ ╚═╝')));
|
|
55
|
-
console.log();
|
|
56
|
-
console.log(bold(' create-ndpr') + dim(' — NDPA compliance scaffolder'));
|
|
57
|
-
console.log(dim(' Powered by @tantainnovative/ndpr-toolkit'));
|
|
35
|
+
console.log(bold(cyan(' create-ndpr')) + dim(' — NDPA compliance scaffolder'));
|
|
36
|
+
console.log(dim(` Generates @tantainnovative/ndpr-toolkit@${TOOLKIT_VERSION} integrations`));
|
|
37
|
+
if (FORCE) {
|
|
38
|
+
console.log(yellow(' --force enabled: existing generated files may be replaced.'));
|
|
39
|
+
}
|
|
58
40
|
console.log();
|
|
59
41
|
}
|
|
60
42
|
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Stack detection
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
43
|
function detectStack() {
|
|
66
|
-
const detected = {
|
|
67
|
-
framework: null, // 'nextjs-app' | 'nextjs-pages' | 'express' | null
|
|
68
|
-
orm: null, // 'prisma' | 'drizzle' | null
|
|
69
|
-
hasPackageJson: false,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Check package.json exists
|
|
44
|
+
const detected = { framework: null, orm: null };
|
|
73
45
|
const pkgPath = join(CWD, 'package.json');
|
|
46
|
+
let dependencies = {};
|
|
74
47
|
if (existsSync(pkgPath)) {
|
|
75
|
-
detected.hasPackageJson = true;
|
|
76
48
|
try {
|
|
77
49
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (deps['express']) {
|
|
84
|
-
detected.framework = 'express';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (deps['drizzle-orm'] || deps['drizzle-kit']) {
|
|
88
|
-
detected.orm = 'drizzle';
|
|
89
|
-
}
|
|
90
|
-
if (deps['@prisma/client'] || deps['prisma']) {
|
|
91
|
-
detected.orm = 'prisma';
|
|
92
|
-
}
|
|
50
|
+
dependencies = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
51
|
+
if (dependencies.express) detected.framework = 'express';
|
|
52
|
+
if (dependencies['drizzle-orm'] || dependencies['drizzle-kit']) detected.orm = 'drizzle';
|
|
53
|
+
if (dependencies['@prisma/client'] || dependencies.prisma) detected.orm = 'prisma';
|
|
93
54
|
} catch {
|
|
94
|
-
|
|
55
|
+
console.log(yellow(' package.json could not be parsed; continuing with manual choices.'));
|
|
95
56
|
}
|
|
96
57
|
}
|
|
97
58
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const hasAppDir =
|
|
104
|
-
existsSync(join(CWD, 'app')) ||
|
|
105
|
-
existsSync(join(CWD, 'src', 'app'));
|
|
106
|
-
detected.framework = hasAppDir ? 'nextjs-app' : 'nextjs-pages';
|
|
59
|
+
const nextConfigs = ['next.config.js', 'next.config.cjs', 'next.config.mjs', 'next.config.ts'];
|
|
60
|
+
const hasNext = Boolean(dependencies.next) || nextConfigs.some((file) => existsSync(join(CWD, file)));
|
|
61
|
+
if (hasNext) {
|
|
62
|
+
const hasApp = existsSync(join(CWD, 'app')) || existsSync(join(CWD, 'src', 'app'));
|
|
63
|
+
detected.framework = hasApp ? 'nextjs-app' : 'nextjs-pages';
|
|
107
64
|
}
|
|
108
65
|
|
|
109
|
-
// ORM detection via project structure (if not already found in deps)
|
|
110
66
|
if (!detected.orm) {
|
|
111
|
-
if (existsSync(join(CWD, 'prisma', 'schema.prisma')))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const drizzleConfigs = ['drizzle.config.ts', 'drizzle.config.js', 'drizzle.config.mjs'];
|
|
115
|
-
if (drizzleConfigs.some((f) => existsSync(join(CWD, f)))) {
|
|
116
|
-
detected.orm = 'drizzle';
|
|
117
|
-
}
|
|
67
|
+
if (existsSync(join(CWD, 'prisma', 'schema.prisma'))) detected.orm = 'prisma';
|
|
68
|
+
else if (['ts', 'js', 'mjs', 'cjs'].some((ext) => existsSync(join(CWD, `drizzle.config.${ext}`)))) {
|
|
69
|
+
detected.orm = 'drizzle';
|
|
118
70
|
}
|
|
119
71
|
}
|
|
120
|
-
|
|
121
72
|
return detected;
|
|
122
73
|
}
|
|
123
74
|
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Readline helpers
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
75
|
function createRL() {
|
|
129
|
-
return createInterface({
|
|
130
|
-
input: process.stdin,
|
|
131
|
-
output: process.stdout,
|
|
132
|
-
});
|
|
76
|
+
return createInterface({ input: process.stdin, output: process.stdout });
|
|
133
77
|
}
|
|
134
|
-
|
|
135
78
|
function ask(rl, question) {
|
|
136
|
-
return new Promise((resolve) =>
|
|
137
|
-
rl.question(question, (answer) => resolve(answer.trim()));
|
|
138
|
-
});
|
|
79
|
+
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
139
80
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
console.log(red(` ${label} is required.`));
|
|
147
|
-
}
|
|
81
|
+
async function askValidated(rl, question, label, validate) {
|
|
82
|
+
while (true) {
|
|
83
|
+
const value = await ask(rl, question);
|
|
84
|
+
const problem = validate(value);
|
|
85
|
+
if (!problem) return value;
|
|
86
|
+
console.log(red(` ${label}: ${problem}`));
|
|
148
87
|
}
|
|
149
|
-
return value;
|
|
150
88
|
}
|
|
151
|
-
|
|
152
89
|
async function askChoice(rl, question, choices, defaultIndex = 0) {
|
|
153
|
-
const lines = choices.map((
|
|
154
|
-
const prompt = `${question}\n${lines}\n Choice [${defaultIndex + 1}]: `;
|
|
90
|
+
const lines = choices.map((choice, index) => ` ${index + 1}) ${choice}`).join('\n');
|
|
155
91
|
while (true) {
|
|
156
|
-
const raw = await ask(rl,
|
|
92
|
+
const raw = await ask(rl, `${question}\n${lines}\n Choice [${defaultIndex + 1}]: `);
|
|
157
93
|
if (!raw) return defaultIndex;
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
console.log(yellow(`
|
|
94
|
+
const selected = Number.parseInt(raw, 10);
|
|
95
|
+
if (Number.isInteger(selected) && selected >= 1 && selected <= choices.length) return selected - 1;
|
|
96
|
+
console.log(yellow(` Enter a number from 1-${choices.length}.`));
|
|
161
97
|
}
|
|
162
98
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const lines = options.map((o, i) => ` ${i + 1}) ${o}`).join('\n');
|
|
167
|
-
const prompt = `${question}${defaultStr}\n${lines}\n Enter numbers separated by commas (or press Enter for default): `;
|
|
168
|
-
|
|
99
|
+
async function askCheckboxes(rl, question, options, defaults) {
|
|
100
|
+
const lines = options.map((option, index) => ` ${index + 1}) ${option}`).join('\n');
|
|
101
|
+
const defaultText = defaults.map((index) => index + 1).join(',');
|
|
169
102
|
while (true) {
|
|
170
|
-
const raw = await ask(
|
|
103
|
+
const raw = await ask(
|
|
104
|
+
rl,
|
|
105
|
+
`${question} [default: ${defaultText}]\n${lines}\n Enter comma-separated numbers: `,
|
|
106
|
+
);
|
|
171
107
|
if (!raw) return defaults;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
for (const p of parts) {
|
|
176
|
-
const n = parseInt(p, 10);
|
|
177
|
-
if (isNaN(n) || n < 1 || n > options.length) {
|
|
178
|
-
console.log(yellow(` Invalid option: ${p}. Enter numbers from 1-${options.length}.`));
|
|
179
|
-
valid = false;
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
indices.push(n - 1);
|
|
108
|
+
const values = raw.split(',').map((value) => Number.parseInt(value.trim(), 10));
|
|
109
|
+
if (values.every((value) => Number.isInteger(value) && value >= 1 && value <= options.length)) {
|
|
110
|
+
return [...new Set(values.map((value) => value - 1))];
|
|
183
111
|
}
|
|
184
|
-
|
|
112
|
+
console.log(yellow(` Enter only numbers from 1-${options.length}.`));
|
|
185
113
|
}
|
|
186
114
|
}
|
|
187
|
-
|
|
188
115
|
async function askYesNo(rl, question, defaultYes = true) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
if (!answer) return defaultYes;
|
|
192
|
-
return answer.toLowerCase().startsWith('y');
|
|
116
|
+
const answer = await ask(rl, `${question} ${defaultYes ? '[Y/n]' : '[y/N]'}: `);
|
|
117
|
+
return answer ? answer.toLowerCase().startsWith('y') : defaultYes;
|
|
193
118
|
}
|
|
194
119
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
120
|
+
function javascriptStringLiteral(value) {
|
|
121
|
+
return JSON.stringify(String(value))
|
|
122
|
+
.replaceAll('\u2028', '\\u2028')
|
|
123
|
+
.replaceAll('\u2029', '\\u2029');
|
|
124
|
+
}
|
|
125
|
+
function javascriptTemplateLiteralContent(value) {
|
|
126
|
+
return JSON.stringify(String(value))
|
|
127
|
+
.slice(1, -1)
|
|
128
|
+
.replaceAll('`', '\\`')
|
|
129
|
+
.replaceAll('${', '\\${')
|
|
130
|
+
.replaceAll('\u2028', '\\u2028')
|
|
131
|
+
.replaceAll('\u2029', '\\u2029');
|
|
132
|
+
}
|
|
133
|
+
function commentTemplateValue(value) {
|
|
134
|
+
return String(value)
|
|
135
|
+
.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, ' ')
|
|
136
|
+
.replaceAll('*/', '* /');
|
|
137
|
+
}
|
|
138
|
+
function tenantSlug(value) {
|
|
139
|
+
const slug = String(value).toLowerCase().normalize('NFKD')
|
|
140
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
141
|
+
return slug || 'ndpr-tenant';
|
|
142
|
+
}
|
|
143
|
+
function createTemplateVars({ orgName, dpoEmail, framework, orm }) {
|
|
144
|
+
return {
|
|
145
|
+
ORG_NAME_COMMENT: commentTemplateValue(orgName),
|
|
146
|
+
ORG_NAME_LITERAL: javascriptStringLiteral(orgName),
|
|
147
|
+
DPO_EMAIL_LITERAL: javascriptStringLiteral(dpoEmail),
|
|
148
|
+
ORG_NAME_TEMPLATE: javascriptTemplateLiteralContent(orgName),
|
|
149
|
+
DPO_EMAIL_TEMPLATE: javascriptTemplateLiteralContent(dpoEmail),
|
|
150
|
+
TENANT_ID: tenantSlug(orgName),
|
|
151
|
+
TOOLKIT_VERSION,
|
|
152
|
+
FRAMEWORK: framework,
|
|
153
|
+
ORM: orm,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
198
156
|
|
|
199
|
-
/**
|
|
200
|
-
* Render a template with `{{KEY}}` variable substitution and minimal
|
|
201
|
-
* conditional-block support.
|
|
202
|
-
*
|
|
203
|
-
* Conditional syntax:
|
|
204
|
-
*
|
|
205
|
-
* // {{#if ORM=prisma}}
|
|
206
|
-
* import { PrismaClient } from '@prisma/client';
|
|
207
|
-
* // {{/if}}
|
|
208
|
-
*
|
|
209
|
-
* // {{#if ORM=drizzle}}
|
|
210
|
-
* import { db } from '@/drizzle';
|
|
211
|
-
* // {{/if}}
|
|
212
|
-
*
|
|
213
|
-
* // {{#if ORM=none}}
|
|
214
|
-
* // TODO: wire to your storage of choice
|
|
215
|
-
* const store = new Map();
|
|
216
|
-
* // {{/if}}
|
|
217
|
-
*
|
|
218
|
-
* The block is kept when `vars[KEY] === value` (e.g. `vars.ORM === 'prisma'`),
|
|
219
|
-
* otherwise the block and its markers are stripped. Marker comments (with
|
|
220
|
-
* optional leading `//` and any trailing whitespace) are always removed.
|
|
221
|
-
*/
|
|
222
157
|
function renderTemplate(templateName, vars) {
|
|
223
158
|
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
224
|
-
if (!existsSync(templatePath)) {
|
|
225
|
-
throw new Error(`Template not found: ${templateName}`);
|
|
226
|
-
}
|
|
159
|
+
if (!existsSync(templatePath)) throw new Error(`Template not found: ${templateName}`);
|
|
227
160
|
let content = readFileSync(templatePath, 'utf8');
|
|
228
|
-
|
|
229
|
-
// Conditional blocks first (so variable substitution doesn't munge the
|
|
230
|
-
// marker syntax). Pattern allows an optional `// ` prefix on the markers
|
|
231
|
-
// so templates can comment them out and remain valid TypeScript.
|
|
232
161
|
const ifPattern =
|
|
233
162
|
/(?:[ \t]*\/\/[ \t]*)?\{\{#if[ \t]+([A-Z_]+)=([a-zA-Z0-9_-]+)\}\}\s*([\s\S]*?)(?:[ \t]*\/\/[ \t]*)?\{\{\/if\}\}[ \t]*\n?/g;
|
|
234
|
-
content = content.replace(ifPattern, (_match, key, expected, body) =>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
163
|
+
content = content.replace(ifPattern, (_match, key, expected, body) =>
|
|
164
|
+
vars[key] === expected ? (body.endsWith('\n') ? body : `${body}\n`) : '',
|
|
165
|
+
);
|
|
166
|
+
const unresolvedDirective = content.match(/\{\{(?:#if|\/if)[^}]*\}\}/);
|
|
167
|
+
if (unresolvedDirective) {
|
|
168
|
+
throw new Error(`Unresolved template directive ${unresolvedDirective[0]} in ${templateName}`);
|
|
169
|
+
}
|
|
170
|
+
content = content.replace(/\{\{([A-Z_]+)\}\}/g, (placeholder, key) => {
|
|
171
|
+
if (!Object.hasOwn(vars, key)) {
|
|
172
|
+
throw new Error(`Unresolved template variable ${placeholder} in ${templateName}`);
|
|
238
173
|
}
|
|
239
|
-
return
|
|
174
|
+
return String(vars[key]);
|
|
240
175
|
});
|
|
241
|
-
|
|
242
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
243
|
-
content = content.replaceAll(`{{${key}}}`, value);
|
|
244
|
-
}
|
|
245
176
|
return content;
|
|
246
177
|
}
|
|
247
178
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
179
|
+
const GENERATED_FILES = [];
|
|
180
|
+
const SKIPPED_FILES = [];
|
|
181
|
+
function skip(relativePath, reason) {
|
|
182
|
+
SKIPPED_FILES.push(relativePath);
|
|
183
|
+
console.log(` ${dim('-')} ${dim(relativePath)} ${dim(`(skipped — ${reason})`)}`);
|
|
184
|
+
}
|
|
185
|
+
function writeGenerated(relativePath, content, { neverOverwrite = false } = {}) {
|
|
186
|
+
const destination = join(CWD, relativePath);
|
|
187
|
+
const shouldOverwrite = FORCE && !neverOverwrite;
|
|
188
|
+
const directory = dirname(destination);
|
|
189
|
+
mkdirSync(directory, { recursive: true });
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(destination, content, {
|
|
192
|
+
encoding: 'utf8',
|
|
193
|
+
flag: shouldOverwrite ? 'w' : 'wx',
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error && error.code === 'EEXIST') {
|
|
197
|
+
skip(
|
|
198
|
+
relativePath,
|
|
199
|
+
neverOverwrite
|
|
200
|
+
? 'existing file must be merged manually'
|
|
201
|
+
: 'already exists; pass --force to replace',
|
|
202
|
+
);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
throw error;
|
|
252
206
|
}
|
|
253
|
-
|
|
207
|
+
GENERATED_FILES.push(relativePath);
|
|
208
|
+
console.log(` ${green('+')} ${relativePath}`);
|
|
209
|
+
return true;
|
|
254
210
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// File generation
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
|
|
260
|
-
const GENERATED_FILES = [];
|
|
261
|
-
|
|
262
|
-
function generate(destRelative, templateName, vars = {}) {
|
|
263
|
-
const destPath = join(CWD, destRelative);
|
|
264
|
-
const content = renderTemplate(templateName, vars);
|
|
265
|
-
writeFile(destPath, content);
|
|
266
|
-
GENERATED_FILES.push(destRelative);
|
|
267
|
-
console.log(` ${green('+')} ${destRelative}`);
|
|
211
|
+
function generate(relativePath, templateName, vars, options) {
|
|
212
|
+
return writeGenerated(relativePath, renderTemplate(templateName, vars), options);
|
|
268
213
|
}
|
|
269
214
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
215
|
+
const PAGES_METHODS = {
|
|
216
|
+
consent: ['GET', 'POST', 'DELETE'],
|
|
217
|
+
dsr: ['GET', 'POST'],
|
|
218
|
+
breach: ['GET', 'POST'],
|
|
219
|
+
dpia: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
220
|
+
'lawful-basis': ['GET', 'POST', 'PUT', 'DELETE'],
|
|
221
|
+
'cross-border': ['GET', 'POST', 'PUT', 'DELETE'],
|
|
222
|
+
};
|
|
223
|
+
function renderPagesRoute(templateName, vars, methods) {
|
|
224
|
+
let content = renderTemplate(templateName, vars)
|
|
225
|
+
.replace('Next.js App Router', 'Next.js Pages Router')
|
|
226
|
+
.replace(
|
|
227
|
+
"import { NextRequest, NextResponse } from 'next/server';",
|
|
228
|
+
"import { NextRequest, NextResponse } from 'next/server';\nimport type { NextApiRequest, NextApiResponse } from 'next';",
|
|
229
|
+
);
|
|
230
|
+
const map = methods.map((method) => `${method}`).join(', ');
|
|
231
|
+
content += `\n\ntype AppRouteHandler = (request: NextRequest) => Promise<Response>;\nconst pageHandlers: Record<string, AppRouteHandler> = { ${map} };\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n const method = (req.method ?? '').toUpperCase();\n const routeHandler = pageHandlers[method];\n if (!routeHandler) {\n res.setHeader('Allow', Object.keys(pageHandlers));\n return res.status(405).json({ error: 'Method not allowed' });\n }\n\n const headers = new Headers();\n for (const [name, value] of Object.entries(req.headers)) {\n if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));\n else if (value !== undefined) headers.set(name, value);\n }\n headers.delete('content-length');\n const init: Omit<RequestInit, 'signal'> = { method, headers };\n if (method !== 'GET' && method !== 'HEAD') {\n init.body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body ?? {});\n if (!headers.has('content-type')) headers.set('content-type', 'application/json');\n }\n\n const request = new NextRequest(new URL(req.url ?? '/', 'http://localhost'), init);\n const response = await routeHandler(request);\n response.headers.forEach((value, name) => res.setHeader(name, value));\n const responseBody = await response.text();\n if (!responseBody) return res.status(response.status).end();\n return res.status(response.status).send(responseBody);\n}\n`;
|
|
232
|
+
return content;
|
|
275
233
|
}
|
|
276
234
|
|
|
277
|
-
function
|
|
278
|
-
|
|
235
|
+
function renderExpressIndex(selectedModules) {
|
|
236
|
+
const routeNames = {
|
|
237
|
+
consent: 'consentRouter', dsr: 'dsrRouter', breach: 'breachRouter',
|
|
238
|
+
dpia: 'dpiaRouter', 'lawful-basis': 'lawfulBasisRouter',
|
|
239
|
+
'cross-border': 'crossBorderRouter',
|
|
240
|
+
};
|
|
241
|
+
const enabled = selectedModules.filter((module) => routeNames[module]);
|
|
242
|
+
const imports = enabled.map((module) =>
|
|
243
|
+
`import { ${routeNames[module]} } from './routes/${module}';`,
|
|
244
|
+
).join('\n');
|
|
245
|
+
const mounts = enabled.map((module) =>
|
|
246
|
+
` router.use('/${module}', ${routeNames[module]});`,
|
|
247
|
+
).join('\n');
|
|
248
|
+
return `import { Router } from 'express';\n${imports}\n\nexport function createNDPRRouter(): Router {\n const router = Router();\n${mounts}\n return router;\n}\n`;
|
|
279
249
|
}
|
|
280
250
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
251
|
+
const NEXT_TEMPLATES = {
|
|
252
|
+
consent: 'nextjs-consent-route.ts', dsr: 'nextjs-dsr-route.ts',
|
|
253
|
+
breach: 'nextjs-breach-route.ts', dpia: 'nextjs-dpia-route.ts',
|
|
254
|
+
'lawful-basis': 'nextjs-lawful-basis-route.ts',
|
|
255
|
+
'cross-border': 'nextjs-cross-border-route.ts',
|
|
256
|
+
};
|
|
257
|
+
const EXPRESS_TEMPLATES = {
|
|
258
|
+
consent: 'express-consent-route.ts', dsr: 'express-dsr-route.ts',
|
|
259
|
+
breach: 'express-breach-route.ts', dpia: 'express-dpia-route.ts',
|
|
260
|
+
'lawful-basis': 'express-lawful-basis-route.ts',
|
|
261
|
+
'cross-border': 'express-cross-border-route.ts',
|
|
262
|
+
};
|
|
284
263
|
|
|
285
|
-
|
|
286
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Render the exact integration files produced by the interactive CLI without
|
|
266
|
+
* reading prompts or writing to disk. The verifier consumes this same API.
|
|
267
|
+
*/
|
|
268
|
+
function renderIntegrationFiles({
|
|
269
|
+
framework,
|
|
270
|
+
orm,
|
|
271
|
+
selectedModules,
|
|
272
|
+
vars,
|
|
273
|
+
useSrcDirectory = false,
|
|
274
|
+
}) {
|
|
275
|
+
if (!['nextjs-app', 'nextjs-pages', 'express', 'none'].includes(framework)) {
|
|
276
|
+
throw new Error(`Unsupported framework: ${framework}`);
|
|
277
|
+
}
|
|
278
|
+
if (!['prisma', 'drizzle', 'none'].includes(orm)) {
|
|
279
|
+
throw new Error(`Unsupported ORM: ${orm}`);
|
|
280
|
+
}
|
|
287
281
|
|
|
288
|
-
|
|
289
|
-
const
|
|
282
|
+
const templateVars = { ...vars, FRAMEWORK: framework, ORM: orm };
|
|
283
|
+
const modules = [...new Set(selectedModules)];
|
|
284
|
+
const files = [];
|
|
285
|
+
const addTemplate = (relativePath, templateName, routeVars = templateVars, options) => {
|
|
286
|
+
files.push({
|
|
287
|
+
relativePath,
|
|
288
|
+
content: renderTemplate(templateName, routeVars),
|
|
289
|
+
...(options ? { options } : {}),
|
|
290
|
+
});
|
|
291
|
+
};
|
|
290
292
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
'
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
293
|
+
if (orm === 'prisma') {
|
|
294
|
+
addTemplate(
|
|
295
|
+
'prisma/schema.prisma',
|
|
296
|
+
'prisma-schema.prisma',
|
|
297
|
+
templateVars,
|
|
298
|
+
{ neverOverwrite: true },
|
|
299
|
+
);
|
|
300
|
+
} else if (orm === 'drizzle') {
|
|
301
|
+
addTemplate('src/drizzle/ndpr-schema.ts', 'drizzle-schema.ts');
|
|
302
|
+
addTemplate('src/drizzle/index.ts', 'drizzle-client.ts');
|
|
301
303
|
}
|
|
302
304
|
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
|
|
306
|
+
const sourceRoot = useSrcDirectory ? 'src/' : '';
|
|
307
|
+
const appDir = `${sourceRoot}app`;
|
|
308
|
+
const pagesDir = `${sourceRoot}pages`;
|
|
309
|
+
const contextPath = `${sourceRoot}ndpr/request-context.ts`;
|
|
310
|
+
const drizzleBase = framework === 'nextjs-app'
|
|
311
|
+
? (useSrcDirectory ? '../../../drizzle' : '../../../src/drizzle')
|
|
312
|
+
: (useSrcDirectory ? '../../drizzle' : '../../src/drizzle');
|
|
313
|
+
addTemplate(contextPath, 'nextjs-request-context.ts');
|
|
314
|
+
|
|
315
|
+
if (modules.includes('consent')) {
|
|
316
|
+
addTemplate(
|
|
317
|
+
framework === 'nextjs-app'
|
|
318
|
+
? `${appDir}/ndpr-layout.tsx`
|
|
319
|
+
: `${pagesDir}/ndpr-provider.tsx`,
|
|
320
|
+
'nextjs-layout.tsx',
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const [moduleName, template] of Object.entries(NEXT_TEMPLATES)) {
|
|
325
|
+
if (!modules.includes(moduleName)) continue;
|
|
326
|
+
const routeVars = {
|
|
327
|
+
...templateVars,
|
|
328
|
+
NDPR_CONTEXT_IMPORT: framework === 'nextjs-app'
|
|
329
|
+
? '../../../ndpr/request-context'
|
|
330
|
+
: '../../ndpr/request-context',
|
|
331
|
+
NDPR_DB_IMPORT: drizzleBase,
|
|
332
|
+
NDPR_SCHEMA_IMPORT: `${drizzleBase}/ndpr-schema`,
|
|
333
|
+
};
|
|
334
|
+
if (framework === 'nextjs-app') {
|
|
335
|
+
addTemplate(`${appDir}/api/${moduleName}/route.ts`, template, routeVars);
|
|
336
|
+
} else {
|
|
337
|
+
files.push({
|
|
338
|
+
relativePath: `${pagesDir}/api/${moduleName}.ts`,
|
|
339
|
+
content: renderPagesRoute(template, routeVars, PAGES_METHODS[moduleName]),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else if (framework === 'express') {
|
|
344
|
+
addTemplate('src/ndpr/request-context.ts', 'express-request-context.ts');
|
|
345
|
+
for (const [moduleName, template] of Object.entries(EXPRESS_TEMPLATES)) {
|
|
346
|
+
if (!modules.includes(moduleName)) continue;
|
|
347
|
+
addTemplate(`src/ndpr/routes/${moduleName}.ts`, template, {
|
|
348
|
+
...templateVars,
|
|
349
|
+
NDPR_CONTEXT_IMPORT: '../request-context',
|
|
350
|
+
NDPR_DB_IMPORT: '../../drizzle',
|
|
351
|
+
NDPR_SCHEMA_IMPORT: '../../drizzle/ndpr-schema',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
files.push({
|
|
355
|
+
relativePath: 'src/ndpr/index.ts',
|
|
356
|
+
content: renderExpressIndex(modules),
|
|
357
|
+
});
|
|
307
358
|
}
|
|
359
|
+
|
|
360
|
+
return files;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function main() {
|
|
364
|
+
printBanner();
|
|
365
|
+
const detected = detectStack();
|
|
366
|
+
console.log(bold(' Detected project setup:'));
|
|
367
|
+
console.log(` ${cyan('Framework:')} ${detected.framework ?? dim('not detected')}`);
|
|
368
|
+
console.log(` ${cyan('ORM:')} ${detected.orm ?? dim('not detected')}`);
|
|
308
369
|
console.log();
|
|
309
370
|
|
|
310
371
|
const rl = createRL();
|
|
311
|
-
|
|
312
372
|
try {
|
|
313
|
-
|
|
314
|
-
console.log(bold(' Organisation details'));
|
|
315
|
-
console.log(dim(' These are embedded in the generated files.'));
|
|
316
|
-
console.log();
|
|
317
|
-
|
|
318
|
-
const orgName = await askRequired(
|
|
373
|
+
const orgName = await askValidated(
|
|
319
374
|
rl,
|
|
320
|
-
` ${cyan('Organisation name')}
|
|
375
|
+
` ${cyan('Organisation name')}: `,
|
|
321
376
|
'Organisation name',
|
|
377
|
+
(value) => !value ? 'is required' : value.length > 120 ? 'must be at most 120 characters' : /[\u0000-\u001f]/.test(value) ? 'contains control characters' : null,
|
|
322
378
|
);
|
|
323
|
-
|
|
324
|
-
const dpoEmail = await askRequired(
|
|
379
|
+
const dpoEmail = await askValidated(
|
|
325
380
|
rl,
|
|
326
|
-
` ${cyan('DPO email address')}
|
|
381
|
+
` ${cyan('DPO email address')}: `,
|
|
327
382
|
'DPO email',
|
|
383
|
+
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'must be a valid email address',
|
|
328
384
|
);
|
|
329
|
-
console.log();
|
|
330
|
-
|
|
331
|
-
// --- Framework confirmation/override ---
|
|
332
|
-
const frameworkLabels = [
|
|
333
|
-
'Next.js — App Router',
|
|
334
|
-
'Next.js — Pages Router',
|
|
335
|
-
'Express',
|
|
336
|
-
'None (generate shared files only)',
|
|
337
|
-
];
|
|
338
|
-
const frameworkValues = ['nextjs-app', 'nextjs-pages', 'express', 'none'];
|
|
339
385
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const framework = frameworkValues[frameworkIdx];
|
|
351
|
-
console.log();
|
|
352
|
-
|
|
353
|
-
// --- Modules ---
|
|
354
|
-
const moduleOptions = [
|
|
355
|
-
'consent — NDPA §25-26 consent management',
|
|
356
|
-
'dsr — Data Subject Rights requests (§34-38)',
|
|
357
|
-
'breach — Breach notification workflow (§40)',
|
|
358
|
-
'policy — Privacy policy generation',
|
|
359
|
-
'dpia — Data Protection Impact Assessment',
|
|
360
|
-
'lawful-basis — Lawful basis register',
|
|
361
|
-
'cross-border — Cross-border transfer management',
|
|
362
|
-
'ropa — Record of Processing Activities',
|
|
386
|
+
const frameworkLabels = ['Next.js — App Router', 'Next.js — Pages Router', 'Express', 'None'];
|
|
387
|
+
const frameworkValues = ['nextjs-app', 'nextjs-pages', 'express', 'none'];
|
|
388
|
+
const frameworkDefault = Math.max(0, frameworkValues.indexOf(detected.framework ?? 'none'));
|
|
389
|
+
const frameworkIndex = await askChoice(rl, ` ${cyan('Framework')}`, frameworkLabels, frameworkDefault);
|
|
390
|
+
const framework = frameworkValues[frameworkIndex];
|
|
391
|
+
|
|
392
|
+
const moduleLabels = [
|
|
393
|
+
'consent — NDPA §25-26', 'dsr — data-subject rights', 'breach — notification',
|
|
394
|
+
'policy — privacy policy', 'dpia — impact assessment',
|
|
395
|
+
'lawful-basis — processing register', 'cross-border — transfer register', 'ropa — processing activities',
|
|
363
396
|
];
|
|
364
397
|
const moduleValues = ['consent', 'dsr', 'breach', 'policy', 'dpia', 'lawful-basis', 'cross-border', 'ropa'];
|
|
398
|
+
const moduleIndices = await askCheckboxes(rl, ` ${cyan('Modules')}`, moduleLabels, [0, 1, 2]);
|
|
399
|
+
const selectedModules = moduleIndices.map((index) => moduleValues[index]);
|
|
365
400
|
|
|
366
|
-
|
|
367
|
-
const selectedModuleIndices = await askCheckboxes(
|
|
368
|
-
rl,
|
|
369
|
-
` ${cyan('Which modules do you want to include?')}`,
|
|
370
|
-
moduleOptions,
|
|
371
|
-
[0, 1, 2], // consent, dsr, breach by default
|
|
372
|
-
);
|
|
373
|
-
const selectedModules = selectedModuleIndices.map((i) => moduleValues[i]);
|
|
374
|
-
console.log();
|
|
375
|
-
|
|
376
|
-
// --- ORM ---
|
|
377
|
-
const ormLabels = ['Prisma', 'Drizzle', 'None (skip database schema)'];
|
|
401
|
+
const ormLabels = ['Prisma', 'Drizzle', 'None (development stores only)'];
|
|
378
402
|
const ormValues = ['prisma', 'drizzle', 'none'];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
console.log(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
detectedOrmIdx,
|
|
389
|
-
);
|
|
390
|
-
const orm = ormValues[ormIdx];
|
|
391
|
-
console.log();
|
|
392
|
-
|
|
393
|
-
// --- Confirm ---
|
|
394
|
-
console.log(bold(' Summary'));
|
|
395
|
-
console.log(` ${cyan('Organisation:')} ${orgName}`);
|
|
396
|
-
console.log(` ${cyan('DPO email:')} ${dpoEmail}`);
|
|
397
|
-
console.log(` ${cyan('Framework:')} ${frameworkLabels[frameworkIdx]}`);
|
|
398
|
-
console.log(` ${cyan('ORM:')} ${ormLabels[ormIdx]}`);
|
|
399
|
-
console.log(` ${cyan('Modules:')} ${selectedModules.join(', ')}`);
|
|
400
|
-
console.log();
|
|
401
|
-
|
|
402
|
-
const confirmed = await askYesNo(rl, ` ${cyan('Generate files?')}`, true);
|
|
403
|
-
if (!confirmed) {
|
|
403
|
+
const ormDefault = Math.max(0, ormValues.indexOf(detected.orm ?? 'none'));
|
|
404
|
+
const ormIndex = await askChoice(rl, ` ${cyan('ORM')}`, ormLabels, ormDefault);
|
|
405
|
+
const orm = ormValues[ormIndex];
|
|
406
|
+
|
|
407
|
+
console.log(`\n ${cyan('Organisation:')} ${orgName}`);
|
|
408
|
+
console.log(` ${cyan('Framework:')} ${frameworkLabels[frameworkIndex]}`);
|
|
409
|
+
console.log(` ${cyan('ORM:')} ${ormLabels[ormIndex]}`);
|
|
410
|
+
console.log(` ${cyan('Modules:')} ${selectedModules.join(', ') || 'none'}\n`);
|
|
411
|
+
if (!await askYesNo(rl, ` ${cyan('Generate files?')}`)) {
|
|
404
412
|
console.log(yellow('\n Aborted. No files were written.\n'));
|
|
405
|
-
|
|
413
|
+
return;
|
|
406
414
|
}
|
|
407
|
-
console.log();
|
|
408
415
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// -------------------------------------------------------------------------
|
|
412
|
-
|
|
413
|
-
console.log(bold(' Generating files...'));
|
|
414
|
-
console.log();
|
|
415
|
-
|
|
416
|
-
const vars = {
|
|
417
|
-
ORG_NAME: orgName,
|
|
418
|
-
DPO_EMAIL: dpoEmail,
|
|
419
|
-
// Drives `{{#if ORM=prisma}}` / `{{#if ORM=drizzle}}` / `{{#if ORM=none}}`
|
|
420
|
-
// conditional blocks in the route templates.
|
|
421
|
-
ORM: orm,
|
|
422
|
-
FRAMEWORK: framework,
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
// .env.example
|
|
416
|
+
const vars = createTemplateVars({ orgName, dpoEmail, framework, orm });
|
|
417
|
+
console.log(`\n${bold(' Generating files...')}\n`);
|
|
426
418
|
generate('.env.example', 'env-example', vars);
|
|
427
419
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Routes that already support all 3 ORMs via per-{{#if ORM=...}} blocks.
|
|
445
|
-
// Add a template to this set as it gets ORM-aware. Other route templates
|
|
446
|
-
// still hard-code Prisma, so we skip them when orm === 'none' to avoid
|
|
447
|
-
// emitting code with a broken `@prisma/client` import.
|
|
448
|
-
//
|
|
449
|
-
// All Next.js and Express route templates went ORM-aware in 3.6.2.
|
|
450
|
-
// The express-setup.ts wrapper still assumes Prisma — see the
|
|
451
|
-
// separate skip handling below.
|
|
452
|
-
const ORM_AWARE_TEMPLATES = new Set([
|
|
453
|
-
'nextjs-consent-route.ts',
|
|
454
|
-
'nextjs-dsr-route.ts',
|
|
455
|
-
'nextjs-breach-route.ts',
|
|
456
|
-
'nextjs-dpia-route.ts',
|
|
457
|
-
'nextjs-lawful-basis-route.ts',
|
|
458
|
-
'nextjs-cross-border-route.ts',
|
|
459
|
-
'express-consent-route.ts',
|
|
460
|
-
'express-dpia-route.ts',
|
|
461
|
-
'express-lawful-basis-route.ts',
|
|
462
|
-
'express-cross-border-route.ts',
|
|
463
|
-
]);
|
|
464
|
-
const generateRoute = (dest, templateName) => {
|
|
465
|
-
if (orm === 'none' && !ORM_AWARE_TEMPLATES.has(templateName)) {
|
|
466
|
-
skip(
|
|
467
|
-
dest,
|
|
468
|
-
`template hardcodes Prisma; skipping because you chose ORM=none. ` +
|
|
469
|
-
`Re-run create-ndpr with ORM=prisma|drizzle or copy the template from ` +
|
|
470
|
-
`https://github.com/mr-tanta/ndpr-toolkit/tree/main/packages/create-ndpr/templates/${templateName} ` +
|
|
471
|
-
`and adapt it to your storage layer.`,
|
|
472
|
-
);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
generate(dest, templateName, vars);
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
// Framework-specific files
|
|
479
|
-
if (framework === 'nextjs-app') {
|
|
480
|
-
const appDir = existsSync(join(CWD, 'src', 'app')) ? 'src/app' : 'app';
|
|
481
|
-
|
|
482
|
-
// Layout wrapper
|
|
483
|
-
generate(`${appDir}/ndpr-layout.tsx`, 'nextjs-layout.tsx', vars);
|
|
484
|
-
|
|
485
|
-
// API routes per selected module
|
|
486
|
-
if (selectedModules.includes('consent')) {
|
|
487
|
-
generateRoute(`${appDir}/api/consent/route.ts`, 'nextjs-consent-route.ts');
|
|
488
|
-
}
|
|
489
|
-
if (selectedModules.includes('dsr')) {
|
|
490
|
-
generateRoute(`${appDir}/api/dsr/route.ts`, 'nextjs-dsr-route.ts');
|
|
491
|
-
}
|
|
492
|
-
if (selectedModules.includes('breach')) {
|
|
493
|
-
generateRoute(`${appDir}/api/breach/route.ts`, 'nextjs-breach-route.ts');
|
|
494
|
-
}
|
|
495
|
-
if (selectedModules.includes('dpia')) {
|
|
496
|
-
generateRoute(`${appDir}/api/dpia/route.ts`, 'nextjs-dpia-route.ts');
|
|
497
|
-
}
|
|
498
|
-
if (selectedModules.includes('lawful-basis')) {
|
|
499
|
-
generateRoute(`${appDir}/api/lawful-basis/route.ts`, 'nextjs-lawful-basis-route.ts');
|
|
500
|
-
}
|
|
501
|
-
if (selectedModules.includes('cross-border')) {
|
|
502
|
-
generateRoute(`${appDir}/api/cross-border/route.ts`, 'nextjs-cross-border-route.ts');
|
|
503
|
-
}
|
|
504
|
-
} else if (framework === 'nextjs-pages') {
|
|
505
|
-
console.log(yellow(' Note: Pages Router API routes generated under pages/api/'));
|
|
506
|
-
if (selectedModules.includes('consent')) {
|
|
507
|
-
generateRoute('pages/api/consent.ts', 'nextjs-consent-route.ts');
|
|
508
|
-
}
|
|
509
|
-
if (selectedModules.includes('dsr')) {
|
|
510
|
-
generateRoute('pages/api/dsr.ts', 'nextjs-dsr-route.ts');
|
|
511
|
-
}
|
|
512
|
-
if (selectedModules.includes('breach')) {
|
|
513
|
-
generateRoute('pages/api/breach.ts', 'nextjs-breach-route.ts');
|
|
514
|
-
}
|
|
515
|
-
if (selectedModules.includes('dpia')) {
|
|
516
|
-
generateRoute('pages/api/dpia.ts', 'nextjs-dpia-route.ts');
|
|
517
|
-
}
|
|
518
|
-
if (selectedModules.includes('lawful-basis')) {
|
|
519
|
-
generateRoute('pages/api/lawful-basis.ts', 'nextjs-lawful-basis-route.ts');
|
|
520
|
-
}
|
|
521
|
-
if (selectedModules.includes('cross-border')) {
|
|
522
|
-
generateRoute('pages/api/cross-border.ts', 'nextjs-cross-border-route.ts');
|
|
523
|
-
}
|
|
524
|
-
} else if (framework === 'express') {
|
|
525
|
-
// express-setup.ts wires up a PrismaClient directly and is not yet
|
|
526
|
-
// ORM-aware, so it only produces working code for Prisma. Skip it (with
|
|
527
|
-
// a pointer) for Drizzle and None rather than emit a broken import.
|
|
528
|
-
if (orm === 'prisma') {
|
|
529
|
-
generate('src/ndpr/index.ts', 'express-setup.ts', vars);
|
|
530
|
-
} else {
|
|
531
|
-
skip(
|
|
532
|
-
'src/ndpr/index.ts',
|
|
533
|
-
`Express setup template currently assumes Prisma; skipping for ORM=${orm}. ` +
|
|
534
|
-
'See https://github.com/mr-tanta/ndpr-toolkit/tree/main/packages/create-ndpr/templates/express-setup.ts',
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
if (selectedModules.includes('consent')) {
|
|
538
|
-
generateRoute('src/ndpr/routes/consent.ts', 'express-consent-route.ts');
|
|
539
|
-
}
|
|
540
|
-
if (selectedModules.includes('dpia')) {
|
|
541
|
-
generateRoute('src/ndpr/routes/dpia.ts', 'express-dpia-route.ts');
|
|
542
|
-
}
|
|
543
|
-
if (selectedModules.includes('lawful-basis')) {
|
|
544
|
-
generateRoute('src/ndpr/routes/lawful-basis.ts', 'express-lawful-basis-route.ts');
|
|
545
|
-
}
|
|
546
|
-
if (selectedModules.includes('cross-border')) {
|
|
547
|
-
generateRoute('src/ndpr/routes/cross-border.ts', 'express-cross-border-route.ts');
|
|
548
|
-
}
|
|
420
|
+
const useSrcDirectory = framework === 'nextjs-app'
|
|
421
|
+
? existsSync(join(CWD, 'src', 'app'))
|
|
422
|
+
: framework === 'nextjs-pages'
|
|
423
|
+
? existsSync(join(CWD, 'src', 'pages'))
|
|
424
|
+
: false;
|
|
425
|
+
const integrationFiles = renderIntegrationFiles({
|
|
426
|
+
framework,
|
|
427
|
+
orm,
|
|
428
|
+
selectedModules,
|
|
429
|
+
vars,
|
|
430
|
+
useSrcDirectory,
|
|
431
|
+
});
|
|
432
|
+
for (const file of integrationFiles) {
|
|
433
|
+
writeGenerated(file.relativePath, file.content, file.options);
|
|
549
434
|
}
|
|
550
435
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
// -------------------------------------------------------------------------
|
|
554
|
-
// Always scaffolded, regardless of framework/ORM — the audit runs against a
|
|
555
|
-
// declarative config, not your database, so it works for every project.
|
|
556
|
-
if (existsSync(join(CWD, 'ndpr.audit.json'))) {
|
|
557
|
-
skip('ndpr.audit.json', 'already exists — keep your edits');
|
|
558
|
-
} else {
|
|
559
|
-
generateRaw('ndpr.audit.json', readFileSync(join(TEMPLATES_DIR, 'ndpr-audit.json'), 'utf8'));
|
|
560
|
-
}
|
|
561
|
-
if (existsSync(join(CWD, '.github', 'workflows', 'ndpr-audit.yml'))) {
|
|
562
|
-
skip('.github/workflows/ndpr-audit.yml', 'already exists');
|
|
563
|
-
} else {
|
|
564
|
-
generate('.github/workflows/ndpr-audit.yml', 'github-ndpr-audit.yml', vars);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// -------------------------------------------------------------------------
|
|
568
|
-
// Summary
|
|
569
|
-
// -------------------------------------------------------------------------
|
|
570
|
-
|
|
571
|
-
console.log();
|
|
572
|
-
console.log(bold(green(' Done! Files generated:')));
|
|
573
|
-
for (const f of GENERATED_FILES) {
|
|
574
|
-
console.log(` ${green('✓')} ${f}`);
|
|
575
|
-
}
|
|
576
|
-
console.log();
|
|
436
|
+
generate('ndpr.audit.json', 'ndpr-audit.json', vars);
|
|
437
|
+
generate('.github/workflows/ndpr-audit.yml', 'github-ndpr-audit.yml', vars);
|
|
577
438
|
|
|
578
|
-
|
|
439
|
+
console.log(`\n${bold(green(' Done.'))} Generated ${GENERATED_FILES.length} file(s); skipped ${SKIPPED_FILES.length}.\n`);
|
|
579
440
|
console.log(bold(' Next steps:'));
|
|
580
|
-
console.log();
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
console.log();
|
|
586
|
-
console.log(
|
|
587
|
-
console.log(dim(' pnpm add @prisma/client'));
|
|
588
|
-
console.log(dim(' pnpm add -D prisma'));
|
|
589
|
-
console.log(dim(' pnpm prisma migrate dev --name ndpr-init'));
|
|
590
|
-
console.log();
|
|
591
|
-
} else if (orm === 'drizzle') {
|
|
592
|
-
console.log(` ${cyan('1.')} Set your database URL in .env:`);
|
|
593
|
-
console.log(dim(' DATABASE_URL="postgresql://user:password@localhost:5432/mydb_dev"'));
|
|
594
|
-
console.log();
|
|
595
|
-
console.log(` ${cyan('2.')} Install Drizzle and push the schema:`);
|
|
596
|
-
console.log(dim(' pnpm add drizzle-orm @paralleldrive/cuid2'));
|
|
597
|
-
console.log(dim(' pnpm add -D drizzle-kit'));
|
|
598
|
-
console.log(dim(' pnpm drizzle-kit push'));
|
|
599
|
-
console.log();
|
|
441
|
+
console.log(dim(` 1. Install the exact toolkit version: pnpm add @tantainnovative/ndpr-toolkit@${TOOLKIT_VERSION}`));
|
|
442
|
+
console.log(dim(' 2. Set DATABASE_URL and NDPR_TENANT_ID in your server environment.'));
|
|
443
|
+
console.log(dim(' 3. Connect the generated request-context resolver to verified authentication.'));
|
|
444
|
+
if (orm === 'prisma') console.log(dim(' 4. Run: pnpm prisma migrate dev --name ndpr-init'));
|
|
445
|
+
if (orm === 'drizzle') {
|
|
446
|
+
console.log(dim(' 4. Pass your Drizzle instance to configureNDPRDatabase() during server startup.'));
|
|
447
|
+
console.log(dim(' Then run your pinned-compatible drizzle-kit migration command.'));
|
|
600
448
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
|
|
607
|
-
const appDir = existsSync(join(CWD, 'src', 'app')) ? 'src/app' : 'app';
|
|
608
|
-
console.log(` ${cyan(`${orm === 'none' ? '2' : '4'}`.padStart(1))}. Wrap your root layout with NDPRLayout:`);
|
|
609
|
-
console.log(dim(` // ${appDir}/layout.tsx`));
|
|
610
|
-
console.log(dim(` import NDPRLayout from './${appDir.includes('src') ? '' : ''}ndpr-layout';`));
|
|
611
|
-
console.log(dim(' // Render <NDPRLayout>{children}</NDPRLayout> inside your <body>'));
|
|
612
|
-
console.log();
|
|
449
|
+
if (framework === 'nextjs-pages' && selectedModules.includes('consent')) {
|
|
450
|
+
console.log(dim(' 5. Wrap Component with NDPRClientProvider from pages/ndpr-provider in pages/_app.tsx.'));
|
|
451
|
+
} else if (framework === 'nextjs-app' && selectedModules.includes('consent')) {
|
|
452
|
+
console.log(dim(' 5. Render NDPRClientProvider from app/ndpr-layout.tsx inside app/layout.tsx.'));
|
|
613
453
|
} else if (framework === 'express') {
|
|
614
|
-
console.log(
|
|
615
|
-
console.log(dim(" import { createNDPRRouter } from './src/ndpr';"));
|
|
616
|
-
console.log(dim(" app.use('/api/ndpr', createNDPRRouter());"));
|
|
617
|
-
console.log();
|
|
454
|
+
console.log(dim(" 5. Mount createNDPRRouter() at your chosen '/api/ndpr' path."));
|
|
618
455
|
}
|
|
619
|
-
|
|
620
|
-
const auditStep = framework === 'none' ? (orm === 'none' ? 3 : 5) : orm === 'none' ? 3 : 5;
|
|
621
|
-
console.log(` ${cyan(`${auditStep}`)}. Keep compliance from regressing — edit ${bold('ndpr.audit.json')} to match`);
|
|
622
|
-
console.log(dim(' your real posture, then run the audit (also wired into CI for you):'));
|
|
623
|
-
console.log(dim(' npx ndpr audit --min-score 70'));
|
|
624
|
-
console.log();
|
|
625
|
-
|
|
626
|
-
console.log(dim(' Documentation: https://ndprtoolkit.com.ng'));
|
|
627
|
-
console.log(dim(' Repository: https://github.com/mr-tanta/ndpr-toolkit'));
|
|
628
|
-
console.log();
|
|
629
|
-
|
|
456
|
+
console.log(dim(' Review every generated TODO and do not deploy in-memory stores as evidence.\n'));
|
|
630
457
|
} finally {
|
|
631
458
|
rl.close();
|
|
632
459
|
}
|
|
633
460
|
}
|
|
634
461
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
462
|
+
export {
|
|
463
|
+
createTemplateVars,
|
|
464
|
+
renderIntegrationFiles,
|
|
465
|
+
renderPagesRoute,
|
|
466
|
+
renderTemplate,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const isDirectExecution = Boolean(
|
|
470
|
+
process.argv[1]
|
|
471
|
+
&& realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)),
|
|
472
|
+
);
|
|
473
|
+
if (isDirectExecution) {
|
|
474
|
+
main().catch((error) => {
|
|
475
|
+
console.error(red(`\n Error: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
476
|
+
process.exitCode = 1;
|
|
477
|
+
});
|
|
478
|
+
}
|