@gnar-engine/cli 1.0.5 → 1.0.7
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/bootstrap/deploy.localdev.yml +14 -0
- package/bootstrap/secrets.localdev.yml +7 -3
- package/bootstrap/services/notification/Dockerfile +2 -2
- package/bootstrap/services/notification/package.json +14 -32
- package/bootstrap/services/notification/src/app.js +50 -48
- package/bootstrap/services/notification/src/commands/notification.handler.js +96 -0
- package/bootstrap/services/notification/src/config.js +55 -12
- package/bootstrap/services/notification/src/controllers/http.controller.js +87 -0
- package/bootstrap/services/notification/src/controllers/message.controller.js +39 -70
- package/bootstrap/services/notification/src/db/migrations/01-init.js +50 -0
- package/bootstrap/services/notification/src/db/migrations/02-notification-service-init.js +23 -0
- package/bootstrap/services/notification/src/policies/notification.policy.js +49 -0
- package/bootstrap/services/notification/src/schema/notification.schema.js +17 -0
- package/bootstrap/services/notification/src/services/notification.service.js +32 -0
- package/bootstrap/services/portal/src/services/client.js +3 -0
- package/bootstrap/services/user/src/commands/user.handler.js +35 -18
- package/bootstrap/services/user/src/tests/commands/user.test.js +15 -6
- package/install-from-clone.sh +1 -1
- package/package.json +1 -1
- package/src/cli.js +0 -6
- package/src/config.js +13 -1
- package/src/dev/commands.js +7 -3
- package/src/dev/dev.service.js +192 -128
- package/src/helpers/helpers.js +24 -0
- package/src/profiles/command.js +41 -0
- package/src/profiles/profiles.client.js +23 -0
- package/src/scaffolder/commands.js +57 -1
- package/src/scaffolder/scaffolder.handler.js +127 -60
- package/src/services/docker.js +173 -0
- package/templates/entity/src/commands/{{entityName}}.handler.js.hbs +94 -0
- package/templates/entity/src/controllers/{{entityName}}.http.controller.js.hbs +87 -0
- package/templates/entity/src/mysql.db/migrations/03-{{entityName}}-entity-init.js.hbs +23 -0
- package/templates/entity/src/policies/{{entityName}}.policy.js.hbs +49 -0
- package/templates/entity/src/schema/{{entityName}}.schema.js.hbs +17 -0
- package/templates/entity/src/services/mongodb.{{entityName}}.service.js.hbs +70 -0
- package/templates/entity/src/services/mysql.{{entityName}}.service.js.hbs +27 -0
- package/bootstrap/services/notification/Dockerfile.prod +0 -37
- package/bootstrap/services/notification/README.md +0 -3
- package/bootstrap/services/notification/src/commands/command-bus.js +0 -20
- package/bootstrap/services/notification/src/commands/handlers/control.handler.js +0 -18
- package/bootstrap/services/notification/src/commands/handlers/notification.handler.js +0 -157
- package/bootstrap/services/notification/src/services/logger.service.js +0 -16
- package/bootstrap/services/notification/src/services/ses.service.js +0 -23
- package/bootstrap/services/notification/src/templates/admin-order-recieved.hbs +0 -136
- package/bootstrap/services/notification/src/templates/admin-subscription-failed.hbs +0 -87
- package/bootstrap/services/notification/src/templates/customer-order-recieved.hbs +0 -132
- package/bootstrap/services/notification/src/templates/customer-subscription-failed.hbs +0 -77
- package/bootstrap/services/notification/src/tests/notification.test.js +0 -0
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { profiles } from '../profiles/profiles.client.js';
|
|
5
5
|
import { helpers } from '../helpers/helpers.js';
|
|
6
|
-
import { directories } from '../
|
|
6
|
+
import { directories } from '../config.js';
|
|
7
7
|
import Handlebars from 'handlebars';
|
|
8
8
|
|
|
9
9
|
|
|
@@ -36,67 +36,21 @@ export const scaffolder = {
|
|
|
36
36
|
|
|
37
37
|
// Get all files in the templates directory
|
|
38
38
|
const templateFiles = scaffolder.getAllTemplateFiles({
|
|
39
|
-
dir: directories.
|
|
40
|
-
baseDir: directories.
|
|
39
|
+
dir: directories.scaffolderServiceTemplates,
|
|
40
|
+
baseDir: directories.scaffolderServiceTemplates
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Write the files to the service directory
|
|
49
|
-
templateFiles.forEach(file => {
|
|
50
|
-
let sourcePath;
|
|
51
|
-
let targetPath;
|
|
52
|
-
const templateArgs = {
|
|
53
|
-
serviceName,
|
|
54
|
-
database
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
let fileRelativePath = file.relativePath;
|
|
58
|
-
|
|
59
|
-
// Database specific
|
|
60
|
-
if (fileRelativePath.includes('mongodb.')) {
|
|
61
|
-
if (database !== 'mongodb') {
|
|
62
|
-
return;
|
|
63
|
-
} else {
|
|
64
|
-
fileRelativePath = fileRelativePath.replace('mongodb.', '');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (fileRelativePath.includes('mysql.')) {
|
|
69
|
-
if (database !== 'mysql') {
|
|
70
|
-
return;
|
|
71
|
-
} else {
|
|
72
|
-
fileRelativePath = fileRelativePath.replace('mysql.', '');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
43
|
+
// scaffold the hbs templates
|
|
44
|
+
const templateArgs = {
|
|
45
|
+
serviceName,
|
|
46
|
+
database
|
|
47
|
+
};
|
|
75
48
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const renderedContent = compiledTemplate(templateArgs);
|
|
82
|
-
|
|
83
|
-
// Compile the Handlebars template for the filename (excluding .hbs)
|
|
84
|
-
const filenameTemplate = Handlebars.compile(fileRelativePath.replace(/\.hbs$/, ''));
|
|
85
|
-
const renderedFilename = filenameTemplate(templateArgs);
|
|
86
|
-
targetPath = path.join(serviceDir, renderedFilename);
|
|
87
|
-
|
|
88
|
-
// Ensure directory exists
|
|
89
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
90
|
-
fs.writeFileSync(targetPath, renderedContent, 'utf8');
|
|
91
|
-
break;
|
|
92
|
-
default:
|
|
93
|
-
// By default, copy the file to the service directory
|
|
94
|
-
sourcePath = file.fullPath;
|
|
95
|
-
targetPath = path.join(serviceDir, fileRelativePath);
|
|
96
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
97
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
49
|
+
scaffolder.scaffoldHandlebarTemplates({
|
|
50
|
+
templateFiles: templateFiles,
|
|
51
|
+
serviceDir: serviceDir,
|
|
52
|
+
database: database,
|
|
53
|
+
templateArgs: templateArgs
|
|
100
54
|
});
|
|
101
55
|
|
|
102
56
|
// Scaffold deploy.yml
|
|
@@ -173,8 +127,10 @@ export const scaffolder = {
|
|
|
173
127
|
|
|
174
128
|
entries.forEach(entry => {
|
|
175
129
|
const fullPath = path.join(dir, entry.name);
|
|
130
|
+
// if the full path contains src, the 'data' directory can be included
|
|
131
|
+
const includeDataDir = fullPath.includes(`${path.sep}src${path.sep}`) && fullPath.includes('data') ? true : false;
|
|
176
132
|
if (entry.isDirectory()) {
|
|
177
|
-
if (entry.name !== 'node_modules' && entry.name !== 'data') {
|
|
133
|
+
if (entry.name !== 'node_modules' && (entry.name !== 'data' || includeDataDir)) {
|
|
178
134
|
if (!entry.name.startsWith('.') || entry.name == '.gnarengine') {
|
|
179
135
|
scaffolder.getAllTemplateFiles({
|
|
180
136
|
dir: fullPath,
|
|
@@ -446,5 +402,116 @@ export const scaffolder = {
|
|
|
446
402
|
// write deploy.yml file
|
|
447
403
|
const deployYmlContent = yaml.dump(deploy);
|
|
448
404
|
fs.writeFileSync(deployPath, deployYmlContent, 'utf8');
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create new entity in existing service
|
|
409
|
+
*
|
|
410
|
+
* @param {object} param
|
|
411
|
+
* @param {string} param.entityName - The name of the entity to create
|
|
412
|
+
* @param {string} param.inService - The service in which to add the entity
|
|
413
|
+
* @param {string} param.serviceDir - The service directory where the entity will be created
|
|
414
|
+
* @param {string} param.database - The database type (e.g., 'mysql', 'mongodb')
|
|
415
|
+
* @returns {object} - An object containing a success message
|
|
416
|
+
*/
|
|
417
|
+
createNewEntity: ({ entityName, inService, serviceDir, database }) => {
|
|
418
|
+
|
|
419
|
+
entityName = entityName.toLowerCase();
|
|
420
|
+
|
|
421
|
+
// validate serviceDir exists
|
|
422
|
+
if (!fs.existsSync(serviceDir)) {
|
|
423
|
+
throw new Error(`Service directory "${serviceDir}" does not exist`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Get all files in the templates directory
|
|
427
|
+
const templateFiles = scaffolder.getAllTemplateFiles({
|
|
428
|
+
dir: directories.scaffolderEntityTemplates,
|
|
429
|
+
baseDir: directories.scaffolderEntityTemplates
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// scaffold the hbs templates
|
|
433
|
+
const templateArgs = {
|
|
434
|
+
entityName: entityName,
|
|
435
|
+
serviceName: inService,
|
|
436
|
+
database: database
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
scaffolder.scaffoldHandlebarTemplates({
|
|
440
|
+
templateFiles: templateFiles,
|
|
441
|
+
serviceDir: serviceDir,
|
|
442
|
+
database: database,
|
|
443
|
+
templateArgs: templateArgs
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Scaffold handlebar templates
|
|
449
|
+
*
|
|
450
|
+
* @param {object} param
|
|
451
|
+
* @param {string} param.templateFiles - The list of template files
|
|
452
|
+
* @param {string} param.serviceDir - The service directory
|
|
453
|
+
* @param {string} param.serviceName - The service name
|
|
454
|
+
* @param {string} param.database - The database type
|
|
455
|
+
* @param {object} param.templateArgs - The template arguments
|
|
456
|
+
*/
|
|
457
|
+
scaffoldHandlebarTemplates: function ({ templateFiles, serviceDir, serviceName, database, templateArgs }) {
|
|
458
|
+
try {
|
|
459
|
+
// Register Handlebars helpers
|
|
460
|
+
Object.entries(helpers).forEach(([name, fn]) => {
|
|
461
|
+
Handlebars.registerHelper(name, fn);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Write the files to the service directory
|
|
465
|
+
templateFiles.forEach(file => {
|
|
466
|
+
let sourcePath;
|
|
467
|
+
let targetPath;
|
|
468
|
+
|
|
469
|
+
let fileRelativePath = file.relativePath;
|
|
470
|
+
|
|
471
|
+
// Database specific
|
|
472
|
+
if (fileRelativePath.includes('mongodb.')) {
|
|
473
|
+
if (database !== 'mongodb') {
|
|
474
|
+
return;
|
|
475
|
+
} else {
|
|
476
|
+
fileRelativePath = fileRelativePath.replace('mongodb.', '');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (fileRelativePath.includes('mysql.')) {
|
|
481
|
+
if (database !== 'mysql') {
|
|
482
|
+
return;
|
|
483
|
+
} else {
|
|
484
|
+
fileRelativePath = fileRelativePath.replace('mysql.', '');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
switch (file.extension) {
|
|
489
|
+
case '.hbs':
|
|
490
|
+
// Compile the Handlebars template for content
|
|
491
|
+
const templateContent = fs.readFileSync(file.fullPath, 'utf8');
|
|
492
|
+
const compiledTemplate = Handlebars.compile(templateContent);
|
|
493
|
+
const renderedContent = compiledTemplate(templateArgs);
|
|
494
|
+
|
|
495
|
+
// Compile the Handlebars template for the filename (excluding .hbs)
|
|
496
|
+
const filenameTemplate = Handlebars.compile(fileRelativePath.replace(/\.hbs$/, ''));
|
|
497
|
+
const renderedFilename = filenameTemplate(templateArgs);
|
|
498
|
+
targetPath = path.join(serviceDir, renderedFilename);
|
|
499
|
+
|
|
500
|
+
// Ensure directory exists
|
|
501
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
502
|
+
fs.writeFileSync(targetPath, renderedContent, 'utf8');
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
// By default, copy the file to the service directory
|
|
506
|
+
sourcePath = file.fullPath;
|
|
507
|
+
targetPath = path.join(serviceDir, fileRelativePath);
|
|
508
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
509
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
throw new Error('Error scaffolding Handlebars templates: ' + error.message);
|
|
515
|
+
}
|
|
449
516
|
}
|
|
450
517
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import Docker from 'dockerode';
|
|
2
|
+
import { Writable } from 'stream';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
const docker = new Docker();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build Docker image
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {string} options.context - The build context directory
|
|
13
|
+
* @param {string} options.dockerfile - The Dockerfile path relative to the context
|
|
14
|
+
* @param {string} options.imageTag - The tag to assign to the built image
|
|
15
|
+
*/
|
|
16
|
+
export async function buildImage({ context, dockerfile, imageTag }) {
|
|
17
|
+
|
|
18
|
+
console.log('Building image...', imageTag);
|
|
19
|
+
|
|
20
|
+
const tarStream = await docker.buildImage(
|
|
21
|
+
{
|
|
22
|
+
context: context,
|
|
23
|
+
src: fs.readdirSync(context)
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
t: imageTag,
|
|
27
|
+
dockerfile
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
docker.modem.followProgress(tarStream, (err) => (err ? reject(err) : resolve()));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
console.log('Built image:', imageTag);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create docker container
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} options
|
|
42
|
+
* @param {string} options.name - Container name
|
|
43
|
+
* @param {string} options.image - Docker image to use
|
|
44
|
+
* @param {Array} [options.command] - Command to run in the container
|
|
45
|
+
* @param {Object} [options.env] - Environment variables
|
|
46
|
+
* @param {Object} [options.ports] - Port mappings (containerPort: hostPort)
|
|
47
|
+
* @param {Array} [options.binds] - Volume bindings
|
|
48
|
+
* @param {string} [options.restart] - Restart policy
|
|
49
|
+
* @param {boolean} [options.attach] - Whether to attach to container output
|
|
50
|
+
* @param {string} [options.network] - Network name
|
|
51
|
+
* @returns {Promise<Docker.Container>} - The started container
|
|
52
|
+
*/
|
|
53
|
+
export async function createContainer({ name, image, command = [], env = {}, ports = {}, binds = [], restart = 'always', attach = true, network, aliases = [] }) {
|
|
54
|
+
|
|
55
|
+
// remove container first
|
|
56
|
+
try {
|
|
57
|
+
const existingContainer = docker.getContainer(name);
|
|
58
|
+
await existingContainer.inspect();
|
|
59
|
+
await existingContainer.remove({ force: true });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Container does not exist, ignore
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// create container
|
|
65
|
+
const containerConfig = {
|
|
66
|
+
name,
|
|
67
|
+
Image: image,
|
|
68
|
+
Cmd: command,
|
|
69
|
+
Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),
|
|
70
|
+
HostConfig: {
|
|
71
|
+
RestartPolicy: { Name: restart },
|
|
72
|
+
Binds: binds,
|
|
73
|
+
PortBindings: Object.fromEntries(
|
|
74
|
+
Object.entries(ports).map(([cPort, hPort]) => [
|
|
75
|
+
`${cPort}/tcp`,
|
|
76
|
+
[{ HostPort: String(hPort) }]
|
|
77
|
+
])
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
ExposedPorts: Object.fromEntries(
|
|
81
|
+
Object.keys(ports).map(p => [`${p}/tcp`, {}])
|
|
82
|
+
)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const container = await docker.createContainer(containerConfig);
|
|
86
|
+
|
|
87
|
+
// Attach logs before starting the container
|
|
88
|
+
if (attach) {
|
|
89
|
+
const stream = await container.attach({ stream: true, stdout: true, stderr: true, logs: true });
|
|
90
|
+
const stdoutStream = createPrefixStream(name, process.stdout);
|
|
91
|
+
const stderrStream = createPrefixStream(name, process.stderr);
|
|
92
|
+
|
|
93
|
+
container.modem.demuxStream(stream, stdoutStream, stderrStream);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await docker.getNetwork(network).connect({
|
|
97
|
+
Container: container.id,
|
|
98
|
+
EndpointConfig: {
|
|
99
|
+
Aliases: aliases
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return container;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create network
|
|
108
|
+
*
|
|
109
|
+
* @param {String} name - Network name
|
|
110
|
+
*/
|
|
111
|
+
export async function createBridgeNetwork({ name }) {
|
|
112
|
+
try {
|
|
113
|
+
await docker.createNetwork({
|
|
114
|
+
Name: name,
|
|
115
|
+
Driver: 'bridge',
|
|
116
|
+
CheckDuplicate: true
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err.statusCode === 409) {
|
|
120
|
+
// network already exists, ignore
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.error(err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Tansform stream
|
|
129
|
+
*
|
|
130
|
+
* @param {String} name - Container name
|
|
131
|
+
* @returns {Transform} - Transform stream
|
|
132
|
+
*/
|
|
133
|
+
function createPrefixStream(name, targetStream) {
|
|
134
|
+
const color = colorForName(name);
|
|
135
|
+
const labelWidth = 38;
|
|
136
|
+
name = '[' + name + ']';
|
|
137
|
+
const paddedName = name.padEnd(labelWidth, ' ');
|
|
138
|
+
|
|
139
|
+
return new Writable({
|
|
140
|
+
write(chunk, encoding, callback) {
|
|
141
|
+
const lines = chunk.toString().split(/\r?\n/);
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
if (line.trim()) {
|
|
144
|
+
targetStream.write(`${color}${paddedName}${RESET} ${line}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
callback();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Colours for container name
|
|
154
|
+
*/
|
|
155
|
+
const COLORS = [
|
|
156
|
+
'\x1b[31m', // red
|
|
157
|
+
'\x1b[32m', // green
|
|
158
|
+
'\x1b[33m', // yellow
|
|
159
|
+
'\x1b[34m', // blue
|
|
160
|
+
'\x1b[35m', // magenta
|
|
161
|
+
'\x1b[36m', // cyan
|
|
162
|
+
'\x1b[37m', // white
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const RESET = '\x1b[0m';
|
|
166
|
+
|
|
167
|
+
function colorForName(name) {
|
|
168
|
+
let hash = 0;
|
|
169
|
+
for (let i = 0; i < name.length; i++) {
|
|
170
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
171
|
+
}
|
|
172
|
+
return COLORS[Math.abs(hash) % COLORS.length];
|
|
173
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { commands, logger, error } from '@gnar-engine/core';
|
|
2
|
+
import { {{entityName}} } from '../services/{{entityName}}.service.js';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { validate{{pascalCase entityName}} } from '../schema/{{entityName}}.schema.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get single {{entityName}}
|
|
9
|
+
*/
|
|
10
|
+
commands.register('{{serviceName}}Service.getSingle{{pascalCase entityName}}', async ({id}) => {
|
|
11
|
+
if (id) {
|
|
12
|
+
return await {{entityName}}.getById({id: id});
|
|
13
|
+
} else {
|
|
14
|
+
throw new error.badRequest('{{pascalCase entityName}} id required');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get many {{lowerCasePlural entityName}}
|
|
20
|
+
*/
|
|
21
|
+
commands.register('{{serviceName}}Service.getMany{{pascalCasePlural entityName}}', async ({}) => {
|
|
22
|
+
return await {{entityName}}.getAll();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create {{lowerCasePlural entityName}}
|
|
27
|
+
*/
|
|
28
|
+
commands.register('{{serviceName}}Service.create{{pascalCasePlural entityName}}', async ({ {{lowerCasePlural entityName}} }) => {
|
|
29
|
+
const validationErrors = [];
|
|
30
|
+
let createdNew{{pascalCasePlural entityName}} = [];
|
|
31
|
+
|
|
32
|
+
for (const newData of {{lowerCasePlural entityName}}) {
|
|
33
|
+
const { errors } = validate{{pascalCase entityName}}(newData);
|
|
34
|
+
if (errors?.length) {
|
|
35
|
+
validationErrors.push(errors);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const created = await {{entityName}}.create(newData);
|
|
40
|
+
createdNew{{pascalCasePlural entityName}}.push(created);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (validationErrors.length) {
|
|
44
|
+
throw new error.badRequest(`Invalid {{entityName}} data: ${validationErrors}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return createdNew{{pascalCasePlural entityName}};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update {{entityName}}
|
|
52
|
+
*/
|
|
53
|
+
commands.register('{{serviceName}}Service.update{{pascalCase entityName}}', async ({id, new{{pascalCase entityName}}Data}) => {
|
|
54
|
+
|
|
55
|
+
const validationErrors = [];
|
|
56
|
+
|
|
57
|
+
if (!id) {
|
|
58
|
+
throw new error.badRequest('{{pascalCase entityName}} ID required');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const obj = await {{entityName}}.getById({id: id});
|
|
62
|
+
|
|
63
|
+
if (!obj) {
|
|
64
|
+
throw new error.notFound('{{pascalCase entityName}} not found');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
delete new{{pascalCase entityName}}Data.id;
|
|
68
|
+
|
|
69
|
+
const { errors } = validate{{pascalCase entityName}}Update(new{{pascalCase entityName}}Data);
|
|
70
|
+
|
|
71
|
+
if (errors?.length) {
|
|
72
|
+
validationErrors.push(errors);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (validationErrors.length) {
|
|
76
|
+
throw new error.badRequest(`Invalid {{entityName}} data: ${validationErrors}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return await {{entityName}}.update({
|
|
80
|
+
id: id,
|
|
81
|
+
updatedData: new{{pascalCase entityName}}Data
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete {{entityName}}
|
|
87
|
+
*/
|
|
88
|
+
commands.register('{{serviceName}}Service.delete{{pascalCase entityName}}', async ({id}) => {
|
|
89
|
+
const obj = await {{entityName}}.getById({id: id});
|
|
90
|
+
if (!obj) {
|
|
91
|
+
throw new error.notFound('{{pascalCase entityName}} not found');
|
|
92
|
+
}
|
|
93
|
+
return await {{entityName}}.delete({id: id});
|
|
94
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { commands } from '@gnar-engine/core';
|
|
2
|
+
import { authorise } from '../policies/{{entityName}}.policy.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP controller
|
|
6
|
+
*/
|
|
7
|
+
export const httpController = {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get single {{entityName}}
|
|
11
|
+
*/
|
|
12
|
+
getSingle: {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
url: '/{{lowerCasePlural entityName}}/:id',
|
|
15
|
+
preHandler: async (request, reply) => authorise.getSingle(request, reply),
|
|
16
|
+
handler: async (request, reply) => {
|
|
17
|
+
const params = {
|
|
18
|
+
id: request.params.id
|
|
19
|
+
};
|
|
20
|
+
const result = await commands.execute('getSingle{{pascalCase entityName}}', params);
|
|
21
|
+
reply.code(200).send({ {{entityName}}: result });
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get multiple {{lowerCasePlural entityName}}
|
|
27
|
+
*/
|
|
28
|
+
getMany: {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
url: '/{{lowerCasePlural entityName}}/',
|
|
31
|
+
preHandler: async (request, reply) => authorise.getMany(request, reply),
|
|
32
|
+
handler: async (request, reply) => {
|
|
33
|
+
const params = {};
|
|
34
|
+
const results = await commands.execute('getMany{{pascalCasePlural entityName}}', params);
|
|
35
|
+
reply.code(200).send({ {{lowerCasePlural entityName}}: results });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create new {{entityName}}
|
|
41
|
+
*/
|
|
42
|
+
create: {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
url: '/{{lowerCasePlural entityName}}/',
|
|
45
|
+
preHandler: async (request, reply) => authorise.create(request, reply),
|
|
46
|
+
handler: async (request, reply) => {
|
|
47
|
+
const params = {
|
|
48
|
+
{{lowerCasePlural entityName}}: [request.body.{{entityName}}]
|
|
49
|
+
};
|
|
50
|
+
const results = await commands.execute('create{{pascalCasePlural entityName}}', params);
|
|
51
|
+
reply.code(200).send({ {{lowerCasePlural entityName}}: results });
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update {{entityName}}
|
|
57
|
+
*/
|
|
58
|
+
update: {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
url: '/{{lowerCasePlural entityName}}/:id',
|
|
61
|
+
preHandler: async (request, reply) => authorise.update(request, reply),
|
|
62
|
+
handler: async (request, reply) => {
|
|
63
|
+
const params = {
|
|
64
|
+
id: request.params.id,
|
|
65
|
+
new{{pascalCase entityName}}Data: request.body
|
|
66
|
+
};
|
|
67
|
+
const result = await commands.execute('update{{pascalCase entityName}}', params);
|
|
68
|
+
reply.code(200).send({ {{entityName}}: result });
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete {{entityName}}
|
|
74
|
+
*/
|
|
75
|
+
delete: {
|
|
76
|
+
method: 'DELETE',
|
|
77
|
+
url: '/{{lowerCasePlural entityName}}/:id',
|
|
78
|
+
preHandler: async (request, reply) => authorise.delete(request, reply),
|
|
79
|
+
handler: async (request, reply) => {
|
|
80
|
+
const params = {
|
|
81
|
+
id: request.params.id
|
|
82
|
+
};
|
|
83
|
+
await commands.execute('delete{{pascalCase entityName}}', params);
|
|
84
|
+
reply.code(200).send({ message: '{{pascalCase entityName}} deleted' });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { logger, db } from '@gnar-engine/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Up
|
|
5
|
+
*/
|
|
6
|
+
export const up = async () => {
|
|
7
|
+
logger.info('Creating table: {{lowerCasePlural entityName}}');
|
|
8
|
+
await db.query(`
|
|
9
|
+
CREATE TABLE {{lowerCasePlural entityName}} (
|
|
10
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
11
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
13
|
+
)
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Down
|
|
19
|
+
*/
|
|
20
|
+
export const down = async () => {
|
|
21
|
+
logger.info('Dropping table: {{lowerCasePlural entityName}}');
|
|
22
|
+
await db.query('DROP TABLE IF EXISTS {{lowerCasePlural entityName}}');
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { config } from '../config.js';
|
|
2
|
+
|
|
3
|
+
export const authorise = {
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authorise get single {{entityName}}
|
|
7
|
+
*/
|
|
8
|
+
getSingle: async (request, reply) => {
|
|
9
|
+
if (!request.user || request.user.role !== 'service_admin') {
|
|
10
|
+
reply.code(403).send({error: 'not authorised'});
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Authorise get many {{lowerCasePlural entityName}}
|
|
16
|
+
*/
|
|
17
|
+
getMany: async (request, reply) => {
|
|
18
|
+
if (!request.user || request.user.role !== 'service_admin') {
|
|
19
|
+
reply.code(403).send({error: 'not authorised'});
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Authorise create {{lowerCasePlural entityName}}
|
|
25
|
+
*/
|
|
26
|
+
create: async (request, reply) => {
|
|
27
|
+
if (!request.user || request.user.role !== 'service_admin') {
|
|
28
|
+
reply.code(403).send({error: 'not authorised'});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authorise update {{entityName}}
|
|
34
|
+
*/
|
|
35
|
+
update: async (request, reply) => {
|
|
36
|
+
if (!request.user || request.user.role !== 'service_admin') {
|
|
37
|
+
reply.code(403).send({error: 'not authorised'});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Authorise delete {{entityName}}
|
|
43
|
+
*/
|
|
44
|
+
delete: async (request, reply) => {
|
|
45
|
+
if (!request.user || request.user.role !== 'service_admin') {
|
|
46
|
+
reply.code(403).send({error: 'not authorised'});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { schema } from '@gnar-engine/core';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
|
|
4
|
+
export const {{entityName}}Schema = {
|
|
5
|
+
schemaName: '{{serviceName}}Service.{{entityName}}Schema',
|
|
6
|
+
schema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
// Add your properties here
|
|
10
|
+
|
|
11
|
+
},
|
|
12
|
+
required: [],
|
|
13
|
+
additionalProperties: false
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const validate{{pascalCase entityName}} = schema.compile({{entityName}}Schema);
|