@apso/cli 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/commands/server/scaffold.d.ts +1 -1
  2. package/dist/commands/server/scaffold.js +5 -5
  3. package/dist/lib/apsorc-parser.d.ts +27 -1
  4. package/dist/lib/apsorc-parser.js +97 -3
  5. package/dist/lib/controller.d.ts +8 -0
  6. package/dist/lib/controller.js +24 -9
  7. package/dist/lib/dto.d.ts +14 -7
  8. package/dist/lib/dto.js +61 -25
  9. package/dist/lib/entity.d.ts +9 -0
  10. package/dist/lib/entity.js +14 -4
  11. package/dist/lib/gql-dto.js +1 -1
  12. package/dist/lib/index-module.d.ts +11 -0
  13. package/dist/lib/index-module.js +11 -0
  14. package/dist/lib/module.d.ts +13 -1
  15. package/dist/lib/module.js +26 -10
  16. package/dist/lib/service.d.ts +9 -1
  17. package/dist/lib/service.js +22 -7
  18. package/dist/lib/templates/entities/entity-col-json.eta +1 -1
  19. package/dist/lib/templates/rest/dto-rest.eta +2 -2
  20. package/dist/lib/templates/rest/module-rest.eta +1 -1
  21. package/dist/lib/templates/rest/service-rest-spec.eta +38 -0
  22. package/dist/lib/utils/casing.d.ts +2 -1
  23. package/dist/lib/utils/casing.js +18 -8
  24. package/dist/lib/utils/field.js +1 -1
  25. package/dist/lib/utils/relationships/parse.d.ts +45 -1
  26. package/dist/lib/utils/relationships/parse.js +228 -79
  27. package/dist/lib/utils/relationships/parse.spec.d.ts +1 -0
  28. package/dist/lib/utils/relationships/parse.spec.js +229 -0
  29. package/dist/test-nested-relationships.d.ts +1 -0
  30. package/dist/test-nested-relationships.js +112 -0
  31. package/dist/tests/templates/entities/types/json-template.spec.js +2 -2
  32. package/dist/tests/utils/relationship-deduplication.spec.d.ts +1 -0
  33. package/dist/tests/utils/relationship-deduplication.spec.js +98 -0
  34. package/oclif.manifest.json +1 -1
  35. package/package.json +3 -1
@@ -0,0 +1,38 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { <%= it.svcName %> } from './<%= it.entityName %>.service';
3
+ import { getRepositoryToken } from '@nestjs/typeorm';
4
+ import { <%= it.entityName %> } from './<%= it.entityName %>.entity';
5
+
6
+ describe('<%= it.svcName %>', () => {
7
+ let service: <%= it.svcName %>;
8
+ let mockRepository: any;
9
+
10
+ beforeEach(async () => {
11
+ mockRepository = {
12
+ find: jest.fn().mockResolvedValue([]),
13
+ findOne: jest.fn().mockResolvedValue({}),
14
+ save: jest.fn().mockResolvedValue({}),
15
+ create: jest.fn().mockReturnValue({}),
16
+ update: jest.fn().mockResolvedValue({}),
17
+ delete: jest.fn().mockResolvedValue({}),
18
+ };
19
+
20
+ const module: TestingModule = await Test.createTestingModule({
21
+ providers: [
22
+ <%= it.svcName %>,
23
+ {
24
+ provide: getRepositoryToken(<%= it.entityName %>),
25
+ useValue: mockRepository,
26
+ },
27
+ ],
28
+ }).compile();
29
+
30
+ service = module.get<<%= it.svcName %>>(<%= it.svcName %>);
31
+ });
32
+
33
+ it('should be defined', () => {
34
+ expect(service).toBeDefined();
35
+ });
36
+
37
+ // Add more tests as needed
38
+ });
@@ -1,2 +1,3 @@
1
- export declare const camelCase: (str: string, firstUpper?: boolean) => string;
1
+ export declare function camelCase(str: string): string;
2
2
  export declare const snakeCase: (str: string) => string;
3
+ export declare function pascalCase(str: string): string;
@@ -1,16 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.snakeCase = exports.camelCase = void 0;
4
- const camelCase = (str, firstUpper) => {
5
- const firstLetter = firstUpper ? str[0].toUpperCase() : str[0].toLowerCase();
6
- const restOfLetters = str
7
- .slice(1)
8
- .replace(/_[A-Za-z]/g, (underLetter) => underLetter[1].toUpperCase());
9
- return `${firstLetter}${restOfLetters}`;
10
- };
3
+ exports.pascalCase = exports.snakeCase = exports.camelCase = void 0;
4
+ function camelCase(str) {
5
+ return str
6
+ // Insert a space before all caps that are followed by lower case (for PascalCase)
7
+ .replace(/([\da-z])([A-Z])/g, '$1 $2')
8
+ // Replace all separators with spaces
9
+ .replace(/[ _-]+/g, ' ')
10
+ // Lowercase the string, then uppercase the first letter of each word (except the first)
11
+ .toLowerCase()
12
+ .replace(/ (\w)/g, (_, c) => c ? c.toUpperCase() : '')
13
+ // Remove spaces
14
+ .replace(/ /g, '');
15
+ }
11
16
  exports.camelCase = camelCase;
12
17
  const snakeCase = (str) => str[0].toLowerCase() +
13
18
  str
14
19
  .slice(1, str.length)
15
20
  .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
16
21
  exports.snakeCase = snakeCase;
22
+ function pascalCase(str) {
23
+ const c = camelCase(str);
24
+ return c.charAt(0).toUpperCase() + c.slice(1);
25
+ }
26
+ exports.pascalCase = pascalCase;
@@ -57,7 +57,7 @@ const getFieldForTemplate = (fields, entityName) => fields.map((field) => ({
57
57
  : (0, exports.getJsTypeFromFieldType)(field.type),
58
58
  }));
59
59
  exports.getFieldForTemplate = getFieldForTemplate;
60
- const fieldToEnumType = (fieldName, entityName) => `${(0, casing_1.camelCase)(entityName, true)}${(0, casing_1.camelCase)(fieldName, true)}Enum`;
60
+ const fieldToEnumType = (fieldName, entityName) => `${(0, casing_1.pascalCase)(entityName)}${(0, casing_1.pascalCase)(fieldName)}Enum`;
61
61
  exports.fieldToEnumType = fieldToEnumType;
62
62
  const typeExistsInEntity = (entity, type) => (entity === null || entity === void 0 ? void 0 : entity.fields)
63
63
  ? entity === null || entity === void 0 ? void 0 : entity.fields.findIndex((field) => field.type === type)
@@ -1,11 +1,55 @@
1
1
  import { ApsorcRelationship, Relationship, RelationshipMap, RelationshipForTemplate } from "../../types";
2
+ /**
3
+ * Parses a OneToMany relationship definition.
4
+ * These are treated as bidirectional from the .apsorc definition.
5
+ * Only generates the configuration for the 'from' side.
6
+ * @param relationship The .apsorc relationship definition.
7
+ * @returns A RelationshipMap containing only the entry for the 'from' side.
8
+ */
2
9
  export declare const parseOneToMany: (relationship: ApsorcRelationship) => RelationshipMap;
10
+ /**
11
+ * Parses a ManyToOne relationship definition.
12
+ * These are treated as unidirectional from the .apsorc definition.
13
+ * Only generates the configuration for the 'from' side.
14
+ * @param relationship The .apsorc relationship definition.
15
+ * @returns A RelationshipMap containing only the entry for the 'from' side.
16
+ */
3
17
  export declare const parseManytoOne: (relationship: ApsorcRelationship) => RelationshipMap;
18
+ /**
19
+ * Parses a ManyToMany relationship definition.
20
+ * Determines bidirectionality based on flags and inverse definitions.
21
+ * Determines the owning side (which gets the @JoinTable decorator).
22
+ * Calculates reference names (property names), defaulting if 'to_name' is missing.
23
+ * If bidirectional, generates configuration for both 'from' and 'to' sides.
24
+ * @param relationship The .apsorc relationship definition.
25
+ * @param allRelationships The full list of relationship definitions for finding inverses.
26
+ * @returns A RelationshipMap containing entries for one or both sides.
27
+ */
4
28
  export declare const parseManyToMany: (relationship: ApsorcRelationship, allRelationships: ApsorcRelationship[]) => RelationshipMap;
5
- export declare const parseOneToOne: (relationship: ApsorcRelationship) => RelationshipMap;
29
+ /**
30
+ * Parses a single relationship definition from .apsorc.
31
+ * Dispatches to the appropriate type-specific parser (parseManyToMany, parseOneToMany, etc.).
32
+ * For bidirectional relationships, the type-specific parsers handle generating data for both sides.
33
+ * @param relationship The single .apsorc relationship definition to parse.
34
+ * @param allRelationships The full list of relationship definitions (needed for context, e.g., finding inverses).
35
+ * @returns A RelationshipMap, potentially containing entries for both entities involved in the relationship.
36
+ */
6
37
  export declare const parseRelationship: (relationship: ApsorcRelationship, allRelationships: ApsorcRelationship[]) => RelationshipMap;
38
+ /**
39
+ * Parses the entire array of relationship definitions from the .apsorc file.
40
+ * Iterates through each definition, calls parseRelationship, and merges the results.
41
+ * @param relationships The array of relationship definitions from .apsorc.
42
+ * @returns A comprehensive RelationshipMap mapping entity names to their Relationship objects.
43
+ */
7
44
  export declare const parseRelationships: (relationships: ApsorcRelationship[]) => RelationshipMap;
8
45
  export declare const getRelationshipIdField: (relationship: Relationship) => string;
46
+ /**
47
+ * Transforms the parsed Relationship data into the final format needed by the entity/controller templates.
48
+ * Calculates additional helper properties like pluralized names and inverse property names.
49
+ * @param entityName The name of the entity for which to generate template data.
50
+ * @param relationships An array of Relationship objects for the given entity.
51
+ * @returns An array of RelationshipForTemplate objects with extra properties for the template.
52
+ */
9
53
  export declare const getRelationshipForTemplate: (entityName: string, relationships: Relationship[]) => RelationshipForTemplate[];
10
54
  export declare const getNestedRelationships: (entityName: string, relationshipMap: RelationshipMap, prefix?: string, visited?: Set<string>) => string[];
11
55
  export declare const getRelationshipsForImport: (entityName: string, relationships?: Relationship[]) => string[];
@@ -1,31 +1,30 @@
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.parseOneToOne = exports.parseManyToMany = exports.parseManytoOne = exports.parseOneToMany = void 0;
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
- [relationship.from]: [
10
- {
11
- name: relationship.to,
12
- type: "OneToMany",
13
- biDirectional: true
14
- },
15
- ],
16
- [relationship.to]: [
17
- {
18
- name: relationship.from,
19
- type: "ManyToOne",
20
- nullable: relationship.nullable || false,
21
- biDirectional: true,
22
- index: relationship.index || false,
23
- cascadeDelete: relationship.cascadeDelete || false
24
- },
25
- ],
16
+ [relationship.from]: [{ name: relationship.to, type: "OneToMany", biDirectional: true, referenceName: relationship.to_name || relationship.to, inverseReferenceName: relationship.from }],
17
+ [relationship.to]: [{ name: relationship.from, type: "ManyToOne", nullable: relationship.nullable || false, biDirectional: true, index: relationship.index || false, cascadeDelete: relationship.cascadeDelete || false, referenceName: relationship.from, inverseReferenceName: relationship.to_name || relationship.to }],
26
18
  };
27
19
  };
28
20
  exports.parseOneToMany = parseOneToMany;
21
+ /**
22
+ * Parses a ManyToOne relationship definition.
23
+ * These are treated as unidirectional from the .apsorc definition.
24
+ * Only generates the configuration for the 'from' side.
25
+ * @param relationship The .apsorc relationship definition.
26
+ * @returns A RelationshipMap containing only the entry for the 'from' side.
27
+ */
29
28
  const parseManytoOne = (relationship) => {
30
29
  return {
31
30
  [relationship.from]: [
@@ -40,12 +39,23 @@ const parseManytoOne = (relationship) => {
40
39
  };
41
40
  };
42
41
  exports.parseManytoOne = parseManytoOne;
42
+ /**
43
+ * Parses a ManyToMany relationship definition.
44
+ * Determines bidirectionality based on flags and inverse definitions.
45
+ * Determines the owning side (which gets the @JoinTable decorator).
46
+ * Calculates reference names (property names), defaulting if 'to_name' is missing.
47
+ * If bidirectional, generates configuration for both 'from' and 'to' sides.
48
+ * @param relationship The .apsorc relationship definition.
49
+ * @param allRelationships The full list of relationship definitions for finding inverses.
50
+ * @returns A RelationshipMap containing entries for one or both sides.
51
+ */
43
52
  const parseManyToMany = (relationship, allRelationships) => {
44
53
  // Find the inverse relationship definition (if exists)
45
54
  const inverseDefinition = allRelationships.find(def => def.from === relationship.to &&
46
55
  def.to === relationship.from &&
47
- def.type === 'ManyToMany' &&
48
- def.bi_directional);
56
+ def.type === 'ManyToMany');
57
+ // Determine if the relationship should be treated as bidirectional
58
+ const isEffectivelyBiDirectional = Boolean(relationship.bi_directional || (inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.bi_directional));
49
59
  // For self-referencing relationships, find the matching inverse by to_name
50
60
  const isSelfReferencing = relationship.from === relationship.to;
51
61
  let selfRefInverse = null;
@@ -57,8 +67,20 @@ const parseManyToMany = (relationship, allRelationships) => {
57
67
  def.bi_directional &&
58
68
  def.to_name !== relationship.to_name);
59
69
  }
60
- let isOwningSide = Boolean(relationship.joinTableName || relationship.joinColumnName || relationship.inverseJoinColumnName);
61
- const currentPropertyName = relationship.to_name;
70
+ let isOwningSide = relationship.joinTableName || relationship.joinColumnName || relationship.inverseJoinColumnName
71
+ ? true
72
+ : undefined;
73
+ if (isOwningSide === undefined && inverseDefinition) {
74
+ // If current doesn't have details, check if inverse does
75
+ isOwningSide =
76
+ !(inverseDefinition.joinTableName || inverseDefinition.joinColumnName || inverseDefinition.inverseJoinColumnName);
77
+ }
78
+ if (isOwningSide === undefined) {
79
+ // If neither has details, default to the current definition being the owner
80
+ isOwningSide = true;
81
+ }
82
+ // Default currentPropertyName if to_name is not provided
83
+ const currentPropertyName = relationship.to_name || (0, casing_1.camelCase)((0, pluralize_1.default)(relationship.to));
62
84
  let inversePropertyName = inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name;
63
85
  // For self-referencing pairs, store the other to_name as inverseReferenceName
64
86
  if (isSelfReferencing && selfRefInverse) {
@@ -68,95 +90,181 @@ const parseManyToMany = (relationship, allRelationships) => {
68
90
  console.warn(`[APSO WARNING] Missing 'to_name' for ManyToMany: ${relationship.from} -> ${relationship.to}. Skipping this definition.`);
69
91
  return {};
70
92
  }
71
- if (relationship.bi_directional && !inversePropertyName) {
72
- console.warn(`[APSO WARNING] Missing inverse 'to_name' for bidirectional ManyToMany: ${relationship.to} -> ${relationship.from}. Inverse mapping might be incorrect.`);
93
+ // Check for missing inverse 'to_name' only if it's effectively bidirectional
94
+ if (isEffectivelyBiDirectional && !inversePropertyName) {
95
+ 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.`);
96
+ // Default inverse property name if missing (using calculateInversePropertyName logic later)
73
97
  }
98
+ // Calculate reference names, applying defaults if needed
99
+ const fromSideRefName = relationship.to_name || relationship.to;
100
+ const toSideRefName = (inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name) || relationship.from;
74
101
  let joinTableName = relationship.joinTableName;
75
- let joinColumnName = relationship.joinColumnName;
76
- let inverseJoinColumnName = relationship.inverseJoinColumnName;
77
102
  // For self-referencing, ensure both sides get join=true and the same custom names
78
103
  if (isSelfReferencing && relationship.bi_directional) {
79
104
  isOwningSide = true; // Both sides get @JoinTable for self-ref
80
105
  // If current def doesn't have names, but inverse does, grab them
81
106
  if (!joinTableName && (selfRefInverse === null || selfRefInverse === void 0 ? void 0 : selfRefInverse.joinTableName)) {
82
107
  joinTableName = selfRefInverse.joinTableName;
83
- joinColumnName = selfRefInverse.joinColumnName;
84
- inverseJoinColumnName = selfRefInverse.inverseJoinColumnName;
85
108
  }
86
109
  }
87
110
  const fromSideRelationship = {
88
111
  name: relationship.to,
89
112
  type: "ManyToMany",
90
- referenceName: currentPropertyName,
113
+ referenceName: fromSideRefName,
91
114
  join: isOwningSide,
92
- biDirectional: relationship.bi_directional || false,
93
- joinTableName: joinTableName,
94
- joinColumnName: joinColumnName,
95
- inverseJoinColumnName: inverseJoinColumnName,
96
- inverseReferenceName: inversePropertyName || undefined,
115
+ biDirectional: isEffectivelyBiDirectional,
116
+ joinTableName: isOwningSide ? (relationship.joinTableName || undefined) : undefined,
117
+ joinColumnName: isOwningSide ? (relationship.joinColumnName || undefined) : undefined,
118
+ inverseJoinColumnName: isOwningSide ? (relationship.inverseJoinColumnName || undefined) : undefined,
119
+ inverseReferenceName: isEffectivelyBiDirectional ? toSideRefName : undefined, // Only add inverse name if bidirectional
97
120
  };
98
121
  const response = {
99
122
  [relationship.from]: [fromSideRelationship],
100
123
  };
101
- return response;
102
- };
103
- exports.parseManyToMany = parseManyToMany;
104
- const parseOneToOne = (relationship) => {
105
- const response = {
106
- [relationship.from]: [
107
- {
108
- name: relationship.to,
109
- type: "OneToOne",
110
- join: true,
111
- nullable: relationship.nullable || false,
112
- referenceName: relationship.to_name || null,
113
- biDirectional: relationship.bi_directional || false,
114
- },
115
- ],
116
- };
117
- if (relationship.bi_directional) {
124
+ // If bidirectional, add the inverse relationship definition to the map
125
+ if (isEffectivelyBiDirectional) {
126
+ // Calculate the property name for the inverse side
127
+ // If inversePropertyName (from inverse to_name) exists, use it, else default based on 'from' entity
128
+ // const inverseSideReferenceName = inversePropertyName || pluralize(camelCase(relationship.from)); << Old way
129
+ // Determine if the inverse side is the owning side
130
+ const isInverseOwningSide = !isOwningSide;
131
+ const toSideRelationship = {
132
+ name: relationship.from,
133
+ type: "ManyToMany",
134
+ referenceName: toSideRefName,
135
+ join: isInverseOwningSide,
136
+ biDirectional: true,
137
+ // Add join details ONLY if the inverse side IS the owner
138
+ joinTableName: isInverseOwningSide ? ((inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.joinTableName) || undefined) : undefined,
139
+ joinColumnName: isInverseOwningSide ? ((inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.joinColumnName) || undefined) : undefined,
140
+ inverseJoinColumnName: isInverseOwningSide ? ((inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.inverseJoinColumnName) || undefined) : undefined,
141
+ inverseReferenceName: fromSideRefName, // The reference name of the other side
142
+ };
118
143
  response[relationship.to] = [
119
- {
120
- name: relationship.from,
121
- type: "OneToOne",
122
- biDirectional: true,
123
- },
144
+ ...(response[relationship.to] || []),
145
+ toSideRelationship
124
146
  ];
125
147
  }
126
148
  return response;
127
149
  };
128
- exports.parseOneToOne = parseOneToOne;
150
+ exports.parseManyToMany = parseManyToMany;
151
+ /**
152
+ * Helper function to create the Relationship object for the 'from' side of a OneToOne definition.
153
+ * Determines the owning side (@JoinColumn).
154
+ * @param relationship The .apsorc relationship definition.
155
+ * @param inverseDefinition The found inverse definition, if any.
156
+ * @returns The Relationship object for the 'from' side.
157
+ */
158
+ const createFromOneToOneRelationship = (relationship, inverseDefinition) => {
159
+ // Determine owner side: if bidirectional, the side *without* to_name is owner.
160
+ // If not bidirectional, the 'from' side is always the owner.
161
+ const isOwner = relationship.bi_directional ? !relationship.to_name : true;
162
+ return {
163
+ name: relationship.to,
164
+ type: "OneToOne",
165
+ join: isOwner,
166
+ nullable: relationship.nullable || false,
167
+ referenceName: relationship.to_name || relationship.to,
168
+ biDirectional: relationship.bi_directional || false,
169
+ inverseReferenceName: (inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name) || relationship.from,
170
+ };
171
+ };
172
+ /**
173
+ * Helper function to create the Relationship object for the 'to' side of a bidirectional OneToOne definition.
174
+ * @param relationship The original .apsorc relationship definition.
175
+ * @param inverseDefinition The inverse definition (assumed to exist).
176
+ * @param isOwner Indicates if the 'from' side was determined to be the owner.
177
+ * @returns The Relationship object for the 'to' side.
178
+ */
179
+ const createToOneToOneRelationship = (relationship, inverseDefinition, // Assumed to exist if this is called
180
+ isOwner // Is the 'from' side the owner?
181
+ ) => {
182
+ return {
183
+ name: relationship.from,
184
+ type: "OneToOne",
185
+ biDirectional: true,
186
+ join: !isOwner,
187
+ referenceName: inverseDefinition.to_name || relationship.from,
188
+ inverseReferenceName: relationship.to_name || relationship.to,
189
+ };
190
+ };
191
+ /**
192
+ * Parses a single relationship definition from .apsorc.
193
+ * Dispatches to the appropriate type-specific parser (parseManyToMany, parseOneToMany, etc.).
194
+ * For bidirectional relationships, the type-specific parsers handle generating data for both sides.
195
+ * @param relationship The single .apsorc relationship definition to parse.
196
+ * @param allRelationships The full list of relationship definitions (needed for context, e.g., finding inverses).
197
+ * @returns A RelationshipMap, potentially containing entries for both entities involved in the relationship.
198
+ */
129
199
  const parseRelationship = (relationship, allRelationships) => {
130
200
  if (relationship.type === "ManyToMany") {
131
201
  return (0, exports.parseManyToMany)(relationship, allRelationships);
132
202
  }
133
203
  switch (relationship.type) {
134
- case "OneToMany":
204
+ case "OneToMany": {
205
+ const fromSideRefName = relationship.to_name || relationship.to;
206
+ const toSideRefName = relationship.from;
135
207
  return {
136
- [relationship.from]: [{ name: relationship.to, type: "OneToMany", biDirectional: true, referenceName: relationship.to_name || null }],
137
- [relationship.to]: [{ name: relationship.from, type: "ManyToOne", nullable: relationship.nullable || false, biDirectional: true, index: relationship.index || false, cascadeDelete: relationship.cascadeDelete || false }],
208
+ [relationship.from]: [{ name: relationship.to, type: "OneToMany", biDirectional: true, referenceName: fromSideRefName, inverseReferenceName: toSideRefName }],
209
+ [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 }],
138
210
  };
211
+ }
139
212
  case "ManyToOne":
140
213
  return {
141
- [relationship.from]: [{ name: relationship.to, type: "ManyToOne", referenceName: relationship.to_name || null, nullable: relationship.nullable || false, index: relationship.index || false }],
214
+ [relationship.from]: [{ name: relationship.to, type: "ManyToOne", referenceName: relationship.to_name || relationship.to, nullable: relationship.nullable || false, index: relationship.index || false }],
142
215
  };
143
216
  case "OneToOne": {
144
- const inverseOnetoOneDef = allRelationships.find(def => def.from === relationship.to && def.to === relationship.from && def.type === 'OneToOne' && def.bi_directional);
145
- const isOneToOneOwner = relationship.bi_directional ? !relationship.to_name : true;
146
- const fromOneToOne = { name: relationship.to, type: "OneToOne", join: isOneToOneOwner, nullable: relationship.nullable || false, referenceName: relationship.to_name || undefined, biDirectional: relationship.bi_directional || false, inverseReferenceName: (inverseOnetoOneDef === null || inverseOnetoOneDef === void 0 ? void 0 : inverseOnetoOneDef.to_name) || undefined };
147
- const responseMap = { [relationship.from]: [fromOneToOne] };
148
- if (relationship.bi_directional && inverseOnetoOneDef) {
149
- responseMap[relationship.to] = [
150
- ...(responseMap[relationship.to] || []),
151
- { name: relationship.from, type: "OneToOne", biDirectional: true, join: !isOneToOneOwner, referenceName: inverseOnetoOneDef.to_name || undefined, inverseReferenceName: relationship.to_name || undefined }
152
- ];
217
+ const inverseOnetoOneDef = allRelationships.find((def) => def.from === relationship.to &&
218
+ def.to === relationship.from &&
219
+ def.type === "OneToOne");
220
+ // Determine bidirectionality based on current flag or inverse flag
221
+ const isEffectivelyBiDirectional = Boolean(relationship.bi_directional || (inverseOnetoOneDef === null || inverseOnetoOneDef === void 0 ? void 0 : inverseOnetoOneDef.bi_directional));
222
+ // Create the relationship for the 'from' side
223
+ const fromRel = createFromOneToOneRelationship(relationship, inverseOnetoOneDef);
224
+ const responseMap = { [relationship.from]: [fromRel] };
225
+ // If it's effectively bidirectional, create and add the relationship for the 'to' side
226
+ if (isEffectivelyBiDirectional) {
227
+ // Need the inverse definition to exist to correctly create the inverse side
228
+ if (inverseOnetoOneDef) {
229
+ const isFromSideOwner = fromRel.join || false; // Determine owner from the created 'from' side
230
+ const toRel = createToOneToOneRelationship(relationship, inverseOnetoOneDef, isFromSideOwner);
231
+ responseMap[relationship.to] = [
232
+ ...(responseMap[relationship.to] || []),
233
+ toRel,
234
+ ];
235
+ }
236
+ else {
237
+ // Handle the edge case where one side declares bidirectional but the inverse definition is missing
238
+ // We still need to add *something* for the inverse side, likely without join/owner info
239
+ 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.`);
240
+ const fallbackToRel = {
241
+ name: relationship.from,
242
+ type: "OneToOne",
243
+ biDirectional: true,
244
+ join: !(fromRel.join || false),
245
+ referenceName: relationship.from,
246
+ inverseReferenceName: fromRel.referenceName || undefined // Reference the 'from' side's name, default to undefined if null
247
+ };
248
+ responseMap[relationship.to] = [
249
+ ...(responseMap[relationship.to] || []),
250
+ fallbackToRel,
251
+ ];
252
+ }
153
253
  }
154
254
  return responseMap;
155
255
  }
156
256
  }
257
+ // Fallback / Error case?
258
+ console.error(`[APSO ERROR] Unknown relationship type: ${relationship.type}`);
157
259
  return {};
158
260
  };
159
261
  exports.parseRelationship = parseRelationship;
262
+ /**
263
+ * Parses the entire array of relationship definitions from the .apsorc file.
264
+ * Iterates through each definition, calls parseRelationship, and merges the results.
265
+ * @param relationships The array of relationship definitions from .apsorc.
266
+ * @returns A comprehensive RelationshipMap mapping entity names to their Relationship objects.
267
+ */
160
268
  const parseRelationships = (relationships) => {
161
269
  const response = {};
162
270
  for (const rel of relationships) {
@@ -180,6 +288,16 @@ const getRelationshipIdField = (relationship) => {
180
288
  return `${getRelationshipName(relationship)}Id`;
181
289
  };
182
290
  exports.getRelationshipIdField = getRelationshipIdField;
291
+ /**
292
+ * Calculates the final property name on the inverse side of a relationship.
293
+ * Used by the entity template for the TypeORM decorator's inverse side function.
294
+ * Handles self-referencing cases and defaults.
295
+ * @param relationship The Relationship object for the current side.
296
+ * @param entityName The name of the entity for the current side.
297
+ * @param relationshipName The camelCased property name for the current side.
298
+ * @param needsPluralInverse Whether the inverse property name should be plural.
299
+ * @returns The calculated inverse property name.
300
+ */
183
301
  const calculateInversePropertyName = (relationship, entityName, relationshipName, needsPluralInverse) => {
184
302
  const isSelfReferencing = relationship.name === entityName;
185
303
  if (isSelfReferencing && relationship.type === 'ManyToMany') {
@@ -208,24 +326,48 @@ const calculateInversePropertyName = (relationship, entityName, relationshipName
208
326
  const fallback = (0, casing_1.camelCase)(entityName);
209
327
  return needsPluralInverse ? (0, pluralize_1.default)(fallback) : fallback;
210
328
  };
329
+ /**
330
+ * Transforms the parsed Relationship data into the final format needed by the entity/controller templates.
331
+ * Calculates additional helper properties like pluralized names and inverse property names.
332
+ * @param entityName The name of the entity for which to generate template data.
333
+ * @param relationships An array of Relationship objects for the given entity.
334
+ * @returns An array of RelationshipForTemplate objects with extra properties for the template.
335
+ */
211
336
  const getRelationshipForTemplate = (entityName, relationships) => {
212
337
  if (!relationships) {
213
338
  return [];
214
339
  }
215
- return relationships.map((relationship) => {
340
+ // First pass: Group by name and referenceName (more specific)
341
+ const processedRefs = new Map();
342
+ relationships.forEach((relationship) => {
343
+ var _a;
344
+ const name = relationship.name;
345
+ const refName = relationship.referenceName || "default";
346
+ const key = `${name}:${refName}`;
347
+ // Use the relationship with join=true if there are duplicates (this is the owning side)
348
+ if (!processedRefs.has(key) || (relationship.join && !((_a = processedRefs.get(key)) === null || _a === void 0 ? void 0 : _a.join))) {
349
+ processedRefs.set(key, relationship);
350
+ }
351
+ });
352
+ // Only deduplicate by name+referenceName, not just name
353
+ return [...processedRefs.values()].map((relationship) => {
216
354
  const thisReferenceName = relationship.referenceName;
217
355
  const relationshipName = (0, casing_1.camelCase)(thisReferenceName || relationship.name);
218
356
  const needsPluralInverse = relationship.type === 'ManyToOne' || relationship.type === 'ManyToMany';
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
+ }
219
361
  const inverseSidePropertyName = calculateInversePropertyName(relationship, entityName, relationshipName, needsPluralInverse);
220
362
  if (relationship.biDirectional && !relationship.inverseReferenceName &&
221
363
  (relationship.type === 'ManyToMany' || relationship.type === 'OneToMany' || relationship.type === 'OneToOne')) {
222
- console.warn(`[APSO WARNING] Using default inverse property name '${inverseSidePropertyName}' for ${entityName}.${relationshipName}. Define 'to_name' for the inverse relationship ${relationship.name}->${entityName}.`);
364
+ console.info(`[APSO INFO] Using default property name '${inverseSidePropertyName}' for ${relationship.name}->${entityName}. To customize, add 'to_name' in your relationship definition.`);
223
365
  }
224
366
  return {
225
367
  ...relationship,
226
368
  relationshipName,
227
- pluralizedRelationshipName: (0, pluralize_1.default)(relationshipName),
228
- pluralizedName: (0, pluralize_1.default)((0, casing_1.camelCase)(relationship.name)),
369
+ pluralizedRelationshipName: (0, casing_1.camelCase)((0, pluralize_1.default)(thisReferenceName || relationship.name)),
370
+ pluralizedName: (0, casing_1.camelCase)((0, pluralize_1.default)(relationship.name)),
229
371
  camelCasedName: (0, casing_1.camelCase)(relationship.name),
230
372
  camelCasedId: (0, exports.getRelationshipIdField)(relationship),
231
373
  entityName: (0, casing_1.camelCase)(entityName),
@@ -251,15 +393,22 @@ const getNestedRelationships = (entityName, relationshipMap, prefix = "", visite
251
393
  const referenceName = getRelationshipName(relationship);
252
394
  const formattedName = type === "ManyToOne" || type === "OneToOne"
253
395
  ? (0, casing_1.camelCase)(referenceName)
254
- : (0, pluralize_1.default)((0, casing_1.camelCase)(referenceName));
255
- const relationshipPath = prefix
256
- ? `${prefix}.${formattedName}`
257
- : formattedName;
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;
406
+ }
258
407
  const hasChild = relationshipPath.includes(".");
259
408
  const exists = prefix
260
409
  .split(".")
261
410
  .some((a) => a === (0, casing_1.camelCase)(referenceName) ||
262
- a === (0, pluralize_1.default)((0, casing_1.camelCase)(referenceName)));
411
+ a === (0, casing_1.camelCase)((0, pluralize_1.default)(referenceName)));
263
412
  const deepEnough = prefix.split(".").length >= 4;
264
413
  const canAdd = hasChild && !exists && !deepEnough;
265
414
  if (canAdd) {
@@ -0,0 +1 @@
1
+ export {};