create-openibm 0.1.0 → 0.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.
@@ -12,10 +12,6 @@ function schemaIbmi(a) {
12
12
  ` transport = env("IBMI_TRANSPORT")`,
13
13
  ` system = env("IBMI_SYSTEM")`,
14
14
  `}`,
15
- ``,
16
- `generator client {`,
17
- ` output = "./src/generated/ibmi"`,
18
- `}`,
19
15
  ];
20
16
  if (a.features.includes('program')) {
21
17
  blocks.push(``, `// RPG/COBOL program — JS name maps to actual IBM i program via @map`, `program SimpleCalc @map("SIMPLECALC") {`, ` library = "MYLIB"`, ` input PackedDecimal(15, 0) @in`, ` output PackedDecimal(16, 0) @out`, `}`);
@@ -48,9 +44,6 @@ function gitignore() {
48
44
  '.env',
49
45
  '*.tsbuildinfo',
50
46
  '',
51
- '# Generated IBM i client — re-create with: npm run generate',
52
- 'src/generated/',
53
- '',
54
47
  '# OS',
55
48
  '.DS_Store',
56
49
  'Thumbs.db',
@@ -7,5 +7,11 @@ export declare function clientConfigBlock(a: Answers): string;
7
7
  * pulling @openibm/* from the globally linked monorepo packages.
8
8
  */
9
9
  export declare function linkDevScriptMjs(): string;
10
+ /**
11
+ * Stub for src/generated/ibmi/index.ts — created by `npm run generate`.
12
+ * Allows the server to boot without a real IBM i connection.
13
+ * Replace by running: npm run generate
14
+ */
15
+ export declare function generatedStub(): string;
10
16
  /** Zod-based IBM i data-type validators — shared by basic + express templates */
11
17
  export declare function ibmiValidatorsTs(): string;
@@ -51,6 +51,10 @@ function tsconfig() {
51
51
  esModuleInterop: true,
52
52
  skipLibCheck: true,
53
53
  forceConsistentCasingInFileNames: true,
54
+ baseUrl: '.',
55
+ paths: {
56
+ '.openibm': ['./node_modules/.openibm/index.ts'],
57
+ },
54
58
  },
55
59
  include: ['src'],
56
60
  exclude: ['node_modules', 'dist'],
@@ -61,7 +65,7 @@ function indexTs(a) {
61
65
  const hasProgram = a.features.includes('program');
62
66
  const hasTable = a.features.includes('table');
63
67
  const imports = [
64
- `import { createClient } from './generated/ibmi/index.js';`,
68
+ `import { createClient } from '.openibm';`,
65
69
  ...(a.transport === 'odbc' ? [`import 'odbc';`] : []),
66
70
  ];
67
71
  const clientConfig = clientConfigBlock(a);
@@ -235,6 +239,45 @@ export function linkDevScriptMjs() {
235
239
  ``,
236
240
  ].join('\n');
237
241
  }
242
+ /**
243
+ * Stub for src/generated/ibmi/index.ts — created by `npm run generate`.
244
+ * Allows the server to boot without a real IBM i connection.
245
+ * Replace by running: npm run generate
246
+ */
247
+ export function generatedStub() {
248
+ return [
249
+ `// AUTO-GENERATED STUB — run \`npm run generate\` to replace with the real IBM i client.`,
250
+ `/* eslint-disable */`,
251
+ ``,
252
+ `export function createClient(_config: any): {`,
253
+ ` connect(): Promise<void>;`,
254
+ ` disconnect(): Promise<void>;`,
255
+ ` isConnected(): boolean;`,
256
+ ` query: Record<string, any>;`,
257
+ ` program: Record<string, any>;`,
258
+ `} {`,
259
+ ` const warn = () =>`,
260
+ ` console.warn('\\x1b[33m[openibm]\\x1b[0m Stub client — run \`npm run generate\` to connect to IBM i');`,
261
+ ``,
262
+ ` const tableProxy: any = new Proxy({}, {`,
263
+ ` get: (_t, method) => {`,
264
+ ` if (method === 'findMany') return async () => { warn(); return []; };`,
265
+ ` if (method === 'findFirst') return async () => { warn(); return null; };`,
266
+ ` return async () => { warn(); return null; };`,
267
+ ` },`,
268
+ ` });`,
269
+ ``,
270
+ ` return {`,
271
+ ` connect: async () => { warn(); },`,
272
+ ` disconnect: async () => {},`,
273
+ ` isConnected: () => false,`,
274
+ ` query: new Proxy({}, { get: () => tableProxy }),`,
275
+ ` program: new Proxy({}, { get: () => async (..._a: any[]) => { warn(); return {}; } }),`,
276
+ ` };`,
277
+ `}`,
278
+ ``,
279
+ ].join('\n');
280
+ }
238
281
  /** Zod-based IBM i data-type validators — shared by basic + express templates */
239
282
  export function ibmiValidatorsTs() {
240
283
  return [
@@ -2,14 +2,32 @@ import { clientConfigBlock, ibmiValidatorsTs, linkDevScriptMjs } from './basic.j
2
2
  export function expressFiles(a) {
3
3
  const hasProgram = a.features.includes('program');
4
4
  const hasTable = a.features.includes('table');
5
- return {
5
+ const files = {
6
6
  'package.json': packageJson(a),
7
7
  'tsconfig.json': tsconfig(),
8
- 'src/index.ts': indexTs(a, hasProgram, hasTable),
8
+ 'nodemon.json': nodemonJson(),
9
+ 'src/index.ts': indexTs(),
10
+ 'src/app.ts': appTs(hasProgram, hasTable),
11
+ 'src/client.ts': clientTs(a),
12
+ 'src/docs.ts': docsTs(a, hasProgram, hasTable),
13
+ 'src/routes/index.ts': routesIndex(hasProgram, hasTable),
14
+ 'src/routes/health.routes.ts': healthRoutes(),
15
+ 'src/controllers/health.controller.ts': healthController(),
9
16
  'src/ibmi-validators.ts': ibmiValidatorsTs(),
10
17
  'scripts/link-dev.mjs': linkDevScriptMjs(),
18
+ '.vscode/settings.json': vscodeSettings(),
19
+ '.vscode/extensions.json': vscodeExtensions(),
11
20
  'README.md': readme(a, hasProgram, hasTable),
12
21
  };
22
+ if (hasTable) {
23
+ files['src/routes/customers.routes.ts'] = customersRoutes();
24
+ files['src/controllers/customers.controller.ts'] = customersController();
25
+ }
26
+ if (hasProgram) {
27
+ files['src/routes/calculate.routes.ts'] = calculateRoutes();
28
+ files['src/controllers/calculate.controller.ts'] = calculateController();
29
+ }
30
+ return files;
13
31
  }
14
32
  // ── package.json ──────────────────────────────────────────────────────────────
15
33
  function packageJson(a) {
@@ -21,13 +39,14 @@ function packageJson(a) {
21
39
  scripts: {
22
40
  generate: 'openibm generate',
23
41
  build: 'tsc --project tsconfig.json',
24
- dev: 'node --env-file=.env --import tsx/esm src/index.ts',
42
+ dev: 'nodemon',
25
43
  start: 'node dist/index.js',
26
44
  'link:dev': 'node scripts/link-dev.mjs',
27
45
  },
28
46
  dependencies: {
29
47
  '@openibm/driver': '^0.1.0',
30
48
  '@openibm/types': '^0.1.0',
49
+ '@scalar/express-api-reference': '^0.4.0',
31
50
  express: '^4.21.0',
32
51
  morgan: '^1.10.0',
33
52
  'swagger-ui-express': '^5.0.0',
@@ -41,6 +60,7 @@ function packageJson(a) {
41
60
  '@types/morgan': '^1.9.0',
42
61
  '@types/swagger-ui-express': '^4.1.0',
43
62
  '@types/node': '^22.0.0',
63
+ nodemon: '^3.1.0',
44
64
  tsx: '^4.0.0',
45
65
  typescript: '^5.7.0',
46
66
  },
@@ -60,14 +80,80 @@ function tsconfig() {
60
80
  esModuleInterop: true,
61
81
  skipLibCheck: true,
62
82
  forceConsistentCasingInFileNames: true,
83
+ baseUrl: '.',
84
+ paths: {
85
+ '.openibm': ['./node_modules/.openibm/index.ts'],
86
+ },
63
87
  },
64
88
  include: ['src'],
65
89
  exclude: ['node_modules', 'dist'],
66
90
  }, null, 4) + '\n';
67
91
  }
92
+ // ── nodemon.json ──────────────────────────────────────────────────────────────
93
+ function nodemonJson() {
94
+ return JSON.stringify({
95
+ watch: ['src'],
96
+ ext: 'ts,json',
97
+ exec: 'node --env-file=.env --import tsx/esm src/index.ts',
98
+ }, null, 4) + '\n';
99
+ }
68
100
  // ── src/index.ts ──────────────────────────────────────────────────────────────
69
- function indexTs(a, hasProgram, hasTable) {
70
- // ── Swagger paths (built conditionally) ──────────────────────────────────
101
+ function indexTs() {
102
+ return [
103
+ `import { client } from './client.js';`,
104
+ `import app from './app.js';`,
105
+ ``,
106
+ `const PORT = Number(process.env.PORT ?? 3000);`,
107
+ ``,
108
+ `app.listen(PORT, async () => {`,
109
+ ` await client.connect();`,
110
+ ` console.log(\`Server: http://localhost:\${PORT}\`);`,
111
+ ` console.log(\`Swagger: http://localhost:\${PORT}/api-docs\`);`,
112
+ ` console.log(\`Scalar: http://localhost:\${PORT}/scalar\`);`,
113
+ `});`,
114
+ ``,
115
+ ].join('\n');
116
+ }
117
+ // ── src/app.ts ────────────────────────────────────────────────────────────────
118
+ function appTs(hasProgram, hasTable) {
119
+ return [
120
+ `import express from 'express';`,
121
+ `import morgan from 'morgan';`,
122
+ `import { setupDocs } from './docs.js';`,
123
+ `import router from './routes/index.js';`,
124
+ ``,
125
+ `const app = express();`,
126
+ ``,
127
+ `// ── Middleware ────────────────────────────────────────────────────────────────`,
128
+ ``,
129
+ `app.use(express.json());`,
130
+ `app.use(morgan('dev'));`,
131
+ ``,
132
+ `// ── API docs ──────────────────────────────────────────────────────────────────`,
133
+ ``,
134
+ `setupDocs(app);`,
135
+ ``,
136
+ `// ── Routes ───────────────────────────────────────────────────────────────────`,
137
+ ``,
138
+ `app.use('/', router);`,
139
+ ``,
140
+ `export default app;`,
141
+ ``,
142
+ ].join('\n');
143
+ }
144
+ // ── src/client.ts ─────────────────────────────────────────────────────────────
145
+ function clientTs(a) {
146
+ return [
147
+ `import { createClient } from '.openibm';`,
148
+ ``,
149
+ clientConfigBlock(a),
150
+ ``,
151
+ `export { client };`,
152
+ ``,
153
+ ].join('\n');
154
+ }
155
+ // ── src/docs.ts ───────────────────────────────────────────────────────────────
156
+ function docsTs(a, hasProgram, hasTable) {
71
157
  const swaggerPaths = [
72
158
  ` '/health': {`,
73
159
  ` get: {`,
@@ -85,73 +171,259 @@ function indexTs(a, hasProgram, hasTable) {
85
171
  if (hasProgram) {
86
172
  swaggerPaths.push(` '/calculate': {`, ` post: {`, ` tags: ['Programs'],`, ` summary: 'Call SimpleCalc IBM i program',`, ` requestBody: {`, ` required: true,`, ` content: {`, ` 'application/json': {`, ` schema: {`, ` type: 'object',`, ` required: ['input'],`, ` properties: { input: { type: 'integer', example: 5, description: 'IBM i INTEGER' } },`, ` },`, ` },`, ` },`, ` },`, ` responses: {`, ` '200': { description: 'Calculation result', content: { 'application/json': { schema: { example: { input: 5, output: 500 } } } } },`, ` '400': { description: 'Validation error' },`, ` },`, ` },`, ` },`);
87
173
  }
88
- // ── Routes ───────────────────────────────────────────────────────────────
89
- const routes = [
90
- `// ── Health ───────────────────────────────────────────────────────────────────`,
174
+ return [
175
+ `import type { Express } from 'express';`,
176
+ `import swaggerUi from 'swagger-ui-express';`,
177
+ `import { apiReference } from '@scalar/express-api-reference';`,
91
178
  ``,
92
- `app.get('/health', (_req, res) => {`,
93
- ` res.json({ status: 'ok', connected: client.isConnected() });`,
94
- `});`,
179
+ `export const apiSpec = {`,
180
+ ` openapi: '3.0.0',`,
181
+ ` info: { title: '${a.projectName}', version: '1.0.0', description: 'IBM i REST API' },`,
182
+ ` paths: {`,
183
+ ...swaggerPaths,
184
+ ` },`,
185
+ `};`,
186
+ ``,
187
+ `export function setupDocs(app: Express): void {`,
188
+ ` app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiSpec as any));`,
189
+ ` app.use(`,
190
+ ` '/scalar',`,
191
+ ` apiReference({ spec: { content: apiSpec } }) as any,`,
192
+ ` );`,
193
+ `}`,
194
+ ``,
195
+ ].join('\n');
196
+ }
197
+ // ── src/routes/index.ts ───────────────────────────────────────────────────────
198
+ function routesIndex(hasProgram, hasTable) {
199
+ const imports = [
200
+ `import { Router } from 'express';`,
201
+ `import healthRouter from './health.routes.js';`,
95
202
  ];
96
- if (hasTable) {
97
- routes.push(``, `// ── Customers ────────────────────────────────────────────────────────────────`, ``, `app.get('/customers', async (req, res) => {`, ` const state = typeof req.query.state === 'string' ? req.query.state : 'TX';`, ` const rows = await client.query.Customer.findMany({`, ` where: { state },`, ` orderBy: { customerId: 'asc' },`, ` take: 20,`, ` });`, ` res.json(rows);`, `});`, ``, `app.get('/customers/:id', async (req, res) => {`, ` const row = await client.query.Customer.findFirst({`, ` where: { customerId: Number(req.params.id) },`, ` });`, ` if (!row) { res.status(404).json({ error: 'Not found' }); return; }`, ` res.json(row);`, `});`);
98
- }
99
- if (hasProgram) {
100
- routes.push(``, `// ── SimpleCalc ───────────────────────────────────────────────────────────────`, ``, `const calculateSchema = z.object({ input: ibmiInt });`, ``, `app.post('/calculate', async (req, res) => {`, ` const parsed = calculateSchema.safeParse(req.body);`, ` if (!parsed.success) {`, ` res.status(400).json({ errors: parsed.error.flatten() });`, ` return;`, ` }`, ` const result = await client.program.SimpleCalc({ input: parsed.data.input });`, ` res.json({ input: parsed.data.input, output: result.output });`, `});`);
101
- }
102
- const zodImports = hasProgram
103
- ? [`import { z } from 'zod';`, `import { ibmiInt } from './ibmi-validators.js';`]
104
- : [];
203
+ if (hasTable)
204
+ imports.push(`import customersRouter from './customers.routes.js';`);
205
+ if (hasProgram)
206
+ imports.push(`import calculateRouter from './calculate.routes.js';`);
207
+ const mounts = [
208
+ `router.use('/health', healthRouter);`,
209
+ ];
210
+ if (hasTable)
211
+ mounts.push(`router.use('/customers', customersRouter);`);
212
+ if (hasProgram)
213
+ mounts.push(`router.use('/calculate', calculateRouter);`);
105
214
  return [
106
- `import express from 'express';`,
107
- `import morgan from 'morgan';`,
108
- `import swaggerUi from 'swagger-ui-express';`,
109
- `import { createClient } from './generated/ibmi/index.js';`,
110
- ...zodImports,
215
+ ...imports,
111
216
  ``,
112
- `const app = express();`,
113
- `const PORT = Number(process.env.PORT ?? 3000);`,
217
+ `const router = Router();`,
114
218
  ``,
115
- `// ── Middleware ────────────────────────────────────────────────────────────────`,
219
+ ...mounts,
116
220
  ``,
117
- `app.use(express.json());`,
118
- `app.use(morgan('dev'));`,
221
+ `export default router;`,
119
222
  ``,
120
- `// ── IBM i client ──────────────────────────────────────────────────────────────`,
223
+ ].join('\n');
224
+ }
225
+ // ── src/routes/health.routes.ts ───────────────────────────────────────────────
226
+ function healthRoutes() {
227
+ return [
228
+ `import { Router } from 'express';`,
229
+ `import { getHealth } from '../controllers/health.controller.js';`,
121
230
  ``,
122
- clientConfigBlock(a),
231
+ `const router = Router();`,
123
232
  ``,
124
- `// ── OpenAPI / Swagger ─────────────────────────────────────────────────────────`,
233
+ `router.get('/', getHealth);`,
125
234
  ``,
126
- `const apiSpec = {`,
127
- ` openapi: '3.0.0',`,
128
- ` info: { title: '${a.projectName}', version: '1.0.0', description: 'IBM i REST API' },`,
129
- ` paths: {`,
130
- ...swaggerPaths,
131
- ` },`,
132
- `};`,
235
+ `export default router;`,
133
236
  ``,
134
- `app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiSpec as any));`,
237
+ ].join('\n');
238
+ }
239
+ // ── src/controllers/health.controller.ts ─────────────────────────────────────
240
+ function healthController() {
241
+ return [
242
+ `import type { Request, Response } from 'express';`,
243
+ `import { client } from '../client.js';`,
135
244
  ``,
136
- `// ── Routes ───────────────────────────────────────────────────────────────────`,
245
+ `export function getHealth(_req: Request, res: Response): void {`,
246
+ ` res.json({ status: 'ok', connected: client.isConnected() });`,
247
+ `}`,
248
+ ``,
249
+ ].join('\n');
250
+ }
251
+ // ── src/routes/customers.routes.ts ───────────────────────────────────────────
252
+ function customersRoutes() {
253
+ return [
254
+ `import { Router } from 'express';`,
255
+ `import { listCustomers, getCustomer } from '../controllers/customers.controller.js';`,
137
256
  ``,
138
- ...routes,
257
+ `const router = Router();`,
139
258
  ``,
140
- `// ── Start ────────────────────────────────────────────────────────────────────`,
259
+ `router.get('/', listCustomers);`,
260
+ `router.get('/:id', getCustomer);`,
141
261
  ``,
142
- `app.listen(PORT, async () => {`,
143
- ` await client.connect();`,
144
- ` console.log(\`Server: http://localhost:\${PORT}\`);`,
145
- ` console.log(\`Swagger: http://localhost:\${PORT}/api-docs\`);`,
146
- `});`,
262
+ `export default router;`,
263
+ ``,
264
+ ].join('\n');
265
+ }
266
+ // ── src/controllers/customers.controller.ts ──────────────────────────────────
267
+ function customersController() {
268
+ return [
269
+ `import type { Request, Response } from 'express';`,
270
+ `import { client } from '../client.js';`,
271
+ ``,
272
+ `export async function listCustomers(req: Request, res: Response): Promise<void> {`,
273
+ ` const state = typeof req.query.state === 'string' ? req.query.state : 'TX';`,
274
+ ` const rows = await client.query.Customer.findMany({`,
275
+ ` where: { state },`,
276
+ ` orderBy: { customerId: 'asc' },`,
277
+ ` take: 20,`,
278
+ ` });`,
279
+ ` res.json(rows);`,
280
+ `}`,
281
+ ``,
282
+ `export async function getCustomer(req: Request, res: Response): Promise<void> {`,
283
+ ` const row = await client.query.Customer.findFirst({`,
284
+ ` where: { customerId: Number(req.params.id) },`,
285
+ ` });`,
286
+ ` if (!row) { res.status(404).json({ error: 'Not found' }); return; }`,
287
+ ` res.json(row);`,
288
+ `}`,
289
+ ``,
290
+ ].join('\n');
291
+ }
292
+ // ── src/routes/calculate.routes.ts ───────────────────────────────────────────
293
+ function calculateRoutes() {
294
+ return [
295
+ `import { Router } from 'express';`,
296
+ `import { calculate } from '../controllers/calculate.controller.js';`,
297
+ ``,
298
+ `const router = Router();`,
299
+ ``,
300
+ `router.post('/', calculate);`,
301
+ ``,
302
+ `export default router;`,
303
+ ``,
304
+ ].join('\n');
305
+ }
306
+ // ── src/controllers/calculate.controller.ts ──────────────────────────────────
307
+ function calculateController() {
308
+ return [
309
+ `import type { Request, Response } from 'express';`,
310
+ `import { z } from 'zod';`,
311
+ `import { ibmiInt } from '../ibmi-validators.js';`,
312
+ `import { client } from '../client.js';`,
313
+ ``,
314
+ `const calculateSchema = z.object({ input: ibmiInt });`,
315
+ ``,
316
+ `export async function calculate(req: Request, res: Response): Promise<void> {`,
317
+ ` const parsed = calculateSchema.safeParse(req.body);`,
318
+ ` if (!parsed.success) {`,
319
+ ` res.status(400).json({ errors: parsed.error.flatten() });`,
320
+ ` return;`,
321
+ ` }`,
322
+ ` const result = await client.program.SimpleCalc({ input: parsed.data.input });`,
323
+ ` res.json({ input: parsed.data.input, output: result.output });`,
324
+ `}`,
147
325
  ``,
148
326
  ].join('\n');
149
327
  }
328
+ // ── .vscode/settings.json ─────────────────────────────────────────────────────
329
+ function vscodeSettings() {
330
+ return JSON.stringify({
331
+ 'editor.formatOnSave': true,
332
+ 'editor.defaultFormatter': 'esbenp.prettier-vscode',
333
+ 'editor.codeActionsOnSave': {
334
+ 'source.fixAll.eslint': 'explicit',
335
+ 'source.organizeImports': 'never',
336
+ 'source.removeUnusedImports': 'explicit',
337
+ },
338
+ 'editor.tabSize': 2,
339
+ 'editor.insertSpaces': true,
340
+ 'editor.rulers': [80, 120],
341
+ 'editor.wordWrap': 'on',
342
+ 'editor.bracketPairColorization.enabled': true,
343
+ 'editor.guides.bracketPairs': true,
344
+ 'editor.inlineSuggest.enabled': true,
345
+ 'js/ts.tsdk': 'node_modules/typescript/lib',
346
+ 'js/ts.enablePromptUseWorkspaceTsdk': true,
347
+ 'js/ts.preferences.importModuleSpecifier': 'non-relative',
348
+ 'js/ts.updateImportsOnFileMove.enabled': 'always',
349
+ 'js/ts.suggest.autoImports': true,
350
+ 'js/ts.preferences.quoteStyle': 'single',
351
+ 'files.autoSave': 'onFocusChange',
352
+ 'files.trimTrailingWhitespace': true,
353
+ 'files.insertFinalNewline': true,
354
+ 'files.eol': '\n',
355
+ 'files.exclude': {
356
+ '**/.git': true,
357
+ '**/.DS_Store': true,
358
+ '**/dist': true,
359
+ '**/coverage': true,
360
+ },
361
+ 'search.exclude': {
362
+ '**/node_modules': true,
363
+ '**/dist': true,
364
+ '**/coverage': true,
365
+ 'package-lock.json': true,
366
+ 'pnpm-lock.yaml': true,
367
+ },
368
+ 'prettier.singleQuote': true,
369
+ 'prettier.trailingComma': 'all',
370
+ 'prettier.tabWidth': 2,
371
+ 'prettier.semi': true,
372
+ 'prettier.arrowParens': 'always',
373
+ 'prettier.endOfLine': 'lf',
374
+ 'git.autofetch': true,
375
+ 'git.confirmSync': false,
376
+ 'git.enableSmartCommit': true,
377
+ 'explorer.confirmDelete': false,
378
+ 'explorer.confirmDragAndDrop': false,
379
+ 'explorer.fileNesting.enabled': true,
380
+ 'explorer.fileNesting.patterns': {
381
+ '*.ts': '${capture}.js, ${capture}.d.ts, ${capture}.spec.ts, ${capture}.test.ts',
382
+ 'package.json': 'package-lock.json, pnpm-lock.yaml, yarn.lock, .npmrc, .nvmrc, .node-version',
383
+ 'tsconfig.json': 'tsconfig.*.json',
384
+ '.env': '.env.*',
385
+ '.gitignore': '.gitattributes',
386
+ 'README.md': 'CHANGELOG.md, LICENSE',
387
+ },
388
+ '[typescript]': { 'editor.defaultFormatter': 'esbenp.prettier-vscode' },
389
+ '[javascript]': { 'editor.defaultFormatter': 'esbenp.prettier-vscode' },
390
+ '[json]': { 'editor.defaultFormatter': 'esbenp.prettier-vscode' },
391
+ '[ibmi]': { 'editor.defaultFormatter': 'openibm.vscode' },
392
+ 'rest-client.environmentVariables': {
393
+ '$shared': { baseUrl: 'http://localhost:3000' },
394
+ 'local': { baseUrl: 'http://localhost:3000' },
395
+ },
396
+ 'todo-tree.general.tags': ['TODO', 'FIXME', 'NOTE', 'BUG'],
397
+ 'todo-tree.highlights.defaultHighlight': {
398
+ icon: 'alert', type: 'text', foreground: '#fff', iconColour: '#ffcc00',
399
+ },
400
+ 'todo-tree.highlights.customHighlight': {
401
+ 'TODO': { icon: 'check', iconColour: '#3498db' },
402
+ 'FIXME': { icon: 'flame', iconColour: '#e74c3c' },
403
+ 'NOTE': { icon: 'note', iconColour: '#2ecc71' },
404
+ 'BUG': { icon: 'bug', iconColour: '#e74c3c' },
405
+ },
406
+ }, null, 2) + '\n';
407
+ }
408
+ // ── .vscode/extensions.json ───────────────────────────────────────────────────
409
+ function vscodeExtensions() {
410
+ return JSON.stringify({
411
+ recommendations: [
412
+ 'openibm.vscode',
413
+ 'esbenp.prettier-vscode',
414
+ 'dbaeumer.vscode-eslint',
415
+ 'christian-kohler.path-intellisense',
416
+ 'humao.rest-client',
417
+ 'gruntfuggly.todo-tree',
418
+ ],
419
+ }, null, 2) + '\n';
420
+ }
150
421
  // ── README.md ─────────────────────────────────────────────────────────────────
151
- function readme(a, hasTable, hasProgram) {
422
+ function readme(a, hasProgram, hasTable) {
152
423
  const endpoints = [
153
424
  `| \`GET /health\` | Connection status |`,
154
425
  `| \`GET /api-docs\` | Swagger UI |`,
426
+ `| \`GET /scalar\` | Scalar API reference |`,
155
427
  ...(hasTable ? [`| \`GET /customers\` | Query DB2 table (filtered by state) |`] : []),
156
428
  ...(hasTable ? [`| \`GET /customers/:id\`| Find customer by ID |`] : []),
157
429
  ...(hasProgram ? [`| \`POST /calculate\` | Call IBM i program (\`{ input: number }\`) |`] : []),
@@ -161,6 +433,19 @@ function readme(a, hasTable, hasProgram) {
161
433
  ``,
162
434
  `IBM i Express application generated by [create-openibm](https://www.npmjs.com/package/create-openibm).`,
163
435
  ``,
436
+ `## Project structure`,
437
+ ``,
438
+ `\`\`\``,
439
+ `src/`,
440
+ `├── index.ts # Entry point — listen & connect`,
441
+ `├── app.ts # Express setup, middleware, routes`,
442
+ `├── client.ts # IBM i client singleton`,
443
+ `├── docs.ts # OpenAPI spec + Swagger & Scalar setup`,
444
+ `├── ibmi-validators.ts # Zod schemas for IBM i types`,
445
+ `├── controllers/ # Request handlers`,
446
+ `└── routes/ # Express routers`,
447
+ `\`\`\``,
448
+ ``,
164
449
  `## Setup`,
165
450
  ``,
166
451
  `\`\`\`bash`,
@@ -172,7 +457,7 @@ function readme(a, hasTable, hasProgram) {
172
457
  `## Development`,
173
458
  ``,
174
459
  `\`\`\`bash`,
175
- `npm run dev`,
460
+ `npm run dev # starts with nodemon — restarts on file save`,
176
461
  `\`\`\``,
177
462
  ``,
178
463
  `## Endpoints`,
@@ -2,19 +2,35 @@ import { linkDevScriptMjs } from './basic.js';
2
2
  export function nestjsFiles(a) {
3
3
  const hasProgram = a.features.includes('program');
4
4
  const hasTable = a.features.includes('table');
5
- return {
5
+ const files = {
6
6
  'package.json': packageJson(a),
7
7
  'tsconfig.json': tsconfig(),
8
+ 'nodemon.json': nodemonJson(),
8
9
  'src/main.ts': mainTs(a),
9
- 'src/app.module.ts': appModuleTs(),
10
+ 'src/app.module.ts': appModuleTs(hasProgram, hasTable),
11
+ 'src/docs.ts': docsTs(a),
10
12
  'src/ibmi/ibmi.module.ts': ibmiModuleTs(),
11
13
  'src/ibmi/ibmi.service.ts': ibmiServiceTs(a),
12
- 'src/ibmi/ibmi.controller.ts': ibmiControllerTs(a, hasProgram, hasTable),
13
- 'src/ibmi/dto/ibmi-validators.ts': ibmiValidatorsNestTs(),
14
- ...(hasProgram ? { 'src/ibmi/dto/calculate.dto.ts': calculateDtoTs() } : {}),
14
+ 'src/health/health.module.ts': healthModuleTs(),
15
+ 'src/health/health.controller.ts': healthControllerTs(),
16
+ 'src/common/ibmi-validators.ts': ibmiValidatorsNestTs(),
15
17
  'scripts/link-dev.mjs': linkDevScriptMjs(),
18
+ '.vscode/settings.json': vscodeSettings(),
19
+ '.vscode/extensions.json': vscodeExtensions(),
16
20
  'README.md': readme(a, hasProgram, hasTable),
17
21
  };
22
+ if (hasTable) {
23
+ files['src/customers/customers.module.ts'] = customersModuleTs();
24
+ files['src/customers/customers.controller.ts'] = customersControllerTs();
25
+ files['src/customers/customers.service.ts'] = customersServiceTs();
26
+ }
27
+ if (hasProgram) {
28
+ files['src/programs/programs.module.ts'] = programsModuleTs();
29
+ files['src/programs/programs.controller.ts'] = programsControllerTs();
30
+ files['src/programs/programs.service.ts'] = programsServiceTs();
31
+ files['src/programs/dto/calculate.dto.ts'] = calculateDtoTs();
32
+ }
33
+ return files;
18
34
  }
19
35
  // ── package.json ──────────────────────────────────────────────────────────────
20
36
  function packageJson(a) {
@@ -26,7 +42,7 @@ function packageJson(a) {
26
42
  scripts: {
27
43
  generate: 'openibm generate',
28
44
  build: 'tsc --project tsconfig.json',
29
- dev: 'node --env-file=.env --import tsx/esm src/main.ts',
45
+ dev: 'nodemon',
30
46
  start: 'node dist/main.js',
31
47
  'link:dev': 'node scripts/link-dev.mjs',
32
48
  },
@@ -37,6 +53,7 @@ function packageJson(a) {
37
53
  '@nestjs/swagger': '^7.0.0',
38
54
  '@openibm/driver': '^0.1.0',
39
55
  '@openibm/types': '^0.1.0',
56
+ '@scalar/express-api-reference': '^0.4.0',
40
57
  'class-transformer': '^0.5.0',
41
58
  'class-validator': '^0.14.0',
42
59
  morgan: '^1.10.0',
@@ -49,6 +66,7 @@ function packageJson(a) {
49
66
  '@openibm/client': '^0.1.0',
50
67
  '@types/morgan': '^1.9.0',
51
68
  '@types/node': '^22.0.0',
69
+ nodemon: '^3.1.0',
52
70
  tsx: '^4.0.0',
53
71
  typescript: '^5.7.0',
54
72
  },
@@ -70,43 +88,47 @@ function tsconfig() {
70
88
  experimentalDecorators: true,
71
89
  emitDecoratorMetadata: true,
72
90
  forceConsistentCasingInFileNames: true,
91
+ baseUrl: '.',
92
+ paths: {
93
+ '.openibm': ['./node_modules/.openibm/index.ts'],
94
+ },
73
95
  },
74
96
  include: ['src'],
75
97
  exclude: ['node_modules', 'dist'],
76
98
  }, null, 4) + '\n';
77
99
  }
100
+ // ── nodemon.json ──────────────────────────────────────────────────────────────
101
+ function nodemonJson() {
102
+ return JSON.stringify({
103
+ watch: ['src'],
104
+ ext: 'ts,json',
105
+ exec: 'node --env-file=.env --import tsx/esm src/main.ts',
106
+ }, null, 4) + '\n';
107
+ }
78
108
  // ── src/main.ts ───────────────────────────────────────────────────────────────
79
109
  function mainTs(a) {
110
+ void a;
80
111
  return [
81
112
  `import 'reflect-metadata';`,
82
113
  `import { NestFactory } from '@nestjs/core';`,
83
114
  `import { ValidationPipe } from '@nestjs/common';`,
84
- `import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';`,
85
115
  `import morgan from 'morgan';`,
86
116
  `import { AppModule } from './app.module.js';`,
117
+ `import { setupDocs } from './docs.js';`,
87
118
  ``,
88
119
  `async function bootstrap(): Promise<void> {`,
89
120
  ` const app = await NestFactory.create(AppModule);`,
90
121
  ``,
91
- ` // ── HTTP request logging ─────────────────────────────────────────────────`,
92
122
  ` app.use(morgan('dev'));`,
93
- ``,
94
- ` // ── Global validation pipe (class-validator + class-transformer) ─────────`,
95
123
  ` app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));`,
96
124
  ``,
97
- ` // ── Swagger / OpenAPI ────────────────────────────────────────────────────`,
98
- ` const config = new DocumentBuilder()`,
99
- ` .setTitle('${a.projectName}')`,
100
- ` .setDescription('IBM i REST API')`,
101
- ` .setVersion('1.0.0')`,
102
- ` .build();`,
103
- ` const document = SwaggerModule.createDocument(app, config);`,
104
- ` SwaggerModule.setup('api-docs', app, document);`,
125
+ ` setupDocs(app);`,
105
126
  ``,
106
127
  ` const port = Number(process.env.PORT ?? 3000);`,
107
128
  ` await app.listen(port);`,
108
129
  ` console.log(\`Application: http://localhost:\${port}\`);`,
109
130
  ` console.log(\`Swagger UI: http://localhost:\${port}/api-docs\`);`,
131
+ ` console.log(\`Scalar: http://localhost:\${port}/scalar\`);`,
110
132
  `}`,
111
133
  ``,
112
134
  `bootstrap().catch(console.error);`,
@@ -114,29 +136,67 @@ function mainTs(a) {
114
136
  ].join('\n');
115
137
  }
116
138
  // ── src/app.module.ts ─────────────────────────────────────────────────────────
117
- function appModuleTs() {
118
- return [
139
+ function appModuleTs(hasProgram, hasTable) {
140
+ const imports = [
119
141
  `import { Module } from '@nestjs/common';`,
120
142
  `import { IBMiModule } from './ibmi/ibmi.module.js';`,
143
+ `import { HealthModule } from './health/health.module.js';`,
144
+ ];
145
+ const moduleImports = [`IBMiModule`, `HealthModule`];
146
+ if (hasTable) {
147
+ imports.push(`import { CustomersModule } from './customers/customers.module.js';`);
148
+ moduleImports.push(`CustomersModule`);
149
+ }
150
+ if (hasProgram) {
151
+ imports.push(`import { ProgramsModule } from './programs/programs.module.js';`);
152
+ moduleImports.push(`ProgramsModule`);
153
+ }
154
+ return [
155
+ ...imports,
121
156
  ``,
122
157
  `@Module({`,
123
- ` imports: [IBMiModule],`,
158
+ ` imports: [${moduleImports.join(', ')}],`,
124
159
  `})`,
125
160
  `export class AppModule {}`,
126
161
  ``,
127
162
  ].join('\n');
128
163
  }
164
+ // ── src/docs.ts ───────────────────────────────────────────────────────────────
165
+ function docsTs(a) {
166
+ return [
167
+ `import type { INestApplication } from '@nestjs/common';`,
168
+ `import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';`,
169
+ `import { apiReference } from '@scalar/express-api-reference';`,
170
+ ``,
171
+ `export function setupDocs(app: INestApplication): void {`,
172
+ ` const config = new DocumentBuilder()`,
173
+ ` .setTitle('${a.projectName}')`,
174
+ ` .setDescription('IBM i REST API')`,
175
+ ` .setVersion('1.0.0')`,
176
+ ` .build();`,
177
+ ``,
178
+ ` const document = SwaggerModule.createDocument(app, config);`,
179
+ ``,
180
+ ` SwaggerModule.setup('api-docs', app, document);`,
181
+ ``,
182
+ ` app.use(`,
183
+ ` '/scalar',`,
184
+ ` apiReference({ spec: { content: document } }) as any,`,
185
+ ` );`,
186
+ `}`,
187
+ ``,
188
+ ].join('\n');
189
+ }
129
190
  // ── src/ibmi/ibmi.module.ts ───────────────────────────────────────────────────
130
191
  function ibmiModuleTs() {
131
192
  return [
132
- `import { Module } from '@nestjs/common';`,
193
+ `import { Global, Module } from '@nestjs/common';`,
133
194
  `import { IBMiService } from './ibmi.service.js';`,
134
- `import { IBMiController } from './ibmi.controller.js';`,
135
195
  ``,
196
+ `@Global()`,
136
197
  `@Module({`,
137
- ` providers: [IBMiService],`,
138
- ` controllers: [IBMiController],`,
139
- ` exports: [IBMiService],`,
198
+ ` providers: [IBMiService],`,
199
+ ` exports: [IBMiService],`,
140
200
  `})`,
141
201
  `export class IBMiModule {}`,
142
202
  ``,
@@ -161,7 +221,7 @@ function ibmiServiceTs(a) {
161
221
  const extras = [httpOptions, sshOptions, odbcOptions].filter(Boolean).join('\n');
162
222
  return [
163
223
  `import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';`,
164
- `import { createClient } from '../generated/ibmi/index.js';`,
224
+ `import { createClient } from '.openibm';`,
165
225
  ``,
166
226
  `@Injectable()`,
167
227
  `export class IBMiService implements OnModuleInit, OnModuleDestroy {`,
@@ -173,60 +233,195 @@ function ibmiServiceTs(a) {
173
233
  ...(extras ? [extras] : []),
174
234
  ` });`,
175
235
  ``,
176
- ` async onModuleInit(): Promise<void> {`,
177
- ` await this.client.connect();`,
236
+ ` async onModuleInit(): Promise<void> { await this.client.connect(); }`,
237
+ ` async onModuleDestroy(): Promise<void> { await this.client.disconnect(); }`,
238
+ `}`,
239
+ ``,
240
+ ].join('\n');
241
+ }
242
+ // ── src/health/health.module.ts ───────────────────────────────────────────────
243
+ function healthModuleTs() {
244
+ return [
245
+ `import { Module } from '@nestjs/common';`,
246
+ `import { HealthController } from './health.controller.js';`,
247
+ ``,
248
+ `@Module({ controllers: [HealthController] })`,
249
+ `export class HealthModule {}`,
250
+ ``,
251
+ ].join('\n');
252
+ }
253
+ // ── src/health/health.controller.ts ──────────────────────────────────────────
254
+ function healthControllerTs() {
255
+ return [
256
+ `import { Controller, Get } from '@nestjs/common';`,
257
+ `import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';`,
258
+ `import { IBMiService } from '../ibmi/ibmi.service.js';`,
259
+ ``,
260
+ `@ApiTags('Health')`,
261
+ `@Controller('health')`,
262
+ `export class HealthController {`,
263
+ ` constructor(private readonly ibmi: IBMiService) {}`,
264
+ ``,
265
+ ` @Get()`,
266
+ ` @ApiOperation({ summary: 'Connection status' })`,
267
+ ` @ApiResponse({ status: 200, schema: { example: { status: 'ok', connected: true } } })`,
268
+ ` check() {`,
269
+ ` return { status: 'ok', connected: this.ibmi.client.isConnected() };`,
178
270
  ` }`,
271
+ `}`,
179
272
  ``,
180
- ` async onModuleDestroy(): Promise<void> {`,
181
- ` await this.client.disconnect();`,
273
+ ].join('\n');
274
+ }
275
+ // ── src/customers/customers.module.ts ────────────────────────────────────────
276
+ function customersModuleTs() {
277
+ return [
278
+ `import { Module } from '@nestjs/common';`,
279
+ `import { CustomersController } from './customers.controller.js';`,
280
+ `import { CustomersService } from './customers.service.js';`,
281
+ ``,
282
+ `@Module({`,
283
+ ` controllers: [CustomersController],`,
284
+ ` providers: [CustomersService],`,
285
+ `})`,
286
+ `export class CustomersModule {}`,
287
+ ``,
288
+ ].join('\n');
289
+ }
290
+ // ── src/customers/customers.service.ts ───────────────────────────────────────
291
+ function customersServiceTs() {
292
+ return [
293
+ `import { Injectable } from '@nestjs/common';`,
294
+ `import { IBMiService } from '../ibmi/ibmi.service.js';`,
295
+ ``,
296
+ `@Injectable()`,
297
+ `export class CustomersService {`,
298
+ ` constructor(private readonly ibmi: IBMiService) {}`,
299
+ ``,
300
+ ` findAll(state: string) {`,
301
+ ` return this.ibmi.client.query.Customer.findMany({`,
302
+ ` where: { state },`,
303
+ ` orderBy: { customerId: 'asc' },`,
304
+ ` take: 20,`,
305
+ ` });`,
306
+ ` }`,
307
+ ``,
308
+ ` findOne(id: number) {`,
309
+ ` return this.ibmi.client.query.Customer.findFirst({`,
310
+ ` where: { customerId: id },`,
311
+ ` });`,
182
312
  ` }`,
183
313
  `}`,
184
314
  ``,
185
315
  ].join('\n');
186
316
  }
187
- // ── src/ibmi/ibmi.controller.ts ───────────────────────────────────────────────
188
- function ibmiControllerTs(a, hasProgram, hasTable) {
189
- const nestImports = ['Controller', 'Get', 'Post', 'Param', 'Body'];
190
- const swaggerImports = ['ApiOperation', 'ApiResponse', 'ApiTags'];
191
- if (hasTable)
192
- swaggerImports.push('ApiParam', 'ApiQuery');
193
- if (hasProgram)
194
- swaggerImports.push('ApiBody');
195
- const imports = [
196
- `import { ${nestImports.join(', ')} } from '@nestjs/common';`,
197
- `import { ${swaggerImports.join(', ')} } from '@nestjs/swagger';`,
198
- `import { IBMiService } from './ibmi.service.js';`,
199
- ...(hasProgram ? [`import { CalculateDto } from './dto/calculate.dto.js';`] : []),
200
- ];
201
- const methods = [
202
- ` @Get('health')`,
203
- ` @ApiOperation({ summary: 'Connection status' })`,
204
- ` @ApiResponse({ status: 200, description: 'OK', schema: { example: { status: 'ok', connected: true } } })`,
205
- ` health() {`,
206
- ` return { status: 'ok', connected: this.ibmi.client.isConnected() };`,
317
+ // ── src/customers/customers.controller.ts ────────────────────────────────────
318
+ function customersControllerTs() {
319
+ return [
320
+ `import { Controller, Get, Param, Query, NotFoundException } from '@nestjs/common';`,
321
+ `import { ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';`,
322
+ `import { CustomersService } from './customers.service.js';`,
323
+ ``,
324
+ `@ApiTags('Customers')`,
325
+ `@Controller('customers')`,
326
+ `export class CustomersController {`,
327
+ ` constructor(private readonly customers: CustomersService) {}`,
328
+ ``,
329
+ ` @Get()`,
330
+ ` @ApiOperation({ summary: 'List customers filtered by state' })`,
331
+ ` @ApiQuery({ name: 'state', required: false, example: 'TX', description: 'State code — CHAR(2)' })`,
332
+ ` @ApiResponse({ status: 200, description: 'Array of customer rows' })`,
333
+ ` findAll(@Query('state') state = 'TX') {`,
334
+ ` return this.customers.findAll(state);`,
207
335
  ` }`,
208
- ];
209
- if (hasTable) {
210
- methods.push(``, ` @Get('customers')`, ` @ApiOperation({ summary: 'List customers filtered by state' })`, ` @ApiQuery({ name: 'state', required: false, example: 'TX', description: 'State code — CHAR(2)' })`, ` @ApiResponse({ status: 200, description: 'Array of customer rows' })`, ` async findCustomers(@Param() params: Record<string, string>) {`, ` const state = params['state'] ?? 'TX';`, ` return this.ibmi.client.query.Customer.findMany({`, ` where: { state },`, ` orderBy: { customerId: 'asc' },`, ` take: 20,`, ` });`, ` }`, ``, ` @Get('customers/:id')`, ` @ApiOperation({ summary: 'Find customer by ID' })`, ` @ApiParam({ name: 'id', type: Number })`, ` @ApiResponse({ status: 200, description: 'Customer row' })`, ` @ApiResponse({ status: 404, description: 'Not found' })`, ` async findCustomer(@Param('id') id: string) {`, ` return this.ibmi.client.query.Customer.findFirst({`, ` where: { customerId: Number(id) },`, ` });`, ` }`);
211
- }
212
- if (hasProgram) {
213
- methods.push(``, ` @Post('calculate')`, ` @ApiOperation({ summary: 'Call SimpleCalc IBM i program' })`, ` @ApiBody({ type: CalculateDto })`, ` @ApiResponse({ status: 200, description: 'Result', schema: { example: { input: 5, output: 500 } } })`, ` async calculate(@Body() body: CalculateDto) {`, ` const result = await this.ibmi.client.program.SimpleCalc({ input: body.input });`, ` return { input: body.input, output: result.output };`, ` }`);
214
- }
215
- void a;
336
+ ``,
337
+ ` @Get(':id')`,
338
+ ` @ApiOperation({ summary: 'Find customer by ID' })`,
339
+ ` @ApiParam({ name: 'id', type: Number })`,
340
+ ` @ApiResponse({ status: 200, description: 'Customer row' })`,
341
+ ` @ApiResponse({ status: 404, description: 'Not found' })`,
342
+ ` async findOne(@Param('id') id: string) {`,
343
+ ` const row = await this.customers.findOne(Number(id));`,
344
+ ` if (!row) throw new NotFoundException('Customer not found');`,
345
+ ` return row;`,
346
+ ` }`,
347
+ `}`,
348
+ ``,
349
+ ].join('\n');
350
+ }
351
+ // ── src/programs/programs.module.ts ──────────────────────────────────────────
352
+ function programsModuleTs() {
216
353
  return [
217
- ...imports,
354
+ `import { Module } from '@nestjs/common';`,
355
+ `import { ProgramsController } from './programs.controller.js';`,
356
+ `import { ProgramsService } from './programs.service.js';`,
357
+ ``,
358
+ `@Module({`,
359
+ ` controllers: [ProgramsController],`,
360
+ ` providers: [ProgramsService],`,
361
+ `})`,
362
+ `export class ProgramsModule {}`,
363
+ ``,
364
+ ].join('\n');
365
+ }
366
+ // ── src/programs/programs.service.ts ─────────────────────────────────────────
367
+ function programsServiceTs() {
368
+ return [
369
+ `import { Injectable } from '@nestjs/common';`,
370
+ `import { IBMiService } from '../ibmi/ibmi.service.js';`,
218
371
  ``,
219
- `@ApiTags('IBM i')`,
220
- `@Controller()`,
221
- `export class IBMiController {`,
372
+ `@Injectable()`,
373
+ `export class ProgramsService {`,
222
374
  ` constructor(private readonly ibmi: IBMiService) {}`,
223
375
  ``,
224
- ...methods,
376
+ ` calculate(input: number) {`,
377
+ ` return this.ibmi.client.program.SimpleCalc({ input });`,
378
+ ` }`,
379
+ `}`,
380
+ ``,
381
+ ].join('\n');
382
+ }
383
+ // ── src/programs/programs.controller.ts ──────────────────────────────────────
384
+ function programsControllerTs() {
385
+ return [
386
+ `import { Controller, Post, Body } from '@nestjs/common';`,
387
+ `import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';`,
388
+ `import { ProgramsService } from './programs.service.js';`,
389
+ `import { CalculateDto } from './dto/calculate.dto.js';`,
390
+ ``,
391
+ `@ApiTags('Programs')`,
392
+ `@Controller('programs')`,
393
+ `export class ProgramsController {`,
394
+ ` constructor(private readonly programs: ProgramsService) {}`,
395
+ ``,
396
+ ` @Post('calculate')`,
397
+ ` @ApiOperation({ summary: 'Call SimpleCalc IBM i program' })`,
398
+ ` @ApiBody({ type: CalculateDto })`,
399
+ ` @ApiResponse({ status: 200, schema: { example: { input: 5, output: 500 } } })`,
400
+ ` async calculate(@Body() body: CalculateDto) {`,
401
+ ` const result = await this.programs.calculate(body.input);`,
402
+ ` return { input: body.input, output: result.output };`,
403
+ ` }`,
225
404
  `}`,
226
405
  ``,
227
406
  ].join('\n');
228
407
  }
229
- // ── src/ibmi/dto/ibmi-validators.ts ──────────────────────────────────────────
408
+ // ── src/programs/dto/calculate.dto.ts ─────────────────────────────────────────
409
+ function calculateDtoTs() {
410
+ return [
411
+ `import { IsNumber } from 'class-validator';`,
412
+ `import { ApiProperty } from '@nestjs/swagger';`,
413
+ `import { IbmiInt } from '../../common/ibmi-validators.js';`,
414
+ ``,
415
+ `export class CalculateDto {`,
416
+ ` @ApiProperty({ example: 5, description: 'Input integer (IBM i INTEGER: -2147483648 to 2147483647)' })`,
417
+ ` @IsNumber()`,
418
+ ` @IbmiInt()`,
419
+ ` input!: number;`,
420
+ `}`,
421
+ ``,
422
+ ].join('\n');
423
+ }
424
+ // ── src/common/ibmi-validators.ts ─────────────────────────────────────────────
230
425
  function ibmiValidatorsNestTs() {
231
426
  return [
232
427
  `/**`,
@@ -234,12 +429,12 @@ function ibmiValidatorsNestTs() {
234
429
  ` * Use these in your NestJS DTOs alongside standard class-validator decorators.`,
235
430
  ` *`,
236
431
  ` * Usage:`,
237
- ` * import { IbmiChar, IbmiPackedDecimal, IbmiInt } from './ibmi-validators.js';`,
432
+ ` * import { IbmiChar, IbmiPackedDecimal, IbmiInt } from '../common/ibmi-validators.js';`,
238
433
  ` *`,
239
434
  ` * export class MyDto {`,
240
- ` * @IbmiChar(8) name!: string;`,
435
+ ` * @IbmiChar(8) name!: string;`,
241
436
  ` * @IbmiPackedDecimal(6, 2) balance!: number;`,
242
- ` * @IbmiInt() id!: number;`,
437
+ ` * @IbmiInt() id!: number;`,
243
438
  ` * }`,
244
439
  ` */`,
245
440
  `import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator';`,
@@ -390,36 +585,147 @@ function ibmiValidatorsNestTs() {
390
585
  ``,
391
586
  ].join('\n');
392
587
  }
393
- // ── src/ibmi/dto/calculate.dto.ts ─────────────────────────────────────────────
394
- function calculateDtoTs() {
395
- return [
396
- `import { IsNumber } from 'class-validator';`,
397
- `import { ApiProperty } from '@nestjs/swagger';`,
398
- `import { IbmiInt } from './ibmi-validators.js';`,
399
- ``,
400
- `export class CalculateDto {`,
401
- ` @ApiProperty({ example: 5, description: 'Input integer (IBM i INTEGER: -2147483648 to 2147483647)' })`,
402
- ` @IsNumber()`,
403
- ` @IbmiInt()`,
404
- ` input!: number;`,
405
- `}`,
406
- ``,
407
- ].join('\n');
588
+ // ── .vscode/settings.json ─────────────────────────────────────────────────────
589
+ function vscodeSettings() {
590
+ return JSON.stringify({
591
+ 'editor.formatOnSave': true,
592
+ 'editor.defaultFormatter': 'esbenp.prettier-vscode',
593
+ 'editor.codeActionsOnSave': {
594
+ 'source.fixAll.eslint': 'explicit',
595
+ 'source.organizeImports': 'never',
596
+ 'source.removeUnusedImports': 'explicit',
597
+ },
598
+ 'editor.tabSize': 2,
599
+ 'editor.insertSpaces': true,
600
+ 'editor.rulers': [80, 120],
601
+ 'editor.wordWrap': 'on',
602
+ 'editor.bracketPairColorization.enabled': true,
603
+ 'editor.guides.bracketPairs': true,
604
+ 'editor.inlineSuggest.enabled': true,
605
+ 'js/ts.tsdk': 'node_modules/typescript/lib',
606
+ 'js/ts.enablePromptUseWorkspaceTsdk': true,
607
+ 'js/ts.preferences.importModuleSpecifier': 'non-relative',
608
+ 'js/ts.updateImportsOnFileMove.enabled': 'always',
609
+ 'js/ts.suggest.autoImports': true,
610
+ 'js/ts.preferences.quoteStyle': 'single',
611
+ 'files.autoSave': 'onFocusChange',
612
+ 'files.trimTrailingWhitespace': true,
613
+ 'files.insertFinalNewline': true,
614
+ 'files.eol': '\n',
615
+ 'files.exclude': {
616
+ '**/.git': true,
617
+ '**/.DS_Store': true,
618
+ '**/dist': true,
619
+ '**/coverage': true,
620
+ },
621
+ 'search.exclude': {
622
+ '**/node_modules': true,
623
+ '**/dist': true,
624
+ '**/coverage': true,
625
+ 'package-lock.json': true,
626
+ 'pnpm-lock.yaml': true,
627
+ },
628
+ 'prettier.singleQuote': true,
629
+ 'prettier.trailingComma': 'all',
630
+ 'prettier.tabWidth': 2,
631
+ 'prettier.semi': true,
632
+ 'prettier.arrowParens': 'always',
633
+ 'prettier.endOfLine': 'lf',
634
+ 'git.autofetch': true,
635
+ 'git.confirmSync': false,
636
+ 'git.enableSmartCommit': true,
637
+ 'explorer.confirmDelete': false,
638
+ 'explorer.confirmDragAndDrop': false,
639
+ 'explorer.fileNesting.enabled': true,
640
+ 'explorer.fileNesting.patterns': {
641
+ '*.ts': '${capture}.js, ${capture}.d.ts, ${capture}.spec.ts, ${capture}.test.ts',
642
+ '*.controller.ts': '${capture}.controller.spec.ts',
643
+ '*.service.ts': '${capture}.service.spec.ts',
644
+ '*.module.ts': '${capture}.module.spec.ts',
645
+ 'package.json': 'package-lock.json, pnpm-lock.yaml, yarn.lock, .npmrc, .nvmrc, .node-version',
646
+ 'tsconfig.json': 'tsconfig.*.json',
647
+ '.env': '.env.*',
648
+ '.gitignore': '.gitattributes',
649
+ 'README.md': 'CHANGELOG.md, LICENSE',
650
+ },
651
+ '[typescript]': { 'editor.defaultFormatter': 'esbenp.prettier-vscode' },
652
+ '[javascript]': { 'editor.defaultFormatter': 'esbenp.prettier-vscode' },
653
+ '[json]': { 'editor.defaultFormatter': 'esbenp.prettier-vscode' },
654
+ '[ibmi]': { 'editor.defaultFormatter': 'openibm.vscode' },
655
+ 'rest-client.environmentVariables': {
656
+ '$shared': { baseUrl: 'http://localhost:3000' },
657
+ 'local': { baseUrl: 'http://localhost:3000' },
658
+ },
659
+ 'todo-tree.general.tags': ['TODO', 'FIXME', 'NOTE', 'BUG'],
660
+ 'todo-tree.highlights.defaultHighlight': {
661
+ icon: 'alert', type: 'text', foreground: '#fff', iconColour: '#ffcc00',
662
+ },
663
+ 'todo-tree.highlights.customHighlight': {
664
+ 'TODO': { icon: 'check', iconColour: '#3498db' },
665
+ 'FIXME': { icon: 'flame', iconColour: '#e74c3c' },
666
+ 'NOTE': { icon: 'note', iconColour: '#2ecc71' },
667
+ 'BUG': { icon: 'bug', iconColour: '#e74c3c' },
668
+ },
669
+ }, null, 2) + '\n';
670
+ }
671
+ // ── .vscode/extensions.json ───────────────────────────────────────────────────
672
+ function vscodeExtensions() {
673
+ return JSON.stringify({
674
+ recommendations: [
675
+ 'openibm.vscode',
676
+ 'esbenp.prettier-vscode',
677
+ 'dbaeumer.vscode-eslint',
678
+ 'christian-kohler.path-intellisense',
679
+ 'humao.rest-client',
680
+ 'gruntfuggly.todo-tree',
681
+ ],
682
+ }, null, 2) + '\n';
408
683
  }
409
684
  // ── README.md ─────────────────────────────────────────────────────────────────
410
685
  function readme(a, hasProgram, hasTable) {
411
686
  const endpoints = [
412
- `| \`GET /health\` | Connection status |`,
413
- `| \`GET /api-docs\` | Swagger UI |`,
414
- ...(hasTable ? [`| \`GET /customers\` | Query DB2 table (filtered by state) |`] : []),
415
- ...(hasTable ? [`| \`GET /customers/:id\`| Find customer by ID |`] : []),
416
- ...(hasProgram ? [`| \`POST /calculate\` | Call IBM i program (\`{ input: number }\`) |`] : []),
687
+ `| \`GET /health\` | Connection status |`,
688
+ `| \`GET /api-docs\` | Swagger UI |`,
689
+ `| \`GET /scalar\` | Scalar API reference |`,
690
+ ...(hasTable ? [`| \`GET /customers\` | Query DB2 table (filtered by state) |`] : []),
691
+ ...(hasTable ? [`| \`GET /customers/:id\` | Find customer by ID |`] : []),
692
+ ...(hasProgram ? [`| \`POST /programs/calculate\`| Call IBM i program (\`{ input: number }\`) |`] : []),
417
693
  ];
418
694
  return [
419
695
  `# ${a.projectName}`,
420
696
  ``,
421
697
  `IBM i NestJS application generated by [create-openibm](https://www.npmjs.com/package/create-openibm).`,
422
698
  ``,
699
+ `## Project structure`,
700
+ ``,
701
+ `\`\`\``,
702
+ `src/`,
703
+ `├── main.ts # Bootstrap — listen, ValidationPipe, docs`,
704
+ `├── app.module.ts # Root module`,
705
+ `├── docs.ts # Swagger + Scalar setup`,
706
+ `├── ibmi/ # IBM i client (global module)`,
707
+ `│ ├── ibmi.module.ts`,
708
+ `│ └── ibmi.service.ts`,
709
+ `├── health/`,
710
+ `│ ├── health.module.ts`,
711
+ `│ └── health.controller.ts`,
712
+ ...(hasTable ? [
713
+ `├── customers/`,
714
+ `│ ├── customers.module.ts`,
715
+ `│ ├── customers.controller.ts`,
716
+ `│ └── customers.service.ts`,
717
+ ] : []),
718
+ ...(hasProgram ? [
719
+ `├── programs/`,
720
+ `│ ├── programs.module.ts`,
721
+ `│ ├── programs.controller.ts`,
722
+ `│ ├── programs.service.ts`,
723
+ `│ └── dto/calculate.dto.ts`,
724
+ ] : []),
725
+ `└── common/`,
726
+ ` └── ibmi-validators.ts # class-validator decorators for IBM i types`,
727
+ `\`\`\``,
728
+ ``,
423
729
  `## Setup`,
424
730
  ``,
425
731
  `\`\`\`bash`,
@@ -431,15 +737,9 @@ function readme(a, hasProgram, hasTable) {
431
737
  `## Development`,
432
738
  ``,
433
739
  `\`\`\`bash`,
434
- `npm run dev`,
740
+ `npm run dev # starts with nodemon — restarts on file save`,
435
741
  `\`\`\``,
436
742
  ``,
437
- `## Architecture`,
438
- ``,
439
- `- **\`IBMiModule\`** — NestJS module that provides and exports \`IBMiService\``,
440
- `- **\`IBMiService\`** — wraps the generated client; connects on \`onModuleInit\`, disconnects on \`onModuleDestroy\``,
441
- `- **\`IBMiController\`** — exposes HTTP endpoints backed by the IBM i client`,
442
- ``,
443
743
  `## Endpoints`,
444
744
  ``,
445
745
  `| Route | Description |`,
@@ -448,10 +748,10 @@ function readme(a, hasProgram, hasTable) {
448
748
  ``,
449
749
  `## IBM i validators`,
450
750
  ``,
451
- `\`src/ibmi/dto/ibmi-validators.ts\` provides class-validator decorators for IBM i data types:`,
751
+ `\`src/common/ibmi-validators.ts\` provides class-validator decorators for IBM i data types:`,
452
752
  ``,
453
753
  `\`\`\`ts`,
454
- `import { IbmiChar, IbmiPackedDecimal, IbmiInt } from './ibmi-validators.js';`,
754
+ `import { IbmiChar, IbmiPackedDecimal, IbmiInt } from '../common/ibmi-validators.js';`,
455
755
  `import { IsString, IsNumber } from 'class-validator';`,
456
756
  `import { ApiProperty } from '@nestjs/swagger';`,
457
757
  ``,
@@ -461,11 +761,6 @@ function readme(a, hasProgram, hasTable) {
461
761
  ` @IbmiChar(8)`,
462
762
  ` lastName!: string;`,
463
763
  ``,
464
- ` @ApiProperty({ example: 0.00, description: 'DECIMAL(6,2)' })`,
465
- ` @IsNumber()`,
466
- ` @IbmiPackedDecimal(6, 2)`,
467
- ` balanceDue!: number;`,
468
- ``,
469
764
  ` @ApiProperty({ example: 1, description: 'INTEGER' })`,
470
765
  ` @IsNumber()`,
471
766
  ` @IbmiInt()`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openibm",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Create a new IBM i application — interactive scaffold with Express, NestJS, or plain Node.js",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/satyamlohiya/openibm",
@@ -30,16 +30,6 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "sideEffects": false,
33
- "scripts": {
34
- "build": "tsc --project tsconfig.json",
35
- "dev": "tsc --project tsconfig.json --watch",
36
- "typecheck": "tsc --noEmit",
37
- "clean": "rm -rf dist",
38
- "try": "node scripts/try.mjs",
39
- "link": "pnpm run build && pnpm link --global",
40
- "unlink": "pnpm unlink --global create-openibm",
41
- "prepublishOnly": "npm run build"
42
- },
43
33
  "dependencies": {
44
34
  "@clack/prompts": "^0.9.0"
45
35
  },
@@ -52,5 +42,14 @@
52
42
  },
53
43
  "publishConfig": {
54
44
  "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc --project tsconfig.json",
48
+ "dev": "tsc --project tsconfig.json --watch",
49
+ "typecheck": "tsc --noEmit",
50
+ "clean": "rm -rf dist",
51
+ "try": "node scripts/try.mjs",
52
+ "link": "pnpm run build && pnpm link --global",
53
+ "unlink": "pnpm unlink --global create-openibm"
55
54
  }
56
- }
55
+ }