@apso/cli 0.3.0 → 0.4.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/dist/commands/server/new.d.ts +4 -4
- package/dist/commands/server/new.js +33 -36
- package/dist/commands/server/scaffold.d.ts +7 -1
- package/dist/commands/server/scaffold.js +16 -3
- package/dist/lib/base-command.js +14 -6
- package/dist/lib/entity.d.ts +13 -5
- package/dist/lib/entity.js +11 -6
- package/dist/lib/templates/entities/entity.eta +11 -0
- package/dist/lib/types/entity.d.ts +1 -0
- package/dist/lib/types/relationship.d.ts +1 -0
- package/dist/lib/utils/relationships/parse.d.ts +2 -1
- package/dist/lib/utils/relationships/parse.js +13 -1
- package/dist/lib/utils/relationships/parse.spec.js +146 -0
- package/dist/tests/templates/entities/uuid-entity-template.spec.d.ts +1 -0
- package/dist/tests/templates/entities/uuid-entity-template.spec.js +277 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -10,13 +10,13 @@ export default class New extends BaseCommand {
|
|
|
10
10
|
static args: {
|
|
11
11
|
name: import("@oclif/core/lib/interfaces/parser").Arg<string | undefined, Record<string, unknown>>;
|
|
12
12
|
};
|
|
13
|
-
installModules(
|
|
14
|
-
formatApp(
|
|
15
|
-
startApp(
|
|
13
|
+
installModules(projectPath: string): Promise<void>;
|
|
14
|
+
formatApp(projectPath: string): Promise<void>;
|
|
15
|
+
startApp(projectPath: string): Promise<void>;
|
|
16
16
|
cloneTemplate(projectPath: string): Promise<void>;
|
|
17
|
-
run(): Promise<void>;
|
|
18
17
|
validateFlags(): Promise<{
|
|
19
18
|
projectName: string;
|
|
20
19
|
apiType: string;
|
|
21
20
|
}>;
|
|
21
|
+
run(): Promise<void>;
|
|
22
22
|
}
|
|
@@ -12,28 +12,25 @@ class New extends base_command_1.default {
|
|
|
12
12
|
super(...arguments);
|
|
13
13
|
this.CURR_DIR = process.cwd();
|
|
14
14
|
}
|
|
15
|
-
async installModules(
|
|
16
|
-
this.log("
|
|
17
|
-
await this.runNpmCommand(["install", "--force", "--prefix",
|
|
15
|
+
async installModules(projectPath) {
|
|
16
|
+
this.log("Installing modules...");
|
|
17
|
+
await this.runNpmCommand(["install", "--force", "--prefix", projectPath]);
|
|
18
18
|
}
|
|
19
|
-
async formatApp(
|
|
20
|
-
this.log("
|
|
21
|
-
shelljs_1.default.cd(
|
|
22
|
-
await this.runNpmCommand(["run", "format", "--prefix",
|
|
19
|
+
async formatApp(projectPath) {
|
|
20
|
+
this.log("Formatting code...");
|
|
21
|
+
shelljs_1.default.cd(projectPath);
|
|
22
|
+
await this.runNpmCommand(["run", "format", "--prefix", projectPath]);
|
|
23
23
|
}
|
|
24
|
-
async startApp(
|
|
24
|
+
async startApp(projectPath) {
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
|
-
const
|
|
26
|
+
const isWindows = process.platform === "win32";
|
|
27
|
+
const command = isWindows ? "npm.cmd" : "npm";
|
|
27
28
|
const args = ["start"];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// args.push('--cwd')
|
|
34
|
-
// args.push(root)
|
|
35
|
-
// args.push('--verbose')
|
|
36
|
-
const child = (0, child_process_1.spawn)(command, args, { stdio: "inherit", cwd: root });
|
|
29
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
30
|
+
cwd: projectPath,
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
shell: isWindows,
|
|
33
|
+
});
|
|
37
34
|
child.on("close", (code) => {
|
|
38
35
|
if (code !== 0) {
|
|
39
36
|
reject({
|
|
@@ -43,15 +40,18 @@ class New extends base_command_1.default {
|
|
|
43
40
|
}
|
|
44
41
|
resolve();
|
|
45
42
|
});
|
|
43
|
+
child.on("error", (err) => {
|
|
44
|
+
this.log(`Error starting app: ${err.message}`);
|
|
45
|
+
reject(err);
|
|
46
|
+
});
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
async cloneTemplate(projectPath) {
|
|
49
|
-
this.log("
|
|
50
|
+
this.log("Cloning service template...");
|
|
50
51
|
if (!fs.existsSync(projectPath) && this.CURR_DIR !== projectPath) {
|
|
51
52
|
fs.mkdirSync(projectPath);
|
|
52
53
|
}
|
|
53
54
|
shelljs_1.default.cd(projectPath);
|
|
54
|
-
// Execute git clone and capture the result using HTTPS
|
|
55
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
56
|
// Check if the command failed
|
|
57
57
|
if (cloneResult.code !== 0) {
|
|
@@ -66,34 +66,31 @@ class New extends base_command_1.default {
|
|
|
66
66
|
shelljs_1.default.exec(`rm -rf ${projectPath}/.git`);
|
|
67
67
|
shelljs_1.default.cd(this.CURR_DIR);
|
|
68
68
|
}
|
|
69
|
-
async run() {
|
|
70
|
-
this.log("Initializing New Apso Server...");
|
|
71
|
-
const { projectName,
|
|
72
|
-
// apiType,
|
|
73
|
-
} = await this.validateFlags();
|
|
74
|
-
const CURR_DIR = process.cwd();
|
|
75
|
-
const projectPath = path.join(CURR_DIR, projectName);
|
|
76
|
-
await this.cloneTemplate(projectPath);
|
|
77
|
-
await this.installModules(projectPath);
|
|
78
|
-
await this.formatApp(projectPath);
|
|
79
|
-
}
|
|
80
69
|
async validateFlags() {
|
|
81
70
|
var _a;
|
|
82
71
|
const { flags } = await this.parse(New);
|
|
83
72
|
let projectName = flags.name;
|
|
84
73
|
let apiType = (_a = flags.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
85
|
-
if (
|
|
86
|
-
// this.error("`name` must be set (e.g. WoofyApp)");
|
|
74
|
+
if (!projectName) {
|
|
87
75
|
projectName = '';
|
|
88
76
|
}
|
|
89
|
-
if (
|
|
77
|
+
if (!apiType) {
|
|
90
78
|
apiType = "rest";
|
|
91
79
|
}
|
|
92
|
-
if (
|
|
93
|
-
this.error("`apiType` must be
|
|
80
|
+
if (!["rest", "graphql"].includes(apiType)) {
|
|
81
|
+
this.error("`apiType` must be 'rest' or 'graphql'");
|
|
94
82
|
}
|
|
95
83
|
return { projectName, apiType };
|
|
96
84
|
}
|
|
85
|
+
async run() {
|
|
86
|
+
this.log("Initializing New Apso Server...");
|
|
87
|
+
const { projectName } = await this.validateFlags();
|
|
88
|
+
const CURR_DIR = process.cwd();
|
|
89
|
+
const projectPath = path.join(CURR_DIR, projectName);
|
|
90
|
+
await this.cloneTemplate(projectPath);
|
|
91
|
+
await this.installModules(projectPath);
|
|
92
|
+
await this.formatApp(projectPath);
|
|
93
|
+
}
|
|
97
94
|
}
|
|
98
95
|
New.description = "Initialize your server project";
|
|
99
96
|
New.examples = [`$ apso server new --name TestProject`];
|
|
@@ -7,7 +7,13 @@ export default class Scaffold extends BaseCommand {
|
|
|
7
7
|
help: import("@oclif/core/lib/interfaces").BooleanFlag<void>;
|
|
8
8
|
};
|
|
9
9
|
static args: {};
|
|
10
|
-
scaffoldServer(
|
|
10
|
+
scaffoldServer(options: {
|
|
11
|
+
dir: string;
|
|
12
|
+
entity: Entity;
|
|
13
|
+
relationshipMap: RelationshipMap;
|
|
14
|
+
apiType: string;
|
|
15
|
+
allEntities: Entity[];
|
|
16
|
+
}): Promise<void>;
|
|
11
17
|
setupRestFiles(dir: string, entity: Entity, relationshipMap: RelationshipMap, apiType: string): Promise<void>;
|
|
12
18
|
setupGraphqlFiles(dir: string, entity: Entity, relationshipMap: RelationshipMap): Promise<void>;
|
|
13
19
|
run(): Promise<void>;
|
|
@@ -13,13 +13,20 @@ Eta.configure({
|
|
|
13
13
|
cache: false
|
|
14
14
|
});
|
|
15
15
|
class Scaffold extends base_command_1.default {
|
|
16
|
-
async scaffoldServer(
|
|
16
|
+
async scaffoldServer(options) {
|
|
17
|
+
const { dir, entity, relationshipMap, apiType, allEntities } = options;
|
|
17
18
|
const entityBuildStart = perf_hooks_1.performance.now();
|
|
18
19
|
this.log(`Building... ${entity.name}`);
|
|
19
20
|
const entityName = entity.name;
|
|
20
21
|
const filePath = path.join(dir, entityName);
|
|
21
22
|
const entityRelationships = relationshipMap[entity.name] || [];
|
|
22
|
-
await (0, lib_1.createEntity)(
|
|
23
|
+
await (0, lib_1.createEntity)({
|
|
24
|
+
apiBaseDir: filePath,
|
|
25
|
+
entity,
|
|
26
|
+
relationships: entityRelationships,
|
|
27
|
+
apiType,
|
|
28
|
+
allEntities
|
|
29
|
+
});
|
|
23
30
|
switch (apiType) {
|
|
24
31
|
case apsorc_parser_1.ApiType.Graphql:
|
|
25
32
|
await this.setupGraphqlFiles(dir, entity, relationshipMap);
|
|
@@ -71,7 +78,13 @@ class Scaffold extends base_command_1.default {
|
|
|
71
78
|
await (0, lib_1.createEnums)(autogenPath, entities, lowerCaseApiType);
|
|
72
79
|
await Promise.all(entities.map(async (entity) => {
|
|
73
80
|
const scaffoldModel = this.scaffoldServer.bind(this);
|
|
74
|
-
await scaffoldModel(
|
|
81
|
+
await scaffoldModel({
|
|
82
|
+
dir: autogenPath,
|
|
83
|
+
entity,
|
|
84
|
+
relationshipMap,
|
|
85
|
+
apiType: lowerCaseApiType,
|
|
86
|
+
allEntities: entities
|
|
87
|
+
});
|
|
75
88
|
if (process.env.DEBUG) {
|
|
76
89
|
const used = process.memoryUsage();
|
|
77
90
|
console.log(`[mem] heapUsed after ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
3
4
|
const core_1 = require("@oclif/core");
|
|
4
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const os_1 = tslib_1.__importDefault(require("os"));
|
|
5
7
|
class BaseCommand extends core_1.Command {
|
|
6
8
|
async runNpmCommand(args, silent = false) {
|
|
7
9
|
return new Promise((resolve, reject) => {
|
|
8
|
-
const
|
|
10
|
+
const isWindows = os_1.default.platform() === "win32";
|
|
11
|
+
const command = isWindows ? "npm.cmd" : "npm";
|
|
12
|
+
const stdio = silent ? "ignore" : "inherit";
|
|
9
13
|
const cmdStr = `${command} ${args.join(" ")}`;
|
|
10
|
-
this.log(cmdStr);
|
|
11
|
-
const child = (0, child_process_1.spawn)(command, args, {
|
|
14
|
+
this.log(`Running: ${cmdStr}`);
|
|
15
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
16
|
+
stdio,
|
|
17
|
+
shell: isWindows,
|
|
18
|
+
});
|
|
12
19
|
child.on("close", (code) => {
|
|
13
20
|
if (code !== 0) {
|
|
14
|
-
reject({
|
|
15
|
-
command: cmdStr,
|
|
16
|
-
});
|
|
21
|
+
reject({ command: cmdStr, exitCode: code });
|
|
17
22
|
return;
|
|
18
23
|
}
|
|
19
24
|
resolve();
|
|
20
25
|
});
|
|
26
|
+
child.on("error", () => {
|
|
27
|
+
this.error(`Failed to run: ${cmdStr}`);
|
|
28
|
+
});
|
|
21
29
|
});
|
|
22
30
|
}
|
|
23
31
|
}
|
package/dist/lib/entity.d.ts
CHANGED
|
@@ -2,10 +2,18 @@ import { Entity, Relationship } from "./types";
|
|
|
2
2
|
/**
|
|
3
3
|
* Generates a TypeORM entity file based on the entity definition and relationships.
|
|
4
4
|
*
|
|
5
|
-
* @param
|
|
6
|
-
* @param
|
|
7
|
-
* @param
|
|
8
|
-
* @param
|
|
5
|
+
* @param options Configuration options for entity generation
|
|
6
|
+
* @param options.apiBaseDir The base directory for the generated API module (e.g., 'src/autogen/users').
|
|
7
|
+
* @param options.entity The entity definition object from the parsed .apsorc.
|
|
8
|
+
* @param options.relationships An array of processed Relationship objects for this entity.
|
|
9
|
+
* @param options.apiType The type of API being generated (e.g., "rest", "graphql").
|
|
10
|
+
* @param options.allEntities Optional array of all entities, used to determine foreign key types.
|
|
9
11
|
* @returns {Promise<void>} A promise that resolves when the entity file is created.
|
|
10
12
|
*/
|
|
11
|
-
export declare const createEntity: (
|
|
13
|
+
export declare const createEntity: (options: {
|
|
14
|
+
apiBaseDir: string;
|
|
15
|
+
entity: Entity;
|
|
16
|
+
relationships: Relationship[];
|
|
17
|
+
apiType: string;
|
|
18
|
+
allEntities?: Entity[];
|
|
19
|
+
}) => Promise<void>;
|
package/dist/lib/entity.js
CHANGED
|
@@ -12,13 +12,16 @@ 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
|
*
|
|
15
|
-
* @param
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
18
|
-
* @param
|
|
15
|
+
* @param options Configuration options for entity generation
|
|
16
|
+
* @param options.apiBaseDir The base directory for the generated API module (e.g., 'src/autogen/users').
|
|
17
|
+
* @param options.entity The entity definition object from the parsed .apsorc.
|
|
18
|
+
* @param options.relationships An array of processed Relationship objects for this entity.
|
|
19
|
+
* @param options.apiType The type of API being generated (e.g., "rest", "graphql").
|
|
20
|
+
* @param options.allEntities Optional array of all entities, used to determine foreign key types.
|
|
19
21
|
* @returns {Promise<void>} A promise that resolves when the entity file is created.
|
|
20
22
|
*/
|
|
21
|
-
const createEntity = async (
|
|
23
|
+
const createEntity = async (options) => {
|
|
24
|
+
const { apiBaseDir, entity, relationships: relationshipsNew, apiType, allEntities } = options;
|
|
22
25
|
const { name, fields = [], indexes, uniques } = entity;
|
|
23
26
|
const File = path.join(apiBaseDir, `${name}.entity.ts`);
|
|
24
27
|
const columns = (0, field_1.getFieldForTemplate)(fields, name);
|
|
@@ -26,13 +29,15 @@ const createEntity = async (apiBaseDir, entity, relationshipsNew, apiType) => {
|
|
|
26
29
|
.length === 0;
|
|
27
30
|
const createdAt = entity.created_at;
|
|
28
31
|
const updatedAt = entity.updated_at;
|
|
29
|
-
const
|
|
32
|
+
const primaryKeyType = entity.primaryKeyType || 'serial'; // Default to 'serial' if not specified
|
|
33
|
+
const relationships = (0, relationships_1.getRelationshipForTemplate)(name, relationshipsNew, allEntities);
|
|
30
34
|
const entitiesToImport = (0, relationships_1.getRelationshipsForImport)(name, relationships);
|
|
31
35
|
const data = {
|
|
32
36
|
name,
|
|
33
37
|
indexes: indexes || [],
|
|
34
38
|
uniques: uniques || [],
|
|
35
39
|
createPrimaryKey,
|
|
40
|
+
primaryKeyType,
|
|
36
41
|
snakeCasedName: (0, casing_1.snakeCase)(name),
|
|
37
42
|
createdAt,
|
|
38
43
|
updatedAt,
|
|
@@ -52,8 +52,14 @@ const { CREATE, UPDATE } = CrudValidationGroups;
|
|
|
52
52
|
export class <%= it.name %> {
|
|
53
53
|
<% if (it.createPrimaryKey) { %>
|
|
54
54
|
// Fields
|
|
55
|
+
<% if (it.primaryKeyType === 'uuid') { %>
|
|
56
|
+
@Column({ type: 'uuid' })
|
|
57
|
+
@PrimaryColumn()
|
|
58
|
+
id: string;
|
|
59
|
+
<% } else { %>
|
|
55
60
|
@PrimaryGeneratedColumn()
|
|
56
61
|
id!: number;
|
|
62
|
+
<% } %>
|
|
57
63
|
<% } %>
|
|
58
64
|
|
|
59
65
|
<% if (it.createdAt) { %>
|
|
@@ -95,8 +101,13 @@ export class <%= it.name %> {
|
|
|
95
101
|
@JoinColumn({ name: '<%= assoc.camelCasedId %>' })
|
|
96
102
|
<%= assoc.relationshipName %>: <%= assoc.name %>;
|
|
97
103
|
|
|
104
|
+
<% if (assoc.referencedEntityPrimaryKeyType === 'uuid') { %>
|
|
105
|
+
@Column({ type: "uuid", <% if (assoc.nullable) { %> nullable: true <% } %>})
|
|
106
|
+
<%= assoc.camelCasedId %>: string;
|
|
107
|
+
<% } else { %>
|
|
98
108
|
@Column({ type: "integer", <% if (assoc.nullable) { %> nullable: true <% } %>})
|
|
99
109
|
<%= assoc.camelCasedId %>: number;
|
|
110
|
+
<% } %>
|
|
100
111
|
|
|
101
112
|
<% } else if (assoc.type === 'ManyToMany') { %>
|
|
102
113
|
|
|
@@ -36,6 +36,7 @@ export interface RelationshipForTemplate extends Relationship {
|
|
|
36
36
|
joinTable: boolean;
|
|
37
37
|
biDirectional: boolean;
|
|
38
38
|
inversePropertyName?: string;
|
|
39
|
+
referencedEntityPrimaryKeyType?: 'serial' | 'uuid';
|
|
39
40
|
}
|
|
40
41
|
export type RelationshipMap = {
|
|
41
42
|
[key: string]: Relationship[];
|
|
@@ -49,9 +49,10 @@ export declare const getRelationshipIdField: (relationship: Relationship) => str
|
|
|
49
49
|
* Calculates additional helper properties like pluralized names and inverse property names.
|
|
50
50
|
* @param entityName The name of the entity for which to generate template data.
|
|
51
51
|
* @param relationships An array of Relationship objects for the given entity.
|
|
52
|
+
* @param entities Optional array of all entities, used to determine referenced entity's primary key type.
|
|
52
53
|
* @returns An array of RelationshipForTemplate objects with extra properties for the template.
|
|
53
54
|
*/
|
|
54
|
-
export declare const getRelationshipForTemplate: (entityName: string, relationships: Relationship[]) => RelationshipForTemplate[];
|
|
55
|
+
export declare const getRelationshipForTemplate: (entityName: string, relationships: Relationship[], entities?: any[]) => RelationshipForTemplate[];
|
|
55
56
|
/**
|
|
56
57
|
* Generates all valid nested join paths for an entity, avoiding cycles and redundant paths.
|
|
57
58
|
*
|
|
@@ -331,12 +331,20 @@ const calculateInversePropertyName = (relationship, entityName, relationshipName
|
|
|
331
331
|
* Calculates additional helper properties like pluralized names and inverse property names.
|
|
332
332
|
* @param entityName The name of the entity for which to generate template data.
|
|
333
333
|
* @param relationships An array of Relationship objects for the given entity.
|
|
334
|
+
* @param entities Optional array of all entities, used to determine referenced entity's primary key type.
|
|
334
335
|
* @returns An array of RelationshipForTemplate objects with extra properties for the template.
|
|
335
336
|
*/
|
|
336
|
-
const getRelationshipForTemplate = (entityName, relationships) => {
|
|
337
|
+
const getRelationshipForTemplate = (entityName, relationships, entities) => {
|
|
337
338
|
if (!relationships) {
|
|
338
339
|
return [];
|
|
339
340
|
}
|
|
341
|
+
// Create entity map for looking up primary key types
|
|
342
|
+
const entityMap = new Map();
|
|
343
|
+
if (entities) {
|
|
344
|
+
entities.forEach(entity => {
|
|
345
|
+
entityMap.set(entity.name, entity);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
340
348
|
// First pass: Group by name and referenceName (more specific)
|
|
341
349
|
const processedRefs = new Map();
|
|
342
350
|
relationships.forEach((relationship) => {
|
|
@@ -369,6 +377,9 @@ const getRelationshipForTemplate = (entityName, relationships) => {
|
|
|
369
377
|
(relationship.type === 'ManyToMany' || relationship.type === 'OneToMany' || relationship.type === 'OneToOne')) {
|
|
370
378
|
console.info(`[APSO INFO] Using default property name '${inverseSidePropertyName}' for ${relationship.name}->${entityName}. To customize, add 'to_name' in your relationship definition.`);
|
|
371
379
|
}
|
|
380
|
+
// Look up the referenced entity's primary key type
|
|
381
|
+
const referencedEntity = entityMap.get(relationship.name);
|
|
382
|
+
const referencedEntityPrimaryKeyType = (referencedEntity === null || referencedEntity === void 0 ? void 0 : referencedEntity.primaryKeyType) || 'serial'; // Default to 'serial' if not specified
|
|
372
383
|
return {
|
|
373
384
|
...relationship,
|
|
374
385
|
relationshipName,
|
|
@@ -381,6 +392,7 @@ const getRelationshipForTemplate = (entityName, relationships) => {
|
|
|
381
392
|
biDirectional: relationship.biDirectional || false,
|
|
382
393
|
referenceName: thisReferenceName || undefined,
|
|
383
394
|
inverseSidePropertyName,
|
|
395
|
+
referencedEntityPrimaryKeyType,
|
|
384
396
|
};
|
|
385
397
|
});
|
|
386
398
|
};
|
|
@@ -227,3 +227,149 @@ const parse_1 = require("./parse");
|
|
|
227
227
|
});
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
|
+
(0, globals_1.describe)('getRelationshipForTemplate', () => {
|
|
231
|
+
// Mock console.warn to prevent test output clutter
|
|
232
|
+
(0, globals_1.beforeEach)(() => {
|
|
233
|
+
globals_1.jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
234
|
+
});
|
|
235
|
+
(0, globals_1.afterEach)(() => {
|
|
236
|
+
globals_1.jest.restoreAllMocks();
|
|
237
|
+
});
|
|
238
|
+
(0, globals_1.describe)('UUID Primary Key Support', () => {
|
|
239
|
+
(0, globals_1.it)('should populate referencedEntityPrimaryKeyType as uuid when referenced entity has uuid primary key', () => {
|
|
240
|
+
const entities = [
|
|
241
|
+
{ name: 'User', primaryKeyType: 'uuid' },
|
|
242
|
+
{ name: 'Account', primaryKeyType: 'serial' },
|
|
243
|
+
];
|
|
244
|
+
const relationships = [
|
|
245
|
+
{
|
|
246
|
+
name: 'User',
|
|
247
|
+
type: 'ManyToOne',
|
|
248
|
+
referenceName: null,
|
|
249
|
+
nullable: false,
|
|
250
|
+
index: false,
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
const result = (0, parse_1.getRelationshipForTemplate)('Account', relationships, entities);
|
|
254
|
+
(0, globals_1.expect)(result).toHaveLength(1);
|
|
255
|
+
(0, globals_1.expect)(result[0]).toMatchObject({
|
|
256
|
+
name: 'User',
|
|
257
|
+
type: 'ManyToOne',
|
|
258
|
+
referencedEntityPrimaryKeyType: 'uuid',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
(0, globals_1.it)('should populate referencedEntityPrimaryKeyType as serial when referenced entity has serial primary key', () => {
|
|
262
|
+
const entities = [
|
|
263
|
+
{ name: 'User', primaryKeyType: 'serial' },
|
|
264
|
+
{ name: 'Post', primaryKeyType: 'serial' },
|
|
265
|
+
];
|
|
266
|
+
const relationships = [
|
|
267
|
+
{
|
|
268
|
+
name: 'User',
|
|
269
|
+
type: 'ManyToOne',
|
|
270
|
+
referenceName: null,
|
|
271
|
+
nullable: false,
|
|
272
|
+
index: false,
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
const result = (0, parse_1.getRelationshipForTemplate)('Post', relationships, entities);
|
|
276
|
+
(0, globals_1.expect)(result).toHaveLength(1);
|
|
277
|
+
(0, globals_1.expect)(result[0]).toMatchObject({
|
|
278
|
+
name: 'User',
|
|
279
|
+
type: 'ManyToOne',
|
|
280
|
+
referencedEntityPrimaryKeyType: 'serial',
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
(0, globals_1.it)('should default referencedEntityPrimaryKeyType to serial when entity does not specify primaryKeyType', () => {
|
|
284
|
+
const entities = [
|
|
285
|
+
{ name: 'User' },
|
|
286
|
+
{ name: 'Post', primaryKeyType: 'serial' },
|
|
287
|
+
];
|
|
288
|
+
const relationships = [
|
|
289
|
+
{
|
|
290
|
+
name: 'User',
|
|
291
|
+
type: 'ManyToOne',
|
|
292
|
+
referenceName: null,
|
|
293
|
+
nullable: false,
|
|
294
|
+
index: false,
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
const result = (0, parse_1.getRelationshipForTemplate)('Post', relationships, entities);
|
|
298
|
+
(0, globals_1.expect)(result).toHaveLength(1);
|
|
299
|
+
(0, globals_1.expect)(result[0]).toMatchObject({
|
|
300
|
+
name: 'User',
|
|
301
|
+
type: 'ManyToOne',
|
|
302
|
+
referencedEntityPrimaryKeyType: 'serial',
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
(0, globals_1.it)('should default referencedEntityPrimaryKeyType to serial when entities array is not provided', () => {
|
|
306
|
+
const relationships = [
|
|
307
|
+
{
|
|
308
|
+
name: 'User',
|
|
309
|
+
type: 'ManyToOne',
|
|
310
|
+
referenceName: null,
|
|
311
|
+
nullable: false,
|
|
312
|
+
index: false,
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
const result = (0, parse_1.getRelationshipForTemplate)('Post', relationships);
|
|
316
|
+
(0, globals_1.expect)(result).toHaveLength(1);
|
|
317
|
+
(0, globals_1.expect)(result[0]).toMatchObject({
|
|
318
|
+
name: 'User',
|
|
319
|
+
type: 'ManyToOne',
|
|
320
|
+
referencedEntityPrimaryKeyType: 'serial',
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
(0, globals_1.it)('should handle multiple relationships with mixed primary key types', () => {
|
|
324
|
+
const entities = [
|
|
325
|
+
{ name: 'User', primaryKeyType: 'uuid' },
|
|
326
|
+
{ name: 'Organization', primaryKeyType: 'serial' },
|
|
327
|
+
{ name: 'Team', primaryKeyType: 'uuid' },
|
|
328
|
+
{ name: 'Post', primaryKeyType: 'serial' },
|
|
329
|
+
];
|
|
330
|
+
const relationships = [
|
|
331
|
+
{
|
|
332
|
+
name: 'User',
|
|
333
|
+
type: 'ManyToOne',
|
|
334
|
+
referenceName: null,
|
|
335
|
+
nullable: false,
|
|
336
|
+
index: true,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: 'Organization',
|
|
340
|
+
type: 'ManyToOne',
|
|
341
|
+
referenceName: null,
|
|
342
|
+
nullable: false,
|
|
343
|
+
index: false,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'Team',
|
|
347
|
+
type: 'ManyToOne',
|
|
348
|
+
referenceName: null,
|
|
349
|
+
nullable: true,
|
|
350
|
+
index: false,
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
const result = (0, parse_1.getRelationshipForTemplate)('Post', relationships, entities);
|
|
354
|
+
(0, globals_1.expect)(result).toHaveLength(3);
|
|
355
|
+
const userRel = result.find(r => r.name === 'User');
|
|
356
|
+
(0, globals_1.expect)(userRel).toMatchObject({
|
|
357
|
+
name: 'User',
|
|
358
|
+
type: 'ManyToOne',
|
|
359
|
+
referencedEntityPrimaryKeyType: 'uuid',
|
|
360
|
+
});
|
|
361
|
+
const orgRel = result.find(r => r.name === 'Organization');
|
|
362
|
+
(0, globals_1.expect)(orgRel).toMatchObject({
|
|
363
|
+
name: 'Organization',
|
|
364
|
+
type: 'ManyToOne',
|
|
365
|
+
referencedEntityPrimaryKeyType: 'serial',
|
|
366
|
+
});
|
|
367
|
+
const teamRel = result.find(r => r.name === 'Team');
|
|
368
|
+
(0, globals_1.expect)(teamRel).toMatchObject({
|
|
369
|
+
name: 'Team',
|
|
370
|
+
type: 'ManyToOne',
|
|
371
|
+
referencedEntityPrimaryKeyType: 'uuid',
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const Eta = tslib_1.__importStar(require("eta"));
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
6
|
+
const globals_1 = require("@jest/globals");
|
|
7
|
+
(0, globals_1.describe)('UUID Entity Template Tests', () => {
|
|
8
|
+
(0, globals_1.beforeAll)(() => {
|
|
9
|
+
Eta.configure({
|
|
10
|
+
views: path.join(__dirname, '../../../lib/templates'),
|
|
11
|
+
cache: false
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
(0, globals_1.describe)('UUID Primary Key', () => {
|
|
15
|
+
(0, globals_1.it)('should render entity with UUID primary key', async () => {
|
|
16
|
+
const data = {
|
|
17
|
+
name: 'User',
|
|
18
|
+
snakeCasedName: 'user',
|
|
19
|
+
createPrimaryKey: true,
|
|
20
|
+
primaryKeyType: 'uuid',
|
|
21
|
+
createdAt: true,
|
|
22
|
+
updatedAt: true,
|
|
23
|
+
columns: [],
|
|
24
|
+
associations: [],
|
|
25
|
+
indexes: [],
|
|
26
|
+
uniques: [],
|
|
27
|
+
entitiesToImport: [],
|
|
28
|
+
apiType: 'rest',
|
|
29
|
+
importEnums: false
|
|
30
|
+
};
|
|
31
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
32
|
+
// Should use @Column with uuid type
|
|
33
|
+
(0, globals_1.expect)(rendered).toContain("@Column({ type: 'uuid' })");
|
|
34
|
+
// Should use @PrimaryColumn instead of @PrimaryGeneratedColumn
|
|
35
|
+
(0, globals_1.expect)(rendered).toContain('@PrimaryColumn()');
|
|
36
|
+
(0, globals_1.expect)(rendered).not.toContain('@PrimaryGeneratedColumn()');
|
|
37
|
+
// Should declare id as string
|
|
38
|
+
(0, globals_1.expect)(rendered).toContain('id: string;');
|
|
39
|
+
(0, globals_1.expect)(rendered).not.toContain('id!: number;');
|
|
40
|
+
});
|
|
41
|
+
(0, globals_1.it)('should render entity with serial (auto-increment) primary key', async () => {
|
|
42
|
+
const data = {
|
|
43
|
+
name: 'Post',
|
|
44
|
+
snakeCasedName: 'post',
|
|
45
|
+
createPrimaryKey: true,
|
|
46
|
+
primaryKeyType: 'serial',
|
|
47
|
+
createdAt: true,
|
|
48
|
+
updatedAt: true,
|
|
49
|
+
columns: [],
|
|
50
|
+
associations: [],
|
|
51
|
+
indexes: [],
|
|
52
|
+
uniques: [],
|
|
53
|
+
entitiesToImport: [],
|
|
54
|
+
apiType: 'rest',
|
|
55
|
+
importEnums: false
|
|
56
|
+
};
|
|
57
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
58
|
+
// Should use @PrimaryGeneratedColumn
|
|
59
|
+
(0, globals_1.expect)(rendered).toContain('@PrimaryGeneratedColumn()');
|
|
60
|
+
(0, globals_1.expect)(rendered).not.toContain("@Column({ type: 'uuid' })");
|
|
61
|
+
(0, globals_1.expect)(rendered).not.toContain('@PrimaryColumn()');
|
|
62
|
+
// Should declare id as number
|
|
63
|
+
(0, globals_1.expect)(rendered).toContain('id!: number;');
|
|
64
|
+
(0, globals_1.expect)(rendered).not.toContain('id: string;');
|
|
65
|
+
});
|
|
66
|
+
(0, globals_1.it)('should default to serial primary key when primaryKeyType is not specified', async () => {
|
|
67
|
+
const data = {
|
|
68
|
+
name: 'Comment',
|
|
69
|
+
snakeCasedName: 'comment',
|
|
70
|
+
createPrimaryKey: true,
|
|
71
|
+
// primaryKeyType not specified
|
|
72
|
+
createdAt: true,
|
|
73
|
+
updatedAt: true,
|
|
74
|
+
columns: [],
|
|
75
|
+
associations: [],
|
|
76
|
+
indexes: [],
|
|
77
|
+
uniques: [],
|
|
78
|
+
entitiesToImport: [],
|
|
79
|
+
apiType: 'rest',
|
|
80
|
+
importEnums: false
|
|
81
|
+
};
|
|
82
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
83
|
+
// Should default to serial (auto-increment)
|
|
84
|
+
(0, globals_1.expect)(rendered).toContain('@PrimaryGeneratedColumn()');
|
|
85
|
+
(0, globals_1.expect)(rendered).toContain('id!: number;');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
(0, globals_1.describe)('UUID Foreign Keys', () => {
|
|
89
|
+
(0, globals_1.it)('should render ManyToOne relationship with UUID foreign key when referenced entity has UUID primary key', async () => {
|
|
90
|
+
const data = {
|
|
91
|
+
name: 'Account',
|
|
92
|
+
snakeCasedName: 'account',
|
|
93
|
+
createPrimaryKey: true,
|
|
94
|
+
primaryKeyType: 'serial',
|
|
95
|
+
createdAt: true,
|
|
96
|
+
updatedAt: true,
|
|
97
|
+
columns: [],
|
|
98
|
+
associations: [
|
|
99
|
+
{
|
|
100
|
+
name: 'User',
|
|
101
|
+
type: 'ManyToOne',
|
|
102
|
+
relationshipName: 'user',
|
|
103
|
+
pluralizedRelationshipName: 'users',
|
|
104
|
+
pluralizedName: 'users',
|
|
105
|
+
camelCasedName: 'user',
|
|
106
|
+
camelCasedId: 'userId',
|
|
107
|
+
entityName: 'account',
|
|
108
|
+
joinTable: false,
|
|
109
|
+
biDirectional: true,
|
|
110
|
+
inverseSidePropertyName: 'accounts',
|
|
111
|
+
referencedEntityPrimaryKeyType: 'uuid',
|
|
112
|
+
nullable: false,
|
|
113
|
+
index: false
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
indexes: [],
|
|
117
|
+
uniques: [],
|
|
118
|
+
entitiesToImport: ['User'],
|
|
119
|
+
apiType: 'rest',
|
|
120
|
+
importEnums: false
|
|
121
|
+
};
|
|
122
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
123
|
+
// Should have ManyToOne decorator
|
|
124
|
+
(0, globals_1.expect)(rendered).toContain('@ManyToOne(() => User');
|
|
125
|
+
// Should have JoinColumn
|
|
126
|
+
(0, globals_1.expect)(rendered).toContain("@JoinColumn({ name: 'userId' })");
|
|
127
|
+
// Should declare relationship property
|
|
128
|
+
(0, globals_1.expect)(rendered).toContain('user: User;');
|
|
129
|
+
// Should have UUID foreign key column
|
|
130
|
+
(0, globals_1.expect)(rendered).toContain('@Column({ type: "uuid"');
|
|
131
|
+
(0, globals_1.expect)(rendered).toContain('userId: string;');
|
|
132
|
+
// Should NOT have integer foreign key
|
|
133
|
+
(0, globals_1.expect)(rendered).not.toContain('userId: number;');
|
|
134
|
+
});
|
|
135
|
+
(0, globals_1.it)('should render ManyToOne relationship with integer foreign key when referenced entity has serial primary key', async () => {
|
|
136
|
+
const data = {
|
|
137
|
+
name: 'Post',
|
|
138
|
+
snakeCasedName: 'post',
|
|
139
|
+
createPrimaryKey: true,
|
|
140
|
+
primaryKeyType: 'serial',
|
|
141
|
+
createdAt: true,
|
|
142
|
+
updatedAt: true,
|
|
143
|
+
columns: [],
|
|
144
|
+
associations: [
|
|
145
|
+
{
|
|
146
|
+
name: 'User',
|
|
147
|
+
type: 'ManyToOne',
|
|
148
|
+
relationshipName: 'user',
|
|
149
|
+
pluralizedRelationshipName: 'users',
|
|
150
|
+
pluralizedName: 'users',
|
|
151
|
+
camelCasedName: 'user',
|
|
152
|
+
camelCasedId: 'userId',
|
|
153
|
+
entityName: 'post',
|
|
154
|
+
joinTable: false,
|
|
155
|
+
biDirectional: true,
|
|
156
|
+
inverseSidePropertyName: 'posts',
|
|
157
|
+
referencedEntityPrimaryKeyType: 'serial',
|
|
158
|
+
nullable: false,
|
|
159
|
+
index: true
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
indexes: [],
|
|
163
|
+
uniques: [],
|
|
164
|
+
entitiesToImport: ['User'],
|
|
165
|
+
apiType: 'rest',
|
|
166
|
+
importEnums: false
|
|
167
|
+
};
|
|
168
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
169
|
+
// Should have ManyToOne decorator
|
|
170
|
+
(0, globals_1.expect)(rendered).toContain('@ManyToOne(() => User');
|
|
171
|
+
// Should have index since index: true
|
|
172
|
+
(0, globals_1.expect)(rendered).toContain('@Index()');
|
|
173
|
+
// Should have integer foreign key column
|
|
174
|
+
(0, globals_1.expect)(rendered).toContain('@Column({ type: "integer"');
|
|
175
|
+
(0, globals_1.expect)(rendered).toContain('userId: number;');
|
|
176
|
+
// Should NOT have UUID foreign key
|
|
177
|
+
(0, globals_1.expect)(rendered).not.toContain('userId: string;');
|
|
178
|
+
});
|
|
179
|
+
(0, globals_1.it)('should handle nullable UUID foreign keys', async () => {
|
|
180
|
+
const data = {
|
|
181
|
+
name: 'Profile',
|
|
182
|
+
snakeCasedName: 'profile',
|
|
183
|
+
createPrimaryKey: true,
|
|
184
|
+
primaryKeyType: 'uuid',
|
|
185
|
+
createdAt: true,
|
|
186
|
+
updatedAt: true,
|
|
187
|
+
columns: [],
|
|
188
|
+
associations: [
|
|
189
|
+
{
|
|
190
|
+
name: 'Organization',
|
|
191
|
+
type: 'ManyToOne',
|
|
192
|
+
relationshipName: 'organization',
|
|
193
|
+
pluralizedRelationshipName: 'organizations',
|
|
194
|
+
pluralizedName: 'organizations',
|
|
195
|
+
camelCasedName: 'organization',
|
|
196
|
+
camelCasedId: 'organizationId',
|
|
197
|
+
entityName: 'profile',
|
|
198
|
+
joinTable: false,
|
|
199
|
+
biDirectional: false,
|
|
200
|
+
referencedEntityPrimaryKeyType: 'uuid',
|
|
201
|
+
nullable: true,
|
|
202
|
+
index: false
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
indexes: [],
|
|
206
|
+
uniques: [],
|
|
207
|
+
entitiesToImport: ['Organization'],
|
|
208
|
+
apiType: 'rest',
|
|
209
|
+
importEnums: false
|
|
210
|
+
};
|
|
211
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
212
|
+
// Should have nullable UUID foreign key
|
|
213
|
+
(0, globals_1.expect)(rendered).toContain('@Column({ type: "uuid", nullable: true');
|
|
214
|
+
(0, globals_1.expect)(rendered).toContain('organizationId: string;');
|
|
215
|
+
});
|
|
216
|
+
(0, globals_1.it)('should handle multiple relationships with mixed UUID and serial foreign keys', async () => {
|
|
217
|
+
const data = {
|
|
218
|
+
name: 'Comment',
|
|
219
|
+
snakeCasedName: 'comment',
|
|
220
|
+
createPrimaryKey: true,
|
|
221
|
+
primaryKeyType: 'serial',
|
|
222
|
+
createdAt: true,
|
|
223
|
+
updatedAt: true,
|
|
224
|
+
columns: [],
|
|
225
|
+
associations: [
|
|
226
|
+
{
|
|
227
|
+
name: 'User',
|
|
228
|
+
type: 'ManyToOne',
|
|
229
|
+
relationshipName: 'user',
|
|
230
|
+
pluralizedRelationshipName: 'users',
|
|
231
|
+
pluralizedName: 'users',
|
|
232
|
+
camelCasedName: 'user',
|
|
233
|
+
camelCasedId: 'userId',
|
|
234
|
+
entityName: 'comment',
|
|
235
|
+
joinTable: false,
|
|
236
|
+
biDirectional: true,
|
|
237
|
+
inverseSidePropertyName: 'comments',
|
|
238
|
+
referencedEntityPrimaryKeyType: 'uuid',
|
|
239
|
+
nullable: false,
|
|
240
|
+
index: true
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'Post',
|
|
244
|
+
type: 'ManyToOne',
|
|
245
|
+
relationshipName: 'post',
|
|
246
|
+
pluralizedRelationshipName: 'posts',
|
|
247
|
+
pluralizedName: 'posts',
|
|
248
|
+
camelCasedName: 'post',
|
|
249
|
+
camelCasedId: 'postId',
|
|
250
|
+
entityName: 'comment',
|
|
251
|
+
joinTable: false,
|
|
252
|
+
biDirectional: true,
|
|
253
|
+
inverseSidePropertyName: 'comments',
|
|
254
|
+
referencedEntityPrimaryKeyType: 'serial',
|
|
255
|
+
nullable: false,
|
|
256
|
+
index: true
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
indexes: [],
|
|
260
|
+
uniques: [],
|
|
261
|
+
entitiesToImport: ['User', 'Post'],
|
|
262
|
+
apiType: 'rest',
|
|
263
|
+
importEnums: false
|
|
264
|
+
};
|
|
265
|
+
const rendered = await Eta.renderFileAsync('./entities/entity', data);
|
|
266
|
+
// Should have UUID foreign key for User
|
|
267
|
+
(0, globals_1.expect)(rendered).toContain('@Column({ type: "uuid"');
|
|
268
|
+
(0, globals_1.expect)(rendered).toContain('userId: string;');
|
|
269
|
+
// Should have integer foreign key for Post
|
|
270
|
+
(0, globals_1.expect)(rendered).toContain('@Column({ type: "integer"');
|
|
271
|
+
(0, globals_1.expect)(rendered).toContain('postId: number;');
|
|
272
|
+
// Should have both relationships
|
|
273
|
+
(0, globals_1.expect)(rendered).toContain('user: User;');
|
|
274
|
+
(0, globals_1.expect)(rendered).toContain('post: Post;');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
package/oclif.manifest.json
CHANGED