@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.
@@ -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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"}
@@ -0,0 +1,5 @@
1
+ export * from './Bootloader';
2
+ export * from './Infra';
3
+ export * from './InfraBuilder';
4
+ export * from './CodeGenerator';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }