@apso/cli 0.1.7 → 0.1.9
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 +143 -15
- package/dist/commands/server/scaffold.js +40 -4
- package/dist/lib/apsorc-parser.js +29 -5
- package/dist/lib/base-command.d.ts +1 -1
- package/dist/lib/base-command.js +2 -2
- package/dist/lib/controller.js +60 -16
- package/dist/lib/dto.js +14 -18
- package/dist/lib/entity.js +2 -5
- package/dist/lib/enums.js +2 -5
- package/dist/lib/gql-dto.js +2 -6
- package/dist/lib/index-module.js +2 -5
- package/dist/lib/module.js +1 -5
- package/dist/lib/service.js +5 -7
- package/dist/lib/templates/rest/controller-rest.eta +2 -4
- package/dist/lib/utils/file-system.d.ts +1 -1
- package/dist/lib/utils/file-system.js +5 -5
- package/dist/lib/utils/relationships/parse.d.ts +21 -1
- package/dist/lib/utils/relationships/parse.js +97 -39
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,27 +4,58 @@
|
|
|
4
4
|
- [Prerequisites](#prerequisites)
|
|
5
5
|
- [Usage](#usage)
|
|
6
6
|
- [Local Development](#local-development)
|
|
7
|
+
- [Populating an .apsorc File](#populating-an-apsorc-file)
|
|
7
8
|
- [Debugging](##debugging)
|
|
8
9
|
- [Commands](#commands)
|
|
9
10
|
|
|
10
|
-
# Prerequisites
|
|
11
|
-
|
|
12
|
-
You need to have setup access to Mavric's private NPM packages.
|
|
13
|
-
Find out how [here](https://github.com/mavric/.github-private/blob/main/how-to/private-npm.md)
|
|
14
11
|
|
|
15
12
|
# Usage
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```
|
|
14
|
+
Follow these steps to create and run a new APSO server project:
|
|
15
|
+
|
|
16
|
+
1. **Install the CLI globally:**
|
|
17
|
+
```sh
|
|
18
|
+
npm install -g @apso/apso-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. **Initialize a new server project:**
|
|
22
|
+
```sh
|
|
23
|
+
apso server new --name <PROJECT_NAME>
|
|
24
|
+
```
|
|
25
|
+
This creates a new project folder with the necessary boilerplate.
|
|
26
|
+
|
|
27
|
+
3. **Define your database schema:**
|
|
28
|
+
Edit the `.apsorc` file in your new project folder to describe your entities and relationships. See the [Populating an .apsorc File](#populating-an-apsorc-file) section for details and examples.
|
|
29
|
+
|
|
30
|
+
4. **Generate code and database entities:**
|
|
31
|
+
```sh
|
|
32
|
+
apso server scaffold
|
|
33
|
+
```
|
|
34
|
+
This command generates all relevant modules and entity code based on your `.apsorc` file.
|
|
35
|
+
|
|
36
|
+
5. **Configure your database connection:**
|
|
37
|
+
Update your project's `.env` file with the database credentials you want to use.
|
|
38
|
+
|
|
39
|
+
6. **Start the local Postgres instance (Docker):**
|
|
40
|
+
```sh
|
|
41
|
+
npm run compose
|
|
42
|
+
```
|
|
43
|
+
This command uses Docker Compose to start a local Postgres database instance.
|
|
44
|
+
|
|
45
|
+
7. **Provision your schema/database:**
|
|
46
|
+
```sh
|
|
47
|
+
npm run provision
|
|
48
|
+
```
|
|
49
|
+
This sets up your new schema instance in the database.
|
|
50
|
+
|
|
51
|
+
8. **(Optional) Enable automatic model sync for local development:**
|
|
52
|
+
If you want to skip manual migrations and always sync your models with the database (useful for rapid prototyping or starting from scratch), set the following in your `.env` file:
|
|
53
|
+
```env
|
|
54
|
+
DATABASE_SYNC=true
|
|
55
|
+
```
|
|
56
|
+
With this setting, your models will be automatically synced to the database on startup.
|
|
57
|
+
|
|
58
|
+
> For more details on configuring your schema, see the [Populating an .apsorc File](#populating-an-apsorc-file) section below.
|
|
28
59
|
|
|
29
60
|
# Local Development
|
|
30
61
|
|
|
@@ -59,6 +90,103 @@ Now we will run the scaffold command which will generate the all the relevant mo
|
|
|
59
90
|
apso server scaffold
|
|
60
91
|
```
|
|
61
92
|
|
|
93
|
+
# Populating an .apsorc File
|
|
94
|
+
|
|
95
|
+
The `.apsorc` file defines your domain model, including entities and their relationships, for APSO code generation. It is required for scaffolding your backend service.
|
|
96
|
+
|
|
97
|
+
## How to Create and Populate `.apsorc`
|
|
98
|
+
|
|
99
|
+
1. **Location**: Place the `.apsorc` file in the root of your service directory.
|
|
100
|
+
2. **Version**: Set the `version` property to `2` for the latest schema.
|
|
101
|
+
3. **Entities**: Define each domain entity, its fields, and any unique constraints.
|
|
102
|
+
4. **Relationships**: Specify how entities relate (e.g., OneToMany, ManyToOne, etc.).
|
|
103
|
+
5. **rootFolder**: Set the folder where generated code will be placed (e.g., `src`).
|
|
104
|
+
|
|
105
|
+
> **Tip:** You can find sample `.apsorc` files in `apso-cli/test/apsorc-json/` for both v1 and v2 formats.
|
|
106
|
+
|
|
107
|
+
### Example `.apsorc` v2 File
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"version": 2,
|
|
112
|
+
"rootFolder": "src",
|
|
113
|
+
"relationships": [
|
|
114
|
+
{ "from": "User", "to": "WorkspaceUser", "type": "OneToMany", "nullable": true },
|
|
115
|
+
{ "from": "Workspace", "to": "WorkspaceUser", "type": "OneToMany" },
|
|
116
|
+
{ "from": "Workspace", "to": "Application", "type": "OneToMany", "index": true },
|
|
117
|
+
{ "from": "Application", "to": "ApplicationService", "type": "OneToMany" },
|
|
118
|
+
{ "from": "Application", "to": "User", "type": "ManyToOne", "to_name": "owner" },
|
|
119
|
+
{ "from": "ApplicationService", "to": "ApplicationServiceApiKey", "type": "OneToMany" },
|
|
120
|
+
{ "from": "ApplicationService", "to": "ApplicationServiceMetric", "type": "OneToMany" },
|
|
121
|
+
{ "from": "ApplicationService", "to": "InfrastructureStack", "type": "ManyToOne", "to_name": "networkStack", "nullable": true },
|
|
122
|
+
{ "from": "ApplicationService", "to": "InfrastructureStack", "type": "ManyToOne", "to_name": "databaseStack", "nullable": true },
|
|
123
|
+
{ "from": "InfrastructureStack", "to": "InfrastructureStack", "type": "ManyToOne", "to_name": "networkStack", "nullable": true }
|
|
124
|
+
],
|
|
125
|
+
"entities": [
|
|
126
|
+
{
|
|
127
|
+
"name": "User",
|
|
128
|
+
"created_at": true,
|
|
129
|
+
"updated_at": true,
|
|
130
|
+
"fields": [
|
|
131
|
+
{ "name": "cognito_id", "type": "text", "unique": true },
|
|
132
|
+
{ "name": "email", "type": "text", "length": 255, "is_email": true },
|
|
133
|
+
{ "name": "fullName", "type": "text", "nullable": true }
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "Workspace",
|
|
138
|
+
"created_at": true,
|
|
139
|
+
"updated_at": true,
|
|
140
|
+
"fields": [
|
|
141
|
+
{ "name": "name", "type": "text" }
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "WorkspaceUser",
|
|
146
|
+
"created_at": true,
|
|
147
|
+
"updated_at": true,
|
|
148
|
+
"fields": [
|
|
149
|
+
{ "name": "email", "type": "text", "length": 255, "is_email": true },
|
|
150
|
+
{ "name": "invite_code", "type": "text", "length": 64 },
|
|
151
|
+
{ "name": "role", "type": "enum", "values": ["User", "Admin"], "default": "Admin" },
|
|
152
|
+
{ "name": "status", "type": "enum", "values": ["Active", "Invited", "Inactive", "Deleted"] },
|
|
153
|
+
{ "name": "activeAt", "type": "date", "nullable": true }
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "Application",
|
|
158
|
+
"created_at": true,
|
|
159
|
+
"updated_at": true,
|
|
160
|
+
"fields": [
|
|
161
|
+
{ "name": "name", "type": "text" },
|
|
162
|
+
{ "name": "status", "type": "enum", "values": ["Active", "Deleted"] }
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
// ... more entities as needed ...
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For a full example, see [`apso-cli/test/apsorc-json/apsorc.v2.json`](./test/apsorc-json/apsorc.v2.json).
|
|
171
|
+
|
|
172
|
+
## Schema Reference
|
|
173
|
+
|
|
174
|
+
The `.apsorc` file must conform to the [APSO Configuration Schema](./apsorc.schema.json). This schema defines all valid properties, types, and constraints for your configuration file.
|
|
175
|
+
|
|
176
|
+
### Inline Schema (apsorc.schema.json)
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
// ... see full contents in apso-cli/apsorc.schema.json ...
|
|
180
|
+
{
|
|
181
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
182
|
+
"title": "APSO Configuration Schema",
|
|
183
|
+
"description": "Schema for the .apsorc file used by APSO to define entities and relationships.",
|
|
184
|
+
// ... (truncated for brevity) ...
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
See the [full schema file](./apsorc.schema.json) for all details and validation rules.
|
|
189
|
+
|
|
62
190
|
## Debugging
|
|
63
191
|
|
|
64
192
|
For debugging we would use the debug package so you need to import the package in file where you want to debug any code.
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
...(
|
|
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
|
}
|
package/dist/lib/base-command.js
CHANGED
|
@@ -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({
|
package/dist/lib/controller.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
58
|
+
nestedJoinsFile,
|
|
39
59
|
};
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
package/dist/lib/entity.js
CHANGED
|
@@ -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
|
};
|
package/dist/lib/gql-dto.js
CHANGED
|
@@ -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;
|
package/dist/lib/index-module.js
CHANGED
|
@@ -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;
|
package/dist/lib/module.js
CHANGED
|
@@ -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;
|
package/dist/lib/service.js
CHANGED
|
@@ -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.
|
|
40
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
?
|
|
396
|
-
: (0,
|
|
397
|
-
// Create
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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 [];
|
package/oclif.manifest.json
CHANGED