tina4-nodejs 3.13.119 → 3.13.121
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/CLAUDE.md +2 -2
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +21604 -21370
- package/packages/cli/src/bin.ts +13 -6
- package/packages/cli/src/commands/generate.ts +391 -32
- package/packages/cli/src/commands/migrateCreate.ts +32 -37
- package/packages/core/dist/index.js +2394 -339
- package/packages/core/src/mcp.ts +62 -11
- package/packages/orm/dist/index.js +2443 -388
- package/types/cli/src/commands/generate.d.ts +37 -1
- package/types/cli/src/commands/migrateCreate.d.ts +1 -1
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* tina4nodejs generate listener user.created
|
|
34
34
|
*/
|
|
35
35
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
36
|
-
import { join, resolve } from "node:path";
|
|
36
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
37
37
|
|
|
38
38
|
// ── Field type mapping ──────────────────────────────────────────────
|
|
39
39
|
const FIELD_TYPE_MAP: Record<string, { orm: string; sql: string; defaultVal: string }> = {
|
|
@@ -62,9 +62,15 @@ function ensureDir(dir: string): void {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function writeFileSafe(path: string, content: string): void {
|
|
65
|
+
// ALWAYS scan the intended content for `// tina4:edit` markers, regardless
|
|
66
|
+
// of dry-run. The envelope's edit_hints[] MUST promise the same hints in
|
|
67
|
+
// preview (--dry-run) and post-write so an agent can rely on them before
|
|
68
|
+
// committing to disk.
|
|
69
|
+
captureEditHints(path, content);
|
|
70
|
+
|
|
65
71
|
if (__resolution.dryRun) {
|
|
66
|
-
// Dry-run:
|
|
67
|
-
//
|
|
72
|
+
// Dry-run: touch no disk state and print no per-file line to stdout
|
|
73
|
+
// (that would leak into --json).
|
|
68
74
|
return;
|
|
69
75
|
}
|
|
70
76
|
if (existsSync(path)) {
|
|
@@ -152,6 +158,18 @@ export interface ResolutionInput {
|
|
|
152
158
|
fields: string | null;
|
|
153
159
|
}
|
|
154
160
|
|
|
161
|
+
/**
|
|
162
|
+
* One `// tina4:edit …` marker found in a written (or would-be-written)
|
|
163
|
+
* template file. `file` is repo-relative POSIX (matches the rest of the
|
|
164
|
+
* envelope's paths); `line` is 1-based; `label` is the short imperative label
|
|
165
|
+
* that followed the marker on the same line.
|
|
166
|
+
*/
|
|
167
|
+
export interface EditHint {
|
|
168
|
+
file: string;
|
|
169
|
+
line: number;
|
|
170
|
+
label: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
155
173
|
export interface ResolutionBody {
|
|
156
174
|
class_name?: string;
|
|
157
175
|
table_name?: string;
|
|
@@ -159,6 +177,8 @@ export interface ResolutionBody {
|
|
|
159
177
|
migration_path?: string;
|
|
160
178
|
routes?: string[];
|
|
161
179
|
test_paths?: string[];
|
|
180
|
+
edit_hints?: EditHint[];
|
|
181
|
+
next?: string[];
|
|
162
182
|
transformations: ResolutionTransformation[];
|
|
163
183
|
}
|
|
164
184
|
|
|
@@ -176,8 +196,15 @@ export interface ResolutionEnvelope {
|
|
|
176
196
|
* `resolution_contract.envelope` so the tina4 client (or any consumer) can
|
|
177
197
|
* discover the exact contract this framework speaks. Bump when a breaking
|
|
178
198
|
* key rename / removal lands; keep unchanged when new OPTIONAL keys are added.
|
|
199
|
+
*
|
|
200
|
+
* `generate_v1_1` (ADR-0063, 3.13.120) is a PURELY ADDITIVE superset of
|
|
201
|
+
* `generate_v1`: every v1 field is preserved, and two new optional arrays
|
|
202
|
+
* appear — `resolution.edit_hints[]` (one entry per `// tina4:edit` marker
|
|
203
|
+
* baked into a template) and `resolution.next[]` (curated per-verb actionable
|
|
204
|
+
* next steps). `resolution.test_paths[]` was already in v1; v1.1 surfaces it
|
|
205
|
+
* in the human stderr block too.
|
|
179
206
|
*/
|
|
180
|
-
export const RESOLUTION_ENVELOPE_VERSION = "
|
|
207
|
+
export const RESOLUTION_ENVELOPE_VERSION = "generate_v1_1";
|
|
181
208
|
|
|
182
209
|
/**
|
|
183
210
|
* Per-run mutable resolution state. Reset by `resetResolution()` on every
|
|
@@ -216,14 +243,27 @@ function recordTransformation(t: ResolutionTransformation): void {
|
|
|
216
243
|
/** Read-only snapshot of the current resolution — exported for tests that
|
|
217
244
|
* want to inspect it in-process (the CLI itself uses only the envelope). */
|
|
218
245
|
export function currentResolution(): ResolutionEnvelope {
|
|
246
|
+
const body: ResolutionBody = {
|
|
247
|
+
...__resolution.body,
|
|
248
|
+
transformations: [...__resolution.body.transformations],
|
|
249
|
+
};
|
|
250
|
+
if (__resolution.body.edit_hints) {
|
|
251
|
+
body.edit_hints = __resolution.body.edit_hints.map((h) => ({ ...h }));
|
|
252
|
+
}
|
|
253
|
+
if (__resolution.body.next) {
|
|
254
|
+
body.next = [...__resolution.body.next];
|
|
255
|
+
}
|
|
256
|
+
if (__resolution.body.test_paths) {
|
|
257
|
+
body.test_paths = [...__resolution.body.test_paths];
|
|
258
|
+
}
|
|
259
|
+
if (__resolution.body.routes) {
|
|
260
|
+
body.routes = [...__resolution.body.routes];
|
|
261
|
+
}
|
|
219
262
|
return {
|
|
220
263
|
command: "generate",
|
|
221
264
|
target: __resolution.target,
|
|
222
265
|
input: { ...__resolution.input },
|
|
223
|
-
resolution:
|
|
224
|
-
...__resolution.body,
|
|
225
|
-
transformations: [...__resolution.body.transformations],
|
|
226
|
-
},
|
|
266
|
+
resolution: body,
|
|
227
267
|
actions_taken: [...__resolution.actionsTaken],
|
|
228
268
|
dry_run: __resolution.dryRun,
|
|
229
269
|
};
|
|
@@ -243,6 +283,74 @@ function pushTestPath(path: string): void {
|
|
|
243
283
|
__resolution.body.test_paths.push(path);
|
|
244
284
|
}
|
|
245
285
|
|
|
286
|
+
function pushEditHint(hint: EditHint): void {
|
|
287
|
+
if (!__resolution.body.edit_hints) __resolution.body.edit_hints = [];
|
|
288
|
+
__resolution.body.edit_hints.push(hint);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function setNextSteps(steps: string[]): void {
|
|
292
|
+
if (steps.length === 0) return;
|
|
293
|
+
__resolution.body.next = [...steps];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Convert an absolute path to a repo-relative POSIX path. Every other
|
|
298
|
+
* envelope path (file_path, migration_path, test_paths) is repo-relative
|
|
299
|
+
* POSIX ("src/models/Order.ts"), so edit_hints follow the same convention —
|
|
300
|
+
* one path style across the whole envelope, portable across Windows.
|
|
301
|
+
*/
|
|
302
|
+
function toRelPath(absPath: string): string {
|
|
303
|
+
const cwd = process.cwd();
|
|
304
|
+
const rel = relative(cwd, absPath);
|
|
305
|
+
if (!rel) return absPath;
|
|
306
|
+
return sep === "/" ? rel : rel.split(sep).join("/");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Line-anchored `tina4:edit LABEL` marker regex (multi-style, ADR-0063 v1.1).
|
|
310
|
+
//
|
|
311
|
+
// Line-anchored (`^\s*<comment-lead>`) so the marker must be at the START of
|
|
312
|
+
// a code line (after optional whitespace) — a marker embedded inside a
|
|
313
|
+
// template string literal never falsely matches. LABEL is captured greedily
|
|
314
|
+
// until end of line (or the closing `#}` for a Twig comment), then trimmed
|
|
315
|
+
// on push. Four comment styles are recognised, one regex per style — bundled
|
|
316
|
+
// into one alternation so the scanner is O(lines):
|
|
317
|
+
//
|
|
318
|
+
// // tina4:edit LABEL TS/JS/C/Java/Rust (existing)
|
|
319
|
+
// # tina4:edit LABEL Python/Ruby/shell (parity)
|
|
320
|
+
// -- tina4:edit LABEL SQL migrations
|
|
321
|
+
// {# tina4:edit LABEL #} Twig / Frond templates
|
|
322
|
+
//
|
|
323
|
+
// Ports the same shape PHP uses in `bin/tina4php::collectEditHintsFromContent`
|
|
324
|
+
// (`~(?://|--|\{#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$~`). The `#` bare-
|
|
325
|
+
// comment style is added on top so a Ruby/Python-style template (none exist
|
|
326
|
+
// today, but the scanner is now language-agnostic) is covered too.
|
|
327
|
+
const TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Scan `content` for `tina4:edit …` markers (any of the 4 comment styles)
|
|
331
|
+
* and record one EditHint per match against the given absolute path. Called
|
|
332
|
+
* from every `writeFileSafe` — including under `--dry-run` — so the envelope
|
|
333
|
+
* promises the same hints in preview and post-write.
|
|
334
|
+
*
|
|
335
|
+
* File-extension gate keeps the scan cheap: only text/code files where a
|
|
336
|
+
* marker could reasonably live are opened. TS/JS + SQL + Twig cover every
|
|
337
|
+
* template the generator emits today; expanded here (from TS/JS-only in
|
|
338
|
+
* 3.13.120) so `generate form`, `generate view` and `generate migration`
|
|
339
|
+
* carry `edit_hints[]` rather than returning `[]` (parity with PHP, whose
|
|
340
|
+
* regex has always been language-agnostic).
|
|
341
|
+
*/
|
|
342
|
+
function captureEditHints(absPath: string, content: string): void {
|
|
343
|
+
if (!/\.(ts|tsx|js|mjs|cjs|jsx|sql|twig|html\.twig)$/.test(absPath)) return;
|
|
344
|
+
const relPath = toRelPath(absPath);
|
|
345
|
+
const lines = content.split("\n");
|
|
346
|
+
for (let i = 0; i < lines.length; i++) {
|
|
347
|
+
const match = TINA4_EDIT_MARKER.exec(lines[i]);
|
|
348
|
+
if (match) {
|
|
349
|
+
pushEditHint({ file: relPath, line: i + 1, label: match[1].trim() });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
246
354
|
/**
|
|
247
355
|
* Emit the resolution — as JSON on STDOUT for `--json`, otherwise as a human
|
|
248
356
|
* block on STDERR (stderr so a caller piping stdout for other output isn't
|
|
@@ -282,6 +390,28 @@ function printResolution(): void {
|
|
|
282
390
|
lines.push(` To keep the raw name '${reserved.from}' as the table:`);
|
|
283
391
|
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
|
|
284
392
|
}
|
|
393
|
+
// v1.1 (ADR-0063): surface the already-populated test_paths[], and the two
|
|
394
|
+
// new arrays (edit_hints, next) when either is non-empty. Sections stay
|
|
395
|
+
// absent when the corresponding array is empty — a listener/service
|
|
396
|
+
// scaffold prints exactly what a model scaffold prints, minus what does
|
|
397
|
+
// not apply.
|
|
398
|
+
if (b.test_paths && b.test_paths.length > 0) {
|
|
399
|
+
lines.push("");
|
|
400
|
+
lines.push(" Tests:");
|
|
401
|
+
for (const testPath of b.test_paths) lines.push(` ${testPath}`);
|
|
402
|
+
}
|
|
403
|
+
if (b.edit_hints && b.edit_hints.length > 0) {
|
|
404
|
+
lines.push("");
|
|
405
|
+
lines.push(" Edit these lines:");
|
|
406
|
+
for (const hint of b.edit_hints) {
|
|
407
|
+
lines.push(` ${hint.file}:${hint.line} ${hint.label}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (b.next && b.next.length > 0) {
|
|
411
|
+
lines.push("");
|
|
412
|
+
lines.push(" Next:");
|
|
413
|
+
for (const step of b.next) lines.push(` ${step}`);
|
|
414
|
+
}
|
|
285
415
|
lines.push("");
|
|
286
416
|
process.stderr.write(lines.join("\n"));
|
|
287
417
|
}
|
|
@@ -341,6 +471,10 @@ export function parseCliArgs(args: string[]): { flags: Record<string, string | b
|
|
|
341
471
|
const booleanFlags = new Set([
|
|
342
472
|
"no-browser", "no-reload", "production", "managed", "all", "clear",
|
|
343
473
|
"public", "no-migration",
|
|
474
|
+
// Suppress the co-emitted migration test (used by the migrate:create
|
|
475
|
+
// delegation — a plain migrate:create is "just a migration, no test",
|
|
476
|
+
// matching its pre-3.13.121 UX now that it routes through generate migration).
|
|
477
|
+
"no-test",
|
|
344
478
|
// Resolution transparency (Feature B, 3.13.117): both accept NO value.
|
|
345
479
|
"json", "dry-run",
|
|
346
480
|
]);
|
|
@@ -460,7 +594,7 @@ export const GENERATORS: Record<string, GeneratorSpec> = {
|
|
|
460
594
|
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
461
595
|
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
462
596
|
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
463
|
-
migration: { handler: (n, f) => generateMigration(n, f), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
597
|
+
migration: { handler: (n, f) => generateMigration(n, f, undefined, undefined, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
464
598
|
middleware: { handler: generateMiddleware, usage: "<Name>", summary: "Middleware with before/after hooks" },
|
|
465
599
|
test: { handler: generateTest, usage: "<name> [--model Name]", summary: "Test file" },
|
|
466
600
|
form: { handler: generateForm, usage: '<Name> [--fields "..."]', summary: "Form template with inputs matching model fields" },
|
|
@@ -477,6 +611,105 @@ export const GENERATORS: Record<string, GeneratorSpec> = {
|
|
|
477
611
|
/** Comma-separated generator names for usage/error output — derived, never a hand-kept list. */
|
|
478
612
|
const GENERATOR_LIST = Object.keys(GENERATORS).join(", ");
|
|
479
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Curated per-verb next steps — populates `resolution.next[]` (envelope) and
|
|
616
|
+
* the "Next:" block on stderr (human). Grounded on the real code paths a
|
|
617
|
+
* developer takes after each generator. Cap of 5 (short, actionable).
|
|
618
|
+
*
|
|
619
|
+
* The context carries the RESOLVED name and table (the reserved-word
|
|
620
|
+
* pluraliser has already run at dispatch time), so a step references the
|
|
621
|
+
* same table/route the generated files bind to. `name` is the CLI positional
|
|
622
|
+
* as-typed; `table` is `toTableName(name)`.
|
|
623
|
+
*/
|
|
624
|
+
interface NextContext { name: string; table: string; }
|
|
625
|
+
const NEXT_STEPS: Record<string, (c: NextContext) => string[]> = {
|
|
626
|
+
model: ({ name, table }) => [
|
|
627
|
+
`Edit src/models/${name}.ts to add fields beyond the default 'name'`,
|
|
628
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
629
|
+
`Run its test: npx tsx tests/${table}_model.test.ts`,
|
|
630
|
+
`Add CRUD scaffolding: npx tina4nodejs generate crud ${name}`,
|
|
631
|
+
],
|
|
632
|
+
route: ({ name, table }) => [
|
|
633
|
+
`Fill the AI-FILL stubs in src/routes/api/${name.replace(/^\//, "")}/`,
|
|
634
|
+
`Run its test: npx tsx tests/${table}.test.ts`,
|
|
635
|
+
`Serve and try: npx tina4nodejs serve -> curl http://localhost:7148/api/${name.replace(/^\//, "")}`,
|
|
636
|
+
],
|
|
637
|
+
crud: ({ name, table }) => [
|
|
638
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
639
|
+
`Serve and try: npx tina4nodejs serve -> visit /swagger`,
|
|
640
|
+
`Run the gate test: npx tsx tests/${toPlural(table)}.test.ts`,
|
|
641
|
+
`Change fields: edit src/models/${name}.ts then re-run generate crud`,
|
|
642
|
+
],
|
|
643
|
+
migration: () => [
|
|
644
|
+
`Apply pending migrations: npx tina4nodejs migrate`,
|
|
645
|
+
`Check status: npx tina4nodejs migrate:status`,
|
|
646
|
+
`Roll back the batch: npx tina4nodejs migrate:rollback`,
|
|
647
|
+
],
|
|
648
|
+
middleware: ({ name }) => [
|
|
649
|
+
`Wire it: router.middleware(before${name}, after${name}) — or bind per-route`,
|
|
650
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`,
|
|
651
|
+
],
|
|
652
|
+
test: ({ name }) => [
|
|
653
|
+
`Fill the TODOs in tests/${toSnake(name)}.test.ts`,
|
|
654
|
+
`Run it: npx tsx tests/${toSnake(name)}.test.ts`,
|
|
655
|
+
],
|
|
656
|
+
form: ({ name, table }) => [
|
|
657
|
+
`Render from a route: res.render("forms/${table}.twig", { item })`,
|
|
658
|
+
`Add the POST route: npx tina4nodejs generate route ${toPlural(table)} --model ${name}`,
|
|
659
|
+
],
|
|
660
|
+
view: ({ table }) => [
|
|
661
|
+
`Wire routes to render list -> ${toPlural(table)}.twig, detail -> ${table}.twig`,
|
|
662
|
+
`Customize the templates in src/templates/pages/`,
|
|
663
|
+
],
|
|
664
|
+
auth: () => [
|
|
665
|
+
`Apply the migration: npx tina4nodejs migrate`,
|
|
666
|
+
`Run the auth test: npx tsx tests/auth.test.ts`,
|
|
667
|
+
`Try register: curl -X POST http://localhost:7148/api/auth/register -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
|
|
668
|
+
`Login: curl -X POST http://localhost:7148/api/auth/login -d '{"email":"a@b.c","password":"secret12"}' -H 'content-type: application/json'`,
|
|
669
|
+
],
|
|
670
|
+
service: ({ name }) => [
|
|
671
|
+
`Wire ServiceRunner in app.ts: await ServiceRunner.discover("src/services"); ServiceRunner.start();`,
|
|
672
|
+
`Fill the task body in src/services/${toSnake(name)}.ts`,
|
|
673
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`,
|
|
674
|
+
],
|
|
675
|
+
queue: ({ name }) => {
|
|
676
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "topic";
|
|
677
|
+
return [
|
|
678
|
+
`Fill handle${toPascal(name)}() in src/services/${slug}_consumer.ts`,
|
|
679
|
+
`Produce a job: publish${toPascal(name)}({ ... })`,
|
|
680
|
+
`Run the worker: npx tina4nodejs queue work ${name}`,
|
|
681
|
+
`Run its test: npx tsx tests/${slug}.test.ts`,
|
|
682
|
+
];
|
|
683
|
+
},
|
|
684
|
+
validator: ({ name }) => [
|
|
685
|
+
`Add rules in src/validators/${toSnake(name)}.ts (.email/.minLength/.integer/.inList/.pattern)`,
|
|
686
|
+
`Run its test: npx tsx tests/${toSnake(name)}.test.ts`,
|
|
687
|
+
],
|
|
688
|
+
seeder: ({ name, table }) => [
|
|
689
|
+
`Override any fields that need a specific shape in src/seeds/${table}_seeder.ts`,
|
|
690
|
+
`Seed the table: npx tina4nodejs seed`,
|
|
691
|
+
`Run its test: npx tsx tests/${table}_seeder.test.ts`,
|
|
692
|
+
],
|
|
693
|
+
websocket: ({ name }) => {
|
|
694
|
+
const raw = name.trim();
|
|
695
|
+
const slugRaw = toSnake(raw.replace(/^\/+|\/+$/g, "").replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "ws";
|
|
696
|
+
const base = slugRaw.startsWith("ws_") ? slugRaw.slice(3) : slugRaw;
|
|
697
|
+
return [
|
|
698
|
+
`Import once in app.ts to register: import "./src/routes/ws_${base}.js";`,
|
|
699
|
+
`Fill the "message" branch in src/routes/ws_${base}.ts`,
|
|
700
|
+
`Run its test: npx tsx tests/ws_${base}.test.ts`,
|
|
701
|
+
];
|
|
702
|
+
},
|
|
703
|
+
listener: ({ name }) => {
|
|
704
|
+
const slug = toSnake(name.replace(/[^0-9a-zA-Z]+/g, "_")).replace(/^_+|_+$/g, "") || "event";
|
|
705
|
+
return [
|
|
706
|
+
`Import once in app.ts to register: import "./src/listeners/${slug}.js";`,
|
|
707
|
+
`Fill the reaction in src/listeners/${slug}.ts`,
|
|
708
|
+
`Run its test: npx tsx tests/${slug}.test.ts`,
|
|
709
|
+
];
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
|
|
480
713
|
// ── Main entry point ────────────────────────────────────────────────
|
|
481
714
|
|
|
482
715
|
export async function generate(what: string, name: string, extraArgs: string[] = []): Promise<void> {
|
|
@@ -491,8 +724,17 @@ export async function generate(what: string, name: string, extraArgs: string[] =
|
|
|
491
724
|
process.exit(1);
|
|
492
725
|
}
|
|
493
726
|
|
|
494
|
-
// Auth doesn't require a name
|
|
727
|
+
// Auth doesn't require a name.
|
|
495
728
|
const noNameGenerators = new Set(["auth"]);
|
|
729
|
+
// bin.ts always passes argv[1] as `name`. For a no-name generator that means
|
|
730
|
+
// `generate auth --json` arrives here as name="--json", extraArgs=[]. Rescue
|
|
731
|
+
// the flag: shift a leading `--foo` name into extraArgs so parseCliArgs
|
|
732
|
+
// actually sees it. Fixes `--json` (and any other flag) being silently
|
|
733
|
+
// eaten by the no-name verbs.
|
|
734
|
+
if (noNameGenerators.has(what) && name.startsWith("--")) {
|
|
735
|
+
extraArgs = [name, ...extraArgs];
|
|
736
|
+
name = "";
|
|
737
|
+
}
|
|
496
738
|
if (!noNameGenerators.has(what) && !name) {
|
|
497
739
|
console.error(` Usage: tina4nodejs generate ${what} <name> [options]`);
|
|
498
740
|
process.exit(1);
|
|
@@ -520,11 +762,73 @@ export async function generate(what: string, name: string, extraArgs: string[] =
|
|
|
520
762
|
process.exit(1);
|
|
521
763
|
}
|
|
522
764
|
|
|
765
|
+
// v1.1 (ADR-0063): populate `resolution.next[]` from the per-verb curator
|
|
766
|
+
// AFTER dispatch, so the table name reflects any reserved-word pluralisation
|
|
767
|
+
// that fired during the run (Order -> orders). Prefer the resolution's own
|
|
768
|
+
// table_name (already set by generateModel/generateMigration) so we do NOT
|
|
769
|
+
// re-invoke toTableName() — that would record a DUPLICATE
|
|
770
|
+
// reserved_word_pluralize transformation on the envelope.
|
|
771
|
+
const nextFn = NEXT_STEPS[what];
|
|
772
|
+
if (nextFn) {
|
|
773
|
+
const resolvedTable = __resolution.body.table_name
|
|
774
|
+
?? (name
|
|
775
|
+
? (SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name))
|
|
776
|
+
: "");
|
|
777
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
778
|
+
}
|
|
779
|
+
|
|
523
780
|
// Emit the resolution AFTER dispatch so `actions_taken` reflects the real
|
|
524
781
|
// writes (or the empty list under `--dry-run`).
|
|
525
782
|
printResolution();
|
|
526
783
|
}
|
|
527
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Programmatic entry point for in-process consumers (MCP tools, tests, hosted
|
|
787
|
+
* agents) — does everything `generate()` does EXCEPT print.
|
|
788
|
+
*
|
|
789
|
+
* Reset the resolution → dispatch to the requested generator → populate `next[]`
|
|
790
|
+
* → return the envelope. Files still land on disk (unless `--dry-run` is passed
|
|
791
|
+
* in `extraArgs`); only the human "Created …" per-file log and the
|
|
792
|
+
* `printResolution()` output are suppressed (via `jsonMode: true`, the same
|
|
793
|
+
* suppression `--json` uses on the CLI).
|
|
794
|
+
*
|
|
795
|
+
* Used by the MCP `migration_create` tool (packages/core/src/mcp.ts) so the
|
|
796
|
+
* ADR-0063 `generate_v1_1` envelope drives every surface (CLI, MCP, tests)
|
|
797
|
+
* without a subprocess round-trip.
|
|
798
|
+
*/
|
|
799
|
+
export async function generateProgrammatic(
|
|
800
|
+
what: string,
|
|
801
|
+
name: string,
|
|
802
|
+
extraArgs: string[] = [],
|
|
803
|
+
): Promise<ResolutionEnvelope> {
|
|
804
|
+
const spec = GENERATORS[what];
|
|
805
|
+
if (!spec) throw new Error(`Unknown generator: ${what} (available: ${GENERATOR_LIST})`);
|
|
806
|
+
|
|
807
|
+
const { flags } = parseCliArgs(extraArgs);
|
|
808
|
+
const dryRun = Boolean(flags["dry-run"]);
|
|
809
|
+
// jsonMode:true suppresses writeFileSafe's per-file console.log so nothing
|
|
810
|
+
// leaks to stdout (which the JSON-RPC caller would parse as tool output).
|
|
811
|
+
// Files are still written — jsonMode gates PRINTS only, not disk writes.
|
|
812
|
+
resetResolution(what, { name, fields: (flags.fields as string) ?? null }, { dryRun, jsonMode: true });
|
|
813
|
+
|
|
814
|
+
spec.handler(name, flags);
|
|
815
|
+
|
|
816
|
+
// v1.1 (ADR-0063): populate `resolution.next[]` from the per-verb curator —
|
|
817
|
+
// same logic `generate()` runs, using the RESOLVED table_name when the
|
|
818
|
+
// dispatched handler recorded one (avoids a duplicate reserved_word
|
|
819
|
+
// transformation on the envelope).
|
|
820
|
+
const nextFn = NEXT_STEPS[what];
|
|
821
|
+
if (nextFn) {
|
|
822
|
+
const resolvedTable = __resolution.body.table_name
|
|
823
|
+
?? (name
|
|
824
|
+
? (SQL_RESERVED_TABLE_NAMES.has(toSnake(name)) ? pluralizeReserved(toSnake(name)) : toSnake(name))
|
|
825
|
+
: "");
|
|
826
|
+
setNextSteps(nextFn({ name: name || "", table: resolvedTable }));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return currentResolution();
|
|
830
|
+
}
|
|
831
|
+
|
|
528
832
|
// ── Model ───────────────────────────────────────────────────────────
|
|
529
833
|
|
|
530
834
|
function generateModel(name: string, flags: Record<string, string | boolean>, emitTest = true): void {
|
|
@@ -549,6 +853,7 @@ function generateModel(name: string, flags: Record<string, string | boolean>, em
|
|
|
549
853
|
// Build field definitions
|
|
550
854
|
const fieldLines: string[] = [
|
|
551
855
|
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
|
|
856
|
+
` // tina4:edit add or change fields for this model (string,int,float,bool,text,datetime)`,
|
|
552
857
|
];
|
|
553
858
|
for (const [fname, ftype] of fields) {
|
|
554
859
|
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
@@ -637,6 +942,7 @@ ${modelImportBase}
|
|
|
637
942
|
export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
|
|
638
943
|
|
|
639
944
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
945
|
+
// tina4:edit tune pagination defaults or add filter/sort parsing here
|
|
640
946
|
const page = parseInt(req.query.page as string) || 1;
|
|
641
947
|
const limit = parseInt(req.query.limit as string) || 20;
|
|
642
948
|
const offset = (page - 1) * limit;
|
|
@@ -674,6 +980,7 @@ ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Creat
|
|
|
674
980
|
|
|
675
981
|
// ${writeDoc}
|
|
676
982
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
983
|
+
// tina4:edit validate the body before persist (Validator or hand-checks)
|
|
677
984
|
${extend("validate / business rules before persist",
|
|
678
985
|
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
679
986
|
// save() returns false on failure rather than throwing - check it, or a failed
|
|
@@ -695,6 +1002,7 @@ ${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular
|
|
|
695
1002
|
|
|
696
1003
|
// ${writeDoc}
|
|
697
1004
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
1005
|
+
// tina4:edit fill the create handler (see AI-FILL fill-spec below)
|
|
698
1006
|
${aiFill(`create_${singular}`, {
|
|
699
1007
|
intent: `validate the body and persist a new ${singular}`,
|
|
700
1008
|
given: "req.body -> the posted fields",
|
|
@@ -761,6 +1069,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
761
1069
|
res.json({ error: "Not found" }, 404);
|
|
762
1070
|
return;
|
|
763
1071
|
}
|
|
1072
|
+
// tina4:edit guard which fields may be updated and who may update this row
|
|
764
1073
|
${extend("guard which fields / who may update",
|
|
765
1074
|
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`)} Object.assign(item, req.body as Record<string, unknown>);
|
|
766
1075
|
// save() returns false on failure rather than throwing - check it, or a failed
|
|
@@ -782,6 +1091,7 @@ ${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by
|
|
|
782
1091
|
|
|
783
1092
|
// ${writeDoc}
|
|
784
1093
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
1094
|
+
// tina4:edit fill the update handler (see AI-FILL fill-spec below)
|
|
785
1095
|
${aiFill(`update_${singular}`, {
|
|
786
1096
|
intent: `load, mutate and save an existing ${singular}`,
|
|
787
1097
|
given: "req.params.id -> id; req.body -> changed fields",
|
|
@@ -855,7 +1165,10 @@ function generateCrud(name: string, flags: Record<string, string | boolean>): vo
|
|
|
855
1165
|
const routeName = toPlural(table);
|
|
856
1166
|
const isPublic = Boolean(flags.public);
|
|
857
1167
|
|
|
858
|
-
|
|
1168
|
+
// Human-only banners; suppressed under --json to keep stdout parseable
|
|
1169
|
+
// (a console.log during --json produced invalid JSON). writeFileSafe already
|
|
1170
|
+
// gates its own "Created …" lines the same way.
|
|
1171
|
+
if (!__resolution.jsonMode) console.log(`\n Generating CRUD for ${name}...\n`);
|
|
859
1172
|
|
|
860
1173
|
// 1. Model + migration (its own model test is suppressed — the gate test
|
|
861
1174
|
// below is CRUD's single, broader co-emitted test).
|
|
@@ -875,14 +1188,16 @@ function generateCrud(name: string, flags: Record<string, string | boolean>): vo
|
|
|
875
1188
|
// 5. Test — real secure-by-default boot-gate (reads public, writes gated).
|
|
876
1189
|
generateTest(routeName, { model: name, "secure-writes": true, public: isPublic });
|
|
877
1190
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1191
|
+
if (!__resolution.jsonMode) {
|
|
1192
|
+
console.log(`\n CRUD generation complete for ${name}.`);
|
|
1193
|
+
console.log(" Run: tina4nodejs migrate");
|
|
1194
|
+
console.log(" Visit: /swagger to see the API docs");
|
|
1195
|
+
}
|
|
881
1196
|
}
|
|
882
1197
|
|
|
883
1198
|
// ── Migration ───────────────────────────────────────────────────────
|
|
884
1199
|
|
|
885
|
-
function generateMigration(
|
|
1200
|
+
export function generateMigration(
|
|
886
1201
|
name: string,
|
|
887
1202
|
flags: Record<string, string | boolean>,
|
|
888
1203
|
fieldsOverride?: Array<[string, string]>,
|
|
@@ -938,6 +1253,15 @@ function generateMigration(
|
|
|
938
1253
|
let upSql: string;
|
|
939
1254
|
let downSql: string;
|
|
940
1255
|
|
|
1256
|
+
// ADR-0063 (scaffolding envelope v1.1): `-- tina4:edit <label>` markers
|
|
1257
|
+
// sit above each SQL block so an operator (or an AI agent) can grep to
|
|
1258
|
+
// the genuine first-edit spot. Wording mirrors Ruby's `generate_migration`
|
|
1259
|
+
// + PHP's `sqlEditHintMarker` calls for cross-language parity. The create-
|
|
1260
|
+
// branch marker sits INSIDE the column-list parentheses but OUTSIDE
|
|
1261
|
+
// `colLines.join(",\n")` so its trailing `,` does not become part of the
|
|
1262
|
+
// parsed label (the migration runner's splitStatements strips `--` line
|
|
1263
|
+
// comments, so a comma-less marker line inside a DDL body is still
|
|
1264
|
+
// syntactically inert on every engine).
|
|
941
1265
|
if (isCreate) {
|
|
942
1266
|
const colLines = [" id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
943
1267
|
for (const [fname, ftype] of fields) {
|
|
@@ -947,11 +1271,20 @@ function generateMigration(
|
|
|
947
1271
|
}
|
|
948
1272
|
colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
|
|
949
1273
|
|
|
950
|
-
upSql =
|
|
951
|
-
|
|
1274
|
+
upSql =
|
|
1275
|
+
`CREATE TABLE IF NOT EXISTS ${table} (\n` +
|
|
1276
|
+
` -- tina4:edit add columns beyond id + created_at\n` +
|
|
1277
|
+
`${colLines.join(",\n")}\n);`;
|
|
1278
|
+
downSql =
|
|
1279
|
+
`-- tina4:edit mirror the CREATE's added columns in the rollback\n` +
|
|
1280
|
+
`DROP TABLE IF EXISTS ${table};`;
|
|
952
1281
|
} else {
|
|
953
|
-
upSql =
|
|
954
|
-
|
|
1282
|
+
upSql =
|
|
1283
|
+
`-- tina4:edit write your UP migration SQL here\n` +
|
|
1284
|
+
`-- Example: ALTER TABLE ${table} ADD COLUMN new_col TEXT DEFAULT '';`;
|
|
1285
|
+
downSql =
|
|
1286
|
+
`-- tina4:edit write your DOWN rollback SQL here\n` +
|
|
1287
|
+
`-- Example: ALTER TABLE ${table} DROP COLUMN new_col;`;
|
|
955
1288
|
}
|
|
956
1289
|
|
|
957
1290
|
const now = isoNow();
|
|
@@ -1008,6 +1341,7 @@ export async function before${name}(
|
|
|
1008
1341
|
res: Tina4Response,
|
|
1009
1342
|
next: () => Promise<void>,
|
|
1010
1343
|
): Promise<void> {
|
|
1344
|
+
// tina4:edit replace the Authorization check with the real pre-request rule
|
|
1011
1345
|
const auth = req.headers["authorization"];
|
|
1012
1346
|
if (!auth) {
|
|
1013
1347
|
res.json({ error: "Unauthorized" }, 401);
|
|
@@ -1021,7 +1355,7 @@ export async function after${name}(
|
|
|
1021
1355
|
res: Tina4Response,
|
|
1022
1356
|
next: () => Promise<void>,
|
|
1023
1357
|
): Promise<void> {
|
|
1024
|
-
//
|
|
1358
|
+
// tina4:edit add post-processing (logging, header injection, telemetry)
|
|
1025
1359
|
await next();
|
|
1026
1360
|
}
|
|
1027
1361
|
`;
|
|
@@ -1113,35 +1447,35 @@ process.exit(fail > 0 ? 1 : 0);
|
|
|
1113
1447
|
const list${model}s = tests(
|
|
1114
1448
|
assertTrue([]),
|
|
1115
1449
|
)(function list${model}s() {
|
|
1116
|
-
//
|
|
1450
|
+
// tina4:edit assert against a real GET /api/${toSnake(name)} response (rows, count)
|
|
1117
1451
|
return true;
|
|
1118
1452
|
});
|
|
1119
1453
|
|
|
1120
1454
|
const get${model} = tests(
|
|
1121
1455
|
assertTrue([]),
|
|
1122
1456
|
)(function get${model}() {
|
|
1123
|
-
//
|
|
1457
|
+
// tina4:edit assert against GET /api/${toSnake(name)}/{id} for one seeded row
|
|
1124
1458
|
return true;
|
|
1125
1459
|
});
|
|
1126
1460
|
|
|
1127
1461
|
const create${model} = tests(
|
|
1128
1462
|
assertTrue([]),
|
|
1129
1463
|
)(function create${model}() {
|
|
1130
|
-
//
|
|
1464
|
+
// tina4:edit POST a valid + an invalid body, assert 201 vs 400
|
|
1131
1465
|
return true;
|
|
1132
1466
|
});
|
|
1133
1467
|
|
|
1134
1468
|
const update${model} = tests(
|
|
1135
1469
|
assertTrue([]),
|
|
1136
1470
|
)(function update${model}() {
|
|
1137
|
-
//
|
|
1471
|
+
// tina4:edit PUT changed fields, assert the row was persisted
|
|
1138
1472
|
return true;
|
|
1139
1473
|
});
|
|
1140
1474
|
|
|
1141
1475
|
const delete${model} = tests(
|
|
1142
1476
|
assertTrue([]),
|
|
1143
1477
|
)(function delete${model}() {
|
|
1144
|
-
//
|
|
1478
|
+
// tina4:edit DELETE the id, assert 200 then GET returns 404
|
|
1145
1479
|
return true;
|
|
1146
1480
|
});
|
|
1147
1481
|
|
|
@@ -1158,7 +1492,7 @@ void [list${model}s, get${model}, create${model}, update${model}, delete${model}
|
|
|
1158
1492
|
const test${titleName} = tests(
|
|
1159
1493
|
assertTrue([]),
|
|
1160
1494
|
)(function test${titleName}() {
|
|
1161
|
-
//
|
|
1495
|
+
// tina4:edit assert against the real behaviour under test (no mocks)
|
|
1162
1496
|
return true;
|
|
1163
1497
|
});
|
|
1164
1498
|
|
|
@@ -1222,12 +1556,18 @@ function generateForm(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1222
1556
|
}
|
|
1223
1557
|
}
|
|
1224
1558
|
|
|
1559
|
+
// ADR-0063 (scaffolding envelope v1.1): `{# tina4:edit <label> #}` is the
|
|
1560
|
+
// Twig-comment partner of `// tina4:edit` in TS/JS. Baked at a genuine
|
|
1561
|
+
// first-edit spot so an operator (or an AI agent) can grep to a real
|
|
1562
|
+
// customisation point instead of scanning the whole file. Marker wording
|
|
1563
|
+
// mirrors Ruby's `generate_form` for cross-language parity.
|
|
1225
1564
|
const content =
|
|
1226
1565
|
`{% extends "base.twig" %}\n` +
|
|
1227
1566
|
`{% block title %}${name} {% if item.id %}Edit{% else %}Create{% endif %}{% endblock %}\n` +
|
|
1228
1567
|
`{% block content %}\n` +
|
|
1229
1568
|
`<div class="container mt-4">\n` +
|
|
1230
1569
|
` <h1>{% if item.id %}Edit ${name}{% else %}Create ${name}{% endif %}</h1>\n` +
|
|
1570
|
+
` {# tina4:edit restyle the form beyond the scaffolded defaults #}\n` +
|
|
1231
1571
|
` <form method="post" action="/api/${routeName}{% if item.id %}/{{ item.id }}{% endif %}">\n` +
|
|
1232
1572
|
` {{ form_token() }}\n` +
|
|
1233
1573
|
fieldHtml +
|
|
@@ -1259,11 +1599,16 @@ function generateView(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1259
1599
|
const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
|
|
1260
1600
|
const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
|
|
1261
1601
|
|
|
1602
|
+
// ADR-0063 (scaffolding envelope v1.1): `{# tina4:edit <label> #}` markers
|
|
1603
|
+
// sit at each template's genuine first-edit spot (list = customise the
|
|
1604
|
+
// table; detail = extend the record's view). Wording mirrors Ruby's
|
|
1605
|
+
// `generate_view` for cross-language parity.
|
|
1262
1606
|
const listContent =
|
|
1263
1607
|
`{% extends "base.twig" %}\n` +
|
|
1264
1608
|
`{% block title %}${name}s{% endblock %}\n` +
|
|
1265
1609
|
`{% block content %}\n` +
|
|
1266
1610
|
`<div class="container mt-4">\n` +
|
|
1611
|
+
` {# tina4:edit add sort / filter / pagination controls to the list #}\n` +
|
|
1267
1612
|
` <div class="d-flex justify-content-between align-items-center mb-3">\n` +
|
|
1268
1613
|
` <h1>${name}s</h1>\n` +
|
|
1269
1614
|
` <a href="/${routeName}/create" class="btn btn-primary">Add ${name}</a>\n` +
|
|
@@ -1305,6 +1650,7 @@ function generateView(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1305
1650
|
`{% block title %}${name} Detail{% endblock %}\n` +
|
|
1306
1651
|
`{% block content %}\n` +
|
|
1307
1652
|
`<div class="container mt-4">\n` +
|
|
1653
|
+
` {# tina4:edit extend the detail view with related records or actions #}\n` +
|
|
1308
1654
|
` <div class="d-flex justify-content-between align-items-center mb-3">\n` +
|
|
1309
1655
|
` <h1>${name} #{{ item.id }}</h1>\n` +
|
|
1310
1656
|
` <div>\n` +
|
|
@@ -1322,7 +1668,8 @@ function generateView(name: string, flags: Record<string, string | boolean>): vo
|
|
|
1322
1668
|
// ── Auth (login/register stay PUBLIC) ───────────────────────────────
|
|
1323
1669
|
|
|
1324
1670
|
function generateAuth(_flags: Record<string, string | boolean>): void {
|
|
1325
|
-
|
|
1671
|
+
// Human-only banners; suppressed under --json to keep stdout parseable.
|
|
1672
|
+
if (!__resolution.jsonMode) console.log("\n Generating authentication scaffolding...\n");
|
|
1326
1673
|
|
|
1327
1674
|
// 1. User model + migration (model test suppressed — the auth test below is
|
|
1328
1675
|
// the composite, broader co-emitted test).
|
|
@@ -1352,6 +1699,7 @@ export const secure = false;
|
|
|
1352
1699
|
export const meta = { summary: "Register a new user", tags: ["auth"] };
|
|
1353
1700
|
|
|
1354
1701
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
1702
|
+
// tina4:edit add password-strength / email-format / captcha rules before mint
|
|
1355
1703
|
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
1356
1704
|
|
|
1357
1705
|
if (!email || !password) {
|
|
@@ -1385,6 +1733,7 @@ export const secure = false;
|
|
|
1385
1733
|
export const meta = { summary: "Login and receive JWT token", tags: ["auth"] };
|
|
1386
1734
|
|
|
1387
1735
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
1736
|
+
// tina4:edit add rate-limit / lock-after-N-failures / 2FA before password check
|
|
1388
1737
|
const { email, password } = (req.body ?? {}) as { email?: string; password?: string };
|
|
1389
1738
|
|
|
1390
1739
|
if (!email || !password) {
|
|
@@ -1399,6 +1748,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
1399
1748
|
}
|
|
1400
1749
|
|
|
1401
1750
|
const data = user.toObject();
|
|
1751
|
+
// tina4:edit set token TTL (getToken(payload, secret, expiresInMinutes)) and add scopes if needed
|
|
1402
1752
|
const token = getToken({ userId: data.id, email: data.email, role: data.role });
|
|
1403
1753
|
res.json({ token });
|
|
1404
1754
|
}
|
|
@@ -1482,11 +1832,13 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
1482
1832
|
// 5. Auth test — real register / login / me end-to-end (no mocks).
|
|
1483
1833
|
emitAuthTest();
|
|
1484
1834
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1835
|
+
if (!__resolution.jsonMode) {
|
|
1836
|
+
console.log("\n Authentication scaffolding complete.");
|
|
1837
|
+
console.log(" Run: tina4nodejs migrate");
|
|
1838
|
+
console.log(" POST /api/auth/register — create account (public)");
|
|
1839
|
+
console.log(" POST /api/auth/login — get JWT token (public)");
|
|
1840
|
+
console.log(" GET /api/auth/me — get profile (requires token)");
|
|
1841
|
+
}
|
|
1490
1842
|
}
|
|
1491
1843
|
|
|
1492
1844
|
// ── Service (scheduled background task — ServiceRunner) ──────────────
|
|
@@ -1537,6 +1889,7 @@ function generateService(name: string, flags: Record<string, string | boolean>):
|
|
|
1537
1889
|
*/
|
|
1538
1890
|
|
|
1539
1891
|
export async function ${camel}Task(context: ServiceContext): Promise<void> {
|
|
1892
|
+
// tina4:edit replace the AI-FILL stub below with the scheduled work
|
|
1540
1893
|
${body}}
|
|
1541
1894
|
|
|
1542
1895
|
// Discovered by ServiceRunner.discover("src/services") — it reads name/handler
|
|
@@ -1593,6 +1946,7 @@ export function publish${pascal}(payload: Record<string, unknown>): string {
|
|
|
1593
1946
|
|
|
1594
1947
|
/** Process ONE ${topic} job payload. */
|
|
1595
1948
|
export async function handle${pascal}(payload: unknown): Promise<void> {
|
|
1949
|
+
// tina4:edit implement the per-job handler; return to ack, throw to nack
|
|
1596
1950
|
${body}}
|
|
1597
1951
|
|
|
1598
1952
|
/** Long-running ${topic} worker — consume() yields jobs; ack/nack each. */
|
|
@@ -1655,6 +2009,7 @@ function generateValidator(name: string, _flags: Record<string, string | boolean
|
|
|
1655
2009
|
*/
|
|
1656
2010
|
export function validate${toPascal(name)}(data: Record<string, unknown>): Validator {
|
|
1657
2011
|
const validator = new Validator(data);
|
|
2012
|
+
// tina4:edit add rules for this payload (.email/.minLength/.integer/.inList/.pattern)
|
|
1658
2013
|
${rules} validator.required("name"); // starter rule (matches the model's default field)
|
|
1659
2014
|
return validator;
|
|
1660
2015
|
}
|
|
@@ -1695,6 +2050,7 @@ import ${name} from "../models/${name}.js";
|
|
|
1695
2050
|
* specific shape below. Each callable receives a FakeData instance.
|
|
1696
2051
|
*/
|
|
1697
2052
|
export function fieldOverrides(fake: FakeData): Record<string, unknown> {
|
|
2053
|
+
// tina4:edit override any fields that need a specific shape (seedOrm auto-fills the rest)
|
|
1698
2054
|
${overrides} void fake; // available for overrides above
|
|
1699
2055
|
return {};
|
|
1700
2056
|
}
|
|
@@ -1763,6 +2119,7 @@ export async function ${handlerName}(
|
|
|
1763
2119
|
data: string,
|
|
1764
2120
|
): Promise<void> {
|
|
1765
2121
|
if (event === "open") {
|
|
2122
|
+
// tina4:edit customize the welcome frame (or drop it)
|
|
1766
2123
|
connection.sendJson({ type: "welcome" });
|
|
1767
2124
|
return;
|
|
1768
2125
|
}
|
|
@@ -1770,6 +2127,7 @@ export async function ${handlerName}(
|
|
|
1770
2127
|
return;
|
|
1771
2128
|
}
|
|
1772
2129
|
// event === "message"
|
|
2130
|
+
// tina4:edit handle the inbound "message" frame (broadcast, echo, route, etc.)
|
|
1773
2131
|
${body}}
|
|
1774
2132
|
|
|
1775
2133
|
websocket("${wsPath}", ${handlerName});
|
|
@@ -1814,6 +2172,7 @@ function generateListener(name: string, _flags: Record<string, string | boolean>
|
|
|
1814
2172
|
* Fires when something calls Events.emit("${event}", ...args).
|
|
1815
2173
|
*/
|
|
1816
2174
|
export function ${handlerName}(...args: unknown[]): void {
|
|
2175
|
+
// tina4:edit implement the reaction to '${event}' (email, ORM write, follow-up emit)
|
|
1817
2176
|
${body}}
|
|
1818
2177
|
|
|
1819
2178
|
Events.on("${event}", ${handlerName});
|