@apso/cli 0.2.0 → 0.3.1
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 +124 -0
- package/dist/commands/server/new.d.ts +4 -4
- package/dist/commands/server/new.js +33 -36
- package/dist/lib/base-command.js +13 -6
- 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-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-point.eta +4 -0
- package/dist/lib/templates/graphql/dto/dto-col-polygon.eta +4 -0
- package/dist/lib/templates/rest/dto-rest.eta +24 -0
- package/dist/lib/utils/field.js +10 -1
- 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
|
@@ -305,6 +305,12 @@ This section details the code that Apso CLI automatically generates from your `.
|
|
|
305
305
|
|
|
306
306
|
### 2. Field Type Mapping Table
|
|
307
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
|
+
|
|
308
314
|
| Apso Field Type | TypeORM Column Decorator | Auto-Applied Validation | Notes |
|
|
309
315
|
|-----------------|-----------------------------------------|----------------------------------------|---------------------------------------|
|
|
310
316
|
| `text` | `@Column({ type: 'text' })` | `@IsString()`, `@IsNotEmpty()` | With `length`: `@MaxLength(n)` |
|
|
@@ -315,6 +321,15 @@ This section details the code that Apso CLI automatically generates from your `.
|
|
|
315
321
|
| `decimal` | `@Column({ type: 'decimal', precision: n, scale: m })` | `@IsNumber()` | Precision/scale supported |
|
|
316
322
|
| `numeric` | `@Column({ type: 'numeric', precision: n, scale: m })` | `@IsNumber()` | Precision/scale supported |
|
|
317
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 |
|
|
318
333
|
|
|
319
334
|
#### Decimal/Numeric Field Examples
|
|
320
335
|
|
|
@@ -352,6 +367,115 @@ Generates:
|
|
|
352
367
|
bandwidth_gb: number;
|
|
353
368
|
```
|
|
354
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
|
+
|
|
355
479
|
### 3. Validation Rules Documentation
|
|
356
480
|
|
|
357
481
|
Field properties in `.apsorc` map to validation decorators as follows:
|
|
@@ -10,13 +10,13 @@ export default class New extends BaseCommand {
|
|
|
10
10
|
static args: {
|
|
11
11
|
name: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
|
|
12
12
|
};
|
|
13
|
-
installModules(
|
|
14
|
-
formatApp(
|
|
15
|
-
startApp(
|
|
13
|
+
installModules(projectPath: string): Promise<void>;
|
|
14
|
+
formatApp(projectPath: string): Promise<void>;
|
|
15
|
+
startApp(projectPath: string): Promise<void>;
|
|
16
16
|
cloneTemplate(projectPath: string): Promise<void>;
|
|
17
|
-
run(): Promise<void>;
|
|
18
17
|
validateFlags(): Promise<{
|
|
19
18
|
projectName: string;
|
|
20
19
|
apiType: string;
|
|
21
20
|
}>;
|
|
21
|
+
run(): Promise<void>;
|
|
22
22
|
}
|
|
@@ -12,28 +12,25 @@ class New extends base_command_1.default {
|
|
|
12
12
|
super(...arguments);
|
|
13
13
|
this.CURR_DIR = process.cwd();
|
|
14
14
|
}
|
|
15
|
-
async installModules(
|
|
16
|
-
this.log("
|
|
17
|
-
await this.runNpmCommand(["install", "--force", "--prefix",
|
|
15
|
+
async installModules(projectPath) {
|
|
16
|
+
this.log("Installing modules...");
|
|
17
|
+
await this.runNpmCommand(["install", "--force", "--prefix", projectPath]);
|
|
18
18
|
}
|
|
19
|
-
async formatApp(
|
|
20
|
-
this.log("
|
|
21
|
-
shelljs_1.default.cd(
|
|
22
|
-
await this.runNpmCommand(["run", "format", "--prefix",
|
|
19
|
+
async formatApp(projectPath) {
|
|
20
|
+
this.log("Formatting code...");
|
|
21
|
+
shelljs_1.default.cd(projectPath);
|
|
22
|
+
await this.runNpmCommand(["run", "format", "--prefix", projectPath]);
|
|
23
23
|
}
|
|
24
|
-
async startApp(
|
|
24
|
+
async startApp(projectPath) {
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
|
-
const
|
|
26
|
+
const isWindows = process.platform === "win32";
|
|
27
|
+
const command = isWindows ? "npm.cmd" : "npm";
|
|
27
28
|
const args = ["start"];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// args.push('--cwd')
|
|
34
|
-
// args.push(root)
|
|
35
|
-
// args.push('--verbose')
|
|
36
|
-
const child = (0, child_process_1.spawn)(command, args, { stdio: "inherit", cwd: root });
|
|
29
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
30
|
+
cwd: projectPath,
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
shell: isWindows,
|
|
33
|
+
});
|
|
37
34
|
child.on("close", (code) => {
|
|
38
35
|
if (code !== 0) {
|
|
39
36
|
reject({
|
|
@@ -43,15 +40,18 @@ class New extends base_command_1.default {
|
|
|
43
40
|
}
|
|
44
41
|
resolve();
|
|
45
42
|
});
|
|
43
|
+
child.on("error", (err) => {
|
|
44
|
+
this.log(`Error starting app: ${err.message}`);
|
|
45
|
+
reject(err);
|
|
46
|
+
});
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
async cloneTemplate(projectPath) {
|
|
49
|
-
this.log("
|
|
50
|
+
this.log("Cloning service template...");
|
|
50
51
|
if (!fs.existsSync(projectPath) && this.CURR_DIR !== projectPath) {
|
|
51
52
|
fs.mkdirSync(projectPath);
|
|
52
53
|
}
|
|
53
54
|
shelljs_1.default.cd(projectPath);
|
|
54
|
-
// Execute git clone and capture the result using HTTPS
|
|
55
55
|
const cloneResult = shelljs_1.default.exec(`git clone --depth=1 --branch=main https://github.com/apsoai/service-template.git ${projectPath}`, { silent: true }); // Use silent: true to suppress default output
|
|
56
56
|
// Check if the command failed
|
|
57
57
|
if (cloneResult.code !== 0) {
|
|
@@ -66,34 +66,31 @@ class New extends base_command_1.default {
|
|
|
66
66
|
shelljs_1.default.exec(`rm -rf ${projectPath}/.git`);
|
|
67
67
|
shelljs_1.default.cd(this.CURR_DIR);
|
|
68
68
|
}
|
|
69
|
-
async run() {
|
|
70
|
-
this.log("Initializing New Apso Server...");
|
|
71
|
-
const { projectName,
|
|
72
|
-
// apiType,
|
|
73
|
-
} = await this.validateFlags();
|
|
74
|
-
const CURR_DIR = process.cwd();
|
|
75
|
-
const projectPath = path.join(CURR_DIR, projectName);
|
|
76
|
-
await this.cloneTemplate(projectPath);
|
|
77
|
-
await this.installModules(projectPath);
|
|
78
|
-
await this.formatApp(projectPath);
|
|
79
|
-
}
|
|
80
69
|
async validateFlags() {
|
|
81
70
|
var _a;
|
|
82
71
|
const { flags } = await this.parse(New);
|
|
83
72
|
let projectName = flags.name;
|
|
84
73
|
let apiType = (_a = flags.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
85
|
-
if (
|
|
86
|
-
// this.error("`name` must be set (e.g. WoofyApp)");
|
|
74
|
+
if (!projectName) {
|
|
87
75
|
projectName = '';
|
|
88
76
|
}
|
|
89
|
-
if (
|
|
77
|
+
if (!apiType) {
|
|
90
78
|
apiType = "rest";
|
|
91
79
|
}
|
|
92
|
-
if (
|
|
93
|
-
this.error("`apiType` must be
|
|
80
|
+
if (!["rest", "graphql"].includes(apiType)) {
|
|
81
|
+
this.error("`apiType` must be 'rest' or 'graphql'");
|
|
94
82
|
}
|
|
95
83
|
return { projectName, apiType };
|
|
96
84
|
}
|
|
85
|
+
async run() {
|
|
86
|
+
this.log("Initializing New Apso Server...");
|
|
87
|
+
const { projectName } = await this.validateFlags();
|
|
88
|
+
const CURR_DIR = process.cwd();
|
|
89
|
+
const projectPath = path.join(CURR_DIR, projectName);
|
|
90
|
+
await this.cloneTemplate(projectPath);
|
|
91
|
+
await this.installModules(projectPath);
|
|
92
|
+
await this.formatApp(projectPath);
|
|
93
|
+
}
|
|
97
94
|
}
|
|
98
95
|
New.description = "Initialize your server project";
|
|
99
96
|
New.examples = [`$ apso server new --name TestProject`];
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
3
4
|
const core_1 = require("@oclif/core");
|
|
4
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const os_1 = tslib_1.__importDefault(require("os"));
|
|
5
7
|
class BaseCommand extends core_1.Command {
|
|
6
8
|
async runNpmCommand(args, silent = false) {
|
|
7
9
|
return new Promise((resolve, reject) => {
|
|
8
|
-
const
|
|
10
|
+
const isWindows = os_1.default.platform() === "win32";
|
|
11
|
+
const command = isWindows ? "npm.cmd" : "npm";
|
|
9
12
|
const cmdStr = `${command} ${args.join(" ")}`;
|
|
10
|
-
this.log(cmdStr);
|
|
11
|
-
const child = (0, child_process_1.spawn)(command, args, {
|
|
13
|
+
this.log(`Running: ${cmdStr}`);
|
|
14
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
shell: isWindows,
|
|
17
|
+
});
|
|
12
18
|
child.on("close", (code) => {
|
|
13
19
|
if (code !== 0) {
|
|
14
|
-
reject({
|
|
15
|
-
command: cmdStr,
|
|
16
|
-
});
|
|
20
|
+
reject({ command: cmdStr, exitCode: code });
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
23
|
resolve();
|
|
20
24
|
});
|
|
25
|
+
child.on("error", () => {
|
|
26
|
+
this.error(`Failed to run: ${cmdStr}`);
|
|
27
|
+
});
|
|
21
28
|
});
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -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;
|
|
@@ -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": "geometry",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (geometry: any) => {
|
|
12
|
+
if (!geometry) return null;
|
|
13
|
+
return geometry;
|
|
14
|
+
},
|
|
15
|
+
from: (pgGeometry: any) => {
|
|
16
|
+
if (!pgGeometry) return null;
|
|
17
|
+
return pgGeometry;
|
|
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;
|
|
@@ -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": "geometrycollection",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (geometrycollection: { geometries: Array<any> } | null) => {
|
|
12
|
+
if (!geometrycollection) return null;
|
|
13
|
+
return geometrycollection;
|
|
14
|
+
},
|
|
15
|
+
from: (pgGeometryCollection: any) => {
|
|
16
|
+
if (!pgGeometryCollection) return null;
|
|
17
|
+
return pgGeometryCollection;
|
|
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 %>: { geometries: Array<any> };
|
|
@@ -0,0 +1,37 @@
|
|
|
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": "linestring",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (linestring: { coordinates: Array<{x: number, y: number}> } | null) => {
|
|
12
|
+
if (!linestring) return null;
|
|
13
|
+
const coords = linestring.coordinates.map(coord => `${coord.x} ${coord.y}`).join(',');
|
|
14
|
+
return `LINESTRING(${coords})`;
|
|
15
|
+
},
|
|
16
|
+
from: (pgLinestring: string | null) => {
|
|
17
|
+
if (!pgLinestring) return null;
|
|
18
|
+
// Extract coordinates from LINESTRING(x1 y1,x2 y2,...) format
|
|
19
|
+
const match = pgLinestring.match(/LINESTRING\((.+)\)/);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const coords = match[1].split(',').map(coord => {
|
|
22
|
+
const [x, y] = coord.trim().split(' ');
|
|
23
|
+
return { x: parseFloat(x), y: parseFloat(y) };
|
|
24
|
+
});
|
|
25
|
+
return { coordinates: coords };
|
|
26
|
+
}
|
|
27
|
+
}<% if (field.nullable) {%>,
|
|
28
|
+
nullable: true<% } %>
|
|
29
|
+
})
|
|
30
|
+
<% if (field.index ) {%>
|
|
31
|
+
@Index()
|
|
32
|
+
<% } %>
|
|
33
|
+
<% if (field.primary ) {%>
|
|
34
|
+
@PrimaryColumn()
|
|
35
|
+
<% } %>
|
|
36
|
+
|
|
37
|
+
<%= field.name %>: { coordinates: Array<{ x: number, y: number }> };
|
|
@@ -0,0 +1,44 @@
|
|
|
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": "multilinestring",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (multilinestring: { coordinates: Array<Array<{x: number, y: number}>> } | null) => {
|
|
12
|
+
if (!multilinestring) return null;
|
|
13
|
+
const linestrings = multilinestring.coordinates.map(linestring => {
|
|
14
|
+
const coords = linestring.map(coord => `${coord.x} ${coord.y}`).join(',');
|
|
15
|
+
return `(${coords})`;
|
|
16
|
+
});
|
|
17
|
+
return `MULTILINESTRING(${linestrings.join(',')})`;
|
|
18
|
+
},
|
|
19
|
+
from: (pgMultilinestring: string | null) => {
|
|
20
|
+
if (!pgMultilinestring) return null;
|
|
21
|
+
// Extract coordinates from MULTILINESTRING((x1 y1,x2 y2,...),(x1 y1,x2 y2,...)) format
|
|
22
|
+
const match = pgMultilinestring.match(/MULTILINESTRING\((.+)\)/);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const linestrings = match[1].split('),(').map(linestring => {
|
|
25
|
+
const cleanLinestring = linestring.replace(/[()]/g, '');
|
|
26
|
+
const coords = cleanLinestring.split(',').map(coord => {
|
|
27
|
+
const [x, y] = coord.trim().split(' ');
|
|
28
|
+
return { x: parseFloat(x), y: parseFloat(y) };
|
|
29
|
+
});
|
|
30
|
+
return coords;
|
|
31
|
+
});
|
|
32
|
+
return { coordinates: linestrings };
|
|
33
|
+
}
|
|
34
|
+
}<% if (field.nullable) {%>,
|
|
35
|
+
nullable: true<% } %>
|
|
36
|
+
})
|
|
37
|
+
<% if (field.index ) {%>
|
|
38
|
+
@Index()
|
|
39
|
+
<% } %>
|
|
40
|
+
<% if (field.primary ) {%>
|
|
41
|
+
@PrimaryColumn()
|
|
42
|
+
<% } %>
|
|
43
|
+
|
|
44
|
+
<%= field.name %>: { coordinates: Array<Array<{ x: number, y: number }>> };
|
|
@@ -0,0 +1,37 @@
|
|
|
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": "multipoint",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (multipoint: { coordinates: Array<{x: number, y: number}> } | null) => {
|
|
12
|
+
if (!multipoint) return null;
|
|
13
|
+
const coords = multipoint.coordinates.map(coord => `${coord.x} ${coord.y}`).join(',');
|
|
14
|
+
return `MULTIPOINT(${coords})`;
|
|
15
|
+
},
|
|
16
|
+
from: (pgMultipoint: string | null) => {
|
|
17
|
+
if (!pgMultipoint) return null;
|
|
18
|
+
// Extract coordinates from MULTIPOINT(x1 y1,x2 y2,...) format
|
|
19
|
+
const match = pgMultipoint.match(/MULTIPOINT\((.+)\)/);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const coords = match[1].split(',').map(coord => {
|
|
22
|
+
const [x, y] = coord.trim().split(' ');
|
|
23
|
+
return { x: parseFloat(x), y: parseFloat(y) };
|
|
24
|
+
});
|
|
25
|
+
return { coordinates: coords };
|
|
26
|
+
}
|
|
27
|
+
}<% if (field.nullable) {%>,
|
|
28
|
+
nullable: true<% } %>
|
|
29
|
+
})
|
|
30
|
+
<% if (field.index ) {%>
|
|
31
|
+
@Index()
|
|
32
|
+
<% } %>
|
|
33
|
+
<% if (field.primary ) {%>
|
|
34
|
+
@PrimaryColumn()
|
|
35
|
+
<% } %>
|
|
36
|
+
|
|
37
|
+
<%= field.name %>: { coordinates: Array<{ x: number, y: number }> };
|
|
@@ -0,0 +1,50 @@
|
|
|
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": "multipolygon",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (multipolygon: { coordinates: Array<Array<Array<{x: number, y: number}>>> } | null) => {
|
|
12
|
+
if (!multipolygon) return null;
|
|
13
|
+
const polygons = multipolygon.coordinates.map(polygon => {
|
|
14
|
+
const rings = polygon.map(ring => {
|
|
15
|
+
const coords = ring.map(coord => `${coord.x} ${coord.y}`).join(',');
|
|
16
|
+
return `(${coords})`;
|
|
17
|
+
});
|
|
18
|
+
return `(${rings.join(',')})`;
|
|
19
|
+
});
|
|
20
|
+
return `MULTIPOLYGON(${polygons.join(',')})`;
|
|
21
|
+
},
|
|
22
|
+
from: (pgMultipolygon: string | null) => {
|
|
23
|
+
if (!pgMultipolygon) return null;
|
|
24
|
+
// Extract coordinates from MULTIPOLYGON(((x1 y1,x2 y2,...),(x1 y1,x2 y2,...)),((x1 y1,x2 y2,...),(x1 y1,x2 y2,...))) format
|
|
25
|
+
const match = pgMultipolygon.match(/MULTIPOLYGON\((.+)\)/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
const polygons = match[1].split(')),((').map(polygon => {
|
|
28
|
+
const cleanPolygon = polygon.replace(/[()]/g, '');
|
|
29
|
+
const rings = cleanPolygon.split('),(').map(ring => {
|
|
30
|
+
const coords = ring.split(',').map(coord => {
|
|
31
|
+
const [x, y] = coord.trim().split(' ');
|
|
32
|
+
return { x: parseFloat(x), y: parseFloat(y) };
|
|
33
|
+
});
|
|
34
|
+
return coords;
|
|
35
|
+
});
|
|
36
|
+
return rings;
|
|
37
|
+
});
|
|
38
|
+
return { coordinates: polygons };
|
|
39
|
+
}
|
|
40
|
+
}<% if (field.nullable) {%>,
|
|
41
|
+
nullable: true<% } %>
|
|
42
|
+
})
|
|
43
|
+
<% if (field.index ) {%>
|
|
44
|
+
@Index()
|
|
45
|
+
<% } %>
|
|
46
|
+
<% if (field.primary ) {%>
|
|
47
|
+
@PrimaryColumn()
|
|
48
|
+
<% } %>
|
|
49
|
+
|
|
50
|
+
<%= field.name %>: { coordinates: Array<Array<Array<{ x: number, y: number }>>> };
|
|
@@ -0,0 +1,44 @@
|
|
|
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": "polygon",
|
|
10
|
+
transformer: {
|
|
11
|
+
to: (polygon: { coordinates: Array<Array<{x: number, y: number}>> } | null) => {
|
|
12
|
+
if (!polygon) return null;
|
|
13
|
+
const rings = polygon.coordinates.map(ring => {
|
|
14
|
+
const coords = ring.map(coord => `${coord.x} ${coord.y}`).join(',');
|
|
15
|
+
return `(${coords})`;
|
|
16
|
+
});
|
|
17
|
+
return `POLYGON(${rings.join(',')})`;
|
|
18
|
+
},
|
|
19
|
+
from: (pgPolygon: string | null) => {
|
|
20
|
+
if (!pgPolygon) return null;
|
|
21
|
+
// Extract coordinates from POLYGON((x1 y1,x2 y2,...),(x1 y1,x2 y2,...)) format
|
|
22
|
+
const match = pgPolygon.match(/POLYGON\((.+)\)/);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const rings = match[1].split('),(').map(ring => {
|
|
25
|
+
const cleanRing = ring.replace(/[()]/g, '');
|
|
26
|
+
const coords = cleanRing.split(',').map(coord => {
|
|
27
|
+
const [x, y] = coord.trim().split(' ');
|
|
28
|
+
return { x: parseFloat(x), y: parseFloat(y) };
|
|
29
|
+
});
|
|
30
|
+
return coords;
|
|
31
|
+
});
|
|
32
|
+
return { coordinates: rings };
|
|
33
|
+
}
|
|
34
|
+
}<% if (field.nullable) {%>,
|
|
35
|
+
nullable: true<% } %>
|
|
36
|
+
})
|
|
37
|
+
<% if (field.index ) {%>
|
|
38
|
+
@Index()
|
|
39
|
+
<% } %>
|
|
40
|
+
<% if (field.primary ) {%>
|
|
41
|
+
@PrimaryColumn()
|
|
42
|
+
<% } %>
|
|
43
|
+
|
|
44
|
+
<%= field.name %>: { coordinates: Array<Array<{ x: number, y: number }>> };
|
|
@@ -15,6 +15,18 @@ export class <%= it.entityName %>Create {
|
|
|
15
15
|
<%= field.name %>: JSON;
|
|
16
16
|
<%} else if (field.type === 'timestamp' || field.type === 'timestamptz') {%>
|
|
17
17
|
<%= field.name %>: Date;
|
|
18
|
+
<%} else if (field.type === 'geometry' || field.type === 'geography') {%>
|
|
19
|
+
<%= field.name %>: any;
|
|
20
|
+
<%} else if (field.type === 'point') {%>
|
|
21
|
+
<%= field.name %>: { x: number, y: number };
|
|
22
|
+
<%} else if (field.type === 'linestring' || field.type === 'multipoint') {%>
|
|
23
|
+
<%= field.name %>: { coordinates: Array<{ x: number, y: number }> };
|
|
24
|
+
<%} else if (field.type === 'polygon' || field.type === 'multilinestring') {%>
|
|
25
|
+
<%= field.name %>: { coordinates: Array<Array<{ x: number, y: number }>> };
|
|
26
|
+
<%} else if (field.type === 'multipolygon') {%>
|
|
27
|
+
<%= field.name %>: { coordinates: Array<Array<Array<{ x: number, y: number }>>> };
|
|
28
|
+
<%} else if (field.type === 'geometrycollection') {%>
|
|
29
|
+
<%= field.name %>: { geometries: Array<any> };
|
|
18
30
|
<%} else {%>
|
|
19
31
|
<%= field.name %>: <%= field.dataType %>;
|
|
20
32
|
<% } %>
|
|
@@ -34,6 +46,18 @@ export class <%= it.entityName %> {
|
|
|
34
46
|
<%= field.name %>: JSON;
|
|
35
47
|
<%} else if (field.type === 'timestamp' || field.type === 'timestamptz') {%>
|
|
36
48
|
<%= field.name %>: Date;
|
|
49
|
+
<%} else if (field.type === 'geometry' || field.type === 'geography') {%>
|
|
50
|
+
<%= field.name %>: any;
|
|
51
|
+
<%} else if (field.type === 'point') {%>
|
|
52
|
+
<%= field.name %>: { x: number, y: number };
|
|
53
|
+
<%} else if (field.type === 'linestring' || field.type === 'multipoint') {%>
|
|
54
|
+
<%= field.name %>: { coordinates: Array<{ x: number, y: number }> };
|
|
55
|
+
<%} else if (field.type === 'polygon' || field.type === 'multilinestring') {%>
|
|
56
|
+
<%= field.name %>: { coordinates: Array<Array<{ x: number, y: number }>> };
|
|
57
|
+
<%} else if (field.type === 'multipolygon') {%>
|
|
58
|
+
<%= field.name %>: { coordinates: Array<Array<Array<{ x: number, y: number }>>> };
|
|
59
|
+
<%} else if (field.type === 'geometrycollection') {%>
|
|
60
|
+
<%= field.name %>: { geometries: Array<any> };
|
|
37
61
|
<%} else {%>
|
|
38
62
|
<%= field.name %>: <%= field.dataType %>;
|
|
39
63
|
<% } %>
|
package/dist/lib/utils/field.js
CHANGED
|
@@ -36,7 +36,16 @@ const typeToJsType = {
|
|
|
36
36
|
bytea: "Buffer",
|
|
37
37
|
point: "{ x: number, y: number }",
|
|
38
38
|
int4range: "{ lower: number, upper: number, lowerInclusive: boolean, upperInclusive: boolean }",
|
|
39
|
-
enum: "string",
|
|
39
|
+
enum: "string",
|
|
40
|
+
// PostGIS data types
|
|
41
|
+
geometry: "any",
|
|
42
|
+
geography: "any",
|
|
43
|
+
linestring: "{ coordinates: Array<{ x: number, y: number }> }",
|
|
44
|
+
polygon: "{ coordinates: Array<Array<{ x: number, y: number }>> }",
|
|
45
|
+
multipoint: "{ coordinates: Array<{ x: number, y: number }> }",
|
|
46
|
+
multilinestring: "{ coordinates: Array<Array<{ x: number, y: number }>> }",
|
|
47
|
+
multipolygon: "{ coordinates: Array<Array<Array<{ x: number, y: number }>>> }",
|
|
48
|
+
geometrycollection: "{ geometries: Array<any> }",
|
|
40
49
|
};
|
|
41
50
|
const getJsTypeFromFieldType = (type) => { var _a; return (_a = typeToJsType[type]) !== null && _a !== void 0 ? _a : "string"; };
|
|
42
51
|
exports.getJsTypeFromFieldType = getJsTypeFromFieldType;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const template_test_utils_1 = require("../utils/template-test-utils");
|
|
4
|
+
const globals_1 = require("@jest/globals");
|
|
5
|
+
(0, globals_1.describe)('Geometry Entity Template Tests', () => {
|
|
6
|
+
// Setup Eta configuration once before all tests
|
|
7
|
+
(0, globals_1.beforeAll)(() => {
|
|
8
|
+
(0, template_test_utils_1.configureEta)();
|
|
9
|
+
});
|
|
10
|
+
const templatePath = './entities/entity-col-geometry';
|
|
11
|
+
const testCases = [
|
|
12
|
+
{
|
|
13
|
+
name: 'should render basic geometry field correctly',
|
|
14
|
+
field: {
|
|
15
|
+
name: 'shape',
|
|
16
|
+
dataType: 'any',
|
|
17
|
+
nullable: false
|
|
18
|
+
},
|
|
19
|
+
assertions: [
|
|
20
|
+
{ type: 'contains', text: '@Column({' },
|
|
21
|
+
{ type: 'contains', text: '"type": "geometry"' },
|
|
22
|
+
{ type: 'contains', text: 'transformer: {' },
|
|
23
|
+
{ type: 'contains', text: 'to: (geometry: any) => {' },
|
|
24
|
+
{ type: 'contains', text: 'from: (pgGeometry: any) => {' },
|
|
25
|
+
{ type: 'contains', text: 'shape: any;' },
|
|
26
|
+
{ type: 'notContains', text: 'nullable: true' }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'should render nullable geometry field correctly',
|
|
31
|
+
field: {
|
|
32
|
+
name: 'optionalShape',
|
|
33
|
+
dataType: 'any',
|
|
34
|
+
nullable: true
|
|
35
|
+
},
|
|
36
|
+
assertions: [
|
|
37
|
+
{ type: 'contains', text: '@Column({' },
|
|
38
|
+
{ type: 'contains', text: '"type": "geometry"' },
|
|
39
|
+
{ type: 'contains', text: 'nullable: true' },
|
|
40
|
+
{ type: 'contains', text: 'optionalShape: any;' }
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'should render indexed geometry field correctly',
|
|
45
|
+
field: {
|
|
46
|
+
name: 'spatialData',
|
|
47
|
+
dataType: 'any',
|
|
48
|
+
nullable: false,
|
|
49
|
+
index: true
|
|
50
|
+
},
|
|
51
|
+
assertions: [
|
|
52
|
+
{ type: 'contains', text: '@Index()' },
|
|
53
|
+
{ type: 'contains', text: 'spatialData: any;' }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
(0, template_test_utils_1.runTemplateTests)(templatePath, testCases);
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const template_test_utils_1 = require("../utils/template-test-utils");
|
|
4
|
+
const globals_1 = require("@jest/globals");
|
|
5
|
+
(0, globals_1.describe)('Polygon Entity Template Tests', () => {
|
|
6
|
+
// Setup Eta configuration once before all tests
|
|
7
|
+
(0, globals_1.beforeAll)(() => {
|
|
8
|
+
(0, template_test_utils_1.configureEta)();
|
|
9
|
+
});
|
|
10
|
+
const templatePath = './entities/entity-col-polygon';
|
|
11
|
+
const testCases = [
|
|
12
|
+
{
|
|
13
|
+
name: 'should render basic polygon field correctly',
|
|
14
|
+
field: {
|
|
15
|
+
name: 'boundary',
|
|
16
|
+
dataType: '{ coordinates: Array<Array<{ x: number, y: number }>> }',
|
|
17
|
+
nullable: false
|
|
18
|
+
},
|
|
19
|
+
assertions: [
|
|
20
|
+
{ type: 'contains', text: '@Column({' },
|
|
21
|
+
{ type: 'contains', text: '"type": "polygon"' },
|
|
22
|
+
{ type: 'contains', text: 'transformer: {' },
|
|
23
|
+
{ type: 'contains', text: 'to: (polygon: { coordinates: Array<Array<{x: number, y: number}>> } | null) => {' },
|
|
24
|
+
{ type: 'contains', text: 'from: (pgPolygon: string | null) => {' },
|
|
25
|
+
{ type: 'contains', text: 'boundary: { coordinates: Array<Array<{ x: number, y: number }>> };' },
|
|
26
|
+
{ type: 'notContains', text: 'nullable: true' }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'should render nullable polygon field correctly',
|
|
31
|
+
field: {
|
|
32
|
+
name: 'optionalBoundary',
|
|
33
|
+
dataType: '{ coordinates: Array<Array<{ x: number, y: number }>> }',
|
|
34
|
+
nullable: true
|
|
35
|
+
},
|
|
36
|
+
assertions: [
|
|
37
|
+
{ type: 'contains', text: '@Column({' },
|
|
38
|
+
{ type: 'contains', text: '"type": "polygon"' },
|
|
39
|
+
{ type: 'contains', text: 'nullable: true' },
|
|
40
|
+
{ type: 'contains', text: 'optionalBoundary: { coordinates: Array<Array<{ x: number, y: number }>> };' }
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'should render indexed polygon field correctly',
|
|
45
|
+
field: {
|
|
46
|
+
name: 'area',
|
|
47
|
+
dataType: '{ coordinates: Array<Array<{ x: number, y: number }>> }',
|
|
48
|
+
nullable: false,
|
|
49
|
+
index: true
|
|
50
|
+
},
|
|
51
|
+
assertions: [
|
|
52
|
+
{ type: 'contains', text: '@Index()' },
|
|
53
|
+
{ type: 'contains', text: 'area: { coordinates: Array<Array<{ x: number, y: number }>> };' }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
(0, template_test_utils_1.runTemplateTests)(templatePath, testCases);
|
|
58
|
+
});
|
package/oclif.manifest.json
CHANGED