@apso/cli 0.0.45 → 0.1.4
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 +2 -0
- package/dist/commands/server/scaffold.d.ts +1 -1
- package/dist/commands/server/scaffold.js +5 -6
- package/dist/lib/apsorc-parser.d.ts +27 -1
- package/dist/lib/apsorc-parser.js +97 -3
- package/dist/lib/controller.d.ts +8 -0
- package/dist/lib/controller.js +24 -9
- package/dist/lib/dto.d.ts +13 -7
- package/dist/lib/dto.js +33 -25
- package/dist/lib/entity.d.ts +9 -0
- package/dist/lib/entity.js +13 -3
- package/dist/lib/index-module.d.ts +11 -0
- package/dist/lib/index-module.js +11 -0
- package/dist/lib/module.d.ts +13 -1
- package/dist/lib/module.js +26 -10
- package/dist/lib/service.d.ts +9 -1
- package/dist/lib/service.js +22 -7
- package/dist/lib/templates/entities/entity.eta +30 -15
- package/dist/lib/templates/rest/controller-rest.eta +1 -0
- package/dist/lib/templates/rest/service-rest-spec.eta +38 -0
- package/dist/lib/types/relationship.d.ts +8 -0
- package/dist/lib/utils/relationships/parse-v1.js +15 -9
- package/dist/lib/utils/relationships/parse.d.ts +47 -3
- package/dist/lib/utils/relationships/parse.js +316 -53
- package/dist/lib/utils/relationships/parse.spec.d.ts +1 -0
- package/dist/lib/utils/relationships/parse.spec.js +218 -0
- package/dist/tests/utils/relationship-deduplication.spec.d.ts +1 -0
- package/dist/tests/utils/relationship-deduplication.spec.js +97 -0
- package/oclif.manifest.json +1 -1
- package/package.json +3 -1
|
@@ -1,9 +1,16 @@
|
|
|
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.
|
|
3
|
+
exports.getRelationshipsForImport = exports.getNestedRelationships = 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");
|
|
7
|
+
/**
|
|
8
|
+
* Parses a OneToMany relationship definition.
|
|
9
|
+
* These are treated as bidirectional from the .apsorc definition.
|
|
10
|
+
* Only generates the configuration for the 'from' side.
|
|
11
|
+
* @param relationship The .apsorc relationship definition.
|
|
12
|
+
* @returns A RelationshipMap containing only the entry for the 'from' side.
|
|
13
|
+
*/
|
|
7
14
|
const parseOneToMany = (relationship) => {
|
|
8
15
|
return {
|
|
9
16
|
[relationship.from]: [
|
|
@@ -26,6 +33,13 @@ const parseOneToMany = (relationship) => {
|
|
|
26
33
|
};
|
|
27
34
|
};
|
|
28
35
|
exports.parseOneToMany = parseOneToMany;
|
|
36
|
+
/**
|
|
37
|
+
* Parses a ManyToOne relationship definition.
|
|
38
|
+
* These are treated as unidirectional from the .apsorc definition.
|
|
39
|
+
* Only generates the configuration for the 'from' side.
|
|
40
|
+
* @param relationship The .apsorc relationship definition.
|
|
41
|
+
* @returns A RelationshipMap containing only the entry for the 'from' side.
|
|
42
|
+
*/
|
|
29
43
|
const parseManytoOne = (relationship) => {
|
|
30
44
|
return {
|
|
31
45
|
[relationship.from]: [
|
|
@@ -40,75 +54,240 @@ const parseManytoOne = (relationship) => {
|
|
|
40
54
|
};
|
|
41
55
|
};
|
|
42
56
|
exports.parseManytoOne = parseManytoOne;
|
|
43
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Parses a ManyToMany relationship definition.
|
|
59
|
+
* Determines bidirectionality based on flags and inverse definitions.
|
|
60
|
+
* Determines the owning side (which gets the @JoinTable decorator).
|
|
61
|
+
* Calculates reference names (property names), defaulting if 'to_name' is missing.
|
|
62
|
+
* If bidirectional, generates configuration for both 'from' and 'to' sides.
|
|
63
|
+
* @param relationship The .apsorc relationship definition.
|
|
64
|
+
* @param allRelationships The full list of relationship definitions for finding inverses.
|
|
65
|
+
* @returns A RelationshipMap containing entries for one or both sides.
|
|
66
|
+
*/
|
|
67
|
+
const parseManyToMany = (relationship, allRelationships) => {
|
|
68
|
+
// Find the inverse relationship definition (if exists)
|
|
69
|
+
const inverseDefinition = allRelationships.find(def => def.from === relationship.to &&
|
|
70
|
+
def.to === relationship.from &&
|
|
71
|
+
def.type === 'ManyToMany');
|
|
72
|
+
// Determine if the relationship should be treated as bidirectional
|
|
73
|
+
const isEffectivelyBiDirectional = Boolean(relationship.bi_directional || (inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.bi_directional));
|
|
74
|
+
// For self-referencing relationships, find the matching inverse by to_name
|
|
75
|
+
const isSelfReferencing = relationship.from === relationship.to;
|
|
76
|
+
let selfRefInverse = null;
|
|
77
|
+
if (isSelfReferencing && relationship.bi_directional) {
|
|
78
|
+
// Find the other relationship that completes the pair
|
|
79
|
+
selfRefInverse = allRelationships.find(def => def.from === relationship.from &&
|
|
80
|
+
def.to === relationship.to &&
|
|
81
|
+
def.type === 'ManyToMany' &&
|
|
82
|
+
def.bi_directional &&
|
|
83
|
+
def.to_name !== relationship.to_name);
|
|
84
|
+
}
|
|
85
|
+
let isOwningSide = relationship.joinTableName || relationship.joinColumnName || relationship.inverseJoinColumnName
|
|
86
|
+
? true
|
|
87
|
+
: undefined;
|
|
88
|
+
if (isOwningSide === undefined && inverseDefinition) {
|
|
89
|
+
// If current doesn't have details, check if inverse does
|
|
90
|
+
isOwningSide =
|
|
91
|
+
!(inverseDefinition.joinTableName || inverseDefinition.joinColumnName || inverseDefinition.inverseJoinColumnName);
|
|
92
|
+
}
|
|
93
|
+
if (isOwningSide === undefined) {
|
|
94
|
+
// If neither has details, default to the current definition being the owner
|
|
95
|
+
isOwningSide = true;
|
|
96
|
+
}
|
|
97
|
+
// Default currentPropertyName if to_name is not provided
|
|
98
|
+
const currentPropertyName = relationship.to_name || (0, pluralize_1.default)((0, casing_1.camelCase)(relationship.to));
|
|
99
|
+
let inversePropertyName = inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name;
|
|
100
|
+
// For self-referencing pairs, store the other to_name as inverseReferenceName
|
|
101
|
+
if (isSelfReferencing && selfRefInverse) {
|
|
102
|
+
inversePropertyName = selfRefInverse.to_name;
|
|
103
|
+
}
|
|
104
|
+
if (!currentPropertyName) {
|
|
105
|
+
console.warn(`[APSO WARNING] Missing 'to_name' for ManyToMany: ${relationship.from} -> ${relationship.to}. Skipping this definition.`);
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
// Check for missing inverse 'to_name' only if it's effectively bidirectional
|
|
109
|
+
if (isEffectivelyBiDirectional && !inversePropertyName) {
|
|
110
|
+
console.info(`[APSO INFO] No 'to_name' provided for bidirectional ManyToMany: ${relationship.to} -> ${relationship.from}. Using default inverse property name. Add 'to_name' for explicit control.`);
|
|
111
|
+
// Default inverse property name if missing (using calculateInversePropertyName logic later)
|
|
112
|
+
}
|
|
113
|
+
// Calculate reference names, applying defaults if needed
|
|
114
|
+
const fromSideRefName = relationship.to_name || (relationship.to.toLowerCase() + 's');
|
|
115
|
+
const toSideRefName = (inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name) || (relationship.from.toLowerCase() + 's');
|
|
116
|
+
let joinTableName = relationship.joinTableName;
|
|
117
|
+
// For self-referencing, ensure both sides get join=true and the same custom names
|
|
118
|
+
if (isSelfReferencing && relationship.bi_directional) {
|
|
119
|
+
isOwningSide = true; // Both sides get @JoinTable for self-ref
|
|
120
|
+
// If current def doesn't have names, but inverse does, grab them
|
|
121
|
+
if (!joinTableName && (selfRefInverse === null || selfRefInverse === void 0 ? void 0 : selfRefInverse.joinTableName)) {
|
|
122
|
+
joinTableName = selfRefInverse.joinTableName;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const fromSideRelationship = {
|
|
126
|
+
name: relationship.to,
|
|
127
|
+
type: "ManyToMany",
|
|
128
|
+
referenceName: fromSideRefName,
|
|
129
|
+
join: isOwningSide,
|
|
130
|
+
biDirectional: isEffectivelyBiDirectional,
|
|
131
|
+
joinTableName: isOwningSide ? (relationship.joinTableName || undefined) : undefined,
|
|
132
|
+
joinColumnName: isOwningSide ? (relationship.joinColumnName || undefined) : undefined,
|
|
133
|
+
inverseJoinColumnName: isOwningSide ? (relationship.inverseJoinColumnName || undefined) : undefined,
|
|
134
|
+
inverseReferenceName: isEffectivelyBiDirectional ? toSideRefName : undefined, // Only add inverse name if bidirectional
|
|
135
|
+
};
|
|
44
136
|
const response = {
|
|
45
|
-
[relationship.from]: [
|
|
46
|
-
{
|
|
47
|
-
name: relationship.to,
|
|
48
|
-
type: "ManyToMany",
|
|
49
|
-
referenceName: relationship.to_name || null,
|
|
50
|
-
join: true,
|
|
51
|
-
biDirectional: relationship.bi_directional || false,
|
|
52
|
-
},
|
|
53
|
-
],
|
|
137
|
+
[relationship.from]: [fromSideRelationship],
|
|
54
138
|
};
|
|
55
|
-
|
|
139
|
+
// If bidirectional, add the inverse relationship definition to the map
|
|
140
|
+
if (isEffectivelyBiDirectional) {
|
|
141
|
+
// Calculate the property name for the inverse side
|
|
142
|
+
// If inversePropertyName (from inverse to_name) exists, use it, else default based on 'from' entity
|
|
143
|
+
// const inverseSideReferenceName = inversePropertyName || pluralize(camelCase(relationship.from)); << Old way
|
|
144
|
+
// Determine if the inverse side is the owning side
|
|
145
|
+
const isInverseOwningSide = !isOwningSide;
|
|
146
|
+
const toSideRelationship = {
|
|
147
|
+
name: relationship.from,
|
|
148
|
+
type: "ManyToMany",
|
|
149
|
+
referenceName: toSideRefName,
|
|
150
|
+
join: isInverseOwningSide,
|
|
151
|
+
biDirectional: true,
|
|
152
|
+
// Add join details ONLY if the inverse side IS the owner
|
|
153
|
+
joinTableName: isInverseOwningSide ? ((inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.joinTableName) || undefined) : undefined,
|
|
154
|
+
joinColumnName: isInverseOwningSide ? ((inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.joinColumnName) || undefined) : undefined,
|
|
155
|
+
inverseJoinColumnName: isInverseOwningSide ? ((inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.inverseJoinColumnName) || undefined) : undefined,
|
|
156
|
+
inverseReferenceName: fromSideRefName, // The reference name of the other side
|
|
157
|
+
};
|
|
56
158
|
response[relationship.to] = [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
type: "ManyToMany",
|
|
60
|
-
biDirectional: true,
|
|
61
|
-
},
|
|
159
|
+
...(response[relationship.to] || []),
|
|
160
|
+
toSideRelationship
|
|
62
161
|
];
|
|
63
162
|
}
|
|
64
163
|
return response;
|
|
65
164
|
};
|
|
66
165
|
exports.parseManyToMany = parseManyToMany;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Helper function to create the Relationship object for the 'from' side of a OneToOne definition.
|
|
168
|
+
* Determines the owning side (@JoinColumn).
|
|
169
|
+
* @param relationship The .apsorc relationship definition.
|
|
170
|
+
* @param inverseDefinition The found inverse definition, if any.
|
|
171
|
+
* @returns The Relationship object for the 'from' side.
|
|
172
|
+
*/
|
|
173
|
+
const createFromOneToOneRelationship = (relationship, inverseDefinition) => {
|
|
174
|
+
// Determine owner side: if bidirectional, the side *without* to_name is owner.
|
|
175
|
+
// If not bidirectional, the 'from' side is always the owner.
|
|
176
|
+
const isOwner = relationship.bi_directional ? !relationship.to_name : true;
|
|
177
|
+
return {
|
|
178
|
+
name: relationship.to,
|
|
179
|
+
type: "OneToOne",
|
|
180
|
+
join: isOwner,
|
|
181
|
+
nullable: relationship.nullable || false,
|
|
182
|
+
referenceName: relationship.to_name || relationship.to.toLowerCase(),
|
|
183
|
+
biDirectional: relationship.bi_directional || false,
|
|
184
|
+
inverseReferenceName: (inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name) || relationship.from.toLowerCase(),
|
|
79
185
|
};
|
|
80
|
-
if (relationship.bi_directional) {
|
|
81
|
-
response[relationship.to] = [
|
|
82
|
-
{
|
|
83
|
-
name: relationship.from,
|
|
84
|
-
type: "OneToOne",
|
|
85
|
-
biDirectional: true,
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
}
|
|
89
|
-
return response;
|
|
90
186
|
};
|
|
91
|
-
|
|
92
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Helper function to create the Relationship object for the 'to' side of a bidirectional OneToOne definition.
|
|
189
|
+
* @param relationship The original .apsorc relationship definition.
|
|
190
|
+
* @param inverseDefinition The inverse definition (assumed to exist).
|
|
191
|
+
* @param isOwner Indicates if the 'from' side was determined to be the owner.
|
|
192
|
+
* @returns The Relationship object for the 'to' side.
|
|
193
|
+
*/
|
|
194
|
+
const createToOneToOneRelationship = (relationship, inverseDefinition, // Assumed to exist if this is called
|
|
195
|
+
isOwner // Is the 'from' side the owner?
|
|
196
|
+
) => {
|
|
197
|
+
return {
|
|
198
|
+
name: relationship.from,
|
|
199
|
+
type: "OneToOne",
|
|
200
|
+
biDirectional: true,
|
|
201
|
+
join: !isOwner,
|
|
202
|
+
referenceName: inverseDefinition.to_name || relationship.from.toLowerCase(),
|
|
203
|
+
inverseReferenceName: relationship.to_name || relationship.to.toLowerCase(),
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Parses a single relationship definition from .apsorc.
|
|
208
|
+
* Dispatches to the appropriate type-specific parser (parseManyToMany, parseOneToMany, etc.).
|
|
209
|
+
* For bidirectional relationships, the type-specific parsers handle generating data for both sides.
|
|
210
|
+
* @param relationship The single .apsorc relationship definition to parse.
|
|
211
|
+
* @param allRelationships The full list of relationship definitions (needed for context, e.g., finding inverses).
|
|
212
|
+
* @returns A RelationshipMap, potentially containing entries for both entities involved in the relationship.
|
|
213
|
+
*/
|
|
214
|
+
const parseRelationship = (relationship, allRelationships) => {
|
|
215
|
+
if (relationship.type === "ManyToMany") {
|
|
216
|
+
return (0, exports.parseManyToMany)(relationship, allRelationships);
|
|
217
|
+
}
|
|
93
218
|
switch (relationship.type) {
|
|
94
|
-
case "OneToMany":
|
|
95
|
-
|
|
219
|
+
case "OneToMany": {
|
|
220
|
+
const fromSideRefName = relationship.to_name || (relationship.to.toLowerCase() + 's');
|
|
221
|
+
const toSideRefName = relationship.from.toLowerCase(); // Default singular name for the inverse side
|
|
222
|
+
return {
|
|
223
|
+
[relationship.from]: [{ name: relationship.to, type: "OneToMany", biDirectional: true, referenceName: fromSideRefName, inverseReferenceName: toSideRefName }],
|
|
224
|
+
[relationship.to]: [{ name: relationship.from, type: "ManyToOne", nullable: relationship.nullable || false, biDirectional: true, index: relationship.index || false, cascadeDelete: relationship.cascadeDelete || false, referenceName: toSideRefName, inverseReferenceName: fromSideRefName }],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
96
227
|
case "ManyToOne":
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
case "OneToOne":
|
|
101
|
-
|
|
228
|
+
return {
|
|
229
|
+
[relationship.from]: [{ name: relationship.to, type: "ManyToOne", referenceName: relationship.to_name || relationship.to.toLowerCase(), nullable: relationship.nullable || false, index: relationship.index || false }],
|
|
230
|
+
};
|
|
231
|
+
case "OneToOne": {
|
|
232
|
+
const inverseOnetoOneDef = allRelationships.find((def) => def.from === relationship.to &&
|
|
233
|
+
def.to === relationship.from &&
|
|
234
|
+
def.type === "OneToOne");
|
|
235
|
+
// Determine bidirectionality based on current flag or inverse flag
|
|
236
|
+
const isEffectivelyBiDirectional = Boolean(relationship.bi_directional || (inverseOnetoOneDef === null || inverseOnetoOneDef === void 0 ? void 0 : inverseOnetoOneDef.bi_directional));
|
|
237
|
+
// Create the relationship for the 'from' side
|
|
238
|
+
const fromRel = createFromOneToOneRelationship(relationship, inverseOnetoOneDef);
|
|
239
|
+
const responseMap = { [relationship.from]: [fromRel] };
|
|
240
|
+
// If it's effectively bidirectional, create and add the relationship for the 'to' side
|
|
241
|
+
if (isEffectivelyBiDirectional) {
|
|
242
|
+
// Need the inverse definition to exist to correctly create the inverse side
|
|
243
|
+
if (inverseOnetoOneDef) {
|
|
244
|
+
const isFromSideOwner = fromRel.join || false; // Determine owner from the created 'from' side
|
|
245
|
+
const toRel = createToOneToOneRelationship(relationship, inverseOnetoOneDef, isFromSideOwner);
|
|
246
|
+
responseMap[relationship.to] = [
|
|
247
|
+
...(responseMap[relationship.to] || []),
|
|
248
|
+
toRel,
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Handle the edge case where one side declares bidirectional but the inverse definition is missing
|
|
253
|
+
// We still need to add *something* for the inverse side, likely without join/owner info
|
|
254
|
+
console.warn(`[APSO WARNING] Bidirectional OneToOne defined for ${relationship.from} -> ${relationship.to}, but inverse definition ${relationship.to} -> ${relationship.from} is missing. Inverse side generation may be incomplete.`);
|
|
255
|
+
const fallbackToRel = {
|
|
256
|
+
name: relationship.from,
|
|
257
|
+
type: "OneToOne",
|
|
258
|
+
biDirectional: true,
|
|
259
|
+
join: !(fromRel.join || false),
|
|
260
|
+
referenceName: relationship.from.toLowerCase(),
|
|
261
|
+
inverseReferenceName: fromRel.referenceName || undefined // Reference the 'from' side's name, default to undefined if null
|
|
262
|
+
};
|
|
263
|
+
responseMap[relationship.to] = [
|
|
264
|
+
...(responseMap[relationship.to] || []),
|
|
265
|
+
fallbackToRel,
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return responseMap;
|
|
270
|
+
}
|
|
102
271
|
}
|
|
272
|
+
// Fallback / Error case?
|
|
273
|
+
console.error(`[APSO ERROR] Unknown relationship type: ${relationship.type}`);
|
|
103
274
|
return {};
|
|
104
275
|
};
|
|
105
276
|
exports.parseRelationship = parseRelationship;
|
|
277
|
+
/**
|
|
278
|
+
* Parses the entire array of relationship definitions from the .apsorc file.
|
|
279
|
+
* Iterates through each definition, calls parseRelationship, and merges the results.
|
|
280
|
+
* @param relationships The array of relationship definitions from .apsorc.
|
|
281
|
+
* @returns A comprehensive RelationshipMap mapping entity names to their Relationship objects.
|
|
282
|
+
*/
|
|
106
283
|
const parseRelationships = (relationships) => {
|
|
107
284
|
const response = {};
|
|
108
285
|
for (const rel of relationships) {
|
|
109
|
-
const parsedResult = (0, exports.parseRelationship)(rel);
|
|
286
|
+
const parsedResult = (0, exports.parseRelationship)(rel, relationships);
|
|
110
287
|
for (const key of Object.keys(parsedResult)) {
|
|
111
|
-
|
|
288
|
+
if (parsedResult[key] && parsedResult[key].length > 0) {
|
|
289
|
+
response[key] = [...(response[key] || []), ...parsedResult[key]];
|
|
290
|
+
}
|
|
112
291
|
}
|
|
113
292
|
}
|
|
114
293
|
return response;
|
|
@@ -124,12 +303,95 @@ const getRelationshipIdField = (relationship) => {
|
|
|
124
303
|
return `${getRelationshipName(relationship)}Id`;
|
|
125
304
|
};
|
|
126
305
|
exports.getRelationshipIdField = getRelationshipIdField;
|
|
306
|
+
/**
|
|
307
|
+
* Calculates the final property name on the inverse side of a relationship.
|
|
308
|
+
* Used by the entity template for the TypeORM decorator's inverse side function.
|
|
309
|
+
* Handles self-referencing cases and defaults.
|
|
310
|
+
* @param relationship The Relationship object for the current side.
|
|
311
|
+
* @param entityName The name of the entity for the current side.
|
|
312
|
+
* @param relationshipName The camelCased property name for the current side.
|
|
313
|
+
* @param needsPluralInverse Whether the inverse property name should be plural.
|
|
314
|
+
* @returns The calculated inverse property name.
|
|
315
|
+
*/
|
|
316
|
+
const calculateInversePropertyName = (relationship, entityName, relationshipName, needsPluralInverse) => {
|
|
317
|
+
const isSelfReferencing = relationship.name === entityName;
|
|
318
|
+
if (isSelfReferencing && relationship.type === 'ManyToMany') {
|
|
319
|
+
if (relationship.inverseReferenceName) {
|
|
320
|
+
return (0, casing_1.camelCase)(relationship.inverseReferenceName);
|
|
321
|
+
}
|
|
322
|
+
const fallback = (0, casing_1.camelCase)(entityName);
|
|
323
|
+
return needsPluralInverse ? (0, pluralize_1.default)(fallback) : fallback;
|
|
324
|
+
}
|
|
325
|
+
if (relationship.inverseReferenceName) {
|
|
326
|
+
let inverse = (0, casing_1.camelCase)(relationship.inverseReferenceName);
|
|
327
|
+
if (needsPluralInverse) {
|
|
328
|
+
inverse = (0, pluralize_1.default)(inverse);
|
|
329
|
+
}
|
|
330
|
+
// Check for special plural terms
|
|
331
|
+
const specialPlurals = [
|
|
332
|
+
'children', 'people', 'men', 'women', 'mice', 'geese', 'teeth', 'feet',
|
|
333
|
+
'oxen', 'cacti', 'indices', 'vertices', 'analyses', 'crises', 'diagnoses',
|
|
334
|
+
'media', 'criteria', 'data', 'parents'
|
|
335
|
+
];
|
|
336
|
+
if (specialPlurals.includes(relationship.inverseReferenceName)) {
|
|
337
|
+
return relationship.inverseReferenceName;
|
|
338
|
+
}
|
|
339
|
+
return inverse;
|
|
340
|
+
}
|
|
341
|
+
const fallback = (0, casing_1.camelCase)(entityName);
|
|
342
|
+
return needsPluralInverse ? (0, pluralize_1.default)(fallback) : fallback;
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Transforms the parsed Relationship data into the final format needed by the entity/controller templates.
|
|
346
|
+
* Calculates additional helper properties like pluralized names and inverse property names.
|
|
347
|
+
* @param entityName The name of the entity for which to generate template data.
|
|
348
|
+
* @param relationships An array of Relationship objects for the given entity.
|
|
349
|
+
* @returns An array of RelationshipForTemplate objects with extra properties for the template.
|
|
350
|
+
*/
|
|
127
351
|
const getRelationshipForTemplate = (entityName, relationships) => {
|
|
128
352
|
if (!relationships) {
|
|
129
353
|
return [];
|
|
130
354
|
}
|
|
131
|
-
|
|
132
|
-
|
|
355
|
+
// More comprehensive deduplication to handle all duplicate cases
|
|
356
|
+
const dedupedRelationships = [];
|
|
357
|
+
const processedRefs = new Map();
|
|
358
|
+
// First pass: Group by name and referenceName (more specific)
|
|
359
|
+
relationships.forEach((relationship) => {
|
|
360
|
+
var _a;
|
|
361
|
+
const name = relationship.name;
|
|
362
|
+
const refName = relationship.referenceName || "default";
|
|
363
|
+
const key = `${name}:${refName}`;
|
|
364
|
+
// Use the relationship with join=true if there are duplicates (this is the owning side)
|
|
365
|
+
if (!processedRefs.has(key) || (relationship.join && !((_a = processedRefs.get(key)) === null || _a === void 0 ? void 0 : _a.join))) {
|
|
366
|
+
processedRefs.set(key, relationship);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
// Second pass: Use name-only as a fallback for edge cases
|
|
370
|
+
// This helps with ManyToMany cases where both sides exist with different join flags
|
|
371
|
+
const nameOnlyKeys = new Set();
|
|
372
|
+
processedRefs.forEach((rel) => {
|
|
373
|
+
if (!nameOnlyKeys.has(rel.name)) {
|
|
374
|
+
nameOnlyKeys.add(rel.name);
|
|
375
|
+
dedupedRelationships.push(rel);
|
|
376
|
+
}
|
|
377
|
+
else if (rel.join === true) {
|
|
378
|
+
// If we already have this name but the current one is the owner (join=true),
|
|
379
|
+
// replace the existing one
|
|
380
|
+
const existingIdx = dedupedRelationships.findIndex(r => r.name === rel.name);
|
|
381
|
+
if (existingIdx !== -1 && !dedupedRelationships[existingIdx].join) {
|
|
382
|
+
dedupedRelationships[existingIdx] = rel;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
return dedupedRelationships.map((relationship) => {
|
|
387
|
+
const thisReferenceName = relationship.referenceName;
|
|
388
|
+
const relationshipName = (0, casing_1.camelCase)(thisReferenceName || relationship.name);
|
|
389
|
+
const needsPluralInverse = relationship.type === 'ManyToOne' || relationship.type === 'ManyToMany';
|
|
390
|
+
const inverseSidePropertyName = calculateInversePropertyName(relationship, entityName, relationshipName, needsPluralInverse);
|
|
391
|
+
if (relationship.biDirectional && !relationship.inverseReferenceName &&
|
|
392
|
+
(relationship.type === 'ManyToMany' || relationship.type === 'OneToMany' || relationship.type === 'OneToOne')) {
|
|
393
|
+
console.info(`[APSO INFO] Using default property name '${inverseSidePropertyName}' for ${relationship.name}->${entityName}. To customize, add 'to_name' in your relationship definition.`);
|
|
394
|
+
}
|
|
133
395
|
return {
|
|
134
396
|
...relationship,
|
|
135
397
|
relationshipName,
|
|
@@ -140,7 +402,8 @@ const getRelationshipForTemplate = (entityName, relationships) => {
|
|
|
140
402
|
entityName: (0, casing_1.camelCase)(entityName),
|
|
141
403
|
joinTable: relationship.join || false,
|
|
142
404
|
biDirectional: relationship.biDirectional || false,
|
|
143
|
-
|
|
405
|
+
referenceName: thisReferenceName || undefined,
|
|
406
|
+
inverseSidePropertyName,
|
|
144
407
|
};
|
|
145
408
|
});
|
|
146
409
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const parse_1 = require("./parse");
|
|
5
|
+
(0, globals_1.describe)('parseRelationships', () => {
|
|
6
|
+
// Mock console.warn to prevent test output clutter
|
|
7
|
+
(0, globals_1.beforeEach)(() => {
|
|
8
|
+
globals_1.jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
9
|
+
});
|
|
10
|
+
(0, globals_1.afterEach)(() => {
|
|
11
|
+
globals_1.jest.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
(0, globals_1.describe)('ManyToMany Relationships', () => {
|
|
14
|
+
(0, globals_1.it)('should parse a single bidirectional ManyToMany definition', () => {
|
|
15
|
+
const relationships = [
|
|
16
|
+
{
|
|
17
|
+
from: 'EntityA',
|
|
18
|
+
to: 'EntityB',
|
|
19
|
+
type: 'ManyToMany',
|
|
20
|
+
// eslint-disable-next-line camelcase
|
|
21
|
+
bi_directional: true,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const expected = {
|
|
25
|
+
EntityA: [
|
|
26
|
+
{
|
|
27
|
+
name: 'EntityB',
|
|
28
|
+
type: 'ManyToMany',
|
|
29
|
+
referenceName: 'entitybs',
|
|
30
|
+
join: true,
|
|
31
|
+
biDirectional: true,
|
|
32
|
+
joinTableName: undefined,
|
|
33
|
+
joinColumnName: undefined,
|
|
34
|
+
inverseJoinColumnName: undefined,
|
|
35
|
+
inverseReferenceName: 'entityas', // Simplified default
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
EntityB: [
|
|
39
|
+
{
|
|
40
|
+
name: 'EntityA',
|
|
41
|
+
type: 'ManyToMany',
|
|
42
|
+
referenceName: 'entityas',
|
|
43
|
+
join: false,
|
|
44
|
+
biDirectional: true,
|
|
45
|
+
joinTableName: undefined,
|
|
46
|
+
joinColumnName: undefined,
|
|
47
|
+
inverseJoinColumnName: undefined,
|
|
48
|
+
inverseReferenceName: 'entitybs', // Simplified default
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const result = (0, parse_1.parseRelationships)(relationships);
|
|
53
|
+
(0, globals_1.expect)(result).toEqual(expected);
|
|
54
|
+
});
|
|
55
|
+
(0, globals_1.it)('should parse a single bidirectional ManyToMany with to_name', () => {
|
|
56
|
+
const relationships = [
|
|
57
|
+
{
|
|
58
|
+
from: 'User',
|
|
59
|
+
to: 'Group',
|
|
60
|
+
type: 'ManyToMany',
|
|
61
|
+
// eslint-disable-next-line camelcase
|
|
62
|
+
bi_directional: true,
|
|
63
|
+
// eslint-disable-next-line camelcase
|
|
64
|
+
to_name: 'userGroups',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
const expected = {
|
|
68
|
+
User: [
|
|
69
|
+
{
|
|
70
|
+
name: 'Group',
|
|
71
|
+
type: 'ManyToMany',
|
|
72
|
+
referenceName: 'userGroups',
|
|
73
|
+
join: true,
|
|
74
|
+
biDirectional: true,
|
|
75
|
+
joinTableName: undefined,
|
|
76
|
+
joinColumnName: undefined,
|
|
77
|
+
inverseJoinColumnName: undefined,
|
|
78
|
+
inverseReferenceName: 'users', // Simplified default
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
Group: [
|
|
82
|
+
{
|
|
83
|
+
name: 'User',
|
|
84
|
+
type: 'ManyToMany',
|
|
85
|
+
referenceName: 'users',
|
|
86
|
+
join: false,
|
|
87
|
+
biDirectional: true,
|
|
88
|
+
joinTableName: undefined,
|
|
89
|
+
joinColumnName: undefined,
|
|
90
|
+
inverseJoinColumnName: undefined,
|
|
91
|
+
inverseReferenceName: 'userGroups', // Expect preserved camelCase
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
const result = (0, parse_1.parseRelationships)(relationships);
|
|
96
|
+
(0, globals_1.expect)(result).toEqual(expected);
|
|
97
|
+
});
|
|
98
|
+
(0, globals_1.it)('should parse a unidirectional ManyToMany definition', () => {
|
|
99
|
+
const relationships = [
|
|
100
|
+
{
|
|
101
|
+
from: 'Post',
|
|
102
|
+
to: 'Tag',
|
|
103
|
+
type: 'ManyToMany',
|
|
104
|
+
// bi_directional is false or omitted
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
const expected = {
|
|
108
|
+
Post: [
|
|
109
|
+
{
|
|
110
|
+
name: 'Tag',
|
|
111
|
+
type: 'ManyToMany',
|
|
112
|
+
referenceName: 'tags',
|
|
113
|
+
join: true,
|
|
114
|
+
biDirectional: false,
|
|
115
|
+
joinTableName: undefined,
|
|
116
|
+
joinColumnName: undefined,
|
|
117
|
+
inverseJoinColumnName: undefined,
|
|
118
|
+
inverseReferenceName: undefined, // Correct: No inverse name for unidirectional
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
// No entry expected for Tag
|
|
122
|
+
};
|
|
123
|
+
const result = (0, parse_1.parseRelationships)(relationships);
|
|
124
|
+
(0, globals_1.expect)(result).toEqual(expected);
|
|
125
|
+
});
|
|
126
|
+
(0, globals_1.it)('should handle join table details correctly', () => {
|
|
127
|
+
const relationships = [
|
|
128
|
+
{
|
|
129
|
+
from: 'Student',
|
|
130
|
+
to: 'Course',
|
|
131
|
+
type: 'ManyToMany',
|
|
132
|
+
// eslint-disable-next-line camelcase
|
|
133
|
+
bi_directional: true,
|
|
134
|
+
// UNUSED: eslint-disable-next-line camelcase
|
|
135
|
+
joinTableName: 'enrollments',
|
|
136
|
+
// UNUSED: eslint-disable-next-line camelcase
|
|
137
|
+
joinColumnName: 'student_id',
|
|
138
|
+
// UNUSED: eslint-disable-next-line camelcase
|
|
139
|
+
inverseJoinColumnName: 'course_id',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
const expected = {
|
|
143
|
+
Student: [
|
|
144
|
+
{
|
|
145
|
+
name: 'Course',
|
|
146
|
+
type: 'ManyToMany',
|
|
147
|
+
referenceName: 'courses',
|
|
148
|
+
join: true,
|
|
149
|
+
biDirectional: true,
|
|
150
|
+
joinTableName: 'enrollments',
|
|
151
|
+
joinColumnName: 'student_id',
|
|
152
|
+
inverseJoinColumnName: 'course_id',
|
|
153
|
+
inverseReferenceName: 'students', // Simplified default
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
Course: [
|
|
157
|
+
{
|
|
158
|
+
name: 'Student',
|
|
159
|
+
type: 'ManyToMany',
|
|
160
|
+
referenceName: 'students',
|
|
161
|
+
join: false,
|
|
162
|
+
biDirectional: true,
|
|
163
|
+
joinTableName: undefined,
|
|
164
|
+
joinColumnName: undefined,
|
|
165
|
+
inverseJoinColumnName: undefined,
|
|
166
|
+
inverseReferenceName: 'courses', // Simplified default
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
const result = (0, parse_1.parseRelationships)(relationships);
|
|
171
|
+
(0, globals_1.expect)(result).toEqual(expected);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
(0, globals_1.describe)('Mixed Relationship Types', () => {
|
|
175
|
+
(0, globals_1.it)('should correctly parse ManyToMany alongside other types', () => {
|
|
176
|
+
const relationships = [
|
|
177
|
+
{
|
|
178
|
+
from: 'User',
|
|
179
|
+
to: 'Profile',
|
|
180
|
+
type: 'OneToOne',
|
|
181
|
+
// eslint-disable-next-line camelcase
|
|
182
|
+
bi_directional: true,
|
|
183
|
+
// eslint-disable-next-line camelcase
|
|
184
|
+
to_name: 'userProfile'
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
from: 'User',
|
|
188
|
+
to: 'Post',
|
|
189
|
+
type: 'OneToMany',
|
|
190
|
+
// eslint-disable-next-line camelcase
|
|
191
|
+
bi_directional: true // Implies ManyToOne on Post
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
from: 'Role',
|
|
195
|
+
to: 'User',
|
|
196
|
+
type: 'ManyToMany',
|
|
197
|
+
// eslint-disable-next-line camelcase
|
|
198
|
+
bi_directional: true
|
|
199
|
+
}
|
|
200
|
+
];
|
|
201
|
+
const result = (0, parse_1.parseRelationships)(relationships);
|
|
202
|
+
// Check User relationships
|
|
203
|
+
(0, globals_1.expect)(result.User).toHaveLength(3);
|
|
204
|
+
(0, globals_1.expect)(result.User).toContainEqual(globals_1.expect.objectContaining({ name: 'Profile', type: 'OneToOne', referenceName: 'userProfile', biDirectional: true, join: false })); // Expect preserved camelCase
|
|
205
|
+
(0, globals_1.expect)(result.User).toContainEqual(globals_1.expect.objectContaining({ name: 'Post', type: 'OneToMany', referenceName: 'posts', biDirectional: true })); // Simplified default
|
|
206
|
+
(0, globals_1.expect)(result.User).toContainEqual(globals_1.expect.objectContaining({ name: 'Role', type: 'ManyToMany', referenceName: 'roles', biDirectional: true, join: false })); // Simplified default
|
|
207
|
+
// Check Profile relationship
|
|
208
|
+
(0, globals_1.expect)(result.Profile).toHaveLength(1);
|
|
209
|
+
(0, globals_1.expect)(result.Profile).toContainEqual(globals_1.expect.objectContaining({ name: 'User', type: 'OneToOne', referenceName: 'user', biDirectional: true, join: true })); // Simplified default singular
|
|
210
|
+
// Check Post relationship
|
|
211
|
+
(0, globals_1.expect)(result.Post).toHaveLength(1);
|
|
212
|
+
(0, globals_1.expect)(result.Post).toContainEqual(globals_1.expect.objectContaining({ name: 'User', type: 'ManyToOne', referenceName: 'user', biDirectional: true })); // Simplified default singular
|
|
213
|
+
// Check Role relationship
|
|
214
|
+
(0, globals_1.expect)(result.Role).toHaveLength(1);
|
|
215
|
+
(0, globals_1.expect)(result.Role).toContainEqual(globals_1.expect.objectContaining({ name: 'User', type: 'ManyToMany', referenceName: 'users', biDirectional: true, join: true })); // Simplified default
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|