@aifabrix/builder 2.0.2 → 2.0.4

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.
@@ -71,10 +71,29 @@ async function waitForDbInit(appName) {
71
71
  */
72
72
  async function getContainerPort(appName) {
73
73
  try {
74
- const { stdout: portMapping } = await execAsync(`docker inspect --format='{{range .NetworkSettings.Ports}}{{range .}}{{.HostPort}}{{end}}{{end}}' aifabrix-${appName}`);
75
- const ports = portMapping.trim().split('\n').filter(p => p);
74
+ // Try to get the actual mapped host port from Docker
75
+ // First try docker inspect for the container port mapping
76
+ const { stdout: portMapping } = await execAsync(`docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{if $conf}}{{range $conf}}{{.HostPort}}{{end}}{{end}}{{end}}' aifabrix-${appName}`);
77
+ const ports = portMapping.trim().split('\n').filter(p => p && p !== '');
76
78
  if (ports.length > 0) {
77
- return parseInt(ports[0], 10);
79
+ const port = parseInt(ports[0], 10);
80
+ if (!isNaN(port) && port > 0) {
81
+ return port;
82
+ }
83
+ }
84
+
85
+ // Fallback: try docker ps to get port mapping (format: "0.0.0.0:3010->3000/tcp")
86
+ try {
87
+ const { stdout: psOutput } = await execAsync(`docker ps --filter "name=aifabrix-${appName}" --format "{{.Ports}}"`);
88
+ const portMatch = psOutput.match(/:(\d+)->/);
89
+ if (portMatch) {
90
+ const port = parseInt(portMatch[1], 10);
91
+ if (!isNaN(port) && port > 0) {
92
+ return port;
93
+ }
94
+ }
95
+ } catch (error) {
96
+ // Fall through
78
97
  }
79
98
  } catch (error) {
80
99
  // Fall through to default
@@ -152,12 +171,12 @@ async function checkHealthEndpoint(healthCheckUrl) {
152
171
  async function waitForHealthCheck(appName, timeout = 90, port = null, config = null) {
153
172
  await waitForDbInit(appName);
154
173
 
155
- if (!port) {
156
- port = await getContainerPort(appName);
157
- }
174
+ // Always detect the actual port from Docker to ensure we use the correct mapped port
175
+ const detectedPort = await getContainerPort(appName);
176
+ const healthCheckPort = port || detectedPort;
158
177
 
159
178
  const healthCheckPath = config?.healthCheck?.path || '/health';
160
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
179
+ const healthCheckUrl = `http://localhost:${healthCheckPort}${healthCheckPath}`;
161
180
  const maxAttempts = timeout / 2;
162
181
 
163
182
  for (let attempts = 0; attempts < maxAttempts; attempts++) {
@@ -0,0 +1,209 @@
1
+ /**
2
+ * AI Fabrix Builder Secrets Generation Utilities
3
+ *
4
+ * This module handles secret generation and file management.
5
+ * Generates default secret values and manages secrets files.
6
+ *
7
+ * @fileoverview Secret generation utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const os = require('os');
16
+ const crypto = require('crypto');
17
+ const logger = require('./logger');
18
+
19
+ /**
20
+ * Finds missing secret keys from template
21
+ * @function findMissingSecretKeys
22
+ * @param {string} envTemplate - Environment template content
23
+ * @param {Object} existingSecrets - Existing secrets object
24
+ * @returns {string[]} Array of missing secret keys
25
+ */
26
+ function findMissingSecretKeys(envTemplate, existingSecrets) {
27
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
28
+ const missingKeys = [];
29
+ const seenKeys = new Set();
30
+
31
+ let match;
32
+ while ((match = kvPattern.exec(envTemplate)) !== null) {
33
+ const secretKey = match[1];
34
+ if (!seenKeys.has(secretKey) && !(secretKey in existingSecrets)) {
35
+ missingKeys.push(secretKey);
36
+ seenKeys.add(secretKey);
37
+ }
38
+ }
39
+
40
+ return missingKeys;
41
+ }
42
+
43
+ /**
44
+ * Generates secret value based on key name
45
+ * @function generateSecretValue
46
+ * @param {string} key - Secret key name
47
+ * @returns {string} Generated secret value
48
+ */
49
+ function generateSecretValue(key) {
50
+ const keyLower = key.toLowerCase();
51
+
52
+ if (keyLower.includes('password')) {
53
+ const dbPasswordMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-passwordKeyVault$/i);
54
+ if (dbPasswordMatch) {
55
+ const appName = dbPasswordMatch[1];
56
+ const dbName = appName.replace(/-/g, '_');
57
+ return `${dbName}_pass123`;
58
+ }
59
+ return crypto.randomBytes(32).toString('base64');
60
+ }
61
+
62
+ if (keyLower.includes('url') || keyLower.includes('uri')) {
63
+ const dbUrlMatch = key.match(/^databases-([a-z0-9-_]+)-\d+-urlKeyVault$/i);
64
+ if (dbUrlMatch) {
65
+ const appName = dbUrlMatch[1];
66
+ const dbName = appName.replace(/-/g, '_');
67
+ return `postgresql://${dbName}_user:${dbName}_pass123@\${DB_HOST}:5432/${dbName}`;
68
+ }
69
+ return '';
70
+ }
71
+
72
+ if (keyLower.includes('key') || keyLower.includes('secret') || keyLower.includes('token')) {
73
+ return crypto.randomBytes(32).toString('base64');
74
+ }
75
+
76
+ return '';
77
+ }
78
+
79
+ /**
80
+ * Loads existing secrets from file
81
+ * @function loadExistingSecrets
82
+ * @param {string} resolvedPath - Path to secrets file
83
+ * @returns {Object} Existing secrets object
84
+ */
85
+ function loadExistingSecrets(resolvedPath) {
86
+ if (!fs.existsSync(resolvedPath)) {
87
+ return {};
88
+ }
89
+
90
+ try {
91
+ const content = fs.readFileSync(resolvedPath, 'utf8');
92
+ const secrets = yaml.load(content) || {};
93
+ return typeof secrets === 'object' ? secrets : {};
94
+ } catch (error) {
95
+ logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
96
+ return {};
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Saves secrets file
102
+ * @function saveSecretsFile
103
+ * @param {string} resolvedPath - Path to secrets file
104
+ * @param {Object} secrets - Secrets object to save
105
+ * @throws {Error} If save fails
106
+ */
107
+ function saveSecretsFile(resolvedPath, secrets) {
108
+ const dir = path.dirname(resolvedPath);
109
+ if (!fs.existsSync(dir)) {
110
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
111
+ }
112
+
113
+ const yamlContent = yaml.dump(secrets, {
114
+ indent: 2,
115
+ lineWidth: 120,
116
+ noRefs: true,
117
+ sortKeys: false
118
+ });
119
+
120
+ fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
121
+ }
122
+
123
+ /**
124
+ * Generates missing secret keys in secrets file
125
+ * Scans env.template for kv:// references and adds missing keys with secure defaults
126
+ *
127
+ * @async
128
+ * @function generateMissingSecrets
129
+ * @param {string} envTemplate - Environment template content
130
+ * @param {string} secretsPath - Path to secrets file
131
+ * @returns {Promise<string[]>} Array of newly generated secret keys
132
+ * @throws {Error} If generation fails
133
+ *
134
+ * @example
135
+ * const newKeys = await generateMissingSecrets(template, '~/.aifabrix/secrets.yaml');
136
+ * // Returns: ['new-secret-key', 'another-secret']
137
+ */
138
+ async function generateMissingSecrets(envTemplate, secretsPath) {
139
+ const resolvedPath = secretsPath || path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
140
+ const existingSecrets = loadExistingSecrets(resolvedPath);
141
+ const missingKeys = findMissingSecretKeys(envTemplate, existingSecrets);
142
+
143
+ if (missingKeys.length === 0) {
144
+ return [];
145
+ }
146
+
147
+ const newSecrets = {};
148
+ for (const key of missingKeys) {
149
+ newSecrets[key] = generateSecretValue(key);
150
+ }
151
+
152
+ const updatedSecrets = { ...existingSecrets, ...newSecrets };
153
+ saveSecretsFile(resolvedPath, updatedSecrets);
154
+
155
+ logger.log(`✓ Generated ${missingKeys.length} missing secret key(s): ${missingKeys.join(', ')}`);
156
+ return missingKeys;
157
+ }
158
+
159
+ /**
160
+ * Creates default secrets file if it doesn't exist
161
+ * Generates template with common secrets for local development
162
+ *
163
+ * @async
164
+ * @function createDefaultSecrets
165
+ * @param {string} secretsPath - Path where to create secrets file
166
+ * @returns {Promise<void>} Resolves when file is created
167
+ * @throws {Error} If file creation fails
168
+ *
169
+ * @example
170
+ * await createDefaultSecrets('~/.aifabrix/secrets.yaml');
171
+ * // Default secrets file is created
172
+ */
173
+ async function createDefaultSecrets(secretsPath) {
174
+ const resolvedPath = secretsPath.startsWith('~')
175
+ ? path.join(os.homedir(), secretsPath.slice(1))
176
+ : secretsPath;
177
+
178
+ const dir = path.dirname(resolvedPath);
179
+ if (!fs.existsSync(dir)) {
180
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
181
+ }
182
+
183
+ const defaultSecrets = `# Local Development Secrets
184
+ # Production uses Azure KeyVault
185
+
186
+ # Database Secrets
187
+ postgres-passwordKeyVault: "admin123"
188
+
189
+ # Redis Secrets
190
+ redis-passwordKeyVault: ""
191
+ redis-urlKeyVault: "redis://\${REDIS_HOST}:6379"
192
+
193
+ # Keycloak Secrets
194
+ keycloak-admin-passwordKeyVault: "admin123"
195
+ keycloak-auth-server-urlKeyVault: "http://\${KEYCLOAK_HOST}:8082"
196
+ `;
197
+
198
+ fs.writeFileSync(resolvedPath, defaultSecrets, { mode: 0o600 });
199
+ }
200
+
201
+ module.exports = {
202
+ findMissingSecretKeys,
203
+ generateSecretValue,
204
+ loadExistingSecrets,
205
+ saveSecretsFile,
206
+ generateMissingSecrets,
207
+ createDefaultSecrets
208
+ };
209
+
package/lib/validator.js CHANGED
@@ -185,13 +185,17 @@ async function validateEnvTemplate(appName) {
185
185
  const lines = content.split('\n');
186
186
  lines.forEach((line, index) => {
187
187
  const trimmed = line.trim();
188
+ // Skip empty lines and comments
188
189
  if (trimmed && !trimmed.startsWith('#')) {
189
190
  if (!trimmed.includes('=')) {
190
191
  errors.push(`Line ${index + 1}: Invalid environment variable format (missing =)`);
191
192
  } else {
192
- const [key, value] = trimmed.split('=', 2);
193
- if (!key || !value) {
194
- errors.push(`Line ${index + 1}: Invalid environment variable format`);
193
+ const [key, _value] = trimmed.split('=', 2);
194
+ // Trim key to handle whitespace issues
195
+ // Empty values are allowed (_value can be empty string or undefined)
196
+ const trimmedKey = key ? key.trim() : '';
197
+ if (!trimmedKey) {
198
+ errors.push(`Line ${index + 1}: Invalid environment variable format (missing variable name)`);
195
199
  }
196
200
  }
197
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
19
19
  "dev": "node bin/aifabrix.js",
20
20
  "build": "npm run lint && npm run test:ci",
21
+ "pack": "npm run build && npm pack",
21
22
  "validate": "npm run build",
22
23
  "prepublishOnly": "npm run validate",
23
24
  "precommit": "npm run lint:fix && npm run test"
@@ -52,6 +52,53 @@ permissions:
52
52
  roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
53
53
  description: "Deactivate service users"
54
54
 
55
+ # User Management
56
+ - name: "users:create"
57
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
58
+ description: "Create new users"
59
+
60
+ - name: "users:read"
61
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-observer"]
62
+ description: "View user information and profiles"
63
+
64
+ - name: "users:update"
65
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
66
+ description: "Update user information and manage group memberships"
67
+
68
+ - name: "users:delete"
69
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
70
+ description: "Delete users"
71
+
72
+ # Group Management
73
+ - name: "groups:create"
74
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
75
+ description: "Create new groups"
76
+
77
+ - name: "groups:read"
78
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin", "aifabrix-observer"]
79
+ description: "View group information and members"
80
+
81
+ - name: "groups:update"
82
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
83
+ description: "Update group information"
84
+
85
+ - name: "groups:delete"
86
+ roles: ["aifabrix-platform-admin", "aifabrix-security-admin"]
87
+ description: "Delete groups"
88
+
89
+ # Administrative Permissions
90
+ - name: "admin:read"
91
+ roles: ["aifabrix-platform-admin"]
92
+ description: "Administrative read access to all resources"
93
+
94
+ - name: "admin:write"
95
+ roles: ["aifabrix-platform-admin"]
96
+ description: "Administrative write access to all resources"
97
+
98
+ - name: "admin:delete"
99
+ roles: ["aifabrix-platform-admin"]
100
+ description: "Administrative delete access to all resources"
101
+
55
102
  # Template Applications (environment = null)
56
103
  - name: "applications:create"
57
104
  roles: ["aifabrix-platform-admin", "aifabrix-infrastructure-admin", "aifabrix-deployment-admin"]
@@ -165,4 +212,3 @@ permissions:
165
212
  - name: "dashboard:read"
166
213
  roles: ["aifabrix-platform-admin", "aifabrix-deployment-admin", "aifabrix-developer", "aifabrix-observer"]
167
214
  description: "View dashboard summaries and aggregates"
168
-
@@ -1,6 +1,6 @@
1
1
  # Application Metadata
2
2
  app:
3
- key: miso
3
+ key: miso-controller
4
4
  displayName: "Miso Controller"
5
5
  description: "AI Fabrix Miso Controller - Backend API and orchestration service"
6
6
  type: webapp
@@ -36,21 +36,21 @@ healthCheck:
36
36
  authentication:
37
37
  type: keycloak
38
38
  enableSSO: true
39
- requiredRoles: ["aifabrix-user"]
39
+ requiredRoles:
40
+ - aifabrix-user
40
41
  endpoints:
41
- local: "http://localhost:3000/auth/callback"
42
+ local: http://localhost:3000/auth/callback
42
43
 
43
44
  # Build Configuration
44
45
  build:
45
- context: .. # Docker build context (relative to builder/)
46
- dockerfile: builder/Dockerfile # Dockerfile name (empty = use template)
47
- envOutputPath: ../packages/miso-controller/.env # Copy .env to repo root for local dev
48
- localPort: 3010 # Port for local development (different from Docker port)
49
- language: typescript # Runtime language for template selection
50
- secrets: # Path to secrets file (optional)
46
+ context: .. # Docker build context (relative to builder/)
47
+ dockerfile: builder/miso-controller/Dockerfile # Dockerfile name (empty = use template)
48
+ envOutputPath: # Copy .env to repo root for local dev (relative to builder/) (if null, no .env file is copied) (if empty, .env file is copied to repo root)
49
+ localPort: 3010 # Port for local development (different from Docker port)
50
+ language: typescript # Runtime language for template selection (typescript or python)
51
+ secrets: # Path to secrets file
51
52
 
52
53
  # Docker Compose
53
54
  compose:
54
55
  file: docker-compose.yaml
55
56
  service: miso-controller
56
-
@@ -2,8 +2,6 @@
2
2
  # Shared infrastructure services only
3
3
  # Generated by AI Fabrix Builder SDK
4
4
 
5
- version: "3.9"
6
-
7
5
  services:
8
6
  # PostgreSQL Database with pgvector extension
9
7
  postgres:
@@ -2,8 +2,6 @@
2
2
  # Generated by AI Fabrix Builder SDK
3
3
  # Service definition for local development
4
4
 
5
- version: "3.9"
6
-
7
5
  services:
8
6
  {{app.key}}:
9
7
  image: {{image.name}}:{{image.tag}}
@@ -48,23 +46,34 @@ services:
48
46
  command: >
49
47
  sh -c "
50
48
  export PGHOST=postgres PGPORT=5432 PGUSER=pgadmin &&
51
- export PGPASSWORD="${POSTGRES_PASSWORD}" &&
49
+ export PGPASSWORD=\"${POSTGRES_PASSWORD}\" &&
50
+ echo 'Waiting for PostgreSQL to be ready...' &&
51
+ counter=0 &&
52
+ while [ $counter -lt 30 ]; do
53
+ if pg_isready -h postgres -p 5432 -U pgadmin >/dev/null 2>&1; then
54
+ echo 'PostgreSQL is ready!'
55
+ break
56
+ fi
57
+ echo 'Waiting for PostgreSQL...'
58
+ sleep 1
59
+ counter=$((counter + 1))
60
+ done &&
52
61
  {{#if databases}}
53
62
  {{#each databases}}
54
63
  echo 'Creating {{name}} database and user...' &&
55
- psql -d postgres -c 'CREATE DATABASE {{name}};' || echo '{{name}} database exists' &&
56
- psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || echo '{{name}}_user exists' &&
57
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' &&
58
- psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' &&
59
- psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' &&
64
+ (psql -d postgres -c 'CREATE DATABASE {{name}};' || true) &&
65
+ (psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || true) &&
66
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' || true &&
67
+ psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' || true &&
68
+ psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' || true &&
60
69
  {{/each}}
61
70
  {{else}}
62
71
  echo 'Creating {{app.key}} database and user...' &&
63
- psql -d postgres -c 'CREATE DATABASE {{app.key}};' || echo '{{app.key}} database exists' &&
64
- psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || echo '{{app.key}}_user exists' &&
65
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' &&
66
- psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' &&
67
- psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' &&
72
+ (psql -d postgres -c 'CREATE DATABASE {{app.key}};' || true) &&
73
+ (psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || true) &&
74
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' || true &&
75
+ psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' || true &&
76
+ psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' || true &&
68
77
  {{/if}}
69
78
  echo 'Database initialization complete!'
70
79
  "
@@ -2,8 +2,6 @@
2
2
  # Generated by AI Fabrix Builder SDK
3
3
  # Service definition for local development
4
4
 
5
- version: "3.9"
6
-
7
5
  services:
8
6
  {{app.key}}:
9
7
  image: {{image.name}}:{{image.tag}}
@@ -48,23 +46,34 @@ services:
48
46
  command: >
49
47
  sh -c "
50
48
  export PGHOST=postgres PGPORT=5432 PGUSER=pgadmin &&
51
- export PGPASSWORD="${POSTGRES_PASSWORD}" &&
49
+ export PGPASSWORD=\"${POSTGRES_PASSWORD}\" &&
50
+ echo 'Waiting for PostgreSQL to be ready...' &&
51
+ counter=0 &&
52
+ while [ $counter -lt 30 ]; do
53
+ if pg_isready -h postgres -p 5432 -U pgadmin >/dev/null 2>&1; then
54
+ echo 'PostgreSQL is ready!'
55
+ break
56
+ fi
57
+ echo 'Waiting for PostgreSQL...'
58
+ sleep 1
59
+ counter=$((counter + 1))
60
+ done &&
52
61
  {{#if databases}}
53
62
  {{#each databases}}
54
63
  echo 'Creating {{name}} database and user...' &&
55
- psql -d postgres -c 'CREATE DATABASE {{name}};' || echo '{{name}} database exists' &&
56
- psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || echo '{{name}}_user exists' &&
57
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' &&
58
- psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' &&
59
- psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' &&
64
+ (psql -d postgres -c 'CREATE DATABASE {{name}};' || true) &&
65
+ (psql -d postgres -c \"CREATE USER {{name}}_user WITH PASSWORD '{{name}}_pass123';\" || true) &&
66
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{name}} TO {{name}}_user;' || true &&
67
+ psql -d {{name}} -c 'ALTER SCHEMA public OWNER TO {{name}}_user;' || true &&
68
+ psql -d {{name}} -c 'GRANT ALL ON SCHEMA public TO {{name}}_user;' || true &&
60
69
  {{/each}}
61
70
  {{else}}
62
71
  echo 'Creating {{app.key}} database and user...' &&
63
- psql -d postgres -c 'CREATE DATABASE {{app.key}};' || echo '{{app.key}} database exists' &&
64
- psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || echo '{{app.key}}_user exists' &&
65
- psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' &&
66
- psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' &&
67
- psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' &&
72
+ (psql -d postgres -c 'CREATE DATABASE {{app.key}};' || true) &&
73
+ (psql -d postgres -c \"CREATE USER {{app.key}}_user WITH PASSWORD '{{app.key}}_pass123';\" || true) &&
74
+ psql -d postgres -c 'GRANT ALL PRIVILEGES ON DATABASE {{app.key}} TO {{app.key}}_user;' || true &&
75
+ psql -d {{app.key}} -c 'ALTER SCHEMA public OWNER TO {{app.key}}_user;' || true &&
76
+ psql -d {{app.key}} -c 'GRANT ALL ON SCHEMA public TO {{app.key}}_user;' || true &&
68
77
  {{/if}}
69
78
  echo 'Database initialization complete!'
70
79
  "