@gnar-engine/cli 1.0.7 → 1.0.10

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.
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/control/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -7,8 +7,9 @@ export const config = {
7
7
  serviceName: 'controlService',
8
8
 
9
9
  // environment
10
- environment: process.env.CONTROL_NODE_ENV || 'dev',
10
+ environment: process.env.CONTROL_NODE_ENV || 'development',
11
11
  runTests: process.env.CONTROL_RUN_TESTS || false,
12
+ resetDatabase: process.env.CONTROL_RESET_DATABASE || false,
12
13
 
13
14
  // microservice | modular-monolith
14
15
  architecture: process.env.GLOBAL_ARCHITECTURE || 'microservice',
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/notification/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/page/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -5,6 +5,11 @@ export const config = {
5
5
  // service name
6
6
  serviceName: 'pageService',
7
7
 
8
+ // environment
9
+ environment: process.env.PAGE_NODE_ENV || 'developnent',
10
+ runTests: process.env.PAGE_RUN_TESTS || false,
11
+ resetDatabase: process.env.PAGE_RESET_DATABASE || false,
12
+
8
13
  // microservice | modular-monolith
9
14
  architecture: process.env.GLOBAL_ARCHITECTURE || 'microservice',
10
15
 
@@ -8,13 +8,10 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/portal/ ./
11
+ COPY ./ ./
12
12
 
13
13
  # Install app dependencies
14
14
  RUN npm install
15
15
 
16
16
  # Expose the port the service will run on
17
17
  EXPOSE 5173
18
-
19
- # Start the application
20
- CMD ["npm", "run", "start:dev"]
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/user/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -9,6 +9,7 @@ export const config = {
9
9
  // environment
10
10
  environment: process.env.USER_NODE_ENV || 'dev',
11
11
  runTests: process.env.USER_RUN_TESTS || false,
12
+ resetDatabase: process.env.USER_RESET_DATABASE || false,
12
13
 
13
14
  // microservice | modular-monolith
14
15
  architecture: process.env.GLOBAL_ARCHITECTURE || 'microservice',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnar-engine/cli",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "Gnar Engine Development Framework CLI: Project bootstrap, scaffolder & control plane.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  "handlebars": "^4.7.8",
31
31
  "inquirer": "^12.5.2",
32
32
  "js-yaml": "^4.1.0",
33
+ "tar-fs": "^3.1.1",
33
34
  "uuid": "^11.1.0"
34
35
  }
35
36
  }
package/src/config.js CHANGED
@@ -3,14 +3,14 @@ import { fileURLToPath } from 'url';
3
3
 
4
4
  const __filename = fileURLToPath(import.meta.url);
5
5
  const __dirname = path.dirname(__filename);
6
+
6
7
  export const gnarEngineCliConfig = {
7
8
 
8
- /**
9
- * The path the Gnar Engine service core should be found in the service containers
10
- */
9
+ // The path the Gnar Engine service core should be found in the service containers
11
10
  corePath: '/usr/gnar_engine/app/node_modules/@gnar-engine/core',
12
- coreDevPath: path.join(__dirname, '../../core'),
13
11
 
12
+ // The path to the core source code on the host machine (core dev mode)
13
+ coreDevPath: path.join(__dirname, '../../core'),
14
14
  }
15
15
 
16
16
  export const directories = {
@@ -13,8 +13,10 @@ export function registerDevCommands(program) {
13
13
  .option('-b, --build', 'Build without cache')
14
14
  .option('-d, --detach', 'Run containers in background')
15
15
  .option('-a, --attach-all', 'Attach all services including database and message queues for debugging')
16
- .option('-t --test', 'Run the tests with ephemeral databases')
16
+ .option('-t --test', 'Run all tests with ephemeral databases *NOT IMPLEMENTED')
17
17
  .option('--test-service <service>', 'Run the tests for the specified service with ephemeral databases (e.g. --test-service user)')
18
+ .option('--reset-databases, --reset-databases', 'Drop all service databases, re-running all migrations and seeders *NOT IMPLEMENTED')
19
+ .option('--reset-database <service>', 'Drop the specified service database, re-running all migrations and seeders (e.g. --reset-database user)')
18
20
  .addOption(new Option('--core-dev').hideHelp())
19
21
  .addOption(new Option('--bootstrap-dev').hideHelp())
20
22
  .action(async (options) => {
@@ -44,6 +46,8 @@ export function registerDevCommands(program) {
44
46
  bootstrapDev: options.bootstrapDev || false,
45
47
  test: options.test || false,
46
48
  testService: options.testService || '',
49
+ resetDatabases: options.resetDatabases || false,
50
+ resetDatabase: options.resetDatabase || '',
47
51
  attachAll: options.attachAll || false
48
52
  });
49
53
  } catch (err) {
@@ -8,9 +8,11 @@ import yaml from "js-yaml";
8
8
  import { gnarEngineCliConfig } from "../config.js";
9
9
  import { buildImage, createContainer, createBridgeNetwork } from "../services/docker.js";
10
10
  import { directories } from "../config.js";
11
-
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
12
13
 
13
14
  const docker = new Docker();
15
+ const execAsync = promisify(exec);
14
16
 
15
17
  /**
16
18
  * Start the application locally
@@ -25,11 +27,25 @@ const docker = new Docker();
25
27
  * @param {boolean} [options.bootstrapDev=false] - Whether to set the cli/src/bootstrap directory as the project directory
26
28
  * @param {boolean} [options.test=false] - Whether to run tests with ephemeral databases
27
29
  * @param {string} [options.testService=''] - The service to run tests for (only applicable if test=true)
30
+ * @param {boolean} [options.resetDatabases=false] - Whether to drop all service databases, re-running all migrations and seeders
31
+ * @param {string} [options.resetDatabase=''] - The service database to drop, re-running all migrations and seeders
28
32
  * @param {boolean} [options.removeOrphans=true] - Whether to remove orphaned containers
29
33
  * @param {boolean} [options.attachAll=false] - Attach all services including database and message queues for debugging
30
34
  */
31
- export async function up({ projectDir, build = false, detach = false, coreDev = false, bootstrapDev = false, test = false, testService = '', removeOrphans = true, attachAll = false}) {
32
-
35
+ export async function up({
36
+ projectDir,
37
+ build = false,
38
+ detach = false,
39
+ coreDev = false,
40
+ bootstrapDev = false,
41
+ test = false,
42
+ testService = '',
43
+ resetDatabases = false,
44
+ resetDatabase = '',
45
+ removeOrphans = true,
46
+ attachAll = false
47
+ }) {
48
+
33
49
  // bootstrap dev
34
50
  if (bootstrapDev) {
35
51
  const fileDir = path.dirname(new URL(import.meta.url).pathname);
@@ -59,7 +75,7 @@ export async function up({ projectDir, build = false, detach = false, coreDev =
59
75
  });
60
76
  await fs.writeFile(nginxConfPath, nginxConf);
61
77
 
62
- // create docker-compose.yml dynamically from parsed config and secrets
78
+ // create and up containers
63
79
  const dockerComposePath = path.join(gnarHiddenDir, "docker-compose.dev.yml");
64
80
  const dockerCompose = await buildAndUpContainers({
65
81
  config: parsedConfig.config,
@@ -70,42 +86,11 @@ export async function up({ projectDir, build = false, detach = false, coreDev =
70
86
  bootstrapDev: bootstrapDev,
71
87
  test: test,
72
88
  testService: testService,
73
- attachAll: attachAll
89
+ resetDatabases: resetDatabases,
90
+ resetDatabase: resetDatabase,
91
+ attachAll: attachAll,
92
+ build: build,
74
93
  });
75
-
76
- // // up docker-compose
77
- // const args = ["-f", dockerComposePath, "up"];
78
- //
79
- // if (build) {
80
- // args.push("--build");
81
- // }
82
- //
83
- // if (detach) {
84
- // args.push("-d");
85
- // }
86
- //
87
- // if (removeOrphans) {
88
- // args.push("--remove-orphans")
89
- // }
90
-
91
- // const processRef = spawn(
92
- // "docker-compose",
93
- // args,
94
- // {
95
- // cwd: projectDir,
96
- // stdio: "inherit",
97
- // shell: "/bin/sh"
98
- // }
99
- // );
100
- //
101
- // // handle exit
102
- // const exitCode = await new Promise((resolve) => {
103
- // processRef.on("close", resolve);
104
- // });
105
- //
106
- // if (exitCode !== 0) {
107
- // throw new Error(`docker-compose up exited with code ${exitCode}`);
108
- // }
109
94
  }
110
95
 
111
96
  /**
@@ -144,6 +129,15 @@ export async function down({ projectDir, allContainers = false }) {
144
129
  })
145
130
  );
146
131
 
132
+ // // remove bound mounts
133
+ // await Promise.all(
134
+ // containers.map(async c => {
135
+ // const container = docker.getContainer(c.Id);
136
+ // const containerInfo = await container.inspect();
137
+ // await removeBindMounts({ containerInfo: containerInfo });
138
+ // })
139
+ // );
140
+
147
141
  // remove each container
148
142
  await Promise.all(
149
143
  containers.map(c => {
@@ -157,6 +151,26 @@ export async function down({ projectDir, allContainers = false }) {
157
151
  console.log('Containers stopped and removed.');
158
152
  }
159
153
 
154
+ /**
155
+ * Remove bind mounts
156
+ *
157
+ * @param {object} containerInfo
158
+ */
159
+ export async function removeBindMounts({containerInfo}) {
160
+
161
+ const binds = containerInfo.HostConfig.Binds || [];
162
+
163
+ for (const bind of binds) {
164
+ const hostPath = bind.split(':')[0];
165
+ try {
166
+ await execAsync(`sudo umount ${hostPath}`);
167
+ console.log(`Unmounted ${hostPath}`);
168
+ } catch (err) {
169
+ console.warn(`Failed to unmount ${hostPath}: ${err.message}`);
170
+ }
171
+ }
172
+ }
173
+
160
174
  /**
161
175
  * Create dynamic nginx.conf file for running application locally
162
176
  *
@@ -236,9 +250,24 @@ export async function createDynamicNginxConf({ config, serviceConfDir, projectDi
236
250
  * @param {boolean} build - Whether to re-build images
237
251
  * @param {boolean} test - Whether to run tests with ephemeral databases
238
252
  * @param {string} testService - The service to run tests for (only applicable if test=true)
253
+ * @param {boolean} resetDatabases - Whether to drop all service databases, re-running all migrations and seeders
254
+ * @
239
255
  * @param {boolean} attachAll - Whether to attach to all containers' stdio (otherwise databases and message queue are detached)
240
256
  */
241
- async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir, coreDev = false, bootstrapDev = false, build = false, test = false, testService, attachAll = false }) {
257
+ async function buildAndUpContainers({
258
+ config,
259
+ secrets,
260
+ gnarHiddenDir,
261
+ projectDir,
262
+ coreDev = false,
263
+ bootstrapDev = false,
264
+ build = false,
265
+ test = false,
266
+ testService,
267
+ resetDatabases = false,
268
+ resetDatabase = '',
269
+ attachAll = false
270
+ }) {
242
271
 
243
272
  let mysqlPortsCounter = 3306;
244
273
  let mongoPortsCounter = 27017;
@@ -248,9 +277,10 @@ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir
248
277
 
249
278
  console.log('======== g n a r e n g i n e ========');
250
279
  console.log('⛏️ Starting development environment...');
251
-
252
- // test mode env var adjustments
280
+
281
+ // env var adjustments
253
282
  for (const svc of config.services) {
283
+ // Tests
254
284
  if (test) {
255
285
  if (secrets.services?.[svc.name]?.MYSQL_HOST) {
256
286
  secrets.services[svc.name].MYSQL_HOST = 'db-mysql-test';
@@ -264,6 +294,22 @@ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir
264
294
  }
265
295
  }
266
296
  }
297
+
298
+ // Reset all databases
299
+ if (resetDatabases) {
300
+ if (secrets.services?.[svc.name]) {
301
+ secrets.services[svc.name].RESET_DATABASE = true;
302
+ }
303
+ }
304
+
305
+ // Reset specific database
306
+ else if (resetDatabase) {
307
+ if (svc.name === resetDatabase) {
308
+ if (secrets.services?.[svc.name]) {
309
+ secrets.services[svc.name].RESET_DATABASE = true;
310
+ }
311
+ }
312
+ }
267
313
  }
268
314
 
269
315
  // create bridge network
@@ -279,7 +325,8 @@ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir
279
325
  await buildImage({
280
326
  context: directories.provisioner,
281
327
  dockerfile: 'Dockerfile',
282
- imageTag: provisionerTag
328
+ imageTag: provisionerTag,
329
+ nocache: false
283
330
  });
284
331
  }
285
332
 
@@ -348,7 +395,8 @@ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir
348
395
  await buildImage({
349
396
  context: path.resolve(projectDir, 'services', svc.name),
350
397
  dockerfile: 'Dockerfile',
351
- imageTag: svcTag
398
+ imageTag: svcTag,
399
+ nocache: false
352
400
  });
353
401
  }
354
402
 
@@ -382,6 +430,13 @@ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir
382
430
  serviceVolumes.push(`${gnarEngineCliConfig.coreDevPath}:${gnarEngineCliConfig.corePath}`);
383
431
  }
384
432
 
433
+ if (svc.extra_binds) {
434
+ svc.extra_binds.forEach((bind, index) => {
435
+ bind = `${gnarHiddenDir}/data/${svc.name}-data/bind-${index}/:${bind}`;
436
+ serviceVolumes.push(bind);
437
+ })
438
+ }
439
+
385
440
  // split from "port:port" to { port: port }
386
441
  const ports = {};
387
442
  for (const portMapping of svc.ports || []) {
@@ -396,8 +451,8 @@ async function buildAndUpContainers({ config, secrets, gnarHiddenDir, projectDir
396
451
  env: env,
397
452
  ports: ports,
398
453
  binds: serviceVolumes,
399
- restart: 'always',
400
- attach: true,
454
+ restart: 'no',
455
+ attach: svc.detach ? !svc.detach : true,
401
456
  network: networkName,
402
457
  aliases: [`${svc.name}-service`]
403
458
  });
@@ -1,6 +1,7 @@
1
1
  import Docker from 'dockerode';
2
2
  import { Writable } from 'stream';
3
3
  import path from 'path';
4
+ import tar from 'tar-fs';
4
5
  import fs from 'fs';
5
6
 
6
7
  const docker = new Docker();
@@ -12,24 +13,38 @@ const docker = new Docker();
12
13
  * @param {string} options.context - The build context directory
13
14
  * @param {string} options.dockerfile - The Dockerfile path relative to the context
14
15
  * @param {string} options.imageTag - The tag to assign to the built image
16
+ * @param {boolean} [options.nocache=true] - Whether to build without cache
15
17
  */
16
- export async function buildImage({ context, dockerfile, imageTag }) {
18
+ export async function buildImage({ context, dockerfile, imageTag, nocache = true }) {
17
19
 
18
20
  console.log('Building image...', imageTag);
19
21
 
20
- const tarStream = await docker.buildImage(
21
- {
22
- context: context,
23
- src: fs.readdirSync(context)
24
- },
25
- {
26
- t: imageTag,
27
- dockerfile
28
- }
29
- );
22
+ // Create a tar stream of the full context folder
23
+ const tarStream = tar.pack(context);
24
+
25
+ const stream = await docker.buildImage(tarStream, {
26
+ t: imageTag,
27
+ dockerfile,
28
+ nocache
29
+ });
30
30
 
31
31
  await new Promise((resolve, reject) => {
32
- docker.modem.followProgress(tarStream, (err) => (err ? reject(err) : resolve()));
32
+ docker.modem.followProgress(
33
+ stream,
34
+ (err, res) => {
35
+ if (err) return reject(err);
36
+ resolve(res);
37
+ },
38
+ (event) => {
39
+ if (event.stream) process.stdout.write(event.stream);
40
+
41
+ // Catch BuildKit-style errors
42
+ if (event.error) return reject(new Error(event.error));
43
+ if (event.errorDetail && event.errorDetail.message) {
44
+ return reject(new Error(event.errorDetail.message));
45
+ }
46
+ }
47
+ );
33
48
  });
34
49
 
35
50
  console.log('Built image:', imageTag);
@@ -1,23 +1,93 @@
1
- import { db } from '@gnar-engine/core';
1
+ import { db, utils } from '@gnar-engine/core';
2
2
 
3
3
  export const {{entityName}} = {
4
4
  async getById({ id }) {
5
5
  const [result] = await db.query('SELECT * FROM {{lowerCasePlural entityName}} WHERE id = ?', [id]);
6
- return result || null;
6
+
7
+ if (!result) {
8
+ return null;
9
+ }
10
+
11
+ return db.sql.helpers.objectToCamelCase(result);
7
12
  },
8
13
 
9
- async getAll() {
10
- return await db.query('SELECT * FROM {{lowerCasePlural entityName}}');
14
+ async getAll({ pageSize = 100, pageNum = 1 }) {
15
+ pageSize = Number(pageSize);
16
+ pageNum = Number(pageNum);
17
+ const offset = (pageNum - 1) * pageSize;
18
+
19
+ const [rows] = await db.query(
20
+ 'SELECT * FROM {{lowerCasePlural entityName}} LIMIT ? OFFSET ?',
21
+ [pageSize, offset]
22
+ );
23
+
24
+ const [[{ total }]] = await db.query(
25
+ 'SELECT COUNT(*) AS total FROM {{lowerCasePlural entityName}}'
26
+ );
27
+
28
+ return {
29
+ data: rows.map(row => db.sql.helpers.objectToCamelCase(row)),
30
+ pagination: {
31
+ pageSize,
32
+ pageNum,
33
+ total
34
+ }
35
+ }
11
36
  },
12
37
 
13
38
  async create(data) {
14
- const { insertId } = await db.query('INSERT INTO {{lowerCasePlural entityName}} (created_at, updated_at) VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)');
15
- return await this.getById({ id: insertId });
39
+ const id = utils.uuid();
40
+
41
+ // map columns and insert
42
+ const columns = ['id', ...Object.keys(data).map(db.sql.helpers.toSnake)];
43
+ const placeholders = columns.map(() => '?');
44
+ const values = [id, ...Object.values(data)];
45
+
46
+ const sql = `
47
+ INSERT INTO {{lowerCasePlural entityName}} (
48
+ ${columns.join(', ')},
49
+ created_at,
50
+ updated_at
51
+ )
52
+ VALUES (
53
+ ${placeholders.join(', ')},
54
+ CURRENT_TIMESTAMP,
55
+ CURRENT_TIMESTAMP
56
+ )
57
+ `;
58
+ await db.query(sql, [...values, id]);
59
+
60
+ // return result
61
+ const newItem = await this.getById({ id });
62
+ return db.sql.helpers.objectToCamelCase(newItem);
16
63
  },
17
64
 
18
- async update({ id, ...data }) {
19
- await db.query('UPDATE {{lowerCasePlural entityName}} SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id]);
20
- return await this.getById({ id });
65
+ async update({ id, updatedData }) {
66
+ if (!Object.keys(updatedData).length) {
67
+ return this.getById({ id });
68
+ }
69
+
70
+ // Handle many to many relations
71
+ const newOwnerIds = updatedData.ownerIds ? updatedData.ownerIds : null;
72
+ delete updatedData.ownerIds;
73
+
74
+ // map columns and update
75
+ const columns = Object.keys(updatedData).map((key) => db.sql.helpers.toSnake(key));
76
+ const assignments = columns.map(col => `${col} = ?`);
77
+ const values = Object.values(updatedData);
78
+
79
+ const sql = `
80
+ UPDATE {{lowerCasePlural entityName}}
81
+ SET
82
+ ${assignments.join(', ')},
83
+ updated_at = CURRENT_TIMESTAMP
84
+ WHERE id = ?
85
+ `;
86
+ await db.query(sql, [...values, id]);
87
+
88
+ // return result
89
+ const newItem = await this.getById({ id });
90
+ return db.sql.helpers.objectToCamelCase(newItem);
21
91
  },
22
92
 
23
93
  async delete({ id }) {
@@ -8,7 +8,7 @@ WORKDIR /usr/gnar_engine/app
8
8
  ENV GLOBAL_SERVICE_BASE_DIR=/usr/gnar_engine/app/src/
9
9
 
10
10
  # Copy package.json and package-lock.json
11
- COPY ./services/{{serviceName}}/package*.json ./
11
+ COPY ./package*.json ./
12
12
 
13
13
  # Install nodemon
14
14
  RUN npm install -g nodemon
@@ -18,6 +18,3 @@ RUN npm install
18
18
 
19
19
  # Expose the port the service will run on
20
20
  EXPOSE 3000
21
-
22
- # Start the application
23
- CMD ["nodemon", "--watch", "./gnar_engine", "./gnar_engine/app.js"]
@@ -8,13 +8,9 @@ import { httpController as {{lowerCase serviceName}}PlatformHttpController } fro
8
8
  */
9
9
  export const initService = async () => {
10
10
 
11
- // Run migrations
12
- if (config.db.type == 'mysql') {
13
- db.migrations.runMigrations({config});
14
- }
15
-
16
- // Run seeders
17
- db.seeders.runSeeders({config});
11
+ // Run migrations & seeders
12
+ await db.migrations.runMigrations({config});
13
+ await db.seeders.runSeeders({config});
18
14
 
19
15
  // Import command handlers after the command bus is initialised
20
16
  await import('./commands/{{lowerCase serviceName}}.handler.js');
@@ -5,6 +5,11 @@ export const config = {
5
5
  // service name
6
6
  serviceName: '{{serviceName}}Service',
7
7
 
8
+ // environment
9
+ environment: process.env.{{upperCase serviceName}}_NODE_ENV || 'development',
10
+ runTests: process.env.{{upperCase serviceName}}_RUN_TESTS || false,
11
+ resetDatabase: process.env.{{upperCase serviceName}}_RESET_DATABASE || false,
12
+
8
13
  // microservice | modular-monolith
9
14
  architecture: process.env.GLOBAL_ARCHITECTURE || 'microservice',
10
15
 
@@ -6,8 +6,9 @@ export const config = {
6
6
  serviceName: '{{serviceName}}Service',
7
7
 
8
8
  // environment
9
- environment: process.env.{{upperCase serviceName}}_NODE_ENV || 'dev',
9
+ environment: process.env.{{upperCase serviceName}}_NODE_ENV || 'development',
10
10
  runTests: process.env.{{upperCase serviceName}}_RUN_TESTS || false,
11
+ resetDatabase: process.env.{{upperCase serviceName}}_RESET_DATABASE || false,
11
12
 
12
13
  // microservice | modular-monolith
13
14
  architecture: process.env.GLOBAL_ARCHITECTURE || 'microservice',
@@ -1,28 +1,93 @@
1
- import { db } from '@gnar-engine/core';
1
+ import { db, utils } from '@gnar-engine/core';
2
2
 
3
3
  export const {{serviceName}} = {
4
4
  async getById({ id }) {
5
5
  const [result] = await db.query('SELECT * FROM {{lowerCasePlural serviceName}} WHERE id = ?', [id]);
6
- return result || null;
7
- },
8
6
 
9
- async getByEmail({ email }) {
10
- // Placeholder: implement if your service uses email
11
- return null;
7
+ if (!result) {
8
+ return null;
9
+ }
10
+
11
+ return db.sql.helpers.objectToCamelCase(result);
12
12
  },
13
13
 
14
- async getAll() {
15
- return await db.query('SELECT * FROM {{lowerCasePlural serviceName}}');
14
+ async getAll({ pageSize = 100, pageNum = 1 }) {
15
+ pageSize = Number(pageSize);
16
+ pageNum = Number(pageNum);
17
+ const offset = (pageNum - 1) * pageSize;
18
+
19
+ const [rows] = await db.query(
20
+ 'SELECT * FROM {{lowerCasePlural serviceName}} LIMIT ? OFFSET ?',
21
+ [pageSize, offset]
22
+ );
23
+
24
+ const [[{ total }]] = await db.query(
25
+ 'SELECT COUNT(*) AS total FROM {{lowerCasePlural serviceName}}'
26
+ );
27
+
28
+ return {
29
+ data: rows.map(row => db.sql.helpers.objectToCamelCase(row)),
30
+ pagination: {
31
+ pageSize,
32
+ pageNum,
33
+ total
34
+ }
35
+ }
16
36
  },
17
37
 
18
38
  async create(data) {
19
- const { insertId } = await db.query('INSERT INTO {{lowerCasePlural serviceName}} (created_at, updated_at) VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)');
20
- return await this.getById({ id: insertId });
39
+ const id = utils.uuid();
40
+
41
+ // map columns and insert
42
+ const columns = ['id', ...Object.keys(data).map(db.sql.helpers.toSnake)];
43
+ const placeholders = columns.map(() => '?');
44
+ const values = [id, ...Object.values(data)];
45
+
46
+ const sql = `
47
+ INSERT INTO {{lowerCasePlural serviceName}} (
48
+ ${columns.join(', ')},
49
+ created_at,
50
+ updated_at
51
+ )
52
+ VALUES (
53
+ ${placeholders.join(', ')},
54
+ CURRENT_TIMESTAMP,
55
+ CURRENT_TIMESTAMP
56
+ )
57
+ `;
58
+ await db.query(sql, [...values, id]);
59
+
60
+ // return result
61
+ const newItem = await this.getById({ id });
62
+ return db.sql.helpers.objectToCamelCase(newItem);
21
63
  },
22
64
 
23
- async update({ id, ...data }) {
24
- await db.query('UPDATE {{lowerCasePlural serviceName}} SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id]);
25
- return await this.getById({ id });
65
+ async update({ id, updatedData }) {
66
+ if (!Object.keys(updatedData).length) {
67
+ return this.getById({ id });
68
+ }
69
+
70
+ // Handle many to many relations
71
+ const newOwnerIds = updatedData.ownerIds ? updatedData.ownerIds : null;
72
+ delete updatedData.ownerIds;
73
+
74
+ // map columns and update
75
+ const columns = Object.keys(updatedData).map((key) => db.sql.helpers.toSnake(key));
76
+ const assignments = columns.map(col => `${col} = ?`);
77
+ const values = Object.values(updatedData);
78
+
79
+ const sql = `
80
+ UPDATE {{lowerCasePlural serviceName}}
81
+ SET
82
+ ${assignments.join(', ')},
83
+ updated_at = CURRENT_TIMESTAMP
84
+ WHERE id = ?
85
+ `;
86
+ await db.query(sql, [...values, id]);
87
+
88
+ // return result
89
+ const newItem = await this.getById({ id });
90
+ return db.sql.helpers.objectToCamelCase(newItem);
26
91
  },
27
92
 
28
93
  async delete({ id }) {