@rpal/cli 1.0.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/LICENSE +21 -0
- package/dist/cli/src/index.js +55 -0
- package/dist/core/src/container.js +125 -0
- package/dist/core/src/decorators.js +59 -0
- package/dist/core/src/index.js +14 -0
- package/dist/core/src/interface.js +3 -0
- package/dist/core/src/scope.js +6 -0
- package/dist/core/src/test-container.js +52 -0
- package/dist/core/src/token.js +6 -0
- package/dist/index.js +468 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 paljs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Pal } from '@paljs/core';
|
|
3
|
+
console.log(`
|
|
4
|
+
╔═══════════════════════════════════════════╗
|
|
5
|
+
║ ║
|
|
6
|
+
║ ██████╗ ███████╗██╗ ██╗███████╗ ║
|
|
7
|
+
║ ██╔══██╗██╔════╝██║ ██║██╔════╝ ║
|
|
8
|
+
║ ██║ ██║█████╗ ██║ ██║█████╗ ║
|
|
9
|
+
║ ██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══╝ ║
|
|
10
|
+
║ ██████╔╝███████╗ ╚████╔╝ ███████╗ ║
|
|
11
|
+
║ ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝ ║
|
|
12
|
+
║ ║
|
|
13
|
+
║ CLI Tool v${require('../package.json').version} ║
|
|
14
|
+
║ ║
|
|
15
|
+
╚═══════════════════════════════════════════╝
|
|
16
|
+
`);
|
|
17
|
+
const commands = process.argv.slice(2);
|
|
18
|
+
if (commands.length === 0) {
|
|
19
|
+
console.log(`
|
|
20
|
+
Usage: pal <command>
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
init Initialize a new Pal project
|
|
24
|
+
generate Generate code from templates
|
|
25
|
+
serve Start development server
|
|
26
|
+
build Build for production
|
|
27
|
+
test Run tests
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
pal init my-project
|
|
31
|
+
pal generate service User
|
|
32
|
+
pal serve
|
|
33
|
+
`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const [command, ...args] = commands;
|
|
37
|
+
switch (command) {
|
|
38
|
+
case 'init':
|
|
39
|
+
console.log('📦 Initializing Pal project...');
|
|
40
|
+
console.log(' This would scaffold a new project');
|
|
41
|
+
break;
|
|
42
|
+
case 'generate':
|
|
43
|
+
console.log('🔧 Generating code...');
|
|
44
|
+
console.log(` Template: ${args[0] || 'unknown'}`);
|
|
45
|
+
console.log(` Name: ${args[1] || 'unnamed'}`);
|
|
46
|
+
break;
|
|
47
|
+
case 'serve':
|
|
48
|
+
console.log('🚀 Starting development server...');
|
|
49
|
+
const container = Pal.create();
|
|
50
|
+
console.log(' Container created:', container.constructor.name);
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
console.log(`❌ Unknown command: ${command}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { Scope, DEFAULT_SCOPE } from './scope';
|
|
3
|
+
import { InjectionToken } from './token';
|
|
4
|
+
import { getInjectableMetadata, getConstructorParams } from './decorators';
|
|
5
|
+
export class PalCircularDependencyError extends Error {
|
|
6
|
+
constructor(path) {
|
|
7
|
+
super(`Circular dependency detected: ${path}`);
|
|
8
|
+
this.name = 'PalCircularDependencyError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class PalDependencyNotFoundError extends Error {
|
|
12
|
+
constructor(token, index, target) {
|
|
13
|
+
super(`Cannot resolve dependency "${getTokenName(token)}" at index ${index} in ${target.name || 'Unknown'} constructor`);
|
|
14
|
+
this.name = 'PalDependencyNotFoundError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function getTokenName(token) {
|
|
18
|
+
if (token instanceof InjectionToken)
|
|
19
|
+
return token.name;
|
|
20
|
+
if (typeof token === 'string')
|
|
21
|
+
return token;
|
|
22
|
+
if (typeof token === 'symbol')
|
|
23
|
+
return String(token);
|
|
24
|
+
return token.name || 'Unknown';
|
|
25
|
+
}
|
|
26
|
+
export class PalContainer {
|
|
27
|
+
constructor(parent) {
|
|
28
|
+
this.registrations = new Map();
|
|
29
|
+
this.resolutionStack = [];
|
|
30
|
+
this.singletonCache = new Map();
|
|
31
|
+
this.parent = parent;
|
|
32
|
+
}
|
|
33
|
+
provide(token, provider) {
|
|
34
|
+
const reg = this.createRegistration(token, provider);
|
|
35
|
+
this.registrations.set(token, reg);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
createRegistration(token, provider) {
|
|
39
|
+
const scope = provider.scope ?? DEFAULT_SCOPE;
|
|
40
|
+
if ('useValue' in provider) {
|
|
41
|
+
return { token, factory: () => provider.useValue, scope: Scope.Singleton, instance: provider.useValue };
|
|
42
|
+
}
|
|
43
|
+
if ('useClass' in provider) {
|
|
44
|
+
return { token, factory: (ctx) => this.resolveClass(provider.useClass, ctx), scope };
|
|
45
|
+
}
|
|
46
|
+
if ('useFactory' in provider) {
|
|
47
|
+
return { token, factory: (ctx) => provider.useFactory(ctx), scope };
|
|
48
|
+
}
|
|
49
|
+
throw new Error('Provider must have useValue, useClass, or useFactory');
|
|
50
|
+
}
|
|
51
|
+
get(token) {
|
|
52
|
+
if (this.resolutionStack.includes(token)) {
|
|
53
|
+
const path = [...this.resolutionStack, token].map(getTokenName).join(' -> ');
|
|
54
|
+
throw new PalCircularDependencyError(path);
|
|
55
|
+
}
|
|
56
|
+
const registration = this.findRegistration(token);
|
|
57
|
+
if (!registration) {
|
|
58
|
+
if (token instanceof Function && getInjectableMetadata(token)) {
|
|
59
|
+
return this.resolveClass(token, this);
|
|
60
|
+
}
|
|
61
|
+
throw new PalDependencyNotFoundError(token, 0, token);
|
|
62
|
+
}
|
|
63
|
+
return this.resolve(registration);
|
|
64
|
+
}
|
|
65
|
+
has(token) {
|
|
66
|
+
return this.findRegistration(token) !== undefined;
|
|
67
|
+
}
|
|
68
|
+
createChild() {
|
|
69
|
+
return new PalContainer(this);
|
|
70
|
+
}
|
|
71
|
+
override(token, provider) {
|
|
72
|
+
this.registrations.set(token, this.createRegistration(token, provider));
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
reset() {
|
|
76
|
+
this.registrations.clear();
|
|
77
|
+
this.singletonCache.clear();
|
|
78
|
+
}
|
|
79
|
+
findRegistration(token) {
|
|
80
|
+
return this.registrations.get(token) ?? this.parent?.findRegistration(token);
|
|
81
|
+
}
|
|
82
|
+
resolve(registration) {
|
|
83
|
+
if (registration.scope === Scope.Singleton && registration.instance !== undefined) {
|
|
84
|
+
return registration.instance;
|
|
85
|
+
}
|
|
86
|
+
if (registration.scope === Scope.Singleton) {
|
|
87
|
+
const instance = registration.factory(this);
|
|
88
|
+
registration.instance = instance;
|
|
89
|
+
return instance;
|
|
90
|
+
}
|
|
91
|
+
return registration.factory(this);
|
|
92
|
+
}
|
|
93
|
+
resolveClass(klass, ctx) {
|
|
94
|
+
this.resolutionStack.push(klass);
|
|
95
|
+
try {
|
|
96
|
+
const paramMeta = getConstructorParams(klass);
|
|
97
|
+
const args = paramMeta.map((param) => {
|
|
98
|
+
if (param.token && ctx.has(param.token)) {
|
|
99
|
+
return param.optional && !ctx.has(param.token) ? undefined : ctx.get(param.token);
|
|
100
|
+
}
|
|
101
|
+
if (ctx.has(param.type)) {
|
|
102
|
+
return ctx.get(param.type);
|
|
103
|
+
}
|
|
104
|
+
if (param.type === Object)
|
|
105
|
+
return undefined;
|
|
106
|
+
try {
|
|
107
|
+
return this.resolveClass(param.type, ctx);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return new klass(...args);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
this.resolutionStack.pop();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function createPal(parent) {
|
|
121
|
+
return new PalContainer(parent);
|
|
122
|
+
}
|
|
123
|
+
export const Pal = {
|
|
124
|
+
create: createPal,
|
|
125
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { DEFAULT_SCOPE } from './scope';
|
|
3
|
+
import { INJECTABLE_METADATA_KEY, INJECTABLE_CONSTRUCTOR_KEY } from './interface';
|
|
4
|
+
const paramsCache = new WeakMap();
|
|
5
|
+
export function Injectable(options) {
|
|
6
|
+
return (target) => {
|
|
7
|
+
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, {
|
|
8
|
+
scope: options?.scope ?? DEFAULT_SCOPE,
|
|
9
|
+
token: options?.token,
|
|
10
|
+
}, target);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function Inject(token, type, opts = {}) {
|
|
14
|
+
return (target, _key, index) => {
|
|
15
|
+
const inferredType = type || getParamType(target, index);
|
|
16
|
+
if (!inferredType) {
|
|
17
|
+
console.warn(`@Inject: Could not determine type for parameter ${index}. Provide it explicitly.`);
|
|
18
|
+
}
|
|
19
|
+
storeParamMeta(target, index, { type: inferredType, token, optional: opts.optional });
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function Optional() {
|
|
23
|
+
return (target, _key, index) => {
|
|
24
|
+
const type = getParamType(target, index);
|
|
25
|
+
const existing = getParamMeta(target, index);
|
|
26
|
+
storeParamMeta(target, index, { type, optional: true, token: existing?.token });
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function getParamType(target, index) {
|
|
30
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
|
|
31
|
+
return paramTypes[index];
|
|
32
|
+
}
|
|
33
|
+
function getParamMeta(target, index) {
|
|
34
|
+
const params = getConstructorParams(target);
|
|
35
|
+
return params[index];
|
|
36
|
+
}
|
|
37
|
+
function storeParamMeta(target, index, meta) {
|
|
38
|
+
const params = Reflect.getOwnMetadata(INJECTABLE_CONSTRUCTOR_KEY, target) || [];
|
|
39
|
+
params[index] = meta;
|
|
40
|
+
Reflect.defineMetadata(INJECTABLE_CONSTRUCTOR_KEY, params, target);
|
|
41
|
+
}
|
|
42
|
+
export function getInjectableMetadata(target) {
|
|
43
|
+
return Reflect.getOwnMetadata(INJECTABLE_METADATA_KEY, target);
|
|
44
|
+
}
|
|
45
|
+
export function getConstructorParams(target) {
|
|
46
|
+
const cached = paramsCache.get(target);
|
|
47
|
+
if (cached)
|
|
48
|
+
return cached;
|
|
49
|
+
const params = Reflect.getOwnMetadata(INJECTABLE_CONSTRUCTOR_KEY, target) || [];
|
|
50
|
+
if (!params.length) {
|
|
51
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
|
|
52
|
+
return paramTypes.map((type, index) => ({ type }));
|
|
53
|
+
}
|
|
54
|
+
paramsCache.set(target, params);
|
|
55
|
+
return params;
|
|
56
|
+
}
|
|
57
|
+
export function getInjectionMetadata(target) {
|
|
58
|
+
return getConstructorParams(target).map(p => ({ token: p.token || p.type, optional: p.optional }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './scope';
|
|
2
|
+
export * from './token';
|
|
3
|
+
export * from './decorators';
|
|
4
|
+
export * from './interface';
|
|
5
|
+
export * from './container';
|
|
6
|
+
export * from './test-container';
|
|
7
|
+
import { Pal, PalContainer, createPal, PalCircularDependencyError, PalDependencyNotFoundError } from './container';
|
|
8
|
+
import { PalTestContainer } from './test-container';
|
|
9
|
+
export { Pal as default };
|
|
10
|
+
export const Container = PalContainer;
|
|
11
|
+
export const TestContainer = PalTestContainer;
|
|
12
|
+
export const createContainer = createPal;
|
|
13
|
+
export const CircularDependencyError = PalCircularDependencyError;
|
|
14
|
+
export const DependencyNotFoundError = PalDependencyNotFoundError;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { PalContainer } from './container';
|
|
2
|
+
export class PalTestContainer extends PalContainer {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments);
|
|
5
|
+
this.snapshots = [];
|
|
6
|
+
}
|
|
7
|
+
static create() {
|
|
8
|
+
return new PalTestContainer();
|
|
9
|
+
}
|
|
10
|
+
snapshot() {
|
|
11
|
+
this.snapshots.push({
|
|
12
|
+
registrations: new Map(this.getRegistrations()),
|
|
13
|
+
singletons: new Map(this.getSingletons()),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
restore() {
|
|
17
|
+
const snap = this.snapshots.pop();
|
|
18
|
+
if (!snap)
|
|
19
|
+
return;
|
|
20
|
+
this.restoreFrom(snap);
|
|
21
|
+
}
|
|
22
|
+
reset() {
|
|
23
|
+
super.reset();
|
|
24
|
+
this.snapshots = [];
|
|
25
|
+
}
|
|
26
|
+
clearSnapshots() {
|
|
27
|
+
this.snapshots = [];
|
|
28
|
+
}
|
|
29
|
+
mock(token, mockValue) {
|
|
30
|
+
const mock = new Proxy({}, {
|
|
31
|
+
get(_, prop) {
|
|
32
|
+
if (prop === 'then' || prop === 'catch')
|
|
33
|
+
return undefined;
|
|
34
|
+
if (typeof prop === 'symbol')
|
|
35
|
+
return undefined;
|
|
36
|
+
return mockValue?.[prop] ?? (() => mock);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
this.override(token, { useValue: mock });
|
|
40
|
+
return mock;
|
|
41
|
+
}
|
|
42
|
+
getRegistrations() {
|
|
43
|
+
return this.registrations;
|
|
44
|
+
}
|
|
45
|
+
getSingletons() {
|
|
46
|
+
return this.singletonCache;
|
|
47
|
+
}
|
|
48
|
+
restoreFrom(snap) {
|
|
49
|
+
this.registrations = snap.registrations;
|
|
50
|
+
this.singletonCache = snap.singletons;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
const VERSION = '1.0.0';
|
|
5
|
+
console.log(`
|
|
6
|
+
╔═══════════════════════════════════════════╗
|
|
7
|
+
║ ║
|
|
8
|
+
║ ██████╗ ███████╗██╗ ██╗███████╗ ║
|
|
9
|
+
║ ██╔══██╗██╔════╝██║ ██║██╔════╝ ║
|
|
10
|
+
║ ██║ ██║█████╗ ██║ ██║█████╗ ║
|
|
11
|
+
║ ██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══╝ ║
|
|
12
|
+
║ ██████╔╝███████╗ ╚████╔╝ ███████╗ ║
|
|
13
|
+
║ ╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝ ║
|
|
14
|
+
║ ║
|
|
15
|
+
║ CLI Tool v${VERSION} ║
|
|
16
|
+
║ ║
|
|
17
|
+
╚═══════════════════════════════════════════╝
|
|
18
|
+
`);
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const command = args[0];
|
|
21
|
+
if (!command || command === 'help') {
|
|
22
|
+
showHelp();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
switch (command) {
|
|
26
|
+
case 'make:model':
|
|
27
|
+
makeModel(args.slice(1));
|
|
28
|
+
break;
|
|
29
|
+
case 'make:repository':
|
|
30
|
+
makeRepository(args.slice(1));
|
|
31
|
+
break;
|
|
32
|
+
case 'make:service':
|
|
33
|
+
makeService(args.slice(1));
|
|
34
|
+
break;
|
|
35
|
+
case 'make:migration':
|
|
36
|
+
makeMigration(args.slice(1));
|
|
37
|
+
break;
|
|
38
|
+
case 'make:controller':
|
|
39
|
+
makeController(args.slice(1));
|
|
40
|
+
break;
|
|
41
|
+
case 'make:factory':
|
|
42
|
+
makeFactory(args.slice(1));
|
|
43
|
+
break;
|
|
44
|
+
case 'init':
|
|
45
|
+
initProject(args.slice(1));
|
|
46
|
+
break;
|
|
47
|
+
case 'db:push':
|
|
48
|
+
dbPush();
|
|
49
|
+
break;
|
|
50
|
+
case 'db:seed':
|
|
51
|
+
dbSeed();
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.log(`❌ Unknown command: ${command}`);
|
|
55
|
+
console.log(` Run 'pal help' for available commands`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
function showHelp() {
|
|
59
|
+
console.log(`
|
|
60
|
+
Usage: pal <command> [options]
|
|
61
|
+
|
|
62
|
+
Make Commands:
|
|
63
|
+
make:model <name> Create a new model (entity + repository)
|
|
64
|
+
make:repository <name> Create a new repository
|
|
65
|
+
make:service <name> Create a new service
|
|
66
|
+
make:controller <name> Create a new controller
|
|
67
|
+
make:migration <name> Create a new migration
|
|
68
|
+
make:factory <name> Create a model factory
|
|
69
|
+
|
|
70
|
+
Database Commands:
|
|
71
|
+
db:push Push schema to database
|
|
72
|
+
db:seed Run database seeds
|
|
73
|
+
|
|
74
|
+
Project Commands:
|
|
75
|
+
init [name] Initialize a new Pal project
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
--resource Generate CRUD methods (for model/repository/controller)
|
|
79
|
+
--api Generate API-ready controller
|
|
80
|
+
--force Overwrite existing files
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
pal make:model User
|
|
84
|
+
pal make:repository Post --resource
|
|
85
|
+
pal make:controller UserController --api
|
|
86
|
+
pal init my-api
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
function makeModel(args) {
|
|
90
|
+
const name = args[0];
|
|
91
|
+
if (!name) {
|
|
92
|
+
console.log('❌ Please provide a model name');
|
|
93
|
+
console.log(' Example: pal make:model User');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const options = parseOptions(args);
|
|
97
|
+
const fileName = toKebabCase(name);
|
|
98
|
+
const dir = path.join(process.cwd(), 'src', 'models', fileName);
|
|
99
|
+
ensureDir(dir);
|
|
100
|
+
const entityCode = generateEntity(name, options);
|
|
101
|
+
const repositoryCode = generateRepository(name, options);
|
|
102
|
+
writeFile(path.join(dir, 'index.ts'), entityCode);
|
|
103
|
+
writeFile(path.join(dir, 'repository.ts'), repositoryCode);
|
|
104
|
+
console.log(`✅ Created model: src/models/${fileName}/`);
|
|
105
|
+
console.log(` - index.ts`);
|
|
106
|
+
console.log(` - repository.ts`);
|
|
107
|
+
}
|
|
108
|
+
function makeRepository(args) {
|
|
109
|
+
const name = args[0];
|
|
110
|
+
if (!name) {
|
|
111
|
+
console.log('❌ Please provide a repository name');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const options = parseOptions(args);
|
|
115
|
+
const fileName = toKebabCase(name);
|
|
116
|
+
const dir = path.join(process.cwd(), 'src', 'repositories');
|
|
117
|
+
ensureDir(dir);
|
|
118
|
+
const code = generateRepository(name, options);
|
|
119
|
+
writeFile(path.join(dir, `${fileName}.repository.ts`), code);
|
|
120
|
+
console.log(`✅ Created repository: src/repositories/${fileName}.repository.ts`);
|
|
121
|
+
}
|
|
122
|
+
function makeService(args) {
|
|
123
|
+
const name = args[0];
|
|
124
|
+
if (!name) {
|
|
125
|
+
console.log('❌ Please provide a service name');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const fileName = toKebabCase(name);
|
|
129
|
+
const dir = path.join(process.cwd(), 'src', 'services');
|
|
130
|
+
ensureDir(dir);
|
|
131
|
+
const code = generateService(name);
|
|
132
|
+
writeFile(path.join(dir, `${fileName}.service.ts`), code);
|
|
133
|
+
console.log(`✅ Created service: src/services/${fileName}.service.ts`);
|
|
134
|
+
}
|
|
135
|
+
function makeController(args) {
|
|
136
|
+
const name = args[0];
|
|
137
|
+
if (!name) {
|
|
138
|
+
console.log('❌ Please provide a controller name');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const options = parseOptions(args);
|
|
142
|
+
const fileName = toKebabCase(name);
|
|
143
|
+
const dir = path.join(process.cwd(), 'src', 'controllers');
|
|
144
|
+
ensureDir(dir);
|
|
145
|
+
const code = generateController(name, options);
|
|
146
|
+
writeFile(path.join(dir, `${fileName}.controller.ts`), code);
|
|
147
|
+
console.log(`✅ Created controller: src/controllers/${fileName}.controller.ts`);
|
|
148
|
+
}
|
|
149
|
+
function makeMigration(args) {
|
|
150
|
+
const name = args[0];
|
|
151
|
+
if (!name) {
|
|
152
|
+
console.log('❌ Please provide a migration name');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const timestamp = Date.now();
|
|
156
|
+
const fileName = `${timestamp}_${toKebabCase(name)}.ts`;
|
|
157
|
+
const dir = path.join(process.cwd(), 'database', 'migrations');
|
|
158
|
+
ensureDir(dir);
|
|
159
|
+
const code = generateMigration(name);
|
|
160
|
+
writeFile(path.join(dir, fileName), code);
|
|
161
|
+
console.log(`✅ Created migration: database/migrations/${fileName}`);
|
|
162
|
+
}
|
|
163
|
+
function makeFactory(args) {
|
|
164
|
+
const name = args[0];
|
|
165
|
+
if (!name) {
|
|
166
|
+
console.log('❌ Please provide a factory name');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const fileName = toKebabCase(name);
|
|
170
|
+
const dir = path.join(process.cwd(), 'database', 'factories');
|
|
171
|
+
ensureDir(dir);
|
|
172
|
+
const code = generateFactory(name);
|
|
173
|
+
writeFile(path.join(dir, `${fileName}.factory.ts`), code);
|
|
174
|
+
console.log(`✅ Created factory: database/factories/${fileName}.factory.ts`);
|
|
175
|
+
}
|
|
176
|
+
function initProject(args) {
|
|
177
|
+
const name = args[0] || 'my-pal-app';
|
|
178
|
+
console.log(`📦 Initializing Pal project: ${name}...`);
|
|
179
|
+
const dir = path.join(process.cwd(), name);
|
|
180
|
+
ensureDir(dir);
|
|
181
|
+
ensureDir(path.join(dir, 'src', 'models'));
|
|
182
|
+
ensureDir(path.join(dir, 'src', 'services'));
|
|
183
|
+
ensureDir(path.join(dir, 'src', 'repositories'));
|
|
184
|
+
ensureDir(path.join(dir, 'src', 'controllers'));
|
|
185
|
+
ensureDir(path.join(dir, 'database', 'migrations'));
|
|
186
|
+
ensureDir(path.join(dir, 'database', 'factories'));
|
|
187
|
+
ensureDir(path.join(dir, 'tests'));
|
|
188
|
+
const pkgJson = {
|
|
189
|
+
name: name,
|
|
190
|
+
version: '0.0.1',
|
|
191
|
+
type: 'module',
|
|
192
|
+
scripts: {
|
|
193
|
+
dev: 'pal serve',
|
|
194
|
+
build: 'pal build',
|
|
195
|
+
test: 'pal test',
|
|
196
|
+
'db:push': 'pal db:push',
|
|
197
|
+
'db:seed': 'pal db:seed',
|
|
198
|
+
},
|
|
199
|
+
dependencies: {
|
|
200
|
+
'@paljs/core': 'workspace:*',
|
|
201
|
+
'@paljs/orm': 'workspace:*',
|
|
202
|
+
'@paljs/http': 'workspace:*',
|
|
203
|
+
},
|
|
204
|
+
devDependencies: {
|
|
205
|
+
'typescript': '^5.0.0',
|
|
206
|
+
'@types/node': '^20.0.0',
|
|
207
|
+
'tsx': '^4.0.0',
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const tsconfig = {
|
|
211
|
+
compilerOptions: {
|
|
212
|
+
target: 'ES2022',
|
|
213
|
+
module: 'ESNext',
|
|
214
|
+
moduleResolution: 'bundler',
|
|
215
|
+
experimentalDecorators: true,
|
|
216
|
+
emitDecoratorMetadata: true,
|
|
217
|
+
strict: true,
|
|
218
|
+
esModuleInterop: true,
|
|
219
|
+
skipLibCheck: true,
|
|
220
|
+
outDir: 'dist',
|
|
221
|
+
},
|
|
222
|
+
include: ['src/**/*'],
|
|
223
|
+
};
|
|
224
|
+
writeFile(path.join(dir, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
225
|
+
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
|
226
|
+
writeFile(path.join(dir, 'src', 'index.ts'), '// Welcome to Pal!\n\n');
|
|
227
|
+
console.log(`✅ Project created: ${name}/`);
|
|
228
|
+
console.log(` Run: cd ${name} && pnpm install && pnpm dev`);
|
|
229
|
+
}
|
|
230
|
+
function dbPush() {
|
|
231
|
+
console.log('🔄 Pushing schema to database...');
|
|
232
|
+
console.log(' (This would sync your models to the database)');
|
|
233
|
+
}
|
|
234
|
+
function dbSeed() {
|
|
235
|
+
console.log('🌱 Running database seeds...');
|
|
236
|
+
console.log(' (This would seed your database with initial data)');
|
|
237
|
+
}
|
|
238
|
+
function parseOptions(args) {
|
|
239
|
+
return args
|
|
240
|
+
.filter(a => a.startsWith('--'))
|
|
241
|
+
.reduce((acc, opt) => {
|
|
242
|
+
acc[opt.slice(2)] = true;
|
|
243
|
+
return acc;
|
|
244
|
+
}, {});
|
|
245
|
+
}
|
|
246
|
+
function toKebabCase(str) {
|
|
247
|
+
return str
|
|
248
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
249
|
+
.replace(/[\s_]+/g, '-')
|
|
250
|
+
.toLowerCase();
|
|
251
|
+
}
|
|
252
|
+
function toCamelCase(str) {
|
|
253
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
254
|
+
}
|
|
255
|
+
function ensureDir(dir) {
|
|
256
|
+
if (!fs.existsSync(dir)) {
|
|
257
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function writeFile(filePath, content) {
|
|
261
|
+
if (fs.existsSync(filePath) && !process.argv.includes('--force')) {
|
|
262
|
+
console.log(`⚠️ File exists: ${filePath}`);
|
|
263
|
+
console.log(` Use --force to overwrite`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
fs.writeFileSync(filePath, content);
|
|
267
|
+
}
|
|
268
|
+
function generateEntity(name, options) {
|
|
269
|
+
const camelName = toCamelCase(name);
|
|
270
|
+
const hasResource = options.resource;
|
|
271
|
+
return `import { BaseEntity } from '@paljs/orm';
|
|
272
|
+
|
|
273
|
+
export interface ${name} extends BaseEntity {
|
|
274
|
+
// Add your fields here
|
|
275
|
+
name: string;
|
|
276
|
+
${hasResource ? `
|
|
277
|
+
// Common fields for CRUD:
|
|
278
|
+
// email: string;
|
|
279
|
+
// status: 'active' | 'inactive';
|
|
280
|
+
// description?: string;
|
|
281
|
+
// createdAt: Date;
|
|
282
|
+
// updatedAt: Date;` : ''}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface Create${name}DTO {
|
|
286
|
+
name: string;
|
|
287
|
+
${hasResource ? `// email: string;
|
|
288
|
+
// status?: 'active' | 'inactive';
|
|
289
|
+
// description?: string;` : ''}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface Update${name}DTO {
|
|
293
|
+
name?: string;
|
|
294
|
+
${hasResource ? `// email?: string;
|
|
295
|
+
// status?: 'active' | 'inactive';
|
|
296
|
+
// description?: string;` : ''}
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
function generateRepository(name, options) {
|
|
301
|
+
const camelName = toCamelCase(name);
|
|
302
|
+
const hasResource = options.resource;
|
|
303
|
+
let extraMethods = '';
|
|
304
|
+
if (hasResource) {
|
|
305
|
+
extraMethods = `
|
|
306
|
+
async findByEmail(email: string): Promise<${name} | null> {
|
|
307
|
+
return this.findOne({ email });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async findActive(): Promise<${name}[] | null> {
|
|
311
|
+
return this.findAll({ status: 'active' });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async exists(email: string, excludeId?: string): Promise<boolean> {
|
|
315
|
+
const existing = await this.findByEmail(email);
|
|
316
|
+
if (!existing) return false;
|
|
317
|
+
if (excludeId && existing.id === excludeId) return false;
|
|
318
|
+
return true;
|
|
319
|
+
}`;
|
|
320
|
+
}
|
|
321
|
+
return `import { BaseRepository } from '@paljs/orm';
|
|
322
|
+
import { ${name}, Create${name}DTO, Update${name}DTO } from './index';
|
|
323
|
+
|
|
324
|
+
export class ${name}Repository extends BaseRepository<${name}, Create${name}DTO, Update${name}DTO> {
|
|
325
|
+
protected toEntity(data: Create${name}DTO): ${name} {
|
|
326
|
+
const now = new Date();
|
|
327
|
+
return {
|
|
328
|
+
id: this.generateId(),
|
|
329
|
+
name: data.name,
|
|
330
|
+
${hasResource ? `// email: data.email,
|
|
331
|
+
// status: data.status || 'active',` : ''}
|
|
332
|
+
createdAt: now,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
protected toUpdateData(data: Update${name}DTO): Partial<${name}> {
|
|
337
|
+
return {
|
|
338
|
+
name: data.name,
|
|
339
|
+
${hasResource ? `// email: data.email,
|
|
340
|
+
// status: data.status,
|
|
341
|
+
// updatedAt: new Date(),` : ''}
|
|
342
|
+
};
|
|
343
|
+
}${extraMethods}
|
|
344
|
+
}
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
function generateService(name) {
|
|
348
|
+
const camelName = toCamelCase(name);
|
|
349
|
+
const repoName = `${name}Repository`;
|
|
350
|
+
return `import { Injectable } from '@paljs/core';
|
|
351
|
+
import { ${repoName} } from '../repositories/${toKebabCase(name)}.repository';
|
|
352
|
+
import { Create${name}DTO, Update${name}DTO } from '../models/${toKebabCase(name)}';
|
|
353
|
+
|
|
354
|
+
@Injectable()
|
|
355
|
+
export class ${name}Service {
|
|
356
|
+
constructor(private ${camelName}Repo: ${repoName}) {}
|
|
357
|
+
|
|
358
|
+
async findAll() {
|
|
359
|
+
return this.${camelName}Repo.findAll();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async findOne(id: string) {
|
|
363
|
+
return this.${camelName}Repo.findById(id);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async create(data: Create${name}DTO) {
|
|
367
|
+
return this.${camelName}Repo.create(data);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async update(id: string, data: Update${name}DTO) {
|
|
371
|
+
return this.${camelName}Repo.update(id, data);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async delete(id: string) {
|
|
375
|
+
return this.${camelName}Repo.delete(id);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
function generateController(name, options) {
|
|
381
|
+
const camelName = toCamelCase(name);
|
|
382
|
+
const hasApi = options.api;
|
|
383
|
+
const hasResource = options.resource;
|
|
384
|
+
const kebabName = toKebabCase(name);
|
|
385
|
+
let crudMethods = '';
|
|
386
|
+
if (hasApi) {
|
|
387
|
+
crudMethods = `
|
|
388
|
+
async index() {
|
|
389
|
+
const ${camelName}s = await this.${camelName}Service.findAll();
|
|
390
|
+
return { data: ${camelName}s };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async show(id: string) {
|
|
394
|
+
const ${camelName} = await this.${camelName}Service.findOne(id);
|
|
395
|
+
if (!${camelName}) {
|
|
396
|
+
throw new Error('${name} not found');
|
|
397
|
+
}
|
|
398
|
+
return { data: ${camelName} };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async store(data: any) {
|
|
402
|
+
const ${camelName} = await this.${camelName}Service.create(data);
|
|
403
|
+
return { data: ${camelName}, status: 201 };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async update(id: string, data: any) {
|
|
407
|
+
const ${camelName} = await this.${camelName}Service.update(id, data);
|
|
408
|
+
return { data: ${camelName} };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async destroy(id: string) {
|
|
412
|
+
await this.${camelName}Service.delete(id);
|
|
413
|
+
return { status: 204 };
|
|
414
|
+
}`;
|
|
415
|
+
}
|
|
416
|
+
if (hasResource || hasApi) {
|
|
417
|
+
return `import { Injectable } from '@paljs/core';
|
|
418
|
+
import { ${name}Service } from '../services/${kebabName}.service';
|
|
419
|
+
|
|
420
|
+
@Injectable()
|
|
421
|
+
export class ${name}Controller {
|
|
422
|
+
constructor(private ${camelName}Service: ${name}Service) {} ${crudMethods}
|
|
423
|
+
}
|
|
424
|
+
`;
|
|
425
|
+
}
|
|
426
|
+
return `import { Injectable } from '@paljs/core';
|
|
427
|
+
|
|
428
|
+
@Injectable()
|
|
429
|
+
export class ${name}Controller {
|
|
430
|
+
async index() {
|
|
431
|
+
return { data: [] };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
function generateMigration(name) {
|
|
437
|
+
const tableName = toKebabCase(name) + 's';
|
|
438
|
+
return `import { Migration } from '@paljs/orm';
|
|
439
|
+
|
|
440
|
+
export class Create${name}Table implements Migration {
|
|
441
|
+
up() {
|
|
442
|
+
return \`
|
|
443
|
+
CREATE TABLE ${tableName} (
|
|
444
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
445
|
+
name VARCHAR(255) NOT NULL,
|
|
446
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
447
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
448
|
+
)
|
|
449
|
+
\`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
down() {
|
|
453
|
+
return \`DROP TABLE ${tableName}\`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
function generateFactory(name) {
|
|
459
|
+
return `import { Factory } from '@paljs/test';
|
|
460
|
+
import { ${name} } from '../models/${toKebabCase(name)}';
|
|
461
|
+
|
|
462
|
+
export const ${name}Factory = Factory.define<${name}>(({ sequence }) => ({
|
|
463
|
+
id: \`seed-\${sequence}\`,
|
|
464
|
+
name: \`Test ${name} \${sequence}\`,
|
|
465
|
+
createdAt: new Date(),
|
|
466
|
+
}));
|
|
467
|
+
`;
|
|
468
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rpal/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for PalJS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"pal": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/paljs/paljs.git"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@rpal/core": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"tsx": "^4.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"dev": "tsx src/index.ts"
|
|
29
|
+
}
|
|
30
|
+
}
|