@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.
Files changed (30) hide show
  1. package/README.md +124 -0
  2. package/dist/commands/server/new.d.ts +4 -4
  3. package/dist/commands/server/new.js +33 -36
  4. package/dist/lib/base-command.js +13 -6
  5. package/dist/lib/templates/entities/entity-col-datetime.eta +4 -0
  6. package/dist/lib/templates/entities/entity-col-geography.eta +29 -0
  7. package/dist/lib/templates/entities/entity-col-geometry.eta +29 -0
  8. package/dist/lib/templates/entities/entity-col-geometrycollection.eta +29 -0
  9. package/dist/lib/templates/entities/entity-col-linestring.eta +37 -0
  10. package/dist/lib/templates/entities/entity-col-multilinestring.eta +44 -0
  11. package/dist/lib/templates/entities/entity-col-multipoint.eta +37 -0
  12. package/dist/lib/templates/entities/entity-col-multipolygon.eta +50 -0
  13. package/dist/lib/templates/entities/entity-col-polygon.eta +44 -0
  14. package/dist/lib/templates/graphql/dto/dto-col-geography.eta +4 -0
  15. package/dist/lib/templates/graphql/dto/dto-col-geometry.eta +4 -0
  16. package/dist/lib/templates/graphql/dto/dto-col-geometrycollection.eta +4 -0
  17. package/dist/lib/templates/graphql/dto/dto-col-linestring.eta +4 -0
  18. package/dist/lib/templates/graphql/dto/dto-col-multilinestring.eta +4 -0
  19. package/dist/lib/templates/graphql/dto/dto-col-multipoint.eta +4 -0
  20. package/dist/lib/templates/graphql/dto/dto-col-multipolygon.eta +4 -0
  21. package/dist/lib/templates/graphql/dto/dto-col-point.eta +4 -0
  22. package/dist/lib/templates/graphql/dto/dto-col-polygon.eta +4 -0
  23. package/dist/lib/templates/rest/dto-rest.eta +24 -0
  24. package/dist/lib/utils/field.js +10 -1
  25. package/dist/tests/templates/entities/types/geometry-template.spec.d.ts +1 -0
  26. package/dist/tests/templates/entities/types/geometry-template.spec.js +58 -0
  27. package/dist/tests/templates/entities/types/polygon-template.spec.d.ts +1 -0
  28. package/dist/tests/templates/entities/types/polygon-template.spec.js +58 -0
  29. package/oclif.manifest.json +1 -1
  30. 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(path: string): Promise<void>;
14
- formatApp(root: string): Promise<void>;
15
- startApp(root: string): Promise<void>;
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(path) {
16
- this.log("installing modules");
17
- await this.runNpmCommand(["install", "--force", "--prefix", path]);
15
+ async installModules(projectPath) {
16
+ this.log("Installing modules...");
17
+ await this.runNpmCommand(["install", "--force", "--prefix", projectPath]);
18
18
  }
19
- async formatApp(root) {
20
- this.log("cleaning up");
21
- shelljs_1.default.cd(root);
22
- await this.runNpmCommand(["run", "format", "--prefix", root]);
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(root) {
24
+ async startApp(projectPath) {
25
25
  return new Promise((resolve, reject) => {
26
- const command = "npm";
26
+ const isWindows = process.platform === "win32";
27
+ const command = isWindows ? "npm.cmd" : "npm";
27
28
  const args = ["start"];
28
- // Explicitly set cwd() to work around issues like
29
- // https://github.com/facebook/create-react-app/issues/3326.
30
- // Unfortunately we can only do this for Yarn because npm support for
31
- // equivalent --prefix flag doesn't help with this issue.
32
- // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
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("Creating project in current folder.");
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 (typeof projectName === "undefined") {
86
- // this.error("`name` must be set (e.g. WoofyApp)");
74
+ if (!projectName) {
87
75
  projectName = '';
88
76
  }
89
- if (typeof apiType === "undefined") {
77
+ if (!apiType) {
90
78
  apiType = "rest";
91
79
  }
92
- if (apiType !== "rest" && apiType !== "graphql") {
93
- this.error("`apiType` must be set (e.g. rest or graphql)");
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`];
@@ -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 command = "npm";
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, { stdio: silent ? "ignore" : "inherit" });
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,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;
@@ -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 }>> };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: any;
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: any;
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: { geometries: Array<any> };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: { coordinates: Array<{ x: number, y: number }> };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: { coordinates: Array<Array<{ x: number, y: number }>> };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: { coordinates: Array<{ x: number, y: number }> };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: { coordinates: Array<Array<Array<{ x: number, y: number }>>> };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= field.name %>: { x: number, y: number };
@@ -0,0 +1,4 @@
1
+ <% const {field, isGql} = it; %>
2
+
3
+ @Field(() => JSONResolver, { nullable: true })
4
+ <%= 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
  <% } %>
@@ -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", // handled elsewhere for custom enums
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,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,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
+ });
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.0",
2
+ "version": "0.3.1",
3
3
  "commands": {
4
4
  "server:new": {
5
5
  "id": "server:new",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Apso CLI",
5
5
  "author": "Apso by Mavric - @mavric",
6
6
  "bin": {