@apso/cli 0.1.9 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +511 -0
  2. package/dist/commands/server/scaffold.js +1 -1
  3. package/dist/lib/controller.js +2 -2
  4. package/dist/lib/dto.js +1 -1
  5. package/dist/lib/entity.js +1 -1
  6. package/dist/lib/service.d.ts +1 -8
  7. package/dist/lib/service.js +28 -4
  8. package/dist/lib/templates/entities/entity-col-datetime.eta +4 -0
  9. package/dist/lib/templates/entities/entity-col-geography.eta +29 -0
  10. package/dist/lib/templates/entities/entity-col-geometry.eta +29 -0
  11. package/dist/lib/templates/entities/entity-col-geometrycollection.eta +29 -0
  12. package/dist/lib/templates/entities/entity-col-linestring.eta +37 -0
  13. package/dist/lib/templates/entities/entity-col-multilinestring.eta +44 -0
  14. package/dist/lib/templates/entities/entity-col-multipoint.eta +37 -0
  15. package/dist/lib/templates/entities/entity-col-multipolygon.eta +50 -0
  16. package/dist/lib/templates/entities/entity-col-polygon.eta +44 -0
  17. package/dist/lib/templates/graphql/dto/dto-col-decimal.eta +4 -0
  18. package/dist/lib/templates/graphql/dto/dto-col-geography.eta +4 -0
  19. package/dist/lib/templates/graphql/dto/dto-col-geometry.eta +4 -0
  20. package/dist/lib/templates/graphql/dto/dto-col-geometrycollection.eta +4 -0
  21. package/dist/lib/templates/graphql/dto/dto-col-linestring.eta +4 -0
  22. package/dist/lib/templates/graphql/dto/dto-col-multilinestring.eta +4 -0
  23. package/dist/lib/templates/graphql/dto/dto-col-multipoint.eta +4 -0
  24. package/dist/lib/templates/graphql/dto/dto-col-multipolygon.eta +4 -0
  25. package/dist/lib/templates/graphql/dto/dto-col-numeric.eta +4 -0
  26. package/dist/lib/templates/graphql/dto/dto-col-point.eta +4 -0
  27. package/dist/lib/templates/graphql/dto/dto-col-polygon.eta +4 -0
  28. package/dist/lib/templates/header.eta +8 -3
  29. package/dist/lib/templates/rest/dto-rest.eta +26 -1
  30. package/dist/lib/templates/rest/ormconfig.eta +2 -0
  31. package/dist/lib/templates/rest/service-rest-spec.eta +16 -7
  32. package/dist/lib/types/field.d.ts +3 -1
  33. package/dist/lib/utils/field.d.ts +1 -0
  34. package/dist/lib/utils/field.js +42 -9
  35. package/dist/lib/utils/file-system.d.ts +4 -0
  36. package/dist/lib/utils/file-system.js +9 -1
  37. package/dist/lib/utils/relationships/parse.js +1 -1
  38. package/dist/tests/templates/entities/types/decimal-field-validation.spec.d.ts +1 -0
  39. package/dist/tests/templates/entities/types/decimal-field-validation.spec.js +100 -0
  40. package/dist/tests/templates/entities/types/geometry-template.spec.d.ts +1 -0
  41. package/dist/tests/templates/entities/types/geometry-template.spec.js +58 -0
  42. package/dist/tests/templates/entities/types/polygon-template.spec.d.ts +1 -0
  43. package/dist/tests/templates/entities/types/polygon-template.spec.js +58 -0
  44. package/oclif.manifest.json +1 -1
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  - [Usage](#usage)
6
6
  - [Local Development](#local-development)
7
7
  - [Populating an .apsorc File](#populating-an-apsorc-file)
8
+ - [Auto-Generated Code Reference](#auto-generated-code-reference)
8
9
  - [Debugging](##debugging)
9
10
  - [Commands](#commands)
10
11
 
@@ -57,6 +58,94 @@ Follow these steps to create and run a new APSO server project:
57
58
 
58
59
  > For more details on configuring your schema, see the [Populating an .apsorc File](#populating-an-apsorc-file) section below.
59
60
 
61
+ ---
62
+
63
+ ## 📢 Important: Never Add Custom Code to Autogen Files
64
+
65
+ Apso CLI generates all files in the `autogen/` directory automatically.
66
+ **Any changes you make directly to these files will be overwritten the next time you run Apso CLI.**
67
+ To keep your custom logic safe and maintainable, always use the `extensions/` directory for any customizations.
68
+
69
+ ### How to Extend Apso-Generated Entities
70
+
71
+ #### 1. **Never modify files in `autogen/`**
72
+
73
+ - All files in `src/autogen/` are managed by Apso CLI.
74
+ - These include entities, services, controllers, DTOs, and modules.
75
+ - **Do not add custom endpoints, business logic, or integrations here.**
76
+
77
+ #### 2. **Add custom logic in `extensions/`**
78
+
79
+ - For each entity you want to extend, create a corresponding folder in `src/extensions/[[EntityName]]/`.
80
+ - Add your custom service, controller, and DTOs here.
81
+ - You can inject and extend the autogen service in your extension service.
82
+
83
+ #### 3. **Example Directory Structure**
84
+
85
+ ```
86
+ src/
87
+ autogen/
88
+ LambdaDeployment/
89
+ LambdaDeployment.service.ts # DO NOT MODIFY
90
+ LambdaDeployment.controller.ts
91
+ ...
92
+ extensions/
93
+ LambdaDeployment/
94
+ LambdaDeployment.service.ts # Add custom logic here
95
+ LambdaDeployment.controller.ts
96
+ ...
97
+ ```
98
+
99
+ #### 4. **Example: Extending LambdaDeployment**
100
+
101
+ **Custom Service:**
102
+ ```typescript
103
+ // src/extensions/LambdaDeployment/LambdaDeployment.service.ts
104
+ import { Injectable } from '@nestjs/common';
105
+ import { LambdaDeploymentService as AutogenLambdaDeploymentService } from '../../autogen/LambdaDeployment/LambdaDeployment.service';
106
+
107
+ @Injectable()
108
+ export class LambdaDeploymentService extends AutogenLambdaDeploymentService {
109
+ // Add your custom methods here
110
+ async deployWithLocalService(...) { ... }
111
+ }
112
+ ```
113
+
114
+ **Custom Controller:**
115
+ ```typescript
116
+ // src/extensions/LambdaDeployment/LambdaDeployment.controller.ts
117
+ import { Controller } from '@nestjs/common';
118
+ import { LambdaDeploymentService } from './LambdaDeployment.service';
119
+
120
+ @Controller('lambda-deployment')
121
+ export class LambdaDeploymentController {
122
+ constructor(private readonly lambdaDeploymentService: LambdaDeploymentService) {}
123
+
124
+ // Add custom endpoints here
125
+ }
126
+ ```
127
+
128
+ #### 5. **Why This Matters**
129
+
130
+ - Keeps your custom business logic safe from being overwritten.
131
+ - Makes it easy to regenerate your API as your data model evolves.
132
+ - Maintains a clean separation between generated code and your application logic.
133
+
134
+ #### 6. **Best Practices**
135
+
136
+ - Only use the autogen files for base CRUD and entity logic.
137
+ - Place all custom endpoints, integrations, and business logic in the `extensions/` directory.
138
+ - If you need to override or extend a method, subclass the autogen service in your extension service.
139
+
140
+ ---
141
+
142
+ **Summary:**
143
+ > Always put your custom code in `src/extensions/[[EntityName]]/`.
144
+ > Never modify files in `src/autogen/`.
145
+ > This ensures your work is safe and your project remains maintainable as you evolve your data model with Apso CLI.
146
+
147
+ ---
148
+
60
149
  # Local Development
61
150
 
62
151
  Clone apso-cli on your machine. Navigate to the repo in your code editor and run the below commands
@@ -169,6 +258,428 @@ The `.apsorc` file defines your domain model, including entities and their relat
169
258
 
170
259
  For a full example, see [`apso-cli/test/apsorc-json/apsorc.v2.json`](./test/apsorc-json/apsorc.v2.json).
171
260
 
261
+ ## Auto-Generated Code Reference
262
+
263
+ This section details the code that Apso CLI automatically generates from your `.apsorc` configuration, including fields, TypeORM mappings, validation, and naming conventions. Understanding these rules will help you predict and control your generated backend code.
264
+
265
+ ### 1. Auto-Generated Fields
266
+
267
+ #### Primary Keys
268
+ - Every entity automatically receives an `id` field, even if not defined in the `fields` array.
269
+ - Decorated as `@PrimaryGeneratedColumn()` (auto-incrementing integer).
270
+ - Type: `number`.
271
+
272
+ ```typescript
273
+ @PrimaryGeneratedColumn()
274
+ id: number;
275
+ ```
276
+
277
+ #### Timestamps
278
+ - If `"created_at": true` is set on the entity, generates:
279
+ ```typescript
280
+ @CreateDateColumn()
281
+ created_at: Date;
282
+ ```
283
+ - If `"updated_at": true` is set on the entity, generates:
284
+ ```typescript
285
+ @UpdateDateColumn()
286
+ updated_at: Date;
287
+ ```
288
+ - These are in addition to any fields you define.
289
+
290
+ #### Foreign Key Fields
291
+ - For each relationship, Apso generates the foreign key column and TypeORM decorators.
292
+ - Example:
293
+ ```json
294
+ { "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" }
295
+ ```
296
+ Generates:
297
+ ```typescript
298
+ @ManyToOne(() => User)
299
+ @JoinColumn({ name: 'ownerId' })
300
+ owner: User;
301
+
302
+ @Column({ type: 'integer' })
303
+ ownerId: number;
304
+ ```
305
+
306
+ ### 2. Field Type Mapping Table
307
+
308
+ > **⚠️ PostGIS Requirements:** The following spatial data types require the PostGIS extension to be installed in your PostgreSQL database:
309
+ > ```sql
310
+ > CREATE EXTENSION IF NOT EXISTS postgis;
311
+ > ```
312
+ > Ensure PostGIS is installed before using any of the spatial data types listed below.
313
+
314
+ | Apso Field Type | TypeORM Column Decorator | Auto-Applied Validation | Notes |
315
+ |-----------------|-----------------------------------------|----------------------------------------|---------------------------------------|
316
+ | `text` | `@Column({ type: 'text' })` | `@IsString()`, `@IsNotEmpty()` | With `length`: `@MaxLength(n)` |
317
+ | `json` | `@Column('jsonb')` | None | Uses JSONB in PostgreSQL |
318
+ | `enum` | `@Column({ type: 'enum', enum: [...] })`| None | Values array becomes enum |
319
+ | `boolean` | `@Column({ type: 'boolean' })` | `@IsBoolean()` | Default values supported |
320
+ | `integer` | `@Column({ type: 'integer' })` | `@IsNumber()` | |
321
+ | `decimal` | `@Column({ type: 'decimal', precision: n, scale: m })` | `@IsNumber()` | Precision/scale supported |
322
+ | `numeric` | `@Column({ type: 'numeric', precision: n, scale: m })` | `@IsNumber()` | Precision/scale supported |
323
+ | `timestamp` | `@Column({ type: 'timestamp' })` | None | |
324
+ | `point` | `@Column({ type: 'point' })` | None | PostGIS point geometry ⚠️ Requires PostGIS |
325
+ | `linestring` | `@Column({ type: 'linestring' })` | None | PostGIS line string geometry ⚠️ Requires PostGIS |
326
+ | `polygon` | `@Column({ type: 'polygon' })` | None | PostGIS polygon geometry ⚠️ Requires PostGIS |
327
+ | `multipoint` | `@Column({ type: 'multipoint' })` | None | PostGIS multi-point geometry ⚠️ Requires PostGIS |
328
+ | `multilinestring` | `@Column({ type: 'multilinestring' })` | None | PostGIS multi-line string geometry ⚠️ Requires PostGIS |
329
+ | `multipolygon` | `@Column({ type: 'multipolygon' })` | None | PostGIS multi-polygon geometry ⚠️ Requires PostGIS |
330
+ | `geometry` | `@Column({ type: 'geometry' })` | None | PostGIS generic geometry type ⚠️ Requires PostGIS |
331
+ | `geography` | `@Column({ type: 'geography' })` | None | PostGIS geography type ⚠️ Requires PostGIS |
332
+ | `geometrycollection` | `@Column({ type: 'geometrycollection' })` | None | PostGIS geometry collection ⚠️ Requires PostGIS |
333
+
334
+ #### Decimal/Numeric Field Examples
335
+
336
+ **Basic decimal field:**
337
+ ```json
338
+ {
339
+ "name": "price",
340
+ "type": "decimal",
341
+ "precision": 10,
342
+ "scale": 2,
343
+ "default": 0,
344
+ "nullable": false
345
+ }
346
+ ```
347
+ Generates:
348
+ ```typescript
349
+ @Column({ "type": "decimal", precision: 10, scale: 2, default: 0 })
350
+ price: number;
351
+ ```
352
+
353
+ **Numeric field with custom precision:**
354
+ ```json
355
+ {
356
+ "name": "bandwidth_gb",
357
+ "type": "numeric",
358
+ "precision": 10,
359
+ "scale": 3,
360
+ "default": 0,
361
+ "nullable": false
362
+ }
363
+ ```
364
+ Generates:
365
+ ```typescript
366
+ @Column({ "type": "numeric", precision: 10, scale: 3, default: 0 })
367
+ bandwidth_gb: number;
368
+ ```
369
+
370
+ #### PostGIS Field Examples
371
+
372
+ > **📋 PostGIS Setup Required:** Before using any PostGIS data types, ensure your PostgreSQL database has the PostGIS extension installed:
373
+ > ```sql
374
+ > -- Install PostGIS extension
375
+ > CREATE EXTENSION IF NOT EXISTS postgis;
376
+ >
377
+ > -- Verify installation
378
+ > SELECT PostGIS_Version();
379
+ > ```
380
+ >
381
+ > **Deployment Note:** When deploying applications with PostGIS fields, ensure the PostGIS extension is available in your production database environment.
382
+
383
+ **Point geometry field:**
384
+ ```json
385
+ {
386
+ "name": "location",
387
+ "type": "point",
388
+ "nullable": true
389
+ }
390
+ ```
391
+ Generates:
392
+ ```typescript
393
+ @Column({
394
+ "type": "point",
395
+ transformer: {
396
+ to: (point: {x: number, y: number} | null) => {
397
+ if (!point) return null;
398
+ return `(${point.x},${point.y})`;
399
+ },
400
+ from: (pgPoint: string | null) => {
401
+ if (!pgPoint) return null;
402
+ const [x, y] = pgPoint.substring(1, pgPoint.length - 1).split(',');
403
+ return { x: parseFloat(x), y: parseFloat(y) };
404
+ }
405
+ },
406
+ nullable: true
407
+ })
408
+ location: { x: number, y: number };
409
+ ```
410
+
411
+ **Polygon geometry field:**
412
+ ```json
413
+ {
414
+ "name": "boundary",
415
+ "type": "polygon",
416
+ "nullable": true
417
+ }
418
+ ```
419
+ Generates:
420
+ ```typescript
421
+ @Column({
422
+ "type": "polygon",
423
+ transformer: {
424
+ to: (polygon: { coordinates: Array<Array<{x: number, y: number}>> } | null) => {
425
+ if (!polygon) return null;
426
+ const rings = polygon.coordinates.map(ring => {
427
+ const coords = ring.map(coord => `${coord.x} ${coord.y}`).join(',');
428
+ return `(${coords})`;
429
+ });
430
+ return `POLYGON(${rings.join(',')})`;
431
+ },
432
+ from: (pgPolygon: string | null) => {
433
+ if (!pgPolygon) return null;
434
+ const match = pgPolygon.match(/POLYGON\((.+)\)/);
435
+ if (!match) return null;
436
+ const rings = match[1].split('),(').map(ring => {
437
+ const cleanRing = ring.replace(/[()]/g, '');
438
+ const coords = cleanRing.split(',').map(coord => {
439
+ const [x, y] = coord.trim().split(' ');
440
+ return { x: parseFloat(x), y: parseFloat(y) };
441
+ });
442
+ return coords;
443
+ });
444
+ return { coordinates: rings };
445
+ }
446
+ },
447
+ nullable: true
448
+ })
449
+ boundary: { coordinates: Array<Array<{ x: number, y: number }>> };
450
+ ```
451
+
452
+ **Generic geometry field:**
453
+ ```json
454
+ {
455
+ "name": "shape",
456
+ "type": "geometry",
457
+ "nullable": true
458
+ }
459
+ ```
460
+ Generates:
461
+ ```typescript
462
+ @Column({
463
+ "type": "geometry",
464
+ transformer: {
465
+ to: (geometry: any) => {
466
+ if (!geometry) return null;
467
+ return geometry;
468
+ },
469
+ from: (pgGeometry: any) => {
470
+ if (!pgGeometry) return null;
471
+ return pgGeometry;
472
+ }
473
+ },
474
+ nullable: true
475
+ })
476
+ shape: any;
477
+ ```
478
+
479
+ ### 3. Validation Rules Documentation
480
+
481
+ Field properties in `.apsorc` map to validation decorators as follows:
482
+
483
+ - `"unique": true` → `@Column({ unique: true })`
484
+ - `"nullable": true` → `@Column({ nullable: true })` and `@IsOptional()`
485
+ - `"is_email": true` → `@IsEmail()`
486
+ - `"length": 255` → `@MaxLength(255)`
487
+ - `"precision": 10` → `@Column({ precision: 10 })` (for decimal/numeric types)
488
+ - `"scale": 2` → `@Column({ scale: 2 })` (for decimal/numeric types)
489
+ - `"required": false` → `@IsOptional()` (for CREATE group)
490
+
491
+ ### 4. Relationship Generation Rules
492
+
493
+ #### OneToMany Relationships
494
+
495
+ ```json
496
+ { "from": "User", "to": "Workspace", "type": "OneToMany", "to_name": "ownedWorkspaces" }
497
+ ```
498
+ Generates:
499
+ ```typescript
500
+ @OneToMany(() => Workspace, (workspace) => workspace.user)
501
+ ownedWorkspaces: Workspace[];
502
+ ```
503
+
504
+ #### ManyToOne Relationships
505
+
506
+ ```json
507
+ { "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" }
508
+ ```
509
+ Generates:
510
+ ```typescript
511
+ @ManyToOne(() => User, (user) => user.ownedWorkspaces)
512
+ @JoinColumn({ name: 'userId' })
513
+ owner: User;
514
+
515
+ @Column({ type: 'integer' })
516
+ userId: number;
517
+ ```
518
+
519
+ #### Foreign Key Naming
520
+ - Foreign key columns use camelCase + `Id` (e.g., `userId`).
521
+ - Join column names match the foreign key column name.
522
+
523
+ ### 5. Entity Naming Conventions
524
+ - Entity class names remain as defined in `.apsorc` (e.g., `User`).
525
+ - Table names are lowercased (e.g., `user`).
526
+ - Foreign key columns use camelCase + `Id` (e.g., `userId`).
527
+ - Join column names match the foreign key column name.
528
+
529
+ ### 6. Version 2 Format Differences
530
+
531
+ Version 2 of the `.apsorc` format introduces several enhancements:
532
+ - Separate `relationships` and `entities` arrays.
533
+ - `created_at`/`updated_at` as boolean flags at the entity level.
534
+ - Enhanced field types: `json`, `enum`, `timestamp`, `decimal`, `numeric`.
535
+ - `to_name` property in relationships for custom property names.
536
+ - `precision` and `scale` properties for decimal/numeric fields.
537
+
538
+ Refer to the [example v2 file](#example-apsorc-v2-file) for usage.
539
+
540
+ ## Relationships
541
+
542
+ ### 1. Relationships Best Practices
543
+
544
+ > **Important:** Only define ONE side of a relationship in your `.apsorc` file. Apso CLI will auto-generate the inverse side for you. Defining both sides leads to duplicate properties, compilation errors, and entity conflicts.
545
+
546
+ #### DO/DON'T Examples
547
+
548
+ ```json
549
+ // ❌ WRONG - Creates Conflicts:
550
+ { "from": "User", "to": "Workspace", "type": "OneToMany" },
551
+ { "from": "Workspace", "to": "User", "type": "ManyToOne" }
552
+
553
+ // ✅ CORRECT - One Side Only:
554
+ { "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" }
555
+ ```
556
+
557
+ - **DO:** Define only the direction that best fits your domain model.
558
+ - **DON'T:** Define both directions for the same relationship.
559
+ - **Why?** Apso CLI auto-generates the inverse property and decorators. Duplicating both sides causes duplicate property and foreign key generation.
560
+
561
+ ### 2. Auto-Generated Properties Documentation
562
+
563
+ #### ManyToOne Relationships
564
+ - Generates:
565
+ - Entity property with `@ManyToOne`, `@JoinColumn`, and `@Column` decorators
566
+ - Foreign key column: `{relationName}Id`
567
+ - Example:
568
+ ```typescript
569
+ @ManyToOne(() => User)
570
+ @JoinColumn({ name: 'ownerId' })
571
+ owner: User;
572
+
573
+ @Column({ type: 'integer' })
574
+ ownerId: number;
575
+ ```
576
+
577
+ #### OneToMany Relationships
578
+ - Generates:
579
+ - Array property with `@OneToMany` decorator
580
+ - Inverse mapping to the ManyToOne property
581
+ - Example:
582
+ ```typescript
583
+ @OneToMany(() => Workspace, (workspace) => workspace.owner)
584
+ workspaces: Workspace[];
585
+ ```
586
+
587
+ #### ManyToMany Relationships
588
+ - Generates:
589
+ - Join table and array properties on both entities
590
+ - Example:
591
+ ```typescript
592
+ @ManyToMany(() => Tag, (tag) => tag.posts)
593
+ @JoinTable()
594
+ tags: Tag[];
595
+ ```
596
+ - **Warning:** If you define both a ManyToMany relationship and an explicit join entity, you may get duplicate join tables and properties. Prefer one approach.
597
+
598
+ ### 3. Common Pitfalls and Solutions
599
+
600
+ #### Duplicate Identifier Errors
601
+ - **Cause:** Defining both sides of a relationship (bidirectional definitions)
602
+ - **Solution:** Remove one side; only define the relationship once.
603
+
604
+ #### "Property does not exist" Errors
605
+ - **Cause:** Referencing an inverse property that was not generated (e.g., using a `to_name` that doesn't match)
606
+ - **Solution:** Only use explicitly defined property names; check generated code for actual property names.
607
+
608
+ #### "Multiple properties with same name" Errors
609
+ - **Cause:** Complex or circular relationship chains, or duplicate relationship definitions
610
+ - **Solution:** Simplify relationships, avoid deep nesting, and ensure each relationship is defined only once.
611
+
612
+ #### ManyToMany + Explicit Join Entity Conflicts
613
+ - **Cause:** Defining both a ManyToMany and a join entity for the same relationship
614
+ - **Solution:** Use either a ManyToMany or a join entity, not both.
615
+
616
+ #### Circular Eager Loading Issues
617
+ - **Cause:** Deeply nested or circular relationships
618
+ - **Solution:** Limit eager loading, use lazy relations, and avoid unnecessary deep nesting in your model.
619
+
620
+ ### 4. Relationship Patterns
621
+
622
+ #### Multi-Tenant Architecture
623
+ ```json
624
+ { "from": "Resource", "to": "Workspace", "type": "ManyToOne", "to_name": "workspace" },
625
+ { "from": "Resource", "to": "User", "type": "ManyToOne", "to_name": "createdBy" }
626
+ ```
627
+
628
+ #### Audit Trails, Deployment History, Optional Relationships
629
+ ```json
630
+ { "from": "Deployment", "to": "User", "type": "ManyToOne", "to_name": "deployedBy", "nullable": true },
631
+ { "from": "AuditLog", "to": "Resource", "type": "ManyToOne", "to_name": "resource" }
632
+ ```
633
+
634
+ #### ManyToMany Example
635
+ ```json
636
+ { "from": "User", "to": "Role", "type": "ManyToMany", "to_name": "roles" }
637
+ ```
638
+
639
+ ### 5. Testing and Validation Guide
640
+
641
+ - **Validate Relationship Generation:**
642
+ - After running `apso server scaffold`, inspect the generated entity files in the `autogen` directory (never modify these directly—see above for extension instructions).
643
+ - Check that only one property exists for each relationship per entity.
644
+ - Confirm that foreign key columns and decorators are present as expected.
645
+
646
+ - **Build Testing Procedures:**
647
+ - Run `tsc` or your build process to catch duplicate or missing property errors early.
648
+ - Write unit tests for entity relationships if possible.
649
+
650
+ - **Entity Inspection Checklist:**
651
+ - No duplicate properties or foreign keys
652
+ - All relationships have the correct decorators
653
+ - No circular imports or eager loading loops
654
+
655
+ - **Clean Regeneration Practices:**
656
+ - Before re-scaffolding, remove old generated code:
657
+ ```sh
658
+ rm -rf autogen
659
+ apso server scaffold
660
+ ```
661
+ This prevents stale or duplicate files from causing errors. **Never add custom code to autogen—use extensions as described above.**
662
+
663
+ ### 6. Version 2 Format Clarity
664
+
665
+ - In v2, relationships and entities are defined in separate arrays:
666
+ ```json
667
+ {
668
+ "version": 2,
669
+ "entities": [ ... ],
670
+ "relationships": [ ... ]
671
+ }
672
+ ```
673
+ - Each relationship should only be defined once, with clear `to_name` if you want a custom property name.
674
+ - Field types and validation are specified per the [Auto-Generated Code Reference](#auto-generated-code-reference).
675
+
676
+ > **Summary:**
677
+ >
678
+ > - Only define one side of each relationship in `.apsorc`.
679
+ > - Let Apso CLI auto-generate the inverse side.
680
+ > - Avoid deep nesting and duplicate definitions.
681
+ > - Always inspect generated code and test your build after scaffolding.
682
+
172
683
  ## Schema Reference
173
684
 
174
685
  The `.apsorc` file must conform to the [APSO Configuration Schema](./apsorc.schema.json). This schema defines all valid properties, types, and constraints for your configuration file.
@@ -45,7 +45,7 @@ class Scaffold extends base_command_1.default {
45
45
  console.log(`[mem] heapUsed after createDto for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
46
46
  }
47
47
  const tService = perf_hooks_1.performance.now();
48
- await (0, lib_1.createService)(filePath, entity);
48
+ await (0, lib_1.createService)(filePath, entity, relationshipMap);
49
49
  if (process.env.DEBUG) {
50
50
  console.log(`[timing] createService for ${entity.name}: ${(perf_hooks_1.performance.now() - tService).toFixed(2)}ms`);
51
51
  const used = process.memoryUsage();
@@ -58,7 +58,7 @@ const createController = async (apiBaseDir, entity, relationshipMap) => {
58
58
  nestedJoinsFile,
59
59
  };
60
60
  const startRenderController = perf_hooks_1.performance.now();
61
- const controllerContent = await Eta.renderFileAsync("./rest/controller-rest", data);
61
+ const controllerContent = await Eta.renderFileAsync("./rest/controller-rest", (0, file_system_1.withGeneratedMeta)(data));
62
62
  const renderControllerTime = perf_hooks_1.performance.now() - startRenderController;
63
63
  if (process.env.DEBUG) {
64
64
  console.log(`[createController][${entityName}] renderController: ${renderControllerTime.toFixed(2)} ms`);
@@ -70,7 +70,7 @@ const createController = async (apiBaseDir, entity, relationshipMap) => {
70
70
  console.log(`[createController][${entityName}] writeController: ${writeControllerTime.toFixed(2)} ms`);
71
71
  }
72
72
  const startRenderSpec = perf_hooks_1.performance.now();
73
- const specContent = await Eta.renderFileAsync("./rest/controller-rest-spec", data);
73
+ const specContent = await Eta.renderFileAsync("./rest/controller-rest-spec", (0, file_system_1.withGeneratedMeta)(data));
74
74
  const renderSpecTime = perf_hooks_1.performance.now() - startRenderSpec;
75
75
  if (process.env.DEBUG) {
76
76
  console.log(`[createController][${entityName}] renderSpec: ${renderSpecTime.toFixed(2)} ms`);
package/dist/lib/dto.js CHANGED
@@ -74,7 +74,7 @@ options) => {
74
74
  importEnums: (0, field_1.typeExistsInEntity)(entity, "enum") !== -1,
75
75
  };
76
76
  // Render the unified template only once
77
- const dtoContent = await Eta.renderFileAsync("./rest/dto-rest", data);
77
+ const dtoContent = await Eta.renderFileAsync("./rest/dto-rest", (0, file_system_1.withGeneratedMeta)(data));
78
78
  // Create only one DTO file
79
79
  await (0, file_system_1.createFile)(dtoFile, dtoContent);
80
80
  };
@@ -44,7 +44,7 @@ const createEntity = async (apiBaseDir, entity, relationshipsNew, apiType) => {
44
44
  apiType,
45
45
  };
46
46
  const templatePath = apiType === "graphql" ? "./graphql/gql-entity-graphql" : "./entities/entity";
47
- const content = await Eta.renderFileAsync(templatePath, data);
47
+ const content = await Eta.renderFileAsync(templatePath, (0, file_system_1.withGeneratedMeta)(data));
48
48
  await (0, file_system_1.createFile)(File, content);
49
49
  };
50
50
  exports.createEntity = createEntity;
@@ -1,9 +1,2 @@
1
1
  import { Entity } from "./types";
2
- /**
3
- * Generates NestJS service and spec files for a given entity.
4
- *
5
- * @param apiBaseDir The base directory for the generated API module (e.g., 'src/autogen/users').
6
- * @param entity The entity definition object from the parsed .apsorc.
7
- * @returns {Promise<void>} A promise that resolves when the service and spec files are created.
8
- */
9
- export declare const createService: (apiBaseDir: string, entity: Entity) => Promise<void>;
2
+ export declare const createService: (apiBaseDir: string, entity: Entity, relationshipMap?: any) => Promise<void>;
@@ -5,32 +5,56 @@ const tslib_1 = require("tslib");
5
5
  const Eta = tslib_1.__importStar(require("eta"));
6
6
  const file_system_1 = require("./utils/file-system");
7
7
  const path = tslib_1.__importStar(require("path"));
8
- /**
8
+ /*
9
9
  * Generates NestJS service and spec files for a given entity.
10
10
  *
11
11
  * @param apiBaseDir The base directory for the generated API module (e.g., 'src/autogen/users').
12
12
  * @param entity The entity definition object from the parsed .apsorc.
13
13
  * @returns {Promise<void>} A promise that resolves when the service and spec files are created.
14
14
  */
15
- const createService = async (apiBaseDir, entity) => {
15
+ const createService = async (apiBaseDir, entity, relationshipMap // RelationshipMap, optional for backward compatibility
16
+ ) => {
16
17
  const { name: entityName } = entity;
17
18
  const serviceFileName = path.join(apiBaseDir, `${entityName}.service.ts`);
18
19
  const specFileName = path.join(apiBaseDir, `${entityName}.service.spec.ts`);
19
20
  // Names needed by the templates
20
21
  const svcName = `${entityName}Service`;
21
22
  const repoName = `${entityName}Repository`; // Assuming convention
23
+ // --- NEW: Recursively collect all related entities for the test file ---
24
+ let allRelatedEntities = [entityName];
25
+ if (relationshipMap && relationshipMap[entityName]) {
26
+ // Use a Set to avoid duplicates
27
+ const visited = new Set();
28
+ const collect = (entity) => {
29
+ if (visited.has(entity))
30
+ return;
31
+ visited.add(entity);
32
+ allRelatedEntities.push(entity);
33
+ const rels = relationshipMap[entity] || [];
34
+ for (const rel of rels) {
35
+ if (!visited.has(rel.name)) {
36
+ collect(rel.name);
37
+ }
38
+ }
39
+ };
40
+ allRelatedEntities = [];
41
+ collect(entityName);
42
+ // Remove self-duplicates using spread operator
43
+ allRelatedEntities = [...new Set(allRelatedEntities)];
44
+ }
22
45
  // Data for the Eta templates
23
46
  const data = {
24
47
  svcName,
25
48
  repoName,
26
49
  entityName,
50
+ allRelatedEntities,
27
51
  };
28
52
  // Render service template
29
- const serviceContent = await Eta.renderFileAsync("./rest/service-rest", data);
53
+ const serviceContent = await Eta.renderFileAsync("./rest/service-rest", (0, file_system_1.withGeneratedMeta)(data));
30
54
  await (0, file_system_1.createFile)(serviceFileName, serviceContent);
31
55
  Eta.templates.remove("./rest/service-rest");
32
56
  // Render spec template
33
- const specContent = await Eta.renderFileAsync("./rest/service-rest-spec", data);
57
+ const specContent = await Eta.renderFileAsync("./rest/service-rest-spec", (0, file_system_1.withGeneratedMeta)(data));
34
58
  await (0, file_system_1.createFile)(specFileName, specContent);
35
59
  Eta.templates.remove("./rest/service-rest-spec");
36
60
  };
@@ -0,0 +1,4 @@
1
+ <% const {field} = it; %>
2
+
3
+ @Column({ type: 'timestamp', nullable: <%= Boolean(field.nullable) %> })
4
+ <%= field.name %>: Date;
@@ -0,0 +1,29 @@
1
+ <% const {field} = it; %>
2
+
3
+ @IsOptional({ groups: [UPDATE] })
4
+ <% if (!field.nullable && !field.auto) { %>
5
+ @IsNotEmpty({ groups: [CREATE] })
6
+ <% } %>
7
+
8
+ @Column({
9
+ "type": "geography",
10
+ transformer: {
11
+ to: (geography: any) => {
12
+ if (!geography) return null;
13
+ return geography;
14
+ },
15
+ from: (pgGeography: any) => {
16
+ if (!pgGeography) return null;
17
+ return pgGeography;
18
+ }
19
+ }<% if (field.nullable) {%>,
20
+ nullable: true<% } %>
21
+ })
22
+ <% if (field.index ) {%>
23
+ @Index()
24
+ <% } %>
25
+ <% if (field.primary ) {%>
26
+ @PrimaryColumn()
27
+ <% } %>
28
+
29
+ <%= field.name %>: any;