@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.
- package/README.md +511 -0
- package/dist/commands/server/scaffold.js +1 -1
- package/dist/lib/controller.js +2 -2
- package/dist/lib/dto.js +1 -1
- package/dist/lib/entity.js +1 -1
- package/dist/lib/service.d.ts +1 -8
- package/dist/lib/service.js +28 -4
- package/dist/lib/templates/entities/entity-col-datetime.eta +4 -0
- package/dist/lib/templates/entities/entity-col-geography.eta +29 -0
- package/dist/lib/templates/entities/entity-col-geometry.eta +29 -0
- package/dist/lib/templates/entities/entity-col-geometrycollection.eta +29 -0
- package/dist/lib/templates/entities/entity-col-linestring.eta +37 -0
- package/dist/lib/templates/entities/entity-col-multilinestring.eta +44 -0
- package/dist/lib/templates/entities/entity-col-multipoint.eta +37 -0
- package/dist/lib/templates/entities/entity-col-multipolygon.eta +50 -0
- package/dist/lib/templates/entities/entity-col-polygon.eta +44 -0
- package/dist/lib/templates/graphql/dto/dto-col-decimal.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-geography.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-geometry.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-geometrycollection.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-linestring.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-multilinestring.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-multipoint.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-multipolygon.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-numeric.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-point.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-polygon.eta +4 -0
- package/dist/lib/templates/header.eta +8 -3
- package/dist/lib/templates/rest/dto-rest.eta +26 -1
- package/dist/lib/templates/rest/ormconfig.eta +2 -0
- package/dist/lib/templates/rest/service-rest-spec.eta +16 -7
- package/dist/lib/types/field.d.ts +3 -1
- package/dist/lib/utils/field.d.ts +1 -0
- package/dist/lib/utils/field.js +42 -9
- package/dist/lib/utils/file-system.d.ts +4 -0
- package/dist/lib/utils/file-system.js +9 -1
- package/dist/lib/utils/relationships/parse.js +1 -1
- package/dist/tests/templates/entities/types/decimal-field-validation.spec.d.ts +1 -0
- package/dist/tests/templates/entities/types/decimal-field-validation.spec.js +100 -0
- package/dist/tests/templates/entities/types/geometry-template.spec.d.ts +1 -0
- package/dist/tests/templates/entities/types/geometry-template.spec.js +58 -0
- package/dist/tests/templates/entities/types/polygon-template.spec.d.ts +1 -0
- package/dist/tests/templates/entities/types/polygon-template.spec.js +58 -0
- package/oclif.manifest.json +1 -1
- 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();
|
package/dist/lib/controller.js
CHANGED
|
@@ -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
|
};
|
package/dist/lib/entity.js
CHANGED
|
@@ -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;
|
package/dist/lib/service.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/service.js
CHANGED
|
@@ -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,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;
|