@apso/cli 0.0.44 → 0.1.0

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 CHANGED
@@ -47,6 +47,8 @@ Then navigate to the newly created service and update the apsorc file according
47
47
 
48
48
  Sample of apsorc files for both v1 and v2 are given in apso-cli code at "test/apsorc-json" so you can check it out in order to make sure that your apsorc file follows the right pattern.
49
49
 
50
+ For detailed examples of configuring complex relationships, especially ManyToMany and self-referencing patterns, see the [Relationship Configuration Use Cases](./UseCases.md).
51
+
50
52
  You can also provide the key "apiType" in the apsorc file e.g (Rest, Graphql) incase you want to generate the GraphQL backend by default it would be REST.
51
53
 
52
54
  Install the npm modules before continuing further.
@@ -51,7 +51,18 @@ class New extends base_command_1.default {
51
51
  fs.mkdirSync(projectPath);
52
52
  }
53
53
  shelljs_1.default.cd(projectPath);
54
- shelljs_1.default.exec(`git clone --depth=1 --branch=main git@github.com:apsoai/service-template.git ${projectPath}`);
54
+ // Execute git clone and capture the result using HTTPS
55
+ const cloneResult = shelljs_1.default.exec(`git clone --depth=1 --branch=main https://github.com/apsoai/service-template.git ${projectPath}`, { silent: true }); // Use silent: true to suppress default output
56
+ // Check if the command failed
57
+ if (cloneResult.code !== 0) {
58
+ // Provide a more generic error message for clone failures
59
+ this.error(`Failed to clone the template repository from GitHub.\n` +
60
+ `Error Output:\n${cloneResult.stderr}\n\n` +
61
+ `Please check your network connection and ensure the repository exists at https://github.com/apsoai/service-template.git\n\n` +
62
+ `If the problem persists, please remove the partially created directory "${projectPath}" and try again.`);
63
+ // Throwing error via this.error will stop execution
64
+ }
65
+ // If clone was successful, proceed to remove the .git directory
55
66
  shelljs_1.default.exec(`rm -rf ${projectPath}/.git`);
56
67
  shelljs_1.default.cd(this.CURR_DIR);
57
68
  }
@@ -74,42 +74,57 @@ export class <%= it.name %> {
74
74
 
75
75
  // Associations
76
76
  <% it.associations.forEach((assoc) => { %>
77
+
78
+ <%// Generate property ONLY based on referenceName (to_name) %>
79
+ <%// relationshipName is the camelCased version of referenceName %>
80
+ <%// pluralizedRelationshipName is the pluralized version (for arrays) %>
81
+
77
82
  <% if (assoc.type === 'OneToMany') { %>
78
83
 
79
84
  @OneToMany(
80
85
  () => <%= assoc.name %>,
81
- <%= assoc.camelCasedName %> => <%= assoc.camelCasedName %>.<%= assoc.entityName %>
86
+ <%= assoc.camelCasedName %> => <%= assoc.camelCasedName %>.<%= assoc.inverseSidePropertyName %>
82
87
  )
83
- <%= assoc.pluralizedName %>: <%= assoc.name %>[]
84
-
88
+ <%= assoc.pluralizedRelationshipName %>: <%= assoc.name %>[];
89
+
85
90
  <% } else if (assoc.type === 'ManyToOne') { %>
86
91
  <% if (assoc.index) { %>
87
92
  @Index()
88
93
  <% } %>
89
- @ManyToOne(() => <%= assoc.name %><% if (assoc.biDirectional) { %>, <%= assoc.camelCasedName %> => <%= assoc.camelCasedName %>.<%= it.pluralizedName %><% } %><% if (assoc.cascadeDelete) { %>, { onDelete: "CASCADE" }<% } %>)
94
+ @ManyToOne(() => <%= assoc.name %><% if (assoc.biDirectional) { %>, <%= assoc.camelCasedName %> => <%= assoc.camelCasedName %>.<%= assoc.inverseSidePropertyName %><% } %><% if (assoc.cascadeDelete) { %>, { onDelete: "CASCADE" }<% } %>)
90
95
  @JoinColumn({ name: '<%= assoc.camelCasedId %>' })
91
- <%= assoc.relationshipName %>: <%= assoc.name %>
96
+ <%= assoc.relationshipName %>: <%= assoc.name %>;
92
97
 
93
98
  @Column({ type: "integer", <% if (assoc.nullable) { %> nullable: true <% } %>})
94
99
  <%= assoc.camelCasedId %>: number;
95
-
100
+
96
101
  <% } else if (assoc.type === 'ManyToMany') { %>
97
-
98
- @ManyToMany(() => <%= assoc.name %><% if (assoc.biDirectional) { %>, <%= assoc.camelCasedName %> => <%= assoc.camelCasedName %>.<%= it.pluralizedName %><% } %>)
99
- <% if (assoc.joinTable) { %>@JoinTable()<% } %>
100
- <%= assoc.pluralizedRelationshipName %>: <%= assoc.name %>[]
101
-
102
+
103
+ @ManyToMany(() => <%= assoc.name %><% if (assoc.biDirectional) { %>, <%= assoc.camelCasedName %> => <%= assoc.camelCasedName %>.<%= assoc.inverseSidePropertyName %><% } %>)
104
+ <% if (assoc.joinTable) { %>
105
+ @JoinTable(<%
106
+ let joinTableArgs = [];
107
+ if (assoc.joinTableName) { joinTableArgs.push(`name: '${assoc.joinTableName}'`); }
108
+ if (assoc.joinColumnName) { joinTableArgs.push(`joinColumn: { name: '${assoc.joinColumnName}', referencedColumnName: 'id' }`); }
109
+ if (assoc.inverseJoinColumnName) { joinTableArgs.push(`inverseJoinColumn: { name: '${assoc.inverseJoinColumnName}', referencedColumnName: 'id' }`); }
110
+ if (joinTableArgs.length > 0) {
111
+ %>{
112
+ <%~ joinTableArgs.join(',\n ') %>
113
+ }<% } %>)
114
+ <% } %>
115
+ <%= assoc.pluralizedRelationshipName %>: <%= assoc.name %>[];
116
+
102
117
  <% } else if (assoc.type === 'OneToOne') { %>
103
-
118
+
104
119
  @OneToOne(
105
120
  (type: any) => <%= assoc.name%>,
106
- <% if (assoc.biDirectional) { %>(p) => p.<%= assoc.entityName %>,<% } %>
121
+ <% if (assoc.biDirectional) { %>(<%= assoc.camelCasedName %>) => <%= assoc.camelCasedName %>.<%= assoc.inverseSidePropertyName %>,<% } %>
107
122
  {
108
123
  onDelete: "CASCADE",
109
124
  cascade: ["insert", "update"],
110
125
  })
111
- @JoinColumn()
112
- <%= assoc.relationshipName %>: <%= assoc.name %>
126
+ <% if (assoc.joinTable) { %>@JoinColumn()<% } %>
127
+ <%= assoc.relationshipName %>: <%= assoc.name %>;
113
128
 
114
129
  <% } %>
115
130
  <% }) %>
@@ -25,6 +25,7 @@ import {<%= it.svcName %>} from './<%= it.entityName %>.service';
25
25
  <% if (it.associations.length) { %>
26
26
  join: {
27
27
  <% it.associations.forEach((assoc) => { %>
28
+ <% /* Removed check: relying on distinct pluralizedRelationshipName */ %>
28
29
  <% if (assoc.type === 'OneToMany') { %>
29
30
  <%= assoc.pluralizedRelationshipName %>: { eager: false },
30
31
  <% } else if (assoc.type === 'ManyToOne') { %>
@@ -8,6 +8,9 @@ export interface ApsorcRelationship {
8
8
  bi_directional?: boolean;
9
9
  cascadeDelete?: boolean;
10
10
  index?: boolean;
11
+ joinTableName?: string;
12
+ joinColumnName?: string;
13
+ inverseJoinColumnName?: string;
11
14
  }
12
15
  export interface Relationship {
13
16
  name: string;
@@ -18,6 +21,10 @@ export interface Relationship {
18
21
  biDirectional?: boolean;
19
22
  cascadeDelete?: boolean;
20
23
  index?: boolean;
24
+ joinTableName?: string;
25
+ joinColumnName?: string;
26
+ inverseJoinColumnName?: string;
27
+ inverseReferenceName?: string;
21
28
  }
22
29
  export interface RelationshipForTemplate extends Relationship {
23
30
  relationshipName: string;
@@ -28,6 +35,7 @@ export interface RelationshipForTemplate extends Relationship {
28
35
  entityName: string;
29
36
  joinTable: boolean;
30
37
  biDirectional: boolean;
38
+ inversePropertyName?: string;
31
39
  }
32
40
  export type RelationshipMap = {
33
41
  [key: string]: Relationship[];
@@ -63,17 +63,23 @@ const convertAssociationToRelationship = (entityName, association, entities) =>
63
63
  };
64
64
  const parseV1Relationships = (entities) => {
65
65
  const relationshipMap = {};
66
+ // First, convert all v1 associations to the v2 ApsorcRelationship format
67
+ const apsorcRelationships = [];
66
68
  for (const entity of entities) {
67
- if (!entity.associations) {
68
- continue;
69
- }
70
- for (const association of entity.associations) {
71
- const apsorcRelationship = convertAssociationToRelationship(entity.name, association, entities);
72
- if (apsorcRelationship === null) {
73
- continue;
69
+ if (entity.associations) {
70
+ for (const association of entity.associations) {
71
+ const convertedRel = convertAssociationToRelationship(entity.name, association, entities);
72
+ if (convertedRel) {
73
+ apsorcRelationships.push(convertedRel);
74
+ }
74
75
  }
75
- const parsedResult = (0, parse_1.parseRelationship)(apsorcRelationship);
76
- for (const key of Object.keys(parsedResult)) {
76
+ }
77
+ }
78
+ // Now, process each converted relationship, passing the full list
79
+ for (const apsorcRelationship of apsorcRelationships) {
80
+ const parsedResult = (0, parse_1.parseRelationship)(apsorcRelationship, apsorcRelationships);
81
+ for (const key of Object.keys(parsedResult)) {
82
+ if (parsedResult[key] && parsedResult[key].length > 0) { // Ensure non-empty result
77
83
  relationshipMap[key] = [
78
84
  ...(relationshipMap[key] || []),
79
85
  ...parsedResult[key],
@@ -1,9 +1,9 @@
1
1
  import { ApsorcRelationship, Relationship, RelationshipMap, RelationshipForTemplate } from "../../types";
2
2
  export declare const parseOneToMany: (relationship: ApsorcRelationship) => RelationshipMap;
3
3
  export declare const parseManytoOne: (relationship: ApsorcRelationship) => RelationshipMap;
4
- export declare const parseManyToMany: (relationship: ApsorcRelationship) => RelationshipMap;
4
+ export declare const parseManyToMany: (relationship: ApsorcRelationship, allRelationships: ApsorcRelationship[]) => RelationshipMap;
5
5
  export declare const parseOneToOne: (relationship: ApsorcRelationship) => RelationshipMap;
6
- export declare const parseRelationship: (relationship: ApsorcRelationship) => RelationshipMap;
6
+ export declare const parseRelationship: (relationship: ApsorcRelationship, allRelationships: ApsorcRelationship[]) => RelationshipMap;
7
7
  export declare const parseRelationships: (relationships: ApsorcRelationship[]) => RelationshipMap;
8
8
  export declare const getRelationshipIdField: (relationship: Relationship) => string;
9
9
  export declare const getRelationshipForTemplate: (entityName: string, relationships: Relationship[]) => RelationshipForTemplate[];
@@ -40,27 +40,64 @@ const parseManytoOne = (relationship) => {
40
40
  };
41
41
  };
42
42
  exports.parseManytoOne = parseManytoOne;
43
- const parseManyToMany = (relationship) => {
43
+ const parseManyToMany = (relationship, allRelationships) => {
44
+ // Find the inverse relationship definition (if exists)
45
+ const inverseDefinition = allRelationships.find(def => def.from === relationship.to &&
46
+ def.to === relationship.from &&
47
+ def.type === 'ManyToMany' &&
48
+ def.bi_directional);
49
+ // For self-referencing relationships, find the matching inverse by to_name
50
+ const isSelfReferencing = relationship.from === relationship.to;
51
+ let selfRefInverse = null;
52
+ if (isSelfReferencing && relationship.bi_directional) {
53
+ // Find the other relationship that completes the pair
54
+ selfRefInverse = allRelationships.find(def => def.from === relationship.from &&
55
+ def.to === relationship.to &&
56
+ def.type === 'ManyToMany' &&
57
+ def.bi_directional &&
58
+ def.to_name !== relationship.to_name);
59
+ }
60
+ let isOwningSide = Boolean(relationship.joinTableName || relationship.joinColumnName || relationship.inverseJoinColumnName);
61
+ const currentPropertyName = relationship.to_name;
62
+ let inversePropertyName = inverseDefinition === null || inverseDefinition === void 0 ? void 0 : inverseDefinition.to_name;
63
+ // For self-referencing pairs, store the other to_name as inverseReferenceName
64
+ if (isSelfReferencing && selfRefInverse) {
65
+ inversePropertyName = selfRefInverse.to_name;
66
+ }
67
+ if (!currentPropertyName) {
68
+ console.warn(`[APSO WARNING] Missing 'to_name' for ManyToMany: ${relationship.from} -> ${relationship.to}. Skipping this definition.`);
69
+ return {};
70
+ }
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.`);
73
+ }
74
+ let joinTableName = relationship.joinTableName;
75
+ let joinColumnName = relationship.joinColumnName;
76
+ let inverseJoinColumnName = relationship.inverseJoinColumnName;
77
+ // For self-referencing, ensure both sides get join=true and the same custom names
78
+ if (isSelfReferencing && relationship.bi_directional) {
79
+ isOwningSide = true; // Both sides get @JoinTable for self-ref
80
+ // If current def doesn't have names, but inverse does, grab them
81
+ if (!joinTableName && (selfRefInverse === null || selfRefInverse === void 0 ? void 0 : selfRefInverse.joinTableName)) {
82
+ joinTableName = selfRefInverse.joinTableName;
83
+ joinColumnName = selfRefInverse.joinColumnName;
84
+ inverseJoinColumnName = selfRefInverse.inverseJoinColumnName;
85
+ }
86
+ }
87
+ const fromSideRelationship = {
88
+ name: relationship.to,
89
+ type: "ManyToMany",
90
+ referenceName: currentPropertyName,
91
+ join: isOwningSide,
92
+ biDirectional: relationship.bi_directional || false,
93
+ joinTableName: joinTableName,
94
+ joinColumnName: joinColumnName,
95
+ inverseJoinColumnName: inverseJoinColumnName,
96
+ inverseReferenceName: inversePropertyName || undefined,
97
+ };
44
98
  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
- ],
99
+ [relationship.from]: [fromSideRelationship],
54
100
  };
55
- if (relationship.bi_directional) {
56
- response[relationship.to] = [
57
- {
58
- name: relationship.from,
59
- type: "ManyToMany",
60
- biDirectional: true,
61
- },
62
- ];
63
- }
64
101
  return response;
65
102
  };
66
103
  exports.parseManyToMany = parseManyToMany;
@@ -89,16 +126,33 @@ const parseOneToOne = (relationship) => {
89
126
  return response;
90
127
  };
91
128
  exports.parseOneToOne = parseOneToOne;
92
- const parseRelationship = (relationship) => {
129
+ const parseRelationship = (relationship, allRelationships) => {
130
+ if (relationship.type === "ManyToMany") {
131
+ return (0, exports.parseManyToMany)(relationship, allRelationships);
132
+ }
93
133
  switch (relationship.type) {
94
134
  case "OneToMany":
95
- return (0, exports.parseOneToMany)(relationship);
135
+ 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 }],
138
+ };
96
139
  case "ManyToOne":
97
- return (0, exports.parseManytoOne)(relationship);
98
- case "ManyToMany":
99
- return (0, exports.parseManyToMany)(relationship);
100
- case "OneToOne":
101
- return (0, exports.parseOneToOne)(relationship);
140
+ return {
141
+ [relationship.from]: [{ name: relationship.to, type: "ManyToOne", referenceName: relationship.to_name || null, nullable: relationship.nullable || false, index: relationship.index || false }],
142
+ };
143
+ 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
+ ];
153
+ }
154
+ return responseMap;
155
+ }
102
156
  }
103
157
  return {};
104
158
  };
@@ -106,9 +160,11 @@ exports.parseRelationship = parseRelationship;
106
160
  const parseRelationships = (relationships) => {
107
161
  const response = {};
108
162
  for (const rel of relationships) {
109
- const parsedResult = (0, exports.parseRelationship)(rel);
163
+ const parsedResult = (0, exports.parseRelationship)(rel, relationships);
110
164
  for (const key of Object.keys(parsedResult)) {
111
- response[key] = [...(response[key] || []), ...parsedResult[key]];
165
+ if (parsedResult[key] && parsedResult[key].length > 0) {
166
+ response[key] = [...(response[key] || []), ...parsedResult[key]];
167
+ }
112
168
  }
113
169
  }
114
170
  return response;
@@ -124,12 +180,47 @@ const getRelationshipIdField = (relationship) => {
124
180
  return `${getRelationshipName(relationship)}Id`;
125
181
  };
126
182
  exports.getRelationshipIdField = getRelationshipIdField;
183
+ const calculateInversePropertyName = (relationship, entityName, relationshipName, needsPluralInverse) => {
184
+ const isSelfReferencing = relationship.name === entityName;
185
+ if (isSelfReferencing && relationship.type === 'ManyToMany') {
186
+ if (relationship.inverseReferenceName) {
187
+ return (0, casing_1.camelCase)(relationship.inverseReferenceName);
188
+ }
189
+ const fallback = (0, casing_1.camelCase)(entityName);
190
+ return needsPluralInverse ? (0, pluralize_1.default)(fallback) : fallback;
191
+ }
192
+ if (relationship.inverseReferenceName) {
193
+ let inverse = (0, casing_1.camelCase)(relationship.inverseReferenceName);
194
+ if (needsPluralInverse) {
195
+ inverse = (0, pluralize_1.default)(inverse);
196
+ }
197
+ // Check for special plural terms
198
+ const specialPlurals = [
199
+ 'children', 'people', 'men', 'women', 'mice', 'geese', 'teeth', 'feet',
200
+ 'oxen', 'cacti', 'indices', 'vertices', 'analyses', 'crises', 'diagnoses',
201
+ 'media', 'criteria', 'data', 'parents'
202
+ ];
203
+ if (specialPlurals.includes(relationship.inverseReferenceName)) {
204
+ return relationship.inverseReferenceName;
205
+ }
206
+ return inverse;
207
+ }
208
+ const fallback = (0, casing_1.camelCase)(entityName);
209
+ return needsPluralInverse ? (0, pluralize_1.default)(fallback) : fallback;
210
+ };
127
211
  const getRelationshipForTemplate = (entityName, relationships) => {
128
212
  if (!relationships) {
129
213
  return [];
130
214
  }
131
215
  return relationships.map((relationship) => {
132
- const relationshipName = getRelationshipName(relationship);
216
+ const thisReferenceName = relationship.referenceName;
217
+ const relationshipName = (0, casing_1.camelCase)(thisReferenceName || relationship.name);
218
+ const needsPluralInverse = relationship.type === 'ManyToOne' || relationship.type === 'ManyToMany';
219
+ const inverseSidePropertyName = calculateInversePropertyName(relationship, entityName, relationshipName, needsPluralInverse);
220
+ if (relationship.biDirectional && !relationship.inverseReferenceName &&
221
+ (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}.`);
223
+ }
133
224
  return {
134
225
  ...relationship,
135
226
  relationshipName,
@@ -140,7 +231,8 @@ const getRelationshipForTemplate = (entityName, relationships) => {
140
231
  entityName: (0, casing_1.camelCase)(entityName),
141
232
  joinTable: relationship.join || false,
142
233
  biDirectional: relationship.biDirectional || false,
143
- cascadeDelete: relationship.cascadeDelete || false,
234
+ referenceName: thisReferenceName || undefined,
235
+ inverseSidePropertyName,
144
236
  };
145
237
  });
146
238
  };
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.44",
2
+ "version": "0.1.0",
3
3
  "commands": {
4
4
  "server:new": {
5
5
  "id": "server:new",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.0.44",
3
+ "version": "0.1.0",
4
4
  "description": "Apso CLI",
5
5
  "author": "Apso by Mavric - @mavric",
6
6
  "bin": {