@apso/cli 0.1.6 → 0.1.8

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Salesforce
3
+ Copyright (c) 2019-2025 Mavric Technology, LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -6,8 +6,15 @@ const path = tslib_1.__importStar(require("path"));
6
6
  const lib_1 = require("../../lib");
7
7
  const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
8
8
  const apsorc_parser_1 = require("../../lib/apsorc-parser");
9
+ const perf_hooks_1 = require("perf_hooks");
10
+ const Eta = tslib_1.__importStar(require("eta"));
11
+ Eta.configure({
12
+ views: path.join(__dirname, "../../lib/templates"),
13
+ cache: false
14
+ });
9
15
  class Scaffold extends base_command_1.default {
10
16
  async scaffoldServer(dir, entity, relationshipMap, apiType) {
17
+ const entityBuildStart = perf_hooks_1.performance.now();
11
18
  this.log(`Building... ${entity.name}`);
12
19
  const entityName = entity.name;
13
20
  const filePath = path.join(dir, entityName);
@@ -24,13 +31,31 @@ class Scaffold extends base_command_1.default {
24
31
  break;
25
32
  }
26
33
  await (0, lib_1.createModule)(filePath, entity, { apiType });
34
+ const entityBuildTime = perf_hooks_1.performance.now() - entityBuildStart;
35
+ console.log(`[apso] Finished building entity '${entity.name}' in ${entityBuildTime.toFixed(2)} ms`);
27
36
  }
28
37
  async setupRestFiles(dir, entity, relationshipMap, apiType) {
29
38
  const filePath = path.join(dir, entity.name);
30
39
  const entityRelationships = relationshipMap[entity.name] || [];
40
+ const tDto = perf_hooks_1.performance.now();
31
41
  await (0, lib_1.createDto)(filePath, entity, entityRelationships, { apiType });
42
+ if (process.env.DEBUG) {
43
+ console.log(`[timing] createDto for ${entity.name}: ${(perf_hooks_1.performance.now() - tDto).toFixed(2)}ms`);
44
+ const used = process.memoryUsage();
45
+ console.log(`[mem] heapUsed after createDto for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
46
+ }
47
+ const tService = perf_hooks_1.performance.now();
32
48
  await (0, lib_1.createService)(filePath, entity);
49
+ if (process.env.DEBUG) {
50
+ console.log(`[timing] createService for ${entity.name}: ${(perf_hooks_1.performance.now() - tService).toFixed(2)}ms`);
51
+ const used = process.memoryUsage();
52
+ console.log(`[mem] heapUsed after createService for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
53
+ }
54
+ const tController = perf_hooks_1.performance.now();
33
55
  await (0, lib_1.createController)(filePath, entity, relationshipMap);
56
+ if (process.env.DEBUG) {
57
+ console.log(`[timing] createController for ${entity.name}: ${(perf_hooks_1.performance.now() - tController).toFixed(2)}ms`);
58
+ }
34
59
  }
35
60
  async setupGraphqlFiles(dir, entity, relationshipMap) {
36
61
  const filePath = path.join(dir, entity.name);
@@ -38,17 +63,28 @@ class Scaffold extends base_command_1.default {
38
63
  await (0, lib_1.createGqlDTO)(filePath, entity, entityRelationships);
39
64
  }
40
65
  async run() {
66
+ const totalBuildStart = perf_hooks_1.performance.now();
41
67
  const { rootFolder, entities, relationshipMap, apiType } = (0, lib_1.parseApsorc)();
42
68
  const rootPath = path.join(process.cwd(), rootFolder);
43
69
  const autogenPath = path.join(rootPath, "autogen");
44
70
  const lowerCaseApiType = apiType.toLowerCase();
45
71
  await (0, lib_1.createEnums)(autogenPath, entities, lowerCaseApiType);
46
- entities.forEach((entity) => {
72
+ await Promise.all(entities.map(async (entity) => {
47
73
  const scaffoldModel = this.scaffoldServer.bind(this);
48
- scaffoldModel(autogenPath, entity, relationshipMap, lowerCaseApiType);
49
- });
74
+ await scaffoldModel(autogenPath, entity, relationshipMap, lowerCaseApiType);
75
+ if (process.env.DEBUG) {
76
+ const used = process.memoryUsage();
77
+ console.log(`[mem] heapUsed after ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
78
+ }
79
+ }));
50
80
  await (0, lib_1.createIndexAppModule)(autogenPath, entities, lowerCaseApiType);
51
- await this.runNpmCommand(["run", "format"]);
81
+ const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
82
+ console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
83
+ const formatStart = perf_hooks_1.performance.now();
84
+ console.log("[apso] Formatting files...");
85
+ await this.runNpmCommand(["run", "format", "src/autogen/**/*.ts"], true);
86
+ const formatTime = perf_hooks_1.performance.now() - formatStart;
87
+ console.log(`[apso] Finished formatting in ${formatTime.toFixed(2)} ms`);
52
88
  }
53
89
  }
54
90
  Scaffold.description = "Setup new entities and interfaces for an Apso Server";
@@ -6,6 +6,7 @@ const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
7
7
  const rc_1 = tslib_1.__importDefault(require("rc"));
8
8
  const relationships_1 = require("./utils/relationships");
9
+ const perf_hooks_1 = require("perf_hooks");
9
10
  var ApiType;
10
11
  (function (ApiType) {
11
12
  ApiType["Graphql"] = "graphql";
@@ -42,6 +43,8 @@ const parseRc = () => {
42
43
  };
43
44
  };
44
45
  const parseApsorc = () => {
46
+ const debug = process.env.DEBUG;
47
+ const start = perf_hooks_1.performance.now();
45
48
  const apsoConfig = parseRc();
46
49
  if (apsoConfig.version === 1 &&
47
50
  (apsoConfig === null || apsoConfig === void 0 ? void 0 : apsoConfig.apiType.toLowerCase()) !== ApiType.Rest.toLowerCase()) {
@@ -49,18 +52,39 @@ const parseApsorc = () => {
49
52
  }
50
53
  else {
51
54
  switch (apsoConfig.version) {
52
- case 1:
53
- return {
55
+ case 1: {
56
+ const v1Start = perf_hooks_1.performance.now();
57
+ const result = {
54
58
  rootFolder: apsoConfig.rootFolder,
55
59
  apiType: apsoConfig.apiType,
56
60
  ...(0, exports.parseApsorcV1)(apsoConfig),
57
61
  };
58
- case 2:
59
- return {
62
+ if (debug) {
63
+ console.log(`[timing] parseApsorcV1: ${(perf_hooks_1.performance.now() - v1Start).toFixed(2)}ms`);
64
+ }
65
+ if (debug) {
66
+ console.log(`[timing] parseApsorc total: ${(perf_hooks_1.performance.now() - start).toFixed(2)}ms`);
67
+ }
68
+ return result;
69
+ }
70
+ case 2: {
71
+ const result = {
60
72
  rootFolder: apsoConfig.rootFolder,
61
73
  apiType: apsoConfig.apiType,
62
- ...(0, exports.parseApsorcV2)(apsoConfig),
74
+ ...(() => {
75
+ const relStart = perf_hooks_1.performance.now();
76
+ const parsed = (0, exports.parseApsorcV2)(apsoConfig);
77
+ if (debug) {
78
+ console.log(`[timing] parseApsorcV2: ${(perf_hooks_1.performance.now() - relStart).toFixed(2)}ms`);
79
+ }
80
+ return parsed;
81
+ })(),
63
82
  };
83
+ if (debug) {
84
+ console.log(`[timing] parseApsorc total: ${(perf_hooks_1.performance.now() - start).toFixed(2)}ms`);
85
+ }
86
+ return result;
87
+ }
64
88
  }
65
89
  throw new Error(`Invalid apsorc config version: ${apsoConfig.version}`);
66
90
  }
@@ -1,4 +1,4 @@
1
1
  import { Command } from "@oclif/core";
2
2
  export default abstract class BaseCommand extends Command {
3
- runNpmCommand(args: string[]): Promise<void>;
3
+ runNpmCommand(args: string[], silent?: boolean): Promise<void>;
4
4
  }
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const child_process_1 = require("child_process");
5
5
  class BaseCommand extends core_1.Command {
6
- async runNpmCommand(args) {
6
+ async runNpmCommand(args, silent = false) {
7
7
  return new Promise((resolve, reject) => {
8
8
  const command = "npm";
9
9
  const cmdStr = `${command} ${args.join(" ")}`;
10
10
  this.log(cmdStr);
11
- const child = (0, child_process_1.spawn)(command, args, { stdio: "inherit" });
11
+ const child = (0, child_process_1.spawn)(command, args, { stdio: silent ? "ignore" : "inherit" });
12
12
  child.on("close", (code) => {
13
13
  if (code !== 0) {
14
14
  reject({
@@ -6,7 +6,13 @@ const Eta = tslib_1.__importStar(require("eta"));
6
6
  const path = tslib_1.__importStar(require("path"));
7
7
  const file_system_1 = require("./utils/file-system");
8
8
  const relationships_1 = require("./utils/relationships");
9
+ const parse_1 = require("./utils/relationships/parse");
9
10
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
11
+ const perf_hooks_1 = require("perf_hooks");
12
+ const fs_1 = tslib_1.__importDefault(require("fs"));
13
+ const os_1 = tslib_1.__importDefault(require("os"));
14
+ const util_1 = require("util");
15
+ const unlinkAsync = (0, util_1.promisify)(fs_1.default.unlink);
10
16
  /**
11
17
  * Generates NestJS controller and spec files for a given entity.
12
18
  *
@@ -19,33 +25,71 @@ const createController = async (apiBaseDir, entity, relationshipMap) => {
19
25
  const { name: entityName } = entity;
20
26
  const controllerFileName = path.join(apiBaseDir, `${entityName}.controller.ts`);
21
27
  const specFileName = path.join(apiBaseDir, `${entityName}.controller.spec.ts`);
22
- // Dependencies and names needed by the template
28
+ const startTotal = perf_hooks_1.performance.now();
29
+ const startRel = perf_hooks_1.performance.now();
30
+ const relationships = (0, relationships_1.getRelationshipForTemplate)(entityName, relationshipMap[entityName]);
31
+ const relTime = perf_hooks_1.performance.now() - startRel;
32
+ if (process.env.DEBUG) {
33
+ console.log(`[createController][${entityName}] getRelationshipForTemplate: ${relTime.toFixed(2)} ms`);
34
+ }
35
+ const startNested = perf_hooks_1.performance.now();
36
+ const tmpDir = os_1.default.tmpdir();
37
+ const nestedJoinsFile = path.join(tmpDir, `apso-nested-joins-${entityName}-${Date.now()}.eta`);
38
+ const nestedJoinsStream = fs_1.default.createWriteStream(nestedJoinsFile, { encoding: "utf8" });
39
+ let nestedCount = 0;
40
+ nestedCount = await (0, parse_1.streamNestedRelationships)(entityName, relationshipMap, nestedJoinsStream);
41
+ nestedJoinsStream.end();
42
+ await new Promise((resolve) => {
43
+ nestedJoinsStream.on("finish", resolve);
44
+ });
45
+ const nestedTime = perf_hooks_1.performance.now() - startNested;
46
+ if (process.env.DEBUG) {
47
+ console.log(`[createController][${entityName}] getNestedRelationships (streamed ${nestedCount} paths): ${nestedTime.toFixed(2)} ms`);
48
+ }
23
49
  const svcName = `${entityName}Service`;
24
50
  const ctrlName = `${entityName}Controller`;
25
51
  const pluralEntityName = (0, pluralize_1.default)(entityName);
26
- // Get processed relationships specific to this entity
27
- const relationships = (0, relationships_1.getRelationshipForTemplate)(entityName, relationshipMap[entityName]);
28
- // Get nested relationship paths for the @Crud join configuration
29
- const nestedRelationships = (0, relationships_1.getNestedRelationships)(entityName, relationshipMap);
30
- // Data object for the Eta template
31
52
  const data = {
32
53
  svcName,
33
54
  ctrlName,
34
55
  entityName,
35
56
  pluralEntityName,
36
- // Ensure uniqueness in associations (though getRelationshipForTemplate might handle this)
37
57
  associations: [...new Set(relationships)],
38
- nestedAssociations: [...new Set(nestedRelationships)],
58
+ nestedJoinsFile,
39
59
  };
40
- // Configure Eta
41
- Eta.configure({
42
- views: path.join(__dirname, "templates"),
43
- });
44
- // Render controller template
60
+ const startRenderController = perf_hooks_1.performance.now();
45
61
  const controllerContent = await Eta.renderFileAsync("./rest/controller-rest", data);
46
- (0, file_system_1.createFile)(controllerFileName, controllerContent);
47
- // Render spec template
62
+ const renderControllerTime = perf_hooks_1.performance.now() - startRenderController;
63
+ if (process.env.DEBUG) {
64
+ console.log(`[createController][${entityName}] renderController: ${renderControllerTime.toFixed(2)} ms`);
65
+ }
66
+ const startWriteController = perf_hooks_1.performance.now();
67
+ await (0, file_system_1.createFile)(controllerFileName, controllerContent);
68
+ const writeControllerTime = perf_hooks_1.performance.now() - startWriteController;
69
+ if (process.env.DEBUG) {
70
+ console.log(`[createController][${entityName}] writeController: ${writeControllerTime.toFixed(2)} ms`);
71
+ }
72
+ const startRenderSpec = perf_hooks_1.performance.now();
48
73
  const specContent = await Eta.renderFileAsync("./rest/controller-rest-spec", data);
49
- (0, file_system_1.createFile)(specFileName, specContent);
74
+ const renderSpecTime = perf_hooks_1.performance.now() - startRenderSpec;
75
+ if (process.env.DEBUG) {
76
+ console.log(`[createController][${entityName}] renderSpec: ${renderSpecTime.toFixed(2)} ms`);
77
+ }
78
+ const startWriteSpec = perf_hooks_1.performance.now();
79
+ await (0, file_system_1.createFile)(specFileName, specContent);
80
+ const writeSpecTime = perf_hooks_1.performance.now() - startWriteSpec;
81
+ if (process.env.DEBUG) {
82
+ console.log(`[createController][${entityName}] writeSpec: ${writeSpecTime.toFixed(2)} ms`);
83
+ }
84
+ try {
85
+ await unlinkAsync(nestedJoinsFile);
86
+ }
87
+ catch (error) {
88
+ console.warn(`[createController][${entityName}] Warning: Failed to delete temp file ${nestedJoinsFile}:`, error);
89
+ }
90
+ const totalTime = perf_hooks_1.performance.now() - startTotal;
91
+ if (process.env.DEBUG) {
92
+ console.log(`[createController][${entityName}] TOTAL: ${totalTime.toFixed(2)} ms`);
93
+ }
50
94
  };
51
95
  exports.createController = createController;
package/dist/lib/dto.js CHANGED
@@ -26,23 +26,23 @@ options) => {
26
26
  const dtoFile = path.join(dtoDir, `${entityName}.dto.ts`);
27
27
  const columns = (0, field_1.getFieldForTemplate)(fields, entityName);
28
28
  const relationshipsTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships);
29
- if (process.env.DEBUG) {
30
- console.log('[DTO DEBUG] Generating DTO for:', entityName);
31
- console.log('[DTO DEBUG] relationshipsTemplate:', relationshipsTemplate);
32
- }
29
+ // if (process.env.DEBUG) {
30
+ // console.log('[DTO DEBUG] Generating DTO for:', entityName);
31
+ // console.log('[DTO DEBUG] relationshipsTemplate:', relationshipsTemplate);
32
+ // }
33
33
  // Add foreign key columns for ManyToOne relationships if not present
34
34
  relationshipsTemplate.forEach(rel => {
35
35
  if (rel.type === 'ManyToOne') {
36
36
  const fkName = (0, casing_1.camelCase)(rel.referenceName || rel.name) + 'Id';
37
- if (process.env.DEBUG) {
38
- console.log('[DTO DEBUG]', {
39
- relType: rel.type,
40
- relName: rel.name,
41
- referenceName: rel.referenceName,
42
- fkName,
43
- columns: columns.map(c => c.name)
44
- });
45
- }
37
+ // if (process.env.DEBUG) {
38
+ // console.log('[DTO DEBUG]', {
39
+ // relType: rel.type,
40
+ // relName: rel.name,
41
+ // referenceName: rel.referenceName,
42
+ // fkName,
43
+ // columns: columns.map(c => c.name)
44
+ // });
45
+ // }
46
46
  if (!columns.some(col => col.name === fkName)) {
47
47
  columns.push({
48
48
  name: fkName,
@@ -73,13 +73,9 @@ options) => {
73
73
  apiType: (_a = options === null || options === void 0 ? void 0 : options.apiType) !== null && _a !== void 0 ? _a : "rest",
74
74
  importEnums: (0, field_1.typeExistsInEntity)(entity, "enum") !== -1,
75
75
  };
76
- // Configure Eta
77
- Eta.configure({
78
- views: path.join(__dirname, "templates"),
79
- });
80
76
  // Render the unified template only once
81
77
  const dtoContent = await Eta.renderFileAsync("./rest/dto-rest", data);
82
78
  // Create only one DTO file
83
- (0, file_system_1.createFile)(dtoFile, dtoContent);
79
+ await (0, file_system_1.createFile)(dtoFile, dtoContent);
84
80
  };
85
81
  exports.createDto = createDto;
@@ -2,13 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createEntity = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const Eta = tslib_1.__importStar(require("eta"));
6
5
  const path = tslib_1.__importStar(require("path"));
7
6
  const file_system_1 = require("./utils/file-system");
8
7
  const field_1 = require("./utils/field");
9
8
  const relationships_1 = require("./utils/relationships");
10
9
  const casing_1 = require("./utils/casing");
11
10
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
11
+ const Eta = tslib_1.__importStar(require("eta"));
12
12
  /**
13
13
  * Generates a TypeORM entity file based on the entity definition and relationships.
14
14
  *
@@ -43,11 +43,8 @@ const createEntity = async (apiBaseDir, entity, relationshipsNew, apiType) => {
43
43
  importEnums: (0, field_1.typeExistsInEntity)(entity, "enum") !== -1,
44
44
  apiType,
45
45
  };
46
- Eta.configure({
47
- views: path.join(__dirname, "templates"),
48
- });
49
46
  const templatePath = apiType === "graphql" ? "./graphql/gql-entity-graphql" : "./entities/entity";
50
47
  const content = await Eta.renderFileAsync(templatePath, data);
51
- (0, file_system_1.createFile)(File, content);
48
+ await (0, file_system_1.createFile)(File, content);
52
49
  };
53
50
  exports.createEntity = createEntity;
package/dist/lib/enums.js CHANGED
@@ -2,10 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createEnums = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const Eta = tslib_1.__importStar(require("eta"));
6
5
  const path = tslib_1.__importStar(require("path"));
7
6
  const file_system_1 = require("./utils/file-system");
8
7
  const field_1 = require("./utils/field");
8
+ const Eta = tslib_1.__importStar(require("eta"));
9
9
  const createEnums = async (apiBaseDir, entities, apiType) => {
10
10
  if (entities.length > 0) {
11
11
  const outputFile = path.join(apiBaseDir, "enums.ts");
@@ -18,13 +18,10 @@ const createEnums = async (apiBaseDir, entities, apiType) => {
18
18
  }))
19
19
  .filter(Boolean);
20
20
  if (allEnumsFields.length > 0) {
21
- Eta.configure({
22
- views: path.join(__dirname, "templates"),
23
- });
24
21
  const content = await Eta.renderFileAsync(`./${apiType}/enums-${apiType}`, {
25
22
  allEnumsFields,
26
23
  });
27
- (0, file_system_1.createFile)(outputFile, content);
24
+ await (0, file_system_1.createFile)(outputFile, content);
28
25
  }
29
26
  }
30
27
  };
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createGqlDTO = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const Eta = tslib_1.__importStar(require("eta"));
6
5
  const path = tslib_1.__importStar(require("path"));
7
6
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
7
+ const Eta = tslib_1.__importStar(require("eta"));
8
8
  const field_1 = require("./utils/field");
9
9
  const relationships_1 = require("./utils/relationships");
10
10
  const casing_1 = require("./utils/casing");
@@ -33,11 +33,7 @@ const createGqlDTO = async (apiBaseDir, entity, relationshipsNew) => {
33
33
  entitiesToImport,
34
34
  importEnums: (0, field_1.typeExistsInEntity)(entity, "enum") !== -1,
35
35
  };
36
- Eta.configure({
37
- // This tells Eta to look for templates in the /templates directory
38
- views: path.join(__dirname, "templates"),
39
- });
40
36
  const contentEntity = await Eta.renderFileAsync("./graphql/gql-dto-graphql", data);
41
- (0, file_system_1.createFile)(File, contentEntity);
37
+ await (0, file_system_1.createFile)(File, contentEntity);
42
38
  };
43
39
  exports.createGqlDTO = createGqlDTO;
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createIndexAppModule = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const file_system_1 = require("./utils/file-system");
6
5
  const Eta = tslib_1.__importStar(require("eta"));
6
+ const file_system_1 = require("./utils/file-system");
7
7
  const path = tslib_1.__importStar(require("path"));
8
8
  /**
9
9
  * Generates an index file (e.g., index.ts) that exports all generated entity modules.
@@ -18,12 +18,9 @@ const path = tslib_1.__importStar(require("path"));
18
18
  */
19
19
  const createIndexAppModule = async (apiBaseDir, entities, apiType) => {
20
20
  const File = path.join(apiBaseDir, `index.ts`);
21
- Eta.configure({
22
- views: path.join(__dirname, "templates"),
23
- });
24
21
  const content = await Eta.renderFileAsync(`./${apiType}/index-module-${apiType}`, {
25
22
  entities,
26
23
  });
27
- (0, file_system_1.createFile)(File, content);
24
+ await (0, file_system_1.createFile)(File, content);
28
25
  };
29
26
  exports.createIndexAppModule = createIndexAppModule;
@@ -30,15 +30,11 @@ const createModule = async (apiBaseDir, entity, options) => {
30
30
  resolverName,
31
31
  entityName,
32
32
  };
33
- // Configure Eta
34
- Eta.configure({
35
- views: path.join(__dirname, "templates"),
36
- });
37
33
  // Determine template based on API type
38
34
  const templatePath = (options === null || options === void 0 ? void 0 : options.apiType) === "graphql" ? "./graphql/gql-module-graphql" : "./rest/module-rest";
39
35
  // Render the template
40
36
  const content = await Eta.renderFileAsync(templatePath, data);
41
37
  // Create the module file
42
- (0, file_system_1.createFile)(moduleFileName, content);
38
+ await (0, file_system_1.createFile)(moduleFileName, content);
43
39
  };
44
40
  exports.createModule = createModule;
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createService = void 0;
4
4
  const tslib_1 = require("tslib");
5
- const file_system_1 = require("./utils/file-system");
6
5
  const Eta = tslib_1.__importStar(require("eta"));
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.
@@ -25,15 +25,13 @@ const createService = async (apiBaseDir, entity) => {
25
25
  repoName,
26
26
  entityName,
27
27
  };
28
- // Configure Eta
29
- Eta.configure({
30
- views: path.join(__dirname, "templates"),
31
- });
32
28
  // Render service template
33
29
  const serviceContent = await Eta.renderFileAsync("./rest/service-rest", data);
34
- (0, file_system_1.createFile)(serviceFileName, serviceContent);
30
+ await (0, file_system_1.createFile)(serviceFileName, serviceContent);
31
+ Eta.templates.remove("./rest/service-rest");
35
32
  // Render spec template
36
33
  const specContent = await Eta.renderFileAsync("./rest/service-rest-spec", data);
37
- (0, file_system_1.createFile)(specFileName, specContent);
34
+ await (0, file_system_1.createFile)(specFileName, specContent);
35
+ Eta.templates.remove("./rest/service-rest-spec");
38
36
  };
39
37
  exports.createService = createService;
@@ -36,10 +36,8 @@ import {<%= it.svcName %>} from './<%= it.entityName %>.service';
36
36
  <%= assoc.relationshipName %>: { eager: false },
37
37
  <% } %>
38
38
  <% }) %>
39
- <% if (it.nestedAssociations.length) {%>
40
- <% it.nestedAssociations.forEach((assoc) => { %>
41
- "<%= assoc %>": { eager: false },
42
- <% }) %>
39
+ <% if (it.nestedJoinsFile) {%>
40
+ <%~ includeFile(it.nestedJoinsFile) %>
43
41
  <% }%>
44
42
  },
45
43
  <% } %>
@@ -1,2 +1,2 @@
1
- export declare const createFile: (file: string, content: string) => void;
1
+ export declare const createFile: (file: string, content: string) => Promise<void>;
2
2
  export declare const createDirectoryContents: (templatePath: string, newProjectPath: string, apiType: string) => void;
@@ -4,15 +4,15 @@ exports.createDirectoryContents = exports.createFile = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const path = tslib_1.__importStar(require("path"));
6
6
  const fs = tslib_1.__importStar(require("fs"));
7
- const createDir = (dirname) => {
7
+ const createDir = async (dirname) => {
8
8
  if (!fs.existsSync(dirname)) {
9
- fs.mkdirSync(dirname, { recursive: true });
9
+ await fs.promises.mkdir(dirname, { recursive: true });
10
10
  }
11
11
  };
12
- const createFile = (file, content) => {
12
+ const createFile = async (file, content) => {
13
13
  const dirname = path.dirname(file);
14
- createDir(dirname);
15
- fs.writeFileSync(file, content);
14
+ await createDir(dirname);
15
+ await fs.promises.writeFile(file, content);
16
16
  };
17
17
  exports.createFile = createFile;
18
18
  const createDirectoryContents = (templatePath, newProjectPath, apiType) => {
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import { ApsorcRelationship, Relationship, RelationshipMap, RelationshipForTemplate } from "../../types";
2
3
  /**
3
4
  * Parses a OneToMany relationship definition.
@@ -51,5 +52,24 @@ export declare const getRelationshipIdField: (relationship: Relationship) => str
51
52
  * @returns An array of RelationshipForTemplate objects with extra properties for the template.
52
53
  */
53
54
  export declare const getRelationshipForTemplate: (entityName: string, relationships: Relationship[]) => RelationshipForTemplate[];
54
- export declare const getNestedRelationships: (entityName: string, relationshipMap: RelationshipMap, prefix?: string, visited?: Set<string>) => string[];
55
+ /**
56
+ * Generates all valid nested join paths for an entity, avoiding cycles and redundant paths.
57
+ *
58
+ * Requirements enforced:
59
+ * 1. No cycles by tracking visited (entity, reference) pairs in the current path
60
+ * 2. Allow the same entity with different references (proper name vs alias)
61
+ * 3. Allow different aliases to the same entity in different branches
62
+ * 4. Respect max depth limit
63
+ *
64
+ * @param entityName - Current entity to generate relationships from
65
+ * @param relationshipMap - Map of all entity relationships
66
+ * @param maxDepth - Maximum depth of nested joins (default: 6)
67
+ * @param path - Current join path being built (internal use)
68
+ * @param visitedPairs - Set of (entity, reference) pairs visited in current path (internal use)
69
+ * @param baseEntity - The root entity we started from (internal use)
70
+ * @returns Generator yielding valid join path strings
71
+ */
72
+ export declare function generateNestedRelationships(entityName: string, relationshipMap: RelationshipMap, maxDepth?: number, path?: string[], visitedPairs?: Set<string>, baseEntity?: string): Generator<string>;
73
+ export declare function getNestedRelationships(entityName: string, relationshipMap: RelationshipMap, maxDepth?: number): string[];
74
+ export declare function streamNestedRelationships(entityName: string, relationshipMap: RelationshipMap, writable: NodeJS.WritableStream, maxDepth?: number): Promise<number>;
55
75
  export declare const getRelationshipsForImport: (entityName: string, relationships?: Relationship[]) => string[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getRelationshipsForImport = exports.getNestedRelationships = exports.getRelationshipForTemplate = exports.getRelationshipIdField = exports.parseRelationships = exports.parseRelationship = exports.parseManyToMany = exports.parseManytoOne = exports.parseOneToMany = void 0;
3
+ exports.getRelationshipsForImport = exports.streamNestedRelationships = exports.getNestedRelationships = exports.generateNestedRelationships = exports.getRelationshipForTemplate = exports.getRelationshipIdField = exports.parseRelationships = exports.parseRelationship = exports.parseManyToMany = exports.parseManytoOne = exports.parseOneToMany = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
6
6
  const casing_1 = require("../../utils/casing");
@@ -355,9 +355,15 @@ const getRelationshipForTemplate = (entityName, relationships) => {
355
355
  const relationshipName = (0, casing_1.camelCase)(thisReferenceName || relationship.name);
356
356
  const needsPluralInverse = relationship.type === 'ManyToOne' || relationship.type === 'ManyToMany';
357
357
  // DEBUG LOGGING
358
- if (process.env.DEBUG) {
359
- console.log('[DEBUG] relationship.name:', relationship.name, '| relationship.referenceName:', relationship.referenceName, '| thisReferenceName:', thisReferenceName, '| pluralizedRelationshipName:', (0, casing_1.camelCase)((0, pluralize_1.default)(thisReferenceName || relationship.name)), '| pluralizedName:', (0, casing_1.camelCase)((0, pluralize_1.default)(relationship.name)));
360
- }
358
+ // if (process.env.DEBUG) {
359
+ // console.log(
360
+ // '[DEBUG] relationship.name:', relationship.name,
361
+ // '| relationship.referenceName:', relationship.referenceName,
362
+ // '| thisReferenceName:', thisReferenceName,
363
+ // '| pluralizedRelationshipName:', camelCase(pluralize(thisReferenceName || relationship.name)),
364
+ // '| pluralizedName:', camelCase(pluralize(relationship.name))
365
+ // );
366
+ // }
361
367
  const inverseSidePropertyName = calculateInversePropertyName(relationship, entityName, relationshipName, needsPluralInverse);
362
368
  if (relationship.biDirectional && !relationship.inverseReferenceName &&
363
369
  (relationship.type === 'ManyToMany' || relationship.type === 'OneToMany' || relationship.type === 'OneToOne')) {
@@ -379,47 +385,99 @@ const getRelationshipForTemplate = (entityName, relationships) => {
379
385
  });
380
386
  };
381
387
  exports.getRelationshipForTemplate = getRelationshipForTemplate;
382
- const getNestedRelationships = (entityName, relationshipMap, prefix = "", visited = new Set()) => {
383
- const relationships = [];
384
- if (visited.has(entityName)) {
385
- return relationships;
386
- }
387
- const currentRelationships = relationshipMap[entityName];
388
- if (!currentRelationships)
389
- return relationships;
390
- visited.add(entityName);
391
- for (const relationship of currentRelationships) {
392
- const { name: relationshipName, type } = relationship;
388
+ /**
389
+ * Generates all valid nested join paths for an entity, avoiding cycles and redundant paths.
390
+ *
391
+ * Requirements enforced:
392
+ * 1. No cycles by tracking visited (entity, reference) pairs in the current path
393
+ * 2. Allow the same entity with different references (proper name vs alias)
394
+ * 3. Allow different aliases to the same entity in different branches
395
+ * 4. Respect max depth limit
396
+ *
397
+ * @param entityName - Current entity to generate relationships from
398
+ * @param relationshipMap - Map of all entity relationships
399
+ * @param maxDepth - Maximum depth of nested joins (default: 6)
400
+ * @param path - Current join path being built (internal use)
401
+ * @param visitedPairs - Set of (entity, reference) pairs visited in current path (internal use)
402
+ * @param baseEntity - The root entity we started from (internal use)
403
+ * @returns Generator yielding valid join path strings
404
+ */
405
+ function* generateNestedRelationships(entityName, relationshipMap, maxDepth = 4, path = [], visitedPairs = new Set(), baseEntity = entityName) {
406
+ // Stop if we've reached maximum depth
407
+ if (path.length >= maxDepth)
408
+ return;
409
+ // Get relationships for the current entity
410
+ const relationships = relationshipMap[entityName];
411
+ if (!relationships)
412
+ return;
413
+ for (const relationship of relationships) {
393
414
  const referenceName = getRelationshipName(relationship);
415
+ const type = relationship.type;
416
+ // Format the property name based on relationship type
417
+ // OneToMany and ManyToMany relationships use plural forms
394
418
  const formattedName = type === "ManyToOne" || type === "OneToOne"
395
- ? (0, casing_1.camelCase)(referenceName)
396
- : (0, casing_1.camelCase)((0, pluralize_1.default)(referenceName));
397
- // Create the relationship path while preserving camelCase
398
- let relationshipPath;
399
- if (prefix) {
400
- // Split the path by dots and preserve each segment's casing
401
- const segments = prefix.split('.');
402
- relationshipPath = `${segments.join('.')}.${formattedName}`;
403
- }
404
- else {
405
- relationshipPath = formattedName;
419
+ ? referenceName
420
+ : (0, pluralize_1.default)(referenceName);
421
+ // Create a unique key for this (entity, reference) pair
422
+ const entityReferencePair = `${relationship.name}:${referenceName}`;
423
+ // Check if this exact (entity, reference) pair would create a cycle
424
+ const wouldCreateCycle = visitedPairs.has(entityReferencePair);
425
+ // Special case: prevent immediate self-reference loops (same property name)
426
+ // But allow different properties on the same entity type
427
+ const isImmediateSelfLoop = relationship.name === entityName &&
428
+ path.length > 0 &&
429
+ path[path.length - 1] === formattedName;
430
+ // Skip immediate self-loops (like networkStack.networkStack)
431
+ if (isImmediateSelfLoop)
432
+ continue;
433
+ // Skip cycles of the same (entity, reference) pair
434
+ if (wouldCreateCycle)
435
+ continue;
436
+ // Handle base entity returns: allow functional aliases but block generic relationships
437
+ if (relationship.name === baseEntity) {
438
+ // Check if this is a specific functional alias (different from default entity name)
439
+ const defaultSingularName = (0, casing_1.camelCase)(relationship.name);
440
+ const defaultPluralName = (0, casing_1.camelCase)((0, pluralize_1.default)(relationship.name));
441
+ const originalEntityName = relationship.name;
442
+ const isSpecificFunctionalAlias = relationship.referenceName &&
443
+ relationship.referenceName !== defaultSingularName &&
444
+ relationship.referenceName !== defaultPluralName &&
445
+ relationship.referenceName !== originalEntityName &&
446
+ relationship.referenceName !== relationship.name.toLowerCase();
447
+ // Block generic returns to base entity, allow functional aliases
448
+ if (!isSpecificFunctionalAlias) {
449
+ continue;
450
+ }
406
451
  }
407
- const hasChild = relationshipPath.includes(".");
408
- const exists = prefix
409
- .split(".")
410
- .some((a) => a === (0, casing_1.camelCase)(referenceName) ||
411
- a === (0, casing_1.camelCase)((0, pluralize_1.default)(referenceName)));
412
- const deepEnough = prefix.split(".").length >= 4;
413
- const canAdd = hasChild && !exists && !deepEnough;
414
- if (canAdd) {
415
- relationships.push(relationshipPath);
452
+ // Build the new path
453
+ const newPath = [...path, formattedName];
454
+ // Always yield the join path if it's longer than 1 level
455
+ if (newPath.length > 1) {
456
+ yield newPath.join(".");
416
457
  }
417
- const nestedRelationships = (0, exports.getNestedRelationships)(relationshipName, relationshipMap, relationshipPath, new Set(visited));
418
- relationships.push(...nestedRelationships);
458
+ // Create new visited pairs set with the current relationship pair added
459
+ const newVisitedPairs = new Set(visitedPairs);
460
+ newVisitedPairs.add(entityReferencePair); // Add the relationship we're about to follow
461
+ // Recursively generate deeper nested relationships
462
+ yield* generateNestedRelationships(relationship.name, relationshipMap, maxDepth, newPath, newVisitedPairs, baseEntity);
419
463
  }
420
- return relationships;
421
- };
464
+ }
465
+ exports.generateNestedRelationships = generateNestedRelationships;
466
+ // In-memory collector
467
+ function getNestedRelationships(entityName, relationshipMap, maxDepth) {
468
+ return [...generateNestedRelationships(entityName, relationshipMap, maxDepth)];
469
+ }
422
470
  exports.getNestedRelationships = getNestedRelationships;
471
+ // Streaming function
472
+ async function streamNestedRelationships(entityName, relationshipMap, writable, maxDepth) {
473
+ let count = 0;
474
+ for (const joinPath of generateNestedRelationships(entityName, relationshipMap, maxDepth)) {
475
+ writable.write(`"${joinPath}": { eager: false },\n`);
476
+ count++;
477
+ }
478
+ return count;
479
+ }
480
+ exports.streamNestedRelationships = streamNestedRelationships;
423
481
  const getRelationshipsForImport = (entityName, relationships) => {
424
482
  if (relationships === undefined) {
425
483
  return [];
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.6",
2
+ "version": "0.1.8",
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.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Apso CLI",
5
5
  "author": "Apso by Mavric - @mavric",
6
6
  "bin": {