create-openibm 0.1.0
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/README.md +154 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +156 -0
- package/dist/scaffold.d.ts +12 -0
- package/dist/scaffold.js +22 -0
- package/dist/templates/base.d.ts +2 -0
- package/dist/templates/base.js +58 -0
- package/dist/templates/basic.d.ts +11 -0
- package/dist/templates/basic.js +323 -0
- package/dist/templates/express.d.ts +2 -0
- package/dist/templates/express.js +199 -0
- package/dist/templates/nestjs.d.ts +2 -0
- package/dist/templates/nestjs.js +477 -0
- package/package.json +56 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
export function basicFiles(a) {
|
|
2
|
+
return {
|
|
3
|
+
'package.json': packageJson(a),
|
|
4
|
+
'tsconfig.json': tsconfig(),
|
|
5
|
+
'src/index.ts': indexTs(a),
|
|
6
|
+
'src/ibmi-validators.ts': ibmiValidatorsTs(),
|
|
7
|
+
'scripts/link-dev.mjs': linkDevScriptMjs(),
|
|
8
|
+
'README.md': readme(a),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
// ── package.json ──────────────────────────────────────────────────────────────
|
|
12
|
+
function packageJson(a) {
|
|
13
|
+
return JSON.stringify({
|
|
14
|
+
name: a.projectName,
|
|
15
|
+
version: '0.1.0',
|
|
16
|
+
private: true,
|
|
17
|
+
type: 'module',
|
|
18
|
+
scripts: {
|
|
19
|
+
generate: 'openibm generate',
|
|
20
|
+
build: 'tsc --project tsconfig.json',
|
|
21
|
+
dev: 'node --env-file=.env --import tsx/esm src/index.ts',
|
|
22
|
+
start: 'node dist/index.js',
|
|
23
|
+
'link:dev': 'node scripts/link-dev.mjs',
|
|
24
|
+
},
|
|
25
|
+
dependencies: {
|
|
26
|
+
'@openibm/driver': '^0.1.0',
|
|
27
|
+
'@openibm/types': '^0.1.0',
|
|
28
|
+
zod: '^3.23.0',
|
|
29
|
+
...(a.transport === 'odbc' ? { odbc: '^2.4.0' } : {}),
|
|
30
|
+
...(a.transport === 'ssh' ? { ssh2: '^1.15.0' } : {}),
|
|
31
|
+
},
|
|
32
|
+
devDependencies: {
|
|
33
|
+
'@openibm/client': '^0.1.0',
|
|
34
|
+
'@types/node': '^22.0.0',
|
|
35
|
+
tsx: '^4.0.0',
|
|
36
|
+
typescript: '^5.7.0',
|
|
37
|
+
},
|
|
38
|
+
engines: { node: '>=20.0.0' },
|
|
39
|
+
}, null, 4) + '\n';
|
|
40
|
+
}
|
|
41
|
+
// ── tsconfig.json ─────────────────────────────────────────────────────────────
|
|
42
|
+
function tsconfig() {
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
compilerOptions: {
|
|
45
|
+
target: 'ES2022',
|
|
46
|
+
module: 'NodeNext',
|
|
47
|
+
moduleResolution: 'NodeNext',
|
|
48
|
+
outDir: './dist',
|
|
49
|
+
rootDir: './src',
|
|
50
|
+
strict: true,
|
|
51
|
+
esModuleInterop: true,
|
|
52
|
+
skipLibCheck: true,
|
|
53
|
+
forceConsistentCasingInFileNames: true,
|
|
54
|
+
},
|
|
55
|
+
include: ['src'],
|
|
56
|
+
exclude: ['node_modules', 'dist'],
|
|
57
|
+
}, null, 4) + '\n';
|
|
58
|
+
}
|
|
59
|
+
// ── src/index.ts ──────────────────────────────────────────────────────────────
|
|
60
|
+
function indexTs(a) {
|
|
61
|
+
const hasProgram = a.features.includes('program');
|
|
62
|
+
const hasTable = a.features.includes('table');
|
|
63
|
+
const imports = [
|
|
64
|
+
`import { createClient } from './generated/ibmi/index.js';`,
|
|
65
|
+
...(a.transport === 'odbc' ? [`import 'odbc';`] : []),
|
|
66
|
+
];
|
|
67
|
+
const clientConfig = clientConfigBlock(a);
|
|
68
|
+
const body = [];
|
|
69
|
+
if (hasProgram) {
|
|
70
|
+
body.push(` // ── Program call ─────────────────────────────────────────────────────────`, ` const { output } = await client.program.SimpleCalc({ input: 5 });`, ` console.log('SimpleCalc(5) =>', output); // 500`, ``);
|
|
71
|
+
}
|
|
72
|
+
if (hasTable) {
|
|
73
|
+
body.push(` // ── Table query ──────────────────────────────────────────────────────────`, ` const customers = await client.query.Customer.findMany({`, ` where: { state: 'TX' },`, ` orderBy: { customerId: 'asc' },`, ` take: 5,`, ` });`, ` console.log('TX customers:', customers);`, ``, ` const first = await client.query.Customer.findFirst({ orderBy: { customerId: 'asc' } });`, ` console.log('First customer:', first);`, ``);
|
|
74
|
+
}
|
|
75
|
+
if (!hasProgram && !hasTable) {
|
|
76
|
+
body.push(` // ── Raw SQL ──────────────────────────────────────────────────────────────`, ` const rows = await client.query.sql('SELECT * FROM SYSIBM.SYSDUMMY1');`, ` console.log('SYSDUMMY1:', rows);`, ``);
|
|
77
|
+
}
|
|
78
|
+
return [
|
|
79
|
+
...imports,
|
|
80
|
+
``,
|
|
81
|
+
clientConfig,
|
|
82
|
+
``,
|
|
83
|
+
`async function main(): Promise<void> {`,
|
|
84
|
+
` await client.connect();`,
|
|
85
|
+
` console.log('Connected to IBM i');`,
|
|
86
|
+
``,
|
|
87
|
+
...body,
|
|
88
|
+
` await client.disconnect();`,
|
|
89
|
+
` console.log('Disconnected.');`,
|
|
90
|
+
`}`,
|
|
91
|
+
``,
|
|
92
|
+
`main().catch(console.error);`,
|
|
93
|
+
``,
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
// ── README.md ─────────────────────────────────────────────────────────────────
|
|
97
|
+
function readme(a) {
|
|
98
|
+
return [
|
|
99
|
+
`# ${a.projectName}`,
|
|
100
|
+
``,
|
|
101
|
+
`IBM i Node.js application generated by [create-openibm](https://www.npmjs.com/package/create-openibm).`,
|
|
102
|
+
``,
|
|
103
|
+
`## Setup`,
|
|
104
|
+
``,
|
|
105
|
+
`\`\`\`bash`,
|
|
106
|
+
`cp .env.example .env`,
|
|
107
|
+
`# fill in IBMI_SYSTEM, IBMI_USER, IBMI_PASS`,
|
|
108
|
+
`\`\`\``,
|
|
109
|
+
``,
|
|
110
|
+
`## Generate the typed client`,
|
|
111
|
+
``,
|
|
112
|
+
`\`\`\`bash`,
|
|
113
|
+
`npm run generate`,
|
|
114
|
+
`\`\`\``,
|
|
115
|
+
``,
|
|
116
|
+
`## Development`,
|
|
117
|
+
``,
|
|
118
|
+
`\`\`\`bash`,
|
|
119
|
+
`npm run dev`,
|
|
120
|
+
`\`\`\``,
|
|
121
|
+
``,
|
|
122
|
+
`## Build`,
|
|
123
|
+
``,
|
|
124
|
+
`\`\`\`bash`,
|
|
125
|
+
`npm run build`,
|
|
126
|
+
`npm start`,
|
|
127
|
+
`\`\`\``,
|
|
128
|
+
``,
|
|
129
|
+
`## IBM i validators`,
|
|
130
|
+
``,
|
|
131
|
+
`\`src/ibmi-validators.ts\` provides Zod schemas for IBM i data types:`,
|
|
132
|
+
``,
|
|
133
|
+
`\`\`\`ts`,
|
|
134
|
+
`import { ibmiChar, ibmiPackedDecimal, ibmiInt } from './ibmi-validators.js';`,
|
|
135
|
+
``,
|
|
136
|
+
`const schema = z.object({`,
|
|
137
|
+
` name: ibmiChar(8), // CHAR(8)`,
|
|
138
|
+
` balance: ibmiPackedDecimal(6, 2), // DECIMAL(6,2)`,
|
|
139
|
+
` id: ibmiInt, // INTEGER`,
|
|
140
|
+
`});`,
|
|
141
|
+
`\`\`\``,
|
|
142
|
+
``,
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
// ── Shared helpers ─────────────────────────────────────────────────────────────
|
|
146
|
+
export function clientConfigBlock(a) {
|
|
147
|
+
const lines = [
|
|
148
|
+
`const client = createClient({`,
|
|
149
|
+
` transport: (process.env.IBMI_TRANSPORT ?? '${a.transport}') as '${a.transport}',`,
|
|
150
|
+
` system: process.env.IBMI_SYSTEM ?? 'localhost',`,
|
|
151
|
+
` username: process.env.IBMI_USER ?? '',`,
|
|
152
|
+
` password: process.env.IBMI_PASS ?? '',`,
|
|
153
|
+
];
|
|
154
|
+
if (a.transport === 'http' || a.transport === 'https') {
|
|
155
|
+
const port = a.transport === 'https' ? 47700 : 57700;
|
|
156
|
+
lines.push(` http: {`, ` port: Number(process.env.IBMI_PORT ?? '${port}'),`, ` database: process.env.IBMI_DATABASE ?? '*LOCAL',`, ` },`);
|
|
157
|
+
}
|
|
158
|
+
if (a.transport === 'ssh')
|
|
159
|
+
lines.push(` ssh: { port: Number(process.env.IBMI_SSH_PORT ?? '22') },`);
|
|
160
|
+
if (a.transport === 'odbc')
|
|
161
|
+
lines.push(` odbc: { poolSize: 5 },`);
|
|
162
|
+
lines.push(`});`);
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* scripts/link-dev.mjs — shared by all templates.
|
|
167
|
+
* Uses `pnpm add file:...` so pnpm installs ALL deps in one shot while
|
|
168
|
+
* pulling @openibm/* from the globally linked monorepo packages.
|
|
169
|
+
*/
|
|
170
|
+
export function linkDevScriptMjs() {
|
|
171
|
+
return [
|
|
172
|
+
`/**`,
|
|
173
|
+
` * link-dev.mjs`,
|
|
174
|
+
` * Installs @openibm/* from the locally linked monorepo without hitting npm.`,
|
|
175
|
+
` *`,
|
|
176
|
+
` * Strategy: \`pnpm pack\` creates tarballs where workspace:* is replaced with`,
|
|
177
|
+
` * real semver versions. We reference those tarballs via file: in package.json`,
|
|
178
|
+
` * so pnpm install resolves everything in one shot.`,
|
|
179
|
+
` *`,
|
|
180
|
+
` * Run: pnpm run link:dev`,
|
|
181
|
+
` * Note: Remove pnpm.overrides + file: entries from package.json before publishing.`,
|
|
182
|
+
` */`,
|
|
183
|
+
`import { execSync } from 'node:child_process';`,
|
|
184
|
+
`import { readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs';`,
|
|
185
|
+
`import { resolve, join } from 'node:path';`,
|
|
186
|
+
`import { tmpdir } from 'node:os';`,
|
|
187
|
+
`import { fileURLToPath } from 'node:url';`,
|
|
188
|
+
``,
|
|
189
|
+
`const root = resolve(fileURLToPath(import.meta.url), '..', '..');`,
|
|
190
|
+
`const pkgPath = resolve(root, 'package.json');`,
|
|
191
|
+
`const packDir = join(tmpdir(), 'openibm-packs');`,
|
|
192
|
+
``,
|
|
193
|
+
`mkdirSync(packDir, { recursive: true });`,
|
|
194
|
+
``,
|
|
195
|
+
`// ── Locate globally linked monorepo packages ──────────────────────────────────`,
|
|
196
|
+
`const globalModules = execSync('pnpm root --global', { encoding: 'utf8' }).trim();`,
|
|
197
|
+
``,
|
|
198
|
+
`const deps = ['@openibm/driver', '@openibm/types'];`,
|
|
199
|
+
`const devDeps = ['@openibm/client'];`,
|
|
200
|
+
``,
|
|
201
|
+
`// ── Pack each package into a tarball ─────────────────────────────────────────`,
|
|
202
|
+
`// pnpm pack replaces workspace:* with real semver — no npm registry needed`,
|
|
203
|
+
`const tarballs = {};`,
|
|
204
|
+
``,
|
|
205
|
+
`for (const name of [...deps, ...devDeps]) {`,
|
|
206
|
+
` const linkedPath = join(globalModules, name);`,
|
|
207
|
+
` const realPath = realpathSync(linkedPath);`,
|
|
208
|
+
` const meta = JSON.parse(readFileSync(join(realPath, 'package.json'), 'utf8'));`,
|
|
209
|
+
` const tgzName = meta.name.replace('@', '').replace('/', '-') + '-' + meta.version + '.tgz';`,
|
|
210
|
+
` const tgzPath = join(packDir, tgzName);`,
|
|
211
|
+
``,
|
|
212
|
+
` execSync(\`pnpm pack --pack-destination "\${packDir}"\`, { cwd: realPath, stdio: 'ignore' });`,
|
|
213
|
+
` tarballs[name] = tgzPath;`,
|
|
214
|
+
` console.log(\` packed \${name} → \${tgzPath}\`);`,
|
|
215
|
+
`}`,
|
|
216
|
+
``,
|
|
217
|
+
`// ── Patch package.json ────────────────────────────────────────────────────────`,
|
|
218
|
+
`const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));`,
|
|
219
|
+
``,
|
|
220
|
+
`for (const name of deps) { if (pkg.dependencies?.[name]) pkg.dependencies[name] = \`file:\${tarballs[name]}\`; }`,
|
|
221
|
+
`for (const name of devDeps) { if (pkg.devDependencies?.[name]) pkg.devDependencies[name] = \`file:\${tarballs[name]}\`; }`,
|
|
222
|
+
``,
|
|
223
|
+
`// Overrides ensure transitive deps of @openibm/client also resolve locally`,
|
|
224
|
+
`pkg.pnpm ??= {};`,
|
|
225
|
+
`pkg.pnpm.overrides ??= {};`,
|
|
226
|
+
`for (const name of deps) { pkg.pnpm.overrides[name] = \`file:\${tarballs[name]}\`; }`,
|
|
227
|
+
``,
|
|
228
|
+
`writeFileSync(pkgPath, JSON.stringify(pkg, null, 4) + '\\n');`,
|
|
229
|
+
`console.log('\\n patched package.json with local tarball references');`,
|
|
230
|
+
`console.log(' (remove pnpm.overrides + file: entries before publishing)\\n');`,
|
|
231
|
+
``,
|
|
232
|
+
`// ── Install everything in one shot ────────────────────────────────────────────`,
|
|
233
|
+
`execSync('pnpm install', { stdio: 'inherit', cwd: root });`,
|
|
234
|
+
`console.log('\\n ✓ Done.\\n');`,
|
|
235
|
+
``,
|
|
236
|
+
].join('\n');
|
|
237
|
+
}
|
|
238
|
+
/** Zod-based IBM i data-type validators — shared by basic + express templates */
|
|
239
|
+
export function ibmiValidatorsTs() {
|
|
240
|
+
return [
|
|
241
|
+
`/**`,
|
|
242
|
+
` * IBM i data-type validators for Zod.`,
|
|
243
|
+
` *`,
|
|
244
|
+
` * Usage:`,
|
|
245
|
+
` * import { ibmiChar, ibmiPackedDecimal, ibmiInt } from './ibmi-validators.js';`,
|
|
246
|
+
` *`,
|
|
247
|
+
` * const schema = z.object({`,
|
|
248
|
+
` * name: ibmiChar(8), // CHAR(8)`,
|
|
249
|
+
` * balance: ibmiPackedDecimal(6, 2), // DECIMAL(6,2)`,
|
|
250
|
+
` * id: ibmiInt, // INTEGER`,
|
|
251
|
+
` * });`,
|
|
252
|
+
` */`,
|
|
253
|
+
`import { z } from 'zod';`,
|
|
254
|
+
``,
|
|
255
|
+
`// ── String types ─────────────────────────────────────────────────────────────`,
|
|
256
|
+
``,
|
|
257
|
+
`/** CHAR(n) — fixed-length string, max n characters (trailing spaces are trimmed by DB2) */`,
|
|
258
|
+
`export const ibmiChar = (n: number) =>`,
|
|
259
|
+
` z.string().max(n, \`Must be at most \${n} characters (CHAR(\${n}))\`);`,
|
|
260
|
+
``,
|
|
261
|
+
`/** VARCHAR(n) — variable-length string, max n characters */`,
|
|
262
|
+
`export const ibmiVarChar = (n: number) =>`,
|
|
263
|
+
` z.string().max(n, \`Must be at most \${n} characters (VARCHAR(\${n}))\`);`,
|
|
264
|
+
``,
|
|
265
|
+
`// ── Decimal types ────────────────────────────────────────────────────────────`,
|
|
266
|
+
``,
|
|
267
|
+
`/**`,
|
|
268
|
+
` * DECIMAL(precision, scale) — packed decimal / zoned decimal.`,
|
|
269
|
+
` * e.g. ibmiPackedDecimal(9, 2) allows values from -9999999.99 to 9999999.99`,
|
|
270
|
+
` */`,
|
|
271
|
+
`export const ibmiPackedDecimal = (precision: number, scale: number) => {`,
|
|
272
|
+
` const intDigits = precision - scale;`,
|
|
273
|
+
` const factor = Math.pow(10, scale);`,
|
|
274
|
+
` return z.number()`,
|
|
275
|
+
` .refine(`,
|
|
276
|
+
` v => Math.abs(v) < Math.pow(10, intDigits),`,
|
|
277
|
+
` \`Integer part exceeds DECIMAL(\${precision},\${scale})\`,`,
|
|
278
|
+
` )`,
|
|
279
|
+
` .refine(`,
|
|
280
|
+
` v => Number.isInteger(Math.round(v * factor)),`,
|
|
281
|
+
` \`Max \${scale} decimal places for DECIMAL(\${precision},\${scale})\`,`,
|
|
282
|
+
` );`,
|
|
283
|
+
`};`,
|
|
284
|
+
``,
|
|
285
|
+
`/** NUMERIC(precision, scale) — alias for ibmiPackedDecimal */`,
|
|
286
|
+
`export const ibmiNumeric = ibmiPackedDecimal;`,
|
|
287
|
+
``,
|
|
288
|
+
`// ── Integer types ────────────────────────────────────────────────────────────`,
|
|
289
|
+
``,
|
|
290
|
+
`/** INTEGER — 4-byte signed integer */`,
|
|
291
|
+
`export const ibmiInt = z.number().int()`,
|
|
292
|
+
` .min(-2_147_483_648, 'Underflows INTEGER')`,
|
|
293
|
+
` .max( 2_147_483_647, 'Overflows INTEGER');`,
|
|
294
|
+
``,
|
|
295
|
+
`/** SMALLINT — 2-byte signed integer */`,
|
|
296
|
+
`export const ibmiSmallInt = z.number().int()`,
|
|
297
|
+
` .min(-32_768, 'Underflows SMALLINT')`,
|
|
298
|
+
` .max( 32_767, 'Overflows SMALLINT');`,
|
|
299
|
+
``,
|
|
300
|
+
`/** BIGINT — 8-byte signed integer (capped at JS safe integer range) */`,
|
|
301
|
+
`export const ibmiBigInt = z.number().int()`,
|
|
302
|
+
` .min(-9_007_199_254_740_991, 'Underflows safe BIGINT range')`,
|
|
303
|
+
` .max( 9_007_199_254_740_991, 'Overflows safe BIGINT range');`,
|
|
304
|
+
``,
|
|
305
|
+
`// ── Date / time types ────────────────────────────────────────────────────────`,
|
|
306
|
+
``,
|
|
307
|
+
`/** DATE — ISO date string YYYY-MM-DD */`,
|
|
308
|
+
`export const ibmiDate = z.string()`,
|
|
309
|
+
` .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Must be YYYY-MM-DD (IBM i DATE)');`,
|
|
310
|
+
``,
|
|
311
|
+
`/** TIME — time string HH:MM:SS */`,
|
|
312
|
+
`export const ibmiTime = z.string()`,
|
|
313
|
+
` .regex(/^\\d{2}:\\d{2}:\\d{2}$/, 'Must be HH:MM:SS (IBM i TIME)');`,
|
|
314
|
+
``,
|
|
315
|
+
`/** TIMESTAMP — IBM i format YYYY-MM-DD-HH.MM.SS.ffffff */`,
|
|
316
|
+
`export const ibmiTimestamp = z.string()`,
|
|
317
|
+
` .regex(`,
|
|
318
|
+
` /^\\d{4}-\\d{2}-\\d{2}-\\d{2}\\.\\d{2}\\.\\d{2}\\.\\d{6}$/,`,
|
|
319
|
+
` 'Must be YYYY-MM-DD-HH.MM.SS.ffffff (IBM i TIMESTAMP)',`,
|
|
320
|
+
` );`,
|
|
321
|
+
``,
|
|
322
|
+
].join('\n');
|
|
323
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { clientConfigBlock, ibmiValidatorsTs, linkDevScriptMjs } from './basic.js';
|
|
2
|
+
export function expressFiles(a) {
|
|
3
|
+
const hasProgram = a.features.includes('program');
|
|
4
|
+
const hasTable = a.features.includes('table');
|
|
5
|
+
return {
|
|
6
|
+
'package.json': packageJson(a),
|
|
7
|
+
'tsconfig.json': tsconfig(),
|
|
8
|
+
'src/index.ts': indexTs(a, hasProgram, hasTable),
|
|
9
|
+
'src/ibmi-validators.ts': ibmiValidatorsTs(),
|
|
10
|
+
'scripts/link-dev.mjs': linkDevScriptMjs(),
|
|
11
|
+
'README.md': readme(a, hasProgram, hasTable),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
// ── package.json ──────────────────────────────────────────────────────────────
|
|
15
|
+
function packageJson(a) {
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
name: a.projectName,
|
|
18
|
+
version: '0.1.0',
|
|
19
|
+
private: true,
|
|
20
|
+
type: 'module',
|
|
21
|
+
scripts: {
|
|
22
|
+
generate: 'openibm generate',
|
|
23
|
+
build: 'tsc --project tsconfig.json',
|
|
24
|
+
dev: 'node --env-file=.env --import tsx/esm src/index.ts',
|
|
25
|
+
start: 'node dist/index.js',
|
|
26
|
+
'link:dev': 'node scripts/link-dev.mjs',
|
|
27
|
+
},
|
|
28
|
+
dependencies: {
|
|
29
|
+
'@openibm/driver': '^0.1.0',
|
|
30
|
+
'@openibm/types': '^0.1.0',
|
|
31
|
+
express: '^4.21.0',
|
|
32
|
+
morgan: '^1.10.0',
|
|
33
|
+
'swagger-ui-express': '^5.0.0',
|
|
34
|
+
zod: '^3.23.0',
|
|
35
|
+
...(a.transport === 'odbc' ? { odbc: '^2.4.0' } : {}),
|
|
36
|
+
...(a.transport === 'ssh' ? { ssh2: '^1.15.0' } : {}),
|
|
37
|
+
},
|
|
38
|
+
devDependencies: {
|
|
39
|
+
'@openibm/client': '^0.1.0',
|
|
40
|
+
'@types/express': '^4.17.0',
|
|
41
|
+
'@types/morgan': '^1.9.0',
|
|
42
|
+
'@types/swagger-ui-express': '^4.1.0',
|
|
43
|
+
'@types/node': '^22.0.0',
|
|
44
|
+
tsx: '^4.0.0',
|
|
45
|
+
typescript: '^5.7.0',
|
|
46
|
+
},
|
|
47
|
+
engines: { node: '>=20.0.0' },
|
|
48
|
+
}, null, 4) + '\n';
|
|
49
|
+
}
|
|
50
|
+
// ── tsconfig.json ─────────────────────────────────────────────────────────────
|
|
51
|
+
function tsconfig() {
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
compilerOptions: {
|
|
54
|
+
target: 'ES2022',
|
|
55
|
+
module: 'NodeNext',
|
|
56
|
+
moduleResolution: 'NodeNext',
|
|
57
|
+
outDir: './dist',
|
|
58
|
+
rootDir: './src',
|
|
59
|
+
strict: true,
|
|
60
|
+
esModuleInterop: true,
|
|
61
|
+
skipLibCheck: true,
|
|
62
|
+
forceConsistentCasingInFileNames: true,
|
|
63
|
+
},
|
|
64
|
+
include: ['src'],
|
|
65
|
+
exclude: ['node_modules', 'dist'],
|
|
66
|
+
}, null, 4) + '\n';
|
|
67
|
+
}
|
|
68
|
+
// ── src/index.ts ──────────────────────────────────────────────────────────────
|
|
69
|
+
function indexTs(a, hasProgram, hasTable) {
|
|
70
|
+
// ── Swagger paths (built conditionally) ──────────────────────────────────
|
|
71
|
+
const swaggerPaths = [
|
|
72
|
+
` '/health': {`,
|
|
73
|
+
` get: {`,
|
|
74
|
+
` tags: ['Health'],`,
|
|
75
|
+
` summary: 'Connection status',`,
|
|
76
|
+
` responses: {`,
|
|
77
|
+
` '200': { description: 'OK', content: { 'application/json': { schema: { example: { status: 'ok', connected: true } } } } },`,
|
|
78
|
+
` },`,
|
|
79
|
+
` },`,
|
|
80
|
+
` },`,
|
|
81
|
+
];
|
|
82
|
+
if (hasTable) {
|
|
83
|
+
swaggerPaths.push(` '/customers': {`, ` get: {`, ` tags: ['Customers'],`, ` summary: 'List customers (first 20, filtered by state)',`, ` parameters: [{`, ` in: 'query', name: 'state', required: false,`, ` schema: { type: 'string', maxLength: 2, example: 'TX' },`, ` description: 'State code — CHAR(2)',`, ` }],`, ` responses: { '200': { description: 'Array of customer rows' } },`, ` },`, ` },`, ` '/customers/{id}': {`, ` get: {`, ` tags: ['Customers'],`, ` summary: 'Find customer by ID',`, ` parameters: [{`, ` in: 'path', name: 'id', required: true,`, ` schema: { type: 'integer', example: 1 },`, ` }],`, ` responses: {`, ` '200': { description: 'Customer row' },`, ` '404': { description: 'Not found' },`, ` },`, ` },`, ` },`);
|
|
84
|
+
}
|
|
85
|
+
if (hasProgram) {
|
|
86
|
+
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
|
+
}
|
|
88
|
+
// ── Routes ───────────────────────────────────────────────────────────────
|
|
89
|
+
const routes = [
|
|
90
|
+
`// ── Health ───────────────────────────────────────────────────────────────────`,
|
|
91
|
+
``,
|
|
92
|
+
`app.get('/health', (_req, res) => {`,
|
|
93
|
+
` res.json({ status: 'ok', connected: client.isConnected() });`,
|
|
94
|
+
`});`,
|
|
95
|
+
];
|
|
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
|
+
: [];
|
|
105
|
+
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,
|
|
111
|
+
``,
|
|
112
|
+
`const app = express();`,
|
|
113
|
+
`const PORT = Number(process.env.PORT ?? 3000);`,
|
|
114
|
+
``,
|
|
115
|
+
`// ── Middleware ────────────────────────────────────────────────────────────────`,
|
|
116
|
+
``,
|
|
117
|
+
`app.use(express.json());`,
|
|
118
|
+
`app.use(morgan('dev'));`,
|
|
119
|
+
``,
|
|
120
|
+
`// ── IBM i client ──────────────────────────────────────────────────────────────`,
|
|
121
|
+
``,
|
|
122
|
+
clientConfigBlock(a),
|
|
123
|
+
``,
|
|
124
|
+
`// ── OpenAPI / Swagger ─────────────────────────────────────────────────────────`,
|
|
125
|
+
``,
|
|
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
|
+
`};`,
|
|
133
|
+
``,
|
|
134
|
+
`app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiSpec as any));`,
|
|
135
|
+
``,
|
|
136
|
+
`// ── Routes ───────────────────────────────────────────────────────────────────`,
|
|
137
|
+
``,
|
|
138
|
+
...routes,
|
|
139
|
+
``,
|
|
140
|
+
`// ── Start ────────────────────────────────────────────────────────────────────`,
|
|
141
|
+
``,
|
|
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
|
+
`});`,
|
|
147
|
+
``,
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
// ── README.md ─────────────────────────────────────────────────────────────────
|
|
151
|
+
function readme(a, hasTable, hasProgram) {
|
|
152
|
+
const endpoints = [
|
|
153
|
+
`| \`GET /health\` | Connection status |`,
|
|
154
|
+
`| \`GET /api-docs\` | Swagger UI |`,
|
|
155
|
+
...(hasTable ? [`| \`GET /customers\` | Query DB2 table (filtered by state) |`] : []),
|
|
156
|
+
...(hasTable ? [`| \`GET /customers/:id\`| Find customer by ID |`] : []),
|
|
157
|
+
...(hasProgram ? [`| \`POST /calculate\` | Call IBM i program (\`{ input: number }\`) |`] : []),
|
|
158
|
+
];
|
|
159
|
+
return [
|
|
160
|
+
`# ${a.projectName}`,
|
|
161
|
+
``,
|
|
162
|
+
`IBM i Express application generated by [create-openibm](https://www.npmjs.com/package/create-openibm).`,
|
|
163
|
+
``,
|
|
164
|
+
`## Setup`,
|
|
165
|
+
``,
|
|
166
|
+
`\`\`\`bash`,
|
|
167
|
+
`cp .env.example .env`,
|
|
168
|
+
`# fill in IBMI_SYSTEM, IBMI_USER, IBMI_PASS`,
|
|
169
|
+
`npm run generate`,
|
|
170
|
+
`\`\`\``,
|
|
171
|
+
``,
|
|
172
|
+
`## Development`,
|
|
173
|
+
``,
|
|
174
|
+
`\`\`\`bash`,
|
|
175
|
+
`npm run dev`,
|
|
176
|
+
`\`\`\``,
|
|
177
|
+
``,
|
|
178
|
+
`## Endpoints`,
|
|
179
|
+
``,
|
|
180
|
+
`| Route | Description |`,
|
|
181
|
+
`|---|---|`,
|
|
182
|
+
...endpoints,
|
|
183
|
+
``,
|
|
184
|
+
`## IBM i validators`,
|
|
185
|
+
``,
|
|
186
|
+
`\`src/ibmi-validators.ts\` provides Zod schemas for IBM i data types:`,
|
|
187
|
+
``,
|
|
188
|
+
`\`\`\`ts`,
|
|
189
|
+
`import { ibmiChar, ibmiPackedDecimal, ibmiInt } from './ibmi-validators.js';`,
|
|
190
|
+
``,
|
|
191
|
+
`const schema = z.object({`,
|
|
192
|
+
` name: ibmiChar(8), // CHAR(8)`,
|
|
193
|
+
` balance: ibmiPackedDecimal(6, 2), // DECIMAL(6,2)`,
|
|
194
|
+
` id: ibmiInt, // INTEGER`,
|
|
195
|
+
`});`,
|
|
196
|
+
`\`\`\``,
|
|
197
|
+
``,
|
|
198
|
+
].join('\n');
|
|
199
|
+
}
|