@quatrain/app 1.1.1
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/Bootloader.d.ts +5 -0
- package/dist/Bootloader.d.ts.map +1 -0
- package/dist/Bootloader.js +110 -0
- package/dist/Bootloader.js.map +1 -0
- package/dist/CodeGenerator.d.ts +10 -0
- package/dist/CodeGenerator.d.ts.map +1 -0
- package/dist/CodeGenerator.js +252 -0
- package/dist/CodeGenerator.js.map +1 -0
- package/dist/Infra.d.ts +7 -0
- package/dist/Infra.d.ts.map +1 -0
- package/dist/Infra.js +81 -0
- package/dist/Infra.js.map +1 -0
- package/dist/InfraBuilder.d.ts +27 -0
- package/dist/InfraBuilder.d.ts.map +1 -0
- package/dist/InfraBuilder.js +132 -0
- package/dist/InfraBuilder.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/Bootloader.ts +121 -0
- package/src/CodeGenerator.ts +246 -0
- package/src/Infra.ts +96 -0
- package/src/InfraBuilder.ts +141 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.InfraBuilder = void 0;
|
|
37
|
+
const yaml = __importStar(require("yaml"));
|
|
38
|
+
class InfraBuilder {
|
|
39
|
+
static build(config, appName = 'quatrain-app') {
|
|
40
|
+
const compose = {
|
|
41
|
+
services: {},
|
|
42
|
+
volumes: {}
|
|
43
|
+
};
|
|
44
|
+
const envVars = {};
|
|
45
|
+
compose.services['engine'] = {
|
|
46
|
+
build: {
|
|
47
|
+
context: '.',
|
|
48
|
+
dockerfile: 'Containerfile'
|
|
49
|
+
},
|
|
50
|
+
container_name: `${appName}-engine`,
|
|
51
|
+
restart: 'unless-stopped',
|
|
52
|
+
ports: ['3000:3000', '4001:4000'],
|
|
53
|
+
environment: {
|
|
54
|
+
NODE_ENV: 'production'
|
|
55
|
+
},
|
|
56
|
+
volumes: ['./quatrain.json:/app/quatrain.json:ro'],
|
|
57
|
+
depends_on: []
|
|
58
|
+
};
|
|
59
|
+
if (config.backend && config.backend.adapter === 'PostgresAdapter') {
|
|
60
|
+
const dbUser = 'quatrain';
|
|
61
|
+
const dbPass = 'quatrain_pass';
|
|
62
|
+
const dbName = 'quatrain_db';
|
|
63
|
+
compose.services['postgres'] = {
|
|
64
|
+
image: 'postgres:15-alpine',
|
|
65
|
+
container_name: `${appName}-postgres`,
|
|
66
|
+
restart: 'unless-stopped',
|
|
67
|
+
ports: ['5432:5432'],
|
|
68
|
+
environment: {
|
|
69
|
+
POSTGRES_USER: dbUser,
|
|
70
|
+
POSTGRES_PASSWORD: dbPass,
|
|
71
|
+
POSTGRES_DB: dbName
|
|
72
|
+
},
|
|
73
|
+
volumes: ['postgres_data:/var/lib/postgresql/data']
|
|
74
|
+
};
|
|
75
|
+
compose.volumes['postgres_data'] = {};
|
|
76
|
+
compose.services['engine'].depends_on.push('postgres');
|
|
77
|
+
envVars['DATABASE_URL'] = `postgresql://${dbUser}:${dbPass}@postgres:5432/${dbName}`;
|
|
78
|
+
}
|
|
79
|
+
if (config.storage && config.storage.adapter === 'S3Adapter') {
|
|
80
|
+
const s3User = 'minioadmin';
|
|
81
|
+
const s3Pass = 'minioadminpassword';
|
|
82
|
+
compose.services['minio'] = {
|
|
83
|
+
image: 'minio/minio:latest',
|
|
84
|
+
container_name: `${appName}-minio`,
|
|
85
|
+
restart: 'unless-stopped',
|
|
86
|
+
ports: ['9000:9000', '9001:9001'],
|
|
87
|
+
environment: {
|
|
88
|
+
MINIO_ROOT_USER: s3User,
|
|
89
|
+
MINIO_ROOT_PASSWORD: s3Pass
|
|
90
|
+
},
|
|
91
|
+
command: 'server /data --console-address ":9001"',
|
|
92
|
+
volumes: ['minio_data:/data']
|
|
93
|
+
};
|
|
94
|
+
compose.volumes['minio_data'] = {};
|
|
95
|
+
compose.services['engine'].depends_on.push('minio');
|
|
96
|
+
envVars['S3_ENDPOINT'] = `http://minio:9000`;
|
|
97
|
+
envVars['S3_ACCESS_KEY'] = s3User;
|
|
98
|
+
envVars['S3_SECRET_KEY'] = s3Pass;
|
|
99
|
+
}
|
|
100
|
+
if (config.queue && config.queue.adapter === 'AmqpAdapter') {
|
|
101
|
+
compose.services['rabbitmq'] = {
|
|
102
|
+
image: 'rabbitmq:3-management-alpine',
|
|
103
|
+
container_name: `${appName}-rabbitmq`,
|
|
104
|
+
restart: 'unless-stopped',
|
|
105
|
+
ports: ['5672:5672', '15672:15672']
|
|
106
|
+
};
|
|
107
|
+
compose.services['engine'].depends_on.push('rabbitmq');
|
|
108
|
+
envVars['AMQP_URL'] = `amqp://rabbitmq:5672`;
|
|
109
|
+
}
|
|
110
|
+
if (compose.services['engine'].depends_on?.length === 0) {
|
|
111
|
+
delete compose.services['engine'].depends_on;
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(compose.volumes).length === 0) {
|
|
114
|
+
delete compose.volumes;
|
|
115
|
+
}
|
|
116
|
+
const composeYaml = yaml.stringify(compose);
|
|
117
|
+
const envFile = Object.entries(envVars).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
118
|
+
const dockerfile = `
|
|
119
|
+
FROM oven/bun:latest
|
|
120
|
+
WORKDIR /app
|
|
121
|
+
COPY package.json tsconfig.json quatrain.json ./
|
|
122
|
+
RUN bun install
|
|
123
|
+
COPY src ./src
|
|
124
|
+
COPY data ./data
|
|
125
|
+
EXPOSE 3000 4001
|
|
126
|
+
CMD ["bun", "run", "src/index.ts"]
|
|
127
|
+
`.trim();
|
|
128
|
+
return { compose: composeYaml, env: envFile, dockerfile };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.InfraBuilder = InfraBuilder;
|
|
132
|
+
//# sourceMappingURL=InfraBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InfraBuilder.js","sourceRoot":"","sources":["../src/InfraBuilder.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./Bootloader"), exports);
|
|
18
|
+
__exportStar(require("./Infra"), exports);
|
|
19
|
+
__exportStar(require("./InfraBuilder"), exports);
|
|
20
|
+
__exportStar(require("./CodeGenerator"), exports);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA4B;AAC5B,0CAAuB;AACvB,iDAA8B;AAC9B,kDAA+B"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quatrain/app",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
5
|
+
"description": "Quatrain App Bootloader and configuration helpers",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bun": "src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"LICENSE.md",
|
|
11
|
+
"src/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"NOTICE.md"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Quatrain/Core.git",
|
|
19
|
+
"directory": "packages/app"
|
|
20
|
+
},
|
|
21
|
+
"author": "Quatrain Développement SAS <developers@quatrain.com>",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@quatrain/auth": "^1.2.0",
|
|
24
|
+
"@quatrain/backend": "^1.2.0",
|
|
25
|
+
"@quatrain/core": "^1.2.0",
|
|
26
|
+
"@quatrain/log": "^1.2.0",
|
|
27
|
+
"@quatrain/messaging": "^1.1.0",
|
|
28
|
+
"@quatrain/queue": "^1.2.0",
|
|
29
|
+
"@quatrain/storage": "^1.2.0",
|
|
30
|
+
"yaml": "^2.4.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@tsconfig/recommended": "^1.0.1",
|
|
34
|
+
"@types/node": "^22.10.1",
|
|
35
|
+
"typescript": "^5.2.2"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"wbuild": "tsc --watch"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { Core } from '@quatrain/core'
|
|
4
|
+
import { Log, LogLevel } from '@quatrain/log'
|
|
5
|
+
|
|
6
|
+
// Global singletons from Quatrain
|
|
7
|
+
import { Backend } from '@quatrain/backend'
|
|
8
|
+
import { Auth } from '@quatrain/auth'
|
|
9
|
+
import { Queue } from '@quatrain/queue'
|
|
10
|
+
import { Storage } from '@quatrain/storage'
|
|
11
|
+
import { Messaging } from '@quatrain/messaging'
|
|
12
|
+
|
|
13
|
+
export class AppBootloader {
|
|
14
|
+
/**
|
|
15
|
+
* Remplace récursivement toutes les chaînes 'env(NAME)' par la variable d'environnement correspondante.
|
|
16
|
+
*/
|
|
17
|
+
private static resolveEnv(obj: any): any {
|
|
18
|
+
if (typeof obj === 'string') {
|
|
19
|
+
const match = obj.match(/^env\(([^)]+)\)$/)
|
|
20
|
+
if (match) {
|
|
21
|
+
return process.env[match[1]] || ''
|
|
22
|
+
}
|
|
23
|
+
return obj
|
|
24
|
+
} else if (Array.isArray(obj)) {
|
|
25
|
+
return obj.map(item => this.resolveEnv(item))
|
|
26
|
+
} else if (obj !== null && typeof obj === 'object') {
|
|
27
|
+
const resolved: any = {}
|
|
28
|
+
for (const key of Object.keys(obj)) {
|
|
29
|
+
resolved[key] = this.resolveEnv(obj[key])
|
|
30
|
+
}
|
|
31
|
+
return resolved
|
|
32
|
+
}
|
|
33
|
+
return obj
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Charge la configuration JSON et initialise tous les Adapters.
|
|
38
|
+
*/
|
|
39
|
+
public static async bootstrap(configPath: string = 'quatrain.json'): Promise<void> {
|
|
40
|
+
const fullPath = path.resolve(process.cwd(), configPath)
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(fullPath)) {
|
|
43
|
+
throw new Error(`[Bootloader] Configuration file not found at: ${fullPath}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rawConfig = JSON.parse(fs.readFileSync(fullPath, 'utf8'))
|
|
47
|
+
const config = this.resolveEnv(rawConfig)
|
|
48
|
+
|
|
49
|
+
Log.info(`[Bootloader] Initializing application from ${configPath}`)
|
|
50
|
+
|
|
51
|
+
// Set global log level if provided
|
|
52
|
+
if (config.logLevel) {
|
|
53
|
+
const level = (LogLevel as any)[config.logLevel.toUpperCase()]
|
|
54
|
+
if (level !== undefined) {
|
|
55
|
+
Core.setLogLevel(level)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Initialize Backend ---
|
|
60
|
+
if (config.backend && config.backend.package && config.backend.adapter) {
|
|
61
|
+
Log.info(`[Bootloader] Loading Backend Adapter: ${config.backend.adapter}`)
|
|
62
|
+
try {
|
|
63
|
+
const pkg = require(config.backend.package)
|
|
64
|
+
const AdapterClass = pkg[config.backend.adapter]
|
|
65
|
+
Backend.addBackend(new AdapterClass({ config: config.backend.config || {} }), 'default', true)
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
Log.error(`[Bootloader] Failed to load backend: ${e.message}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Initialize Auth ---
|
|
72
|
+
if (config.auth && config.auth.package && config.auth.adapter) {
|
|
73
|
+
Log.info(`[Bootloader] Loading Auth Adapter: ${config.auth.adapter}`)
|
|
74
|
+
try {
|
|
75
|
+
const pkg = require(config.auth.package)
|
|
76
|
+
const AdapterClass = pkg[config.auth.adapter]
|
|
77
|
+
Auth.addProvider(new AdapterClass({ config: config.auth.config || {} }), 'default', true)
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
Log.error(`[Bootloader] Failed to load auth: ${e.message}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Initialize Queue ---
|
|
84
|
+
if (config.queue && config.queue.package && config.queue.adapter) {
|
|
85
|
+
Log.info(`[Bootloader] Loading Queue Adapter: ${config.queue.adapter}`)
|
|
86
|
+
try {
|
|
87
|
+
const pkg = require(config.queue.package)
|
|
88
|
+
const AdapterClass = pkg[config.queue.adapter]
|
|
89
|
+
Queue.addQueue(new AdapterClass({ config: config.queue.config || {} }), 'default', true)
|
|
90
|
+
} catch (e: any) {
|
|
91
|
+
Log.error(`[Bootloader] Failed to load queue: ${e.message}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Initialize Storage ---
|
|
96
|
+
if (config.storage && config.storage.package && config.storage.adapter) {
|
|
97
|
+
Log.info(`[Bootloader] Loading Storage Adapter: ${config.storage.adapter}`)
|
|
98
|
+
try {
|
|
99
|
+
const pkg = require(config.storage.package)
|
|
100
|
+
const AdapterClass = pkg[config.storage.adapter]
|
|
101
|
+
Storage.addStorage(new AdapterClass({ config: config.storage.config || {} }), 'default', true)
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
Log.error(`[Bootloader] Failed to load storage: ${e.message}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Initialize Messaging ---
|
|
108
|
+
if (config.messaging && config.messaging.package && config.messaging.adapter) {
|
|
109
|
+
Log.info(`[Bootloader] Loading Messaging Adapter: ${config.messaging.adapter}`)
|
|
110
|
+
try {
|
|
111
|
+
const pkg = require(config.messaging.package)
|
|
112
|
+
const AdapterClass = pkg[config.messaging.adapter]
|
|
113
|
+
Messaging.addMessager(new AdapterClass({ config: config.messaging.config || {} }), 'default', true)
|
|
114
|
+
} catch (e: any) {
|
|
115
|
+
Log.error(`[Bootloader] Failed to load messaging: ${e.message}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Log.info(`[Bootloader] Bootstrap completed successfully.`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { Log } from '@quatrain/log'
|
|
4
|
+
|
|
5
|
+
export class CodeGenerator {
|
|
6
|
+
/**
|
|
7
|
+
* Génère une application complète dans le dossier cible
|
|
8
|
+
*/
|
|
9
|
+
public static generate(config: any, targetDir: string): void {
|
|
10
|
+
const fullTargetDir = path.resolve(process.cwd(), targetDir)
|
|
11
|
+
|
|
12
|
+
// Ensure base directories exist
|
|
13
|
+
const dirs = ['src/models', 'src/api', 'data/migrations/default']
|
|
14
|
+
for (const dir of dirs) {
|
|
15
|
+
const d = path.resolve(fullTargetDir, dir)
|
|
16
|
+
if (!fs.existsSync(d)) {
|
|
17
|
+
fs.mkdirSync(d, { recursive: true })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.generatePackageJson(config, fullTargetDir)
|
|
22
|
+
this.generateTsConfig(fullTargetDir)
|
|
23
|
+
|
|
24
|
+
const generatedModels = this.generateModels(config, fullTargetDir)
|
|
25
|
+
this.generateApiEndpoints(config, fullTargetDir, generatedModels)
|
|
26
|
+
this.generateIndex(config, fullTargetDir, generatedModels)
|
|
27
|
+
this.generateMigration(config, fullTargetDir)
|
|
28
|
+
|
|
29
|
+
Log.info(`[CodeGenerator] Application générée avec succès dans ${fullTargetDir}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static generatePackageJson(config: any, targetDir: string): void {
|
|
33
|
+
const packagePath = path.resolve(targetDir, 'package.json')
|
|
34
|
+
|
|
35
|
+
const targetNpmTag = 'latest-dev' // Use 'latest' for production, 'latest-dev' for testing features
|
|
36
|
+
|
|
37
|
+
const packageJson = {
|
|
38
|
+
name: config.name?.toLowerCase().replace(/[^a-z0-9]/g, '-') || "quatrain-generated-app",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
description: "Generated by Quatrain Studio",
|
|
41
|
+
main: "src/index.ts",
|
|
42
|
+
type: "module",
|
|
43
|
+
scripts: {
|
|
44
|
+
start: "bun run src/index.ts"
|
|
45
|
+
},
|
|
46
|
+
dependencies: {
|
|
47
|
+
"@quatrain/core": targetNpmTag,
|
|
48
|
+
"@quatrain/backend": targetNpmTag,
|
|
49
|
+
"@quatrain/backend-sqlite": targetNpmTag,
|
|
50
|
+
"@quatrain/api": targetNpmTag,
|
|
51
|
+
"@quatrain/api-server": targetNpmTag,
|
|
52
|
+
"@quatrain/backend-migrations": targetNpmTag
|
|
53
|
+
} as Record<string, string>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.authMode === 'oauth') {
|
|
57
|
+
packageJson.dependencies['@quatrain/auth-oidc'] = targetNpmTag
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.backend?.adapter === 'PostgresAdapter') {
|
|
61
|
+
packageJson.dependencies['@quatrain/backend-postgres'] = targetNpmTag
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Log.info(`[CodeGenerator] Generating package.json with dependencies`)
|
|
65
|
+
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static generateTsConfig(targetDir: string): void {
|
|
69
|
+
const tsconfig = {
|
|
70
|
+
compilerOptions: {
|
|
71
|
+
target: "ES2022",
|
|
72
|
+
module: "ESNext",
|
|
73
|
+
moduleResolution: "node",
|
|
74
|
+
esModuleInterop: true,
|
|
75
|
+
strict: true,
|
|
76
|
+
skipLibCheck: true,
|
|
77
|
+
forceConsistentCasingInFileNames: true,
|
|
78
|
+
outDir: "dist"
|
|
79
|
+
},
|
|
80
|
+
include: ["src/**/*", "data/migrations/**/*"]
|
|
81
|
+
}
|
|
82
|
+
fs.writeFileSync(path.resolve(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static generateModels(config: any, targetDir: string): string[] {
|
|
86
|
+
const models = config.models || []
|
|
87
|
+
const generatedNames: string[] = []
|
|
88
|
+
|
|
89
|
+
for (const model of models) {
|
|
90
|
+
if (!model.name) continue
|
|
91
|
+
const className = model.name
|
|
92
|
+
const collectionName = model.collectionName || className.toLowerCase()
|
|
93
|
+
generatedNames.push(className)
|
|
94
|
+
|
|
95
|
+
// Use imports from core properties
|
|
96
|
+
const propsCode = model.properties.map((p: any) => {
|
|
97
|
+
const propType = p.type // e.g. "StringProperty"
|
|
98
|
+
return ` {
|
|
99
|
+
name: '${p.name}',
|
|
100
|
+
type: ${propType}.TYPE,
|
|
101
|
+
mandatory: ${p.options?.mandatory ? 'true' : 'false'}
|
|
102
|
+
}`
|
|
103
|
+
}).join(',\n')
|
|
104
|
+
|
|
105
|
+
const propTypesToImport = Array.from(new Set(model.properties.map((p: any) => p.type)))
|
|
106
|
+
|
|
107
|
+
const modelCode = `import { PersistedBaseObject } from '@quatrain/backend'
|
|
108
|
+
import { ${propTypesToImport.join(', ')} } from '@quatrain/core'
|
|
109
|
+
|
|
110
|
+
export const ${className}Properties = [
|
|
111
|
+
${propsCode}
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
export class ${className} extends PersistedBaseObject {
|
|
115
|
+
static PROPS_DEFINITION = ${className}Properties
|
|
116
|
+
static COLLECTION = '${collectionName}'
|
|
117
|
+
|
|
118
|
+
static async factory(src: any = undefined): Promise<${className}> {
|
|
119
|
+
return super.factory(src, ${className})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`
|
|
123
|
+
fs.writeFileSync(path.resolve(targetDir, `src/models/${className}.ts`), modelCode)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return generatedNames
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static generateApiEndpoints(config: any, targetDir: string, models: string[]): void {
|
|
130
|
+
for (const className of models) {
|
|
131
|
+
const apiCode = `import { CrudEndpoint } from '@quatrain/api-server'
|
|
132
|
+
import { ${className} } from '../models/${className}'
|
|
133
|
+
|
|
134
|
+
export class ${className}Api extends CrudEndpoint {
|
|
135
|
+
constructor() {
|
|
136
|
+
super('/api/${className.toLowerCase()}s', ${className})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`
|
|
140
|
+
fs.writeFileSync(path.resolve(targetDir, `src/api/${className}Api.ts`), apiCode)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private static generateIndex(config: any, targetDir: string, models: string[]): void {
|
|
145
|
+
const adapterClass = config.backend?.adapter === 'PostgresAdapter' ? 'PostgresAdapter' : 'SQLiteAdapter'
|
|
146
|
+
const adapterImport = config.backend?.adapter === 'PostgresAdapter' ? '@quatrain/backend-postgres' : '@quatrain/backend-sqlite'
|
|
147
|
+
|
|
148
|
+
const modelImports = models.map(m => `import { ${m} } from './models/${m}'`).join('\n')
|
|
149
|
+
const apiImports = models.map(m => `import { ${m}Api } from './api/${m}Api'`).join('\n')
|
|
150
|
+
const apiRegisters = models.map(m => ` server.addEndpoint(new ${m}Api())`).join('\n')
|
|
151
|
+
|
|
152
|
+
const indexCode = `import * as path from 'path'
|
|
153
|
+
import { Backend, InjectMetaMiddleware } from '@quatrain/backend'
|
|
154
|
+
import { ${adapterClass} } from '${adapterImport}'
|
|
155
|
+
import { ExpressAdapter } from '@quatrain/api-server'
|
|
156
|
+
import { MigrationManager } from '@quatrain/backend-migrations'
|
|
157
|
+
${config.authMode === 'oauth' ? `import { AuthOIDC } from '@quatrain/auth-oidc'\n` : ''}
|
|
158
|
+
${modelImports}
|
|
159
|
+
${apiImports}
|
|
160
|
+
|
|
161
|
+
;(async () => {
|
|
162
|
+
try {
|
|
163
|
+
const adapter = new ${adapterClass}({
|
|
164
|
+
config: ${adapterClass === 'SQLiteAdapter' ? '{ database: path.resolve(process.cwd(), "data/app.sqlite") }' : 'process.env.DATABASE_URL'},
|
|
165
|
+
middlewares: [new InjectMetaMiddleware()]
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
Backend.init(adapter)
|
|
169
|
+
|
|
170
|
+
const mm = new MigrationManager()
|
|
171
|
+
await mm.up()
|
|
172
|
+
|
|
173
|
+
const server = new ExpressAdapter()
|
|
174
|
+
|
|
175
|
+
${apiRegisters}
|
|
176
|
+
|
|
177
|
+
${config.authMode === 'oauth' ? ` // Initialize OIDC Auth
|
|
178
|
+
const oidc = AuthOIDC.init('http://localhost:4001')
|
|
179
|
+
server.getApp().use('/oidc', oidc.callback())
|
|
180
|
+
` : ''}
|
|
181
|
+
|
|
182
|
+
const PORT = Number(process.env.PORT) || 4001
|
|
183
|
+
server.start(PORT, () => {
|
|
184
|
+
console.log(\`🚀 Server started on port \${PORT}\`)
|
|
185
|
+
})
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error(e)
|
|
188
|
+
process.exit(1)
|
|
189
|
+
}
|
|
190
|
+
})()
|
|
191
|
+
`
|
|
192
|
+
fs.writeFileSync(path.resolve(targetDir, 'src/index.ts'), indexCode)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private static generateMigration(config: any, targetDir: string): void {
|
|
196
|
+
const models = config.models || []
|
|
197
|
+
|
|
198
|
+
let upQueries = ''
|
|
199
|
+
let downQueries = ''
|
|
200
|
+
|
|
201
|
+
for (const model of models) {
|
|
202
|
+
if (!model.name) continue
|
|
203
|
+
const collectionName = model.collectionName || model.name.toLowerCase()
|
|
204
|
+
|
|
205
|
+
const columns = model.properties.map((p: any) => {
|
|
206
|
+
// Very simplified SQLite type mapping for the PoC
|
|
207
|
+
let sqlType = 'TEXT'
|
|
208
|
+
if (p.type === 'NumberProperty' || p.type === 'BooleanProperty') sqlType = 'INTEGER'
|
|
209
|
+
return `${p.name} ${sqlType}`
|
|
210
|
+
}).join(',\n ')
|
|
211
|
+
|
|
212
|
+
upQueries += `
|
|
213
|
+
await adapter.rawQuery(\`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
214
|
+
id TEXT PRIMARY KEY,
|
|
215
|
+
status TEXT,
|
|
216
|
+
created_at TEXT,
|
|
217
|
+
updated_at TEXT,
|
|
218
|
+
created_by TEXT,
|
|
219
|
+
updated_by TEXT,
|
|
220
|
+
${columns}
|
|
221
|
+
)\`)\n`
|
|
222
|
+
|
|
223
|
+
downQueries += `
|
|
224
|
+
await adapter.rawQuery('DROP TABLE IF EXISTS ${collectionName}')\n`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const migrationCode = `import { AbstractBackendAdapter } from '@quatrain/backend'
|
|
228
|
+
|
|
229
|
+
export const up = async ({ context: adapter }: { context: AbstractBackendAdapter }) => {
|
|
230
|
+
try {${upQueries}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error('Migration up error', e)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const down = async ({ context: adapter }: { context: AbstractBackendAdapter }) => {
|
|
237
|
+
try {${downQueries}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.error('Migration down error', e)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`
|
|
243
|
+
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)
|
|
244
|
+
fs.writeFileSync(path.resolve(targetDir, `data/migrations/default/${timestamp}_init.ts`), migrationCode)
|
|
245
|
+
}
|
|
246
|
+
}
|
package/src/Infra.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { Log } from '@quatrain/log'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import { InfraBuilder } from './InfraBuilder'
|
|
6
|
+
import { CodeGenerator } from './CodeGenerator'
|
|
7
|
+
|
|
8
|
+
export class AppInfra {
|
|
9
|
+
/**
|
|
10
|
+
* Démarrer l'infrastructure locale (bases de données, storages, brokers)
|
|
11
|
+
* via podman-compose ou docker-compose
|
|
12
|
+
* Si une configuration est fournie, génère d'abord les fichiers compose.yaml et .env dans le dossier app/
|
|
13
|
+
*/
|
|
14
|
+
public static async start(config?: any, targetDir: string = 'app'): Promise<void> {
|
|
15
|
+
let composeFile = path.resolve(process.cwd(), targetDir, 'compose.yaml')
|
|
16
|
+
|
|
17
|
+
if (config) {
|
|
18
|
+
// Générer dynamiquement les fichiers de déploiement
|
|
19
|
+
const fullTargetDir = path.resolve(process.cwd(), targetDir)
|
|
20
|
+
if (!fs.existsSync(fullTargetDir)) {
|
|
21
|
+
fs.mkdirSync(fullTargetDir, { recursive: true })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Generate Application Source Code
|
|
25
|
+
CodeGenerator.generate(config, targetDir)
|
|
26
|
+
|
|
27
|
+
// Generate Infrastructure configuration
|
|
28
|
+
const { compose, env, dockerfile } = InfraBuilder.build(config)
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(composeFile, compose, 'utf8')
|
|
31
|
+
fs.writeFileSync(path.resolve(fullTargetDir, '.env'), env, 'utf8')
|
|
32
|
+
fs.writeFileSync(path.resolve(fullTargetDir, 'Containerfile'), dockerfile, 'utf8')
|
|
33
|
+
|
|
34
|
+
Log.info(`[Infra] Dynamically generated compose.yaml and .env in ${fullTargetDir}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.runCompose('up -d --build', composeFile)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Arrêter l'infrastructure locale
|
|
42
|
+
*/
|
|
43
|
+
public static async stop(targetDir: string = 'app'): Promise<void> {
|
|
44
|
+
const composeFile = path.resolve(process.cwd(), targetDir, 'compose.yaml')
|
|
45
|
+
return this.runCompose('down', composeFile)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private static getComposeCommand(): string {
|
|
49
|
+
// Prioritize podman compose if working 'modern', otherwise fallback to docker compose
|
|
50
|
+
return 'podman compose'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static async runCompose(action: string, composeFile: string): Promise<void> {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
if (!fs.existsSync(composeFile)) {
|
|
56
|
+
Log.error(`[Infra] Compose file not found at: ${composeFile}`)
|
|
57
|
+
return reject(new Error(`Compose file not found: ${composeFile}`))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cmd = `${this.getComposeCommand()} -f ${composeFile} ${action}`
|
|
61
|
+
Log.info(`[Infra] Executing: ${cmd}`)
|
|
62
|
+
|
|
63
|
+
const child = exec(cmd, (error, stdout, stderr) => {
|
|
64
|
+
if (error) {
|
|
65
|
+
// Fallback to docker-compose if podman-compose is not installed
|
|
66
|
+
if (error.message.includes('command not found')) {
|
|
67
|
+
Log.warn(`[Infra] podman compose not found, falling back to docker compose...`)
|
|
68
|
+
exec(`docker compose -f ${composeFile} ${action}`, (err2, out2, errOut2) => {
|
|
69
|
+
if (err2) {
|
|
70
|
+
Log.error(`[Infra] Failed to run infrastructure: ${err2.message}`)
|
|
71
|
+
return reject(err2)
|
|
72
|
+
}
|
|
73
|
+
if (out2) Log.debug(`[Infra] ${out2}`)
|
|
74
|
+
if (errOut2) Log.debug(`[Infra] ${errOut2}`)
|
|
75
|
+
resolve()
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
Log.error(`[Infra] Failed to run infrastructure: ${error.message}`)
|
|
80
|
+
return reject(error)
|
|
81
|
+
}
|
|
82
|
+
if (stdout) Log.debug(`[Infra] ${stdout}`)
|
|
83
|
+
if (stderr) Log.debug(`[Infra] ${stderr}`)
|
|
84
|
+
resolve()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Stream output to console
|
|
88
|
+
if (child.stdout) {
|
|
89
|
+
child.stdout.on('data', (data) => console.log(data.toString().trim()))
|
|
90
|
+
}
|
|
91
|
+
if (child.stderr) {
|
|
92
|
+
child.stderr.on('data', (data) => console.error(data.toString().trim()))
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|