You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is a RESTful API backend in Fastify with TypeScript, Prisma, PostgreSQL, better-auth, adminjs, openapi, Swagger, Scalar, Typebox, and DDD architecture.
- Only use `yarn` for package management, never `npm` or `pnpm`.
5
+
- Follow the module layout and dependency rules strictly. Check with `yarn run deps:validate`.
6
+
- Write unit tests for domain logic and services. Write integration tests for features.
7
+
- If something breaks, capture it in a test first, then fix the code. No untested hotfixes.
8
+
9
+
## Module layout (`src/modules/<module>/`)
10
+
11
+
Every module follows the same skeleton - mirror it when adding a new one:
12
+
13
+
-`domain/` - aggregates, value types, domain errors, pure logic (`<module>.domain.ts`, `<module>.errors.ts`, `<module>.types.ts`).
14
+
-`database/` - always three files: `<module>.repository.ts` (Prisma impl), `.repository.port.ts` (interface), `.repository.mock.ts` (for tests). Record shapes live here too (`<module>.record.ts`).
15
+
-`dtos/` - Typebox request/response schemas + inferred types. Paginated responses have their own `*.paginated.response.dto.ts`.
16
+
-`queries/` - read-side CQRS-like handlers for complex queries (`get-*.query.ts`, `list-*.query.ts`).
17
+
-`patches/` - write-side CQRS-like handlers for complex mutations (`fork-*.patch.ts`, `publish-*.patch.ts`).
- Always go through `transactionManager.run(async (ctx) => { ... })`.
28
+
- Use `repository.insertTx(ctx, entity)` / `updateFields(ctx, ...)` / `softDelete(ctx, ...)` inside the txn.
29
+
- Emit domain audit via `eventRepository.insert(ctx, { type, actorId, resourceType, resourceId, payload })` in the same txn. Event types are dotted (`model.created`, `model.deleted`).
30
+
- Soft delete, don't hard delete: models/users carry `deletedAt`. Call `modelDomain.assertNotDeleted(entity)` before mutating.
31
+
32
+
## Routes
33
+
34
+
- Auth: `preHandler: [requireAuth]` from `#src/shared/hooks/require-auth.ts`.
35
+
- Authorization on a model: `resolveModel('read' | 'write' | 'admin')` from `#src/shared/hooks/resolve-model.ts`.
- Use Typebox schemas for `body` / `params` / `querystring` / `response`. For querystring-heavy routes.
39
+
- Use `fastify.withTypeProvider<TypeBoxTypeProvider>().route({ ... })`.
40
+
- Return shapes: `201 { id }` on create (see `idDtoSchema`), `204` on update/delete, full DTO on read.
41
+
42
+
## Imports
43
+
44
+
Use path aliases, never relative `../../`:
45
+
46
+
-`#src/...` - app source
47
+
-`#prisma/index` - generated Prisma client (output is `generated/prisma`, not `@prisma/client`)
48
+
49
+
## Client/request context
50
+
51
+
- Trusted IP comes from `env.server.ipAddressHeaders` (ordered header list), then `req.ip`. See `src/server/plugins/rate-limit.ts` for the precedence pattern - reuse it, don't reimplement.
52
+
- Never store raw IPs. Hash with a rotating salt if you need uniqueness (PII / GDPR).
53
+
54
+
## Comments
55
+
56
+
- Write self-explanatory code. No decorative comments.
57
+
- A comment earns its place only when the *why* isn't in the code (non-obvious constraint, workaround, surprising invariant).
/** @description The version number, starting at 1 and incrementing by 1 for each new version of a model. */
374
375
versionNumber: number;
375
376
title: string;
376
377
description: string|null;
377
-
netlogoFileKey: string;
378
+
netlogoFileKey: string|null;
378
379
netlogoVersion: string|null;
379
380
infoTab: string|null;
380
381
/** Format: date-time */
381
382
createdAt: string;
382
383
isFinalized: boolean;
383
-
}|null;
384
+
}&{
385
+
netlogoFileDownloadUrl: string|null;
386
+
previewImageUrl: string|null;
387
+
})|null;
384
388
authors: ({
385
389
/** Format: uuid */
386
390
modelId: string;
@@ -415,6 +419,7 @@ export interface paths {
415
419
*/
416
420
createdAt: string;
417
421
}[];
422
+
previewImageUrl: string|null;
418
423
counts: {
419
424
versions: number;
420
425
children: number;
@@ -432,6 +437,108 @@ export interface paths {
432
437
patch?: never;
433
438
trace?: never;
434
439
};
440
+
"/api/v1/models/{id}/family/card": {
441
+
parameters: {
442
+
query?: never;
443
+
header?: never;
444
+
path?: never;
445
+
cookie?: never;
446
+
};
447
+
get: {
448
+
parameters: {
449
+
query?: never;
450
+
header?: never;
451
+
path: {
452
+
id: string;
453
+
};
454
+
cookie?: never;
455
+
};
456
+
requestBody?: never;
457
+
responses: {
458
+
/** @description Default Response */
459
+
200: {
460
+
headers: {
461
+
[name: string]: unknown;
462
+
};
463
+
content: {
464
+
"application/json": {
465
+
self: {
466
+
/** Format: uuid */
467
+
id: string;
468
+
title: string;
469
+
description: string|null;
470
+
visibility: string;
471
+
isEndorsed: boolean;
472
+
/** Format: date-time */
473
+
createdAt: string;
474
+
latestVersionNumber: number|null;
475
+
parentModelId: string|null;
476
+
parentVersionNumber: number|null;
477
+
authorName: string|null;
478
+
versionCount: number;
479
+
linkedVersionNumber: number|null;
480
+
};
481
+
parent: {
482
+
/** Format: uuid */
483
+
id: string;
484
+
title: string;
485
+
description: string|null;
486
+
visibility: string;
487
+
isEndorsed: boolean;
488
+
/** Format: date-time */
489
+
createdAt: string;
490
+
latestVersionNumber: number|null;
491
+
parentModelId: string|null;
492
+
parentVersionNumber: number|null;
493
+
authorName: string|null;
494
+
versionCount: number;
495
+
linkedVersionNumber: number|null;
496
+
}|null;
497
+
siblings: {
498
+
/** Format: uuid */
499
+
id: string;
500
+
title: string;
501
+
description: string|null;
502
+
visibility: string;
503
+
isEndorsed: boolean;
504
+
/** Format: date-time */
505
+
createdAt: string;
506
+
latestVersionNumber: number|null;
507
+
parentModelId: string|null;
508
+
parentVersionNumber: number|null;
509
+
authorName: string|null;
510
+
versionCount: number;
511
+
linkedVersionNumber: number|null;
512
+
}[];
513
+
children: {
514
+
/** Format: uuid */
515
+
id: string;
516
+
title: string;
517
+
description: string|null;
518
+
visibility: string;
519
+
isEndorsed: boolean;
520
+
/** Format: date-time */
521
+
createdAt: string;
522
+
latestVersionNumber: number|null;
523
+
parentModelId: string|null;
524
+
parentVersionNumber: number|null;
525
+
authorName: string|null;
526
+
versionCount: number;
527
+
linkedVersionNumber: number|null;
528
+
}[];
529
+
};
530
+
};
531
+
};
532
+
};
533
+
};
534
+
put?: never;
535
+
post?: never;
536
+
delete?: never;
537
+
options?: never;
538
+
head?: never;
539
+
patch?: never;
540
+
trace?: never;
541
+
};
435
542
"/api/v1/models/{id}/children": {
436
543
parameters: {
437
544
query?: never;
@@ -1086,10 +1193,11 @@ export interface paths {
1086
1193
data: {
1087
1194
/** Format: uuid */
1088
1195
modelId: string;
1196
+
/** @description The version number, starting at 1 and incrementing by 1 for each new version of a model. */
1089
1197
versionNumber: number;
1090
1198
title: string;
1091
1199
description: string|null;
1092
-
netlogoFileKey: string;
1200
+
netlogoFileKey: string|null;
1093
1201
netlogoVersion: string|null;
1094
1202
infoTab: string|null;
1095
1203
/** Format: date-time */
@@ -1102,6 +1210,7 @@ export interface paths {
1102
1210
};
1103
1211
};
1104
1212
put?: never;
1213
+
/** @description Create a new model version. Send as multipart/form-data with a required "file" field (the .nlogox) plus optional "title" and "description" text fields. */
1105
1214
post: {
1106
1215
parameters: {
1107
1216
query?: never;
@@ -1111,14 +1220,7 @@ export interface paths {
1111
1220
};
1112
1221
cookie?: never;
1113
1222
};
1114
-
requestBody: {
1115
-
content: {
1116
-
"application/json": {
1117
-
title?: string;
1118
-
description?: string;
1119
-
};
1120
-
};
1121
-
};
1223
+
requestBody?: never;
1122
1224
responses: {
1123
1225
/** @description Default Response */
1124
1226
201: {
@@ -1209,10 +1311,11 @@ export interface paths {
1209
1311
"application/json": {
1210
1312
/** Format: uuid */
1211
1313
modelId: string;
1314
+
/** @description The version number, starting at 1 and incrementing by 1 for each new version of a model. */
1212
1315
versionNumber: number;
1213
1316
title: string;
1214
1317
description: string|null;
1215
-
netlogoFileKey: string;
1318
+
netlogoFileKey: string|null;
1216
1319
netlogoVersion: string|null;
1217
1320
infoTab: string|null;
1218
1321
/** Format: date-time */
@@ -1231,6 +1334,105 @@ export interface paths {
1231
1334
patch?: never;
1232
1335
trace?: never;
1233
1336
};
1337
+
"/api/v1/models/{id}/versions/{version}/card": {
1338
+
parameters: {
1339
+
query?: never;
1340
+
header?: never;
1341
+
path?: never;
1342
+
cookie?: never;
1343
+
};
1344
+
get: {
1345
+
parameters: {
1346
+
query?: never;
1347
+
header?: never;
1348
+
path: {
1349
+
id: string;
1350
+
version: number;
1351
+
};
1352
+
cookie?: never;
1353
+
};
1354
+
requestBody?: never;
1355
+
responses: {
1356
+
/** @description Default Response */
1357
+
200: {
1358
+
headers: {
1359
+
[name: string]: unknown;
1360
+
};
1361
+
content: {
1362
+
"application/json": {
1363
+
version: {
1364
+
/** Format: uuid */
1365
+
modelId: string;
1366
+
/** @description The version number, starting at 1 and incrementing by 1 for each new version of a model. */
0 commit comments