mcp-for-i 0.1.13 → 0.2.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 +17 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +14 -9
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +122 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +113 -100
- package/dist/controlplane/ui.js +1742 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +310 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +74 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +827 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +43 -22
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +515 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1700 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +50 -7
- package/docs/configuration.md +189 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +363 -188
- package/package.json +56 -53
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { Tools } from "../../ibmi/Tools.js";
|
|
3
|
+
import { clampInteger, json, pickFirst, requireIfsPath, requireObjectTypeValue, requireQsysName, toolError } from "../tools.js";
|
|
4
|
+
/*
|
|
5
|
+
* Read-only inspection of objects, members, IFS files, programs, tables and
|
|
6
|
+
* SQL routines. Every query is capped with FETCH FIRST and every name is
|
|
7
|
+
* validated and upper-cased before it reaches SQL or CL.
|
|
8
|
+
*
|
|
9
|
+
* Release notes (verify on the target system):
|
|
10
|
+
* - QSYS2.OBJECT_STATISTICS: 7.3+. The third OBJECT_NAME argument is newer;
|
|
11
|
+
* when it is rejected the query falls back to filtering on OBJNAME.
|
|
12
|
+
* - QSYS2.IFS_OBJECT_STATISTICS: 7.3 TR7 / 7.4 TR1 and later. Older systems
|
|
13
|
+
* fall back to `ls -ldL` (no CCSID).
|
|
14
|
+
* - QSYS2.PROGRAM_INFO, PROGRAM_EXPORT_IMPORT_INFO, BOUND_MODULE_INFO and
|
|
15
|
+
* BOUND_SRVPGM_INFO: added by Db2 PTF groups on 7.3/7.4; each part is
|
|
16
|
+
* optional and reported as a warning when the service is missing.
|
|
17
|
+
*/
|
|
18
|
+
const OBJECT_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
19
|
+
const CONNECTION_NAME = { type: "string", description: "Saved IBM i connection name." };
|
|
20
|
+
const LIMIT = (fallback, max) => ({ type: "number", description: `Maximum rows per list (default ${fallback}, max ${max}).` });
|
|
21
|
+
const PROGRAM_TYPES = ["*PGM", "*SRVPGM"];
|
|
22
|
+
const PGMREF_TYPES = ["*ALL", "*PGM", "*SRVPGM", "*MODULE", "*SQLPKG", "*QRYDFN"];
|
|
23
|
+
const tools = [
|
|
24
|
+
{
|
|
25
|
+
name: "ibmi.objects.describe",
|
|
26
|
+
description: "Describe an object from QSYS2.OBJECT_STATISTICS: type, attribute, owner, size, text, created/changed/last-used dates, " +
|
|
27
|
+
"save/restore dates and, when the system records it, the source file/member it was created from.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
library: { type: "string", description: "Library name." },
|
|
32
|
+
object: { type: "string", description: "Object name." },
|
|
33
|
+
objectType: { type: "string", description: "Object type such as *PGM or *FILE (default *ALL: every object with that name)." },
|
|
34
|
+
connectionName: CONNECTION_NAME
|
|
35
|
+
},
|
|
36
|
+
required: ["library", "object"],
|
|
37
|
+
additionalProperties: false
|
|
38
|
+
},
|
|
39
|
+
annotations: OBJECT_ANNOTATIONS
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "ibmi.members.describe",
|
|
43
|
+
description: "Describe a member from QSYS2.SYSPARTITIONSTAT: source type, text, record count, created/changed/source-changed/last-used dates, " +
|
|
44
|
+
"data size, record length and the source CCSID.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
library: { type: "string" },
|
|
49
|
+
sourceFile: { type: "string", description: "File (usually a source physical file) that holds the member." },
|
|
50
|
+
member: { type: "string" },
|
|
51
|
+
connectionName: CONNECTION_NAME
|
|
52
|
+
},
|
|
53
|
+
required: ["library", "sourceFile", "member"],
|
|
54
|
+
additionalProperties: false
|
|
55
|
+
},
|
|
56
|
+
annotations: OBJECT_ANNOTATIONS
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "ibmi.ifs.stat",
|
|
60
|
+
description: "Describe an IFS object from QSYS2.IFS_OBJECT_STATISTICS: object type, CCSID, owner, size and created/accessed/changed dates. " +
|
|
61
|
+
"Falls back to `ls -ldL` on releases without that service.",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
path: { type: "string", description: "Absolute IFS path." },
|
|
66
|
+
connectionName: CONNECTION_NAME
|
|
67
|
+
},
|
|
68
|
+
required: ["path"],
|
|
69
|
+
additionalProperties: false
|
|
70
|
+
},
|
|
71
|
+
annotations: OBJECT_ANNOTATIONS
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "ibmi.programs.info",
|
|
75
|
+
description: "Describe a *PGM or *SRVPGM from QSYS2.PROGRAM_INFO (program type, activation group, owner/adopted authority, source location, " +
|
|
76
|
+
"target release), with its bound modules (BOUND_MODULE_INFO), bound service programs (BOUND_SRVPGM_INFO) and exports/imports " +
|
|
77
|
+
"(PROGRAM_EXPORT_IMPORT_INFO). For a service program it also generates binder source from the exports.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
library: { type: "string" },
|
|
82
|
+
program: { type: "string" },
|
|
83
|
+
objectType: { type: "string", enum: PROGRAM_TYPES, description: "Required only when a *PGM and a *SRVPGM share the name." },
|
|
84
|
+
includeModules: { type: "boolean", description: "Include bound modules (default true)." },
|
|
85
|
+
includeServicePrograms: { type: "boolean", description: "Include bound service programs (default true)." },
|
|
86
|
+
includeExports: { type: "boolean", description: "Include exports, imports and binder source (default true)." },
|
|
87
|
+
limit: LIMIT(500, 5000),
|
|
88
|
+
connectionName: CONNECTION_NAME
|
|
89
|
+
},
|
|
90
|
+
required: ["library", "program"],
|
|
91
|
+
additionalProperties: false
|
|
92
|
+
},
|
|
93
|
+
annotations: OBJECT_ANNOTATIONS
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "ibmi.objects.whereUsed",
|
|
97
|
+
description: "Program cross-reference from DSPPGMREF (written to a temporary QTEMP outfile in the same SQL job). " +
|
|
98
|
+
"direction=uses lists the objects a program references; direction=usedBy scans every program, service program and module " +
|
|
99
|
+
"in a library and lists the ones that reference a given object.",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
direction: { type: "string", enum: ["uses", "usedBy"] },
|
|
104
|
+
library: { type: "string", description: "uses: library of the program. usedBy: library whose programs are scanned." },
|
|
105
|
+
program: { type: "string", description: "uses: program, service program or module name." },
|
|
106
|
+
programType: { type: "string", enum: PGMREF_TYPES, description: "uses: object type of the program (default *ALL)." },
|
|
107
|
+
object: { type: "string", description: "usedBy: name of the referenced object." },
|
|
108
|
+
objectLibrary: { type: "string", description: "usedBy: library of the referenced object; references through *LIBL also match." },
|
|
109
|
+
objectType: { type: "string", description: "usedBy: only references of this object type, such as *FILE or *PGM." },
|
|
110
|
+
limit: LIMIT(1000, 10000),
|
|
111
|
+
connectionName: CONNECTION_NAME
|
|
112
|
+
},
|
|
113
|
+
required: ["direction", "library"],
|
|
114
|
+
additionalProperties: false
|
|
115
|
+
},
|
|
116
|
+
annotations: OBJECT_ANNOTATIONS
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "ibmi.tables.describe",
|
|
120
|
+
description: "Describe a table, view or physical file: columns from QSYS2.SYSCOLUMNS, constraints with their key columns from " +
|
|
121
|
+
"QSYS2.SYSCST/SYSKEYCST, and SQL indexes with their keys from QSYS2.SYSINDEXES/SYSKEYS. Accepts system or SQL names.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
library: { type: "string", description: "Library (system name) or SQL schema." },
|
|
126
|
+
table: { type: "string", description: "System name or SQL name of the table, view or file." },
|
|
127
|
+
includeKeys: { type: "boolean", description: "Include constraints and their key columns (default true)." },
|
|
128
|
+
includeIndexes: { type: "boolean", description: "Include SQL indexes over the table (default true)." },
|
|
129
|
+
limit: LIMIT(1000, 8000),
|
|
130
|
+
connectionName: CONNECTION_NAME
|
|
131
|
+
},
|
|
132
|
+
required: ["library", "table"],
|
|
133
|
+
additionalProperties: false
|
|
134
|
+
},
|
|
135
|
+
annotations: OBJECT_ANNOTATIONS
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "ibmi.routines.describe",
|
|
139
|
+
description: "Describe SQL procedures and functions from QSYS2.SYSROUTINES with their parameters from QSYS2.SYSPARMS, " +
|
|
140
|
+
"including a readable signature for every overload.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
library: { type: "string", description: "Routine schema." },
|
|
145
|
+
routine: { type: "string", description: "Routine name or specific name." },
|
|
146
|
+
routineType: { type: "string", enum: ["PROCEDURE", "FUNCTION"] },
|
|
147
|
+
includeDefinition: { type: "boolean", description: "Include the routine body text for SQL routines (default false, capped at 64 KB)." },
|
|
148
|
+
limit: LIMIT(50, 500),
|
|
149
|
+
connectionName: CONNECTION_NAME
|
|
150
|
+
},
|
|
151
|
+
required: ["library", "routine"],
|
|
152
|
+
additionalProperties: false
|
|
153
|
+
},
|
|
154
|
+
annotations: OBJECT_ANNOTATIONS
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
async function handle(ctx, name, args) {
|
|
158
|
+
const conn = await ctx.ensureActive(args?.connectionName);
|
|
159
|
+
switch (name) {
|
|
160
|
+
case "ibmi.objects.describe": return describeObject(conn, args);
|
|
161
|
+
case "ibmi.members.describe": return describeMember(conn, args);
|
|
162
|
+
case "ibmi.ifs.stat": return statIfs(conn, args);
|
|
163
|
+
case "ibmi.programs.info": return programInfo(conn, args);
|
|
164
|
+
case "ibmi.objects.whereUsed": return whereUsed(conn, args);
|
|
165
|
+
case "ibmi.tables.describe": return describeTable(conn, args);
|
|
166
|
+
case "ibmi.routines.describe": return describeRoutine(conn, args);
|
|
167
|
+
default: throw new Error(`Unknown tool: ${name}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ---- objects.describe ----
|
|
171
|
+
async function describeObject(conn, args) {
|
|
172
|
+
const library = requireQsysName(conn, args?.library, "library");
|
|
173
|
+
const object = requireQsysName(conn, args?.object, "object");
|
|
174
|
+
const objectType = args?.objectType ? requireObjectTypeValue(args.objectType, "objectType") : "*ALL";
|
|
175
|
+
const lib = Tools.sqlString(library);
|
|
176
|
+
const types = Tools.sqlString(objectType);
|
|
177
|
+
const obj = Tools.sqlString(object);
|
|
178
|
+
let rows;
|
|
179
|
+
try {
|
|
180
|
+
rows = await conn.runSQL(`select * from table(QSYS2.OBJECT_STATISTICS(${lib}, ${types}, ${obj})) X fetch first 50 rows only`);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Releases without the OBJECT_NAME argument: filter the library listing instead.
|
|
184
|
+
rows = await conn.runSQL(`select * from table(QSYS2.OBJECT_STATISTICS(${lib}, ${types})) X where X.OBJNAME = ${obj} fetch first 50 rows only`);
|
|
185
|
+
}
|
|
186
|
+
if (rows.length === 0) {
|
|
187
|
+
return toolError(`Object ${library}/${object} ${objectType} not found`, { library, object, objectType });
|
|
188
|
+
}
|
|
189
|
+
return json({ library, object, objectType, objects: rows.map(shapeObjectRow) });
|
|
190
|
+
}
|
|
191
|
+
export function shapeObjectRow(row) {
|
|
192
|
+
return compact({
|
|
193
|
+
library: str(row, "OBJLIB", "OBJECT_SCHEMA"),
|
|
194
|
+
name: str(row, "OBJNAME"),
|
|
195
|
+
type: str(row, "OBJTYPE"),
|
|
196
|
+
attribute: str(row, "OBJATTRIBUTE"),
|
|
197
|
+
text: str(row, "OBJTEXT"),
|
|
198
|
+
owner: str(row, "OBJOWNER"),
|
|
199
|
+
definer: str(row, "OBJDEFINER"),
|
|
200
|
+
size: num(row, "OBJSIZE"),
|
|
201
|
+
created: str(row, "OBJCREATED"),
|
|
202
|
+
changed: str(row, "CHANGE_TIMESTAMP"),
|
|
203
|
+
lastUsed: str(row, "LAST_USED_TIMESTAMP"),
|
|
204
|
+
daysUsed: num(row, "DAYS_USED_COUNT"),
|
|
205
|
+
saved: str(row, "SAVE_TIMESTAMP"),
|
|
206
|
+
restored: str(row, "RESTORE_TIMESTAMP"),
|
|
207
|
+
source: compactOrUndefined({
|
|
208
|
+
library: str(row, "SOURCE_LIBRARY"),
|
|
209
|
+
file: str(row, "SOURCE_FILE"),
|
|
210
|
+
member: str(row, "SOURCE_MEMBER"),
|
|
211
|
+
changed: str(row, "SOURCE_TIMESTAMP")
|
|
212
|
+
}),
|
|
213
|
+
sqlObjectType: str(row, "SQL_OBJECT_TYPE"),
|
|
214
|
+
longName: str(row, "OBJLONGNAME"),
|
|
215
|
+
domain: str(row, "OBJECT_DOMAIN"),
|
|
216
|
+
createdSystem: str(row, "CREATED_SYSTEM"),
|
|
217
|
+
createdSystemVersion: str(row, "CREATED_SYSTEM_VERSION"),
|
|
218
|
+
licensedProgram: str(row, "LICENSED_PROGRAM"),
|
|
219
|
+
journaled: str(row, "JOURNALED"),
|
|
220
|
+
attributes: allColumns(row)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// ---- members.describe ----
|
|
224
|
+
async function describeMember(conn, args) {
|
|
225
|
+
const library = requireQsysName(conn, args?.library, "library");
|
|
226
|
+
const sourceFile = requireQsysName(conn, args?.sourceFile, "sourceFile");
|
|
227
|
+
const member = requireQsysName(conn, args?.member, "member");
|
|
228
|
+
const rows = await conn.runSQL(`select P.*, T.ROW_LENGTH as FILE_RECORD_LENGTH, T.TABLE_TEXT as FILE_TEXT, ` +
|
|
229
|
+
`(select C.CCSID from QSYS2.SYSCOLUMNS C where C.SYSTEM_TABLE_SCHEMA = P.SYSTEM_TABLE_SCHEMA ` +
|
|
230
|
+
`and C.SYSTEM_TABLE_NAME = P.SYSTEM_TABLE_NAME and C.SYSTEM_COLUMN_NAME = 'SRCDTA' fetch first 1 row only) as SOURCE_CCSID ` +
|
|
231
|
+
`from QSYS2.SYSPARTITIONSTAT P left join QSYS2.SYSTABLES T ` +
|
|
232
|
+
`on T.SYSTEM_TABLE_SCHEMA = P.SYSTEM_TABLE_SCHEMA and T.SYSTEM_TABLE_NAME = P.SYSTEM_TABLE_NAME ` +
|
|
233
|
+
`where P.SYSTEM_TABLE_SCHEMA = ${Tools.sqlString(library)} and P.SYSTEM_TABLE_NAME = ${Tools.sqlString(sourceFile)} ` +
|
|
234
|
+
`and P.SYSTEM_TABLE_MEMBER = ${Tools.sqlString(member)} fetch first 1 row only`);
|
|
235
|
+
const row = rows[0];
|
|
236
|
+
if (!row)
|
|
237
|
+
return toolError(`Member ${library}/${sourceFile}(${member}) not found`, { library, sourceFile, member });
|
|
238
|
+
return json(compact({
|
|
239
|
+
library,
|
|
240
|
+
sourceFile,
|
|
241
|
+
member: str(row, "SYSTEM_TABLE_MEMBER") || member,
|
|
242
|
+
type: str(row, "SOURCE_TYPE"),
|
|
243
|
+
text: str(row, "PARTITION_TEXT"),
|
|
244
|
+
records: num(row, "NUMBER_ROWS"),
|
|
245
|
+
deletedRecords: num(row, "NUMBER_DELETED_ROWS"),
|
|
246
|
+
dataSize: num(row, "DATA_SIZE"),
|
|
247
|
+
recordLength: num(row, "FILE_RECORD_LENGTH"),
|
|
248
|
+
ccsid: num(row, "SOURCE_CCSID"),
|
|
249
|
+
created: str(row, "CREATE_TIMESTAMP"),
|
|
250
|
+
changed: str(row, "LAST_CHANGE_TIMESTAMP"),
|
|
251
|
+
sourceChanged: str(row, "LAST_SOURCE_UPDATE_TIMESTAMP"),
|
|
252
|
+
lastUsed: str(row, "LAST_USED_TIMESTAMP"),
|
|
253
|
+
daysUsed: num(row, "DAYS_USED_COUNT"),
|
|
254
|
+
saved: str(row, "LAST_SAVE_TIMESTAMP"),
|
|
255
|
+
restored: str(row, "LAST_RESTORE_TIMESTAMP"),
|
|
256
|
+
fileText: str(row, "FILE_TEXT")
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
// ---- ifs.stat ----
|
|
260
|
+
async function statIfs(conn, args) {
|
|
261
|
+
let path = requireIfsPath(args?.path, "path");
|
|
262
|
+
if (path.length > 1)
|
|
263
|
+
path = path.replace(/\/+$/, "") || "/";
|
|
264
|
+
const literal = Tools.sqlString(path);
|
|
265
|
+
let rows;
|
|
266
|
+
let serviceError;
|
|
267
|
+
try {
|
|
268
|
+
rows = await conn.runSQL(`select * from table(QSYS2.IFS_OBJECT_STATISTICS(START_PATH_NAME => ${literal}, SUBTREE_DIRECTORIES => 'NO')) X ` +
|
|
269
|
+
`where X.PATH_NAME = ${literal} fetch first 1 row only`);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
serviceError = String(error?.message || error);
|
|
273
|
+
}
|
|
274
|
+
const row = rows?.[0];
|
|
275
|
+
if (row) {
|
|
276
|
+
return json(compact({
|
|
277
|
+
path: str(row, "PATH_NAME") || path,
|
|
278
|
+
source: "QSYS2.IFS_OBJECT_STATISTICS",
|
|
279
|
+
objectType: str(row, "OBJECT_TYPE"),
|
|
280
|
+
symbolicLink: str(row, "SYMBOLIC_LINK"),
|
|
281
|
+
ccsid: num(row, "CCSID"),
|
|
282
|
+
codePage: num(row, "CODE_PAGE"),
|
|
283
|
+
owner: str(row, "OBJECT_OWNER"),
|
|
284
|
+
group: str(row, "PRIMARY_GROUP"),
|
|
285
|
+
size: num(row, "DATA_SIZE"),
|
|
286
|
+
allocatedSize: num(row, "ALLOCATED_SIZE"),
|
|
287
|
+
created: str(row, "CREATE_TIMESTAMP"),
|
|
288
|
+
accessed: str(row, "ACCESS_TIMESTAMP"),
|
|
289
|
+
dataChanged: str(row, "DATA_CHANGE_TIMESTAMP"),
|
|
290
|
+
objectChanged: str(row, "OBJECT_CHANGE_TIMESTAMP"),
|
|
291
|
+
lastUsed: str(row, "LAST_USED_TIMESTAMP"),
|
|
292
|
+
attributes: allColumns(row)
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
// Fallback for releases without IFS_OBJECT_STATISTICS (or a path it did not return).
|
|
296
|
+
const res = await conn.sendCommand({ command: `ls -ldL ${Tools.escapePath(path)}` });
|
|
297
|
+
if (!res || res.code !== 0) {
|
|
298
|
+
return toolError(`IFS object ${path} not found`, {
|
|
299
|
+
path,
|
|
300
|
+
...(serviceError ? { serviceError } : {}),
|
|
301
|
+
stderr: String(res?.stderr || "").trim()
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const parsed = parseLsLine(String(res.stdout || ""));
|
|
305
|
+
return json(compact({
|
|
306
|
+
path,
|
|
307
|
+
source: "ls",
|
|
308
|
+
...parsed,
|
|
309
|
+
...(serviceError ? { warnings: [`QSYS2.IFS_OBJECT_STATISTICS unavailable: ${serviceError}`] } : {})
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
function parseLsLine(output) {
|
|
313
|
+
const line = output.trim().split(/\r?\n/)[0] || "";
|
|
314
|
+
const parts = line.split(/\s+/);
|
|
315
|
+
if (parts.length < 8)
|
|
316
|
+
return { raw: line };
|
|
317
|
+
const mode = parts[0];
|
|
318
|
+
return {
|
|
319
|
+
objectType: mode.startsWith("d") ? "*DIR" : mode.startsWith("l") ? "*SYMLNK" : "*STMF",
|
|
320
|
+
permissions: mode,
|
|
321
|
+
owner: parts[2],
|
|
322
|
+
group: parts[3],
|
|
323
|
+
size: Number(parts[4]),
|
|
324
|
+
modified: parts.slice(5, 8).join(" "),
|
|
325
|
+
raw: line
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// ---- programs.info ----
|
|
329
|
+
async function programInfo(conn, args) {
|
|
330
|
+
const library = requireQsysName(conn, args?.library, "library");
|
|
331
|
+
const program = requireQsysName(conn, args?.program, "program");
|
|
332
|
+
const requestedType = args?.objectType ? String(args.objectType).trim().toUpperCase() : undefined;
|
|
333
|
+
if (requestedType && !PROGRAM_TYPES.includes(requestedType))
|
|
334
|
+
throw new Error("objectType must be *PGM or *SRVPGM");
|
|
335
|
+
const limit = clampInteger(args?.limit, 1, 5000, 500);
|
|
336
|
+
const where = `PROGRAM_LIBRARY = ${Tools.sqlString(library)} and PROGRAM_NAME = ${Tools.sqlString(program)}`;
|
|
337
|
+
const infoRows = await conn.runSQL(`select * from QSYS2.PROGRAM_INFO where ${where}` +
|
|
338
|
+
(requestedType ? ` and OBJECT_TYPE = ${Tools.sqlString(requestedType)}` : "") +
|
|
339
|
+
` fetch first 2 rows only`);
|
|
340
|
+
if (infoRows.length === 0) {
|
|
341
|
+
return toolError(`Program ${library}/${program}${requestedType ? ` ${requestedType}` : ""} not found`, { library, program, objectType: requestedType });
|
|
342
|
+
}
|
|
343
|
+
if (infoRows.length > 1) {
|
|
344
|
+
throw new Error(`${library}/${program} exists as both *PGM and *SRVPGM; pass objectType`);
|
|
345
|
+
}
|
|
346
|
+
const info = infoRows[0];
|
|
347
|
+
const objectType = str(info, "OBJECT_TYPE") || requestedType || "*PGM";
|
|
348
|
+
const typedWhere = `${where} and OBJECT_TYPE = ${Tools.sqlString(objectType)}`;
|
|
349
|
+
const warnings = [];
|
|
350
|
+
const optional = async (label, sql) => {
|
|
351
|
+
try {
|
|
352
|
+
return await conn.runSQL(sql);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
warnings.push(`${label} unavailable: ${String(error?.message || error)}`);
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const payload = { library, program, objectType, ...shapeProgramInfo(info) };
|
|
360
|
+
if (args?.includeModules !== false) {
|
|
361
|
+
const rows = await optional("QSYS2.BOUND_MODULE_INFO", `select * from QSYS2.BOUND_MODULE_INFO where ${typedWhere} fetch first ${limit} rows only`);
|
|
362
|
+
if (rows)
|
|
363
|
+
payload.modules = rows.map(shapeModuleRow);
|
|
364
|
+
}
|
|
365
|
+
if (args?.includeServicePrograms !== false) {
|
|
366
|
+
const rows = await optional("QSYS2.BOUND_SRVPGM_INFO", `select * from QSYS2.BOUND_SRVPGM_INFO where ${typedWhere} fetch first ${limit} rows only`);
|
|
367
|
+
if (rows)
|
|
368
|
+
payload.servicePrograms = rows.map(row => compact({
|
|
369
|
+
library: str(row, "BOUND_SERVICE_PROGRAM_LIBRARY"),
|
|
370
|
+
name: str(row, "BOUND_SERVICE_PROGRAM"),
|
|
371
|
+
signature: str(row, "BOUND_SERVICE_PROGRAM_SIGNATURE"),
|
|
372
|
+
activation: str(row, "BOUND_SERVICE_PROGRAM_ACTIVATION")
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
if (args?.includeExports !== false) {
|
|
376
|
+
const rows = await optional("QSYS2.PROGRAM_EXPORT_IMPORT_INFO", `select * from QSYS2.PROGRAM_EXPORT_IMPORT_INFO where ${typedWhere} fetch first ${limit} rows only`);
|
|
377
|
+
if (rows) {
|
|
378
|
+
const symbols = rows.map(row => compact({
|
|
379
|
+
symbol: str(row, "SYMBOL_NAME"),
|
|
380
|
+
usage: str(row, "SYMBOL_USAGE"),
|
|
381
|
+
argumentOptimization: str(row, "ARGUMENT_OPTIMIZATION"),
|
|
382
|
+
dataItemSize: num(row, "DATA_ITEM_SIZE")
|
|
383
|
+
}));
|
|
384
|
+
const exports = symbols.filter(item => /EXP/.test(String(item.usage || "")));
|
|
385
|
+
const imports = symbols.filter(item => /IMP/.test(String(item.usage || "")));
|
|
386
|
+
payload.exports = exports;
|
|
387
|
+
payload.imports = imports;
|
|
388
|
+
if (rows.length >= limit)
|
|
389
|
+
warnings.push(`Exports/imports truncated at ${limit} rows; raise limit.`);
|
|
390
|
+
if (objectType === "*SRVPGM")
|
|
391
|
+
payload.binderSource = buildBinderSource(library, program, exports);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (warnings.length > 0)
|
|
395
|
+
payload.warnings = warnings;
|
|
396
|
+
return json(payload);
|
|
397
|
+
}
|
|
398
|
+
function shapeProgramInfo(row) {
|
|
399
|
+
return compact({
|
|
400
|
+
programType: str(row, "PROGRAM_TYPE"),
|
|
401
|
+
attribute: str(row, "PROGRAM_ATTRIBUTE"),
|
|
402
|
+
text: str(row, "TEXT_DESCRIPTION"),
|
|
403
|
+
owner: str(row, "PROGRAM_OWNER"),
|
|
404
|
+
created: str(row, "CREATE_TIMESTAMP"),
|
|
405
|
+
activationGroup: str(row, "ACTIVATION_GROUP"),
|
|
406
|
+
userProfile: str(row, "USER_PROFILE"),
|
|
407
|
+
useAdoptedAuthority: str(row, "USE_ADOPTED_AUTHORITY"),
|
|
408
|
+
state: str(row, "PROGRAM_STATE"),
|
|
409
|
+
domain: str(row, "PROGRAM_DOMAIN"),
|
|
410
|
+
targetRelease: str(row, "TARGET_RELEASE"),
|
|
411
|
+
source: compactOrUndefined({
|
|
412
|
+
library: str(row, "SOURCE_FILE_LIBRARY"),
|
|
413
|
+
file: str(row, "SOURCE_FILE"),
|
|
414
|
+
member: str(row, "SOURCE_FILE_MEMBER"),
|
|
415
|
+
changed: str(row, "SOURCE_FILE_CHANGE_TIMESTAMP")
|
|
416
|
+
}),
|
|
417
|
+
entryModule: compactOrUndefined({
|
|
418
|
+
library: str(row, "PROGRAM_ENTRY_PROCEDURE_MODULE_LIBRARY"),
|
|
419
|
+
name: str(row, "PROGRAM_ENTRY_PROCEDURE_MODULE")
|
|
420
|
+
}),
|
|
421
|
+
counts: compactOrUndefined({
|
|
422
|
+
modules: num(row, "MODULES"),
|
|
423
|
+
servicePrograms: num(row, "SERVICE_PROGRAMS"),
|
|
424
|
+
procedureExports: num(row, "PROCEDURE_EXPORTS"),
|
|
425
|
+
dataExports: num(row, "DATA_EXPORTS")
|
|
426
|
+
}),
|
|
427
|
+
attributes: allColumns(row)
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function shapeModuleRow(row) {
|
|
431
|
+
return compact({
|
|
432
|
+
library: str(row, "BOUND_MODULE_LIBRARY"),
|
|
433
|
+
name: str(row, "BOUND_MODULE"),
|
|
434
|
+
attribute: str(row, "MODULE_ATTRIBUTE"),
|
|
435
|
+
created: str(row, "MODULE_CREATE_TIMESTAMP"),
|
|
436
|
+
source: compactOrUndefined({
|
|
437
|
+
library: str(row, "SOURCE_FILE_LIBRARY"),
|
|
438
|
+
file: str(row, "SOURCE_FILE"),
|
|
439
|
+
member: str(row, "SOURCE_FILE_MEMBER"),
|
|
440
|
+
streamFile: str(row, "SOURCE_STREAM_FILE_PATH"),
|
|
441
|
+
changed: str(row, "SOURCE_CHANGE_TIMESTAMP")
|
|
442
|
+
}),
|
|
443
|
+
debugData: str(row, "DEBUG_DATA"),
|
|
444
|
+
optimization: str(row, "OPTIMIZATION_LEVEL")
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
export function buildBinderSource(library, program, exports) {
|
|
448
|
+
const lines = [
|
|
449
|
+
`/* Binder source for ${library}/${program}, generated from its current exports. */`,
|
|
450
|
+
`/* Keep the export order: appending new exports at the end preserves compatibility. */`,
|
|
451
|
+
"STRPGMEXP PGMLVL(*CURRENT) SIGNATURE(*GEN)"
|
|
452
|
+
];
|
|
453
|
+
const skipped = [];
|
|
454
|
+
for (const item of exports) {
|
|
455
|
+
const symbol = String(item.symbol || "");
|
|
456
|
+
if (!symbol)
|
|
457
|
+
continue;
|
|
458
|
+
if (/["\r\n]/.test(symbol)) {
|
|
459
|
+
skipped.push(symbol);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
lines.push(` EXPORT SYMBOL("${symbol}")`);
|
|
463
|
+
}
|
|
464
|
+
lines.push("ENDPGMEXP");
|
|
465
|
+
return {
|
|
466
|
+
text: lines.join("\n"),
|
|
467
|
+
exportCount: exports.length - skipped.length,
|
|
468
|
+
note: "Export order is the order the system returned. Confirm it matches the existing signature (DSPSRVPGM DETAIL(*SIGNATURE)) before binding with this source.",
|
|
469
|
+
...(skipped.length > 0 ? { skipped } : {})
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
// ---- objects.whereUsed ----
|
|
473
|
+
const FILE_USAGE_BITS = [[1, "input"], [2, "output"], [4, "update"], [8, "unspecified"]];
|
|
474
|
+
async function whereUsed(conn, args) {
|
|
475
|
+
const direction = String(args?.direction || "").trim();
|
|
476
|
+
if (direction !== "uses" && direction !== "usedBy")
|
|
477
|
+
throw new Error("direction must be uses or usedBy");
|
|
478
|
+
const library = requireQsysName(conn, args?.library, "library");
|
|
479
|
+
const limit = clampInteger(args?.limit, 1, 10000, 1000);
|
|
480
|
+
const outfile = uniqueOutfileName();
|
|
481
|
+
let command;
|
|
482
|
+
const filters = [];
|
|
483
|
+
let target;
|
|
484
|
+
if (direction === "uses") {
|
|
485
|
+
const program = requireQsysName(conn, args?.program, "program");
|
|
486
|
+
const programType = String(args?.programType || "*ALL").trim().toUpperCase();
|
|
487
|
+
if (!PGMREF_TYPES.includes(programType))
|
|
488
|
+
throw new Error(`programType must be one of ${PGMREF_TYPES.join(", ")}`);
|
|
489
|
+
command = `DSPPGMREF PGM(${library}/${program}) OUTPUT(*OUTFILE) OBJTYPE(${programType}) OUTFILE(QTEMP/${outfile})`;
|
|
490
|
+
target = { library, program, programType };
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const object = requireQsysName(conn, args?.object, "object");
|
|
494
|
+
command = `DSPPGMREF PGM(${library}/*ALL) OUTPUT(*OUTFILE) OBJTYPE(*ALL) OUTFILE(QTEMP/${outfile})`;
|
|
495
|
+
filters.push(`WHFNAM = ${Tools.sqlString(object)}`);
|
|
496
|
+
target = { library, object };
|
|
497
|
+
if (args?.objectLibrary) {
|
|
498
|
+
const objectLibrary = requireQsysName(conn, args.objectLibrary, "objectLibrary");
|
|
499
|
+
filters.push(`WHLNAM in (${Tools.sqlString(objectLibrary)}, '*LIBL')`);
|
|
500
|
+
target.objectLibrary = objectLibrary;
|
|
501
|
+
}
|
|
502
|
+
if (args?.objectType) {
|
|
503
|
+
const objectType = requireObjectTypeValue(args.objectType, "objectType");
|
|
504
|
+
filters.push(`WHOTYP = ${Tools.sqlString(objectType)}`);
|
|
505
|
+
target.objectType = objectType;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// QTEMP belongs to the SQL job, so the CL command and the SELECT must run in
|
|
509
|
+
// the same runSQL call.
|
|
510
|
+
const rows = await conn.runSQL(`call QSYS2.QCMDEXC(${Tools.sqlString(command)});\n` +
|
|
511
|
+
`select * from QTEMP.${outfile}` +
|
|
512
|
+
(filters.length > 0 ? ` where ${filters.join(" and ")}` : "") +
|
|
513
|
+
` order by WHLIB, WHPNAM, WHLNAM, WHFNAM fetch first ${limit + 1} rows only`);
|
|
514
|
+
const hasMore = rows.length > limit;
|
|
515
|
+
const references = rows.slice(0, limit).map(shapeProgramReference);
|
|
516
|
+
const payload = { direction, ...target, command, returned: references.length, hasMore, references };
|
|
517
|
+
if (direction === "usedBy") {
|
|
518
|
+
const programs = new Map();
|
|
519
|
+
for (const ref of references) {
|
|
520
|
+
const key = `${ref.programLibrary}/${ref.program}/${ref.programType || ""}`;
|
|
521
|
+
if (!programs.has(key))
|
|
522
|
+
programs.set(key, compact({ library: ref.programLibrary, program: ref.program, programType: ref.programType }));
|
|
523
|
+
}
|
|
524
|
+
payload.programs = [...programs.values()];
|
|
525
|
+
}
|
|
526
|
+
return json(payload);
|
|
527
|
+
}
|
|
528
|
+
function uniqueOutfileName() {
|
|
529
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
530
|
+
const bytes = crypto.randomBytes(6);
|
|
531
|
+
let suffix = "";
|
|
532
|
+
for (const byte of bytes)
|
|
533
|
+
suffix += alphabet[byte % alphabet.length];
|
|
534
|
+
return `MCPR${suffix}`;
|
|
535
|
+
}
|
|
536
|
+
// WHSPKG in the DSPPGMREF outfile (QADSPPGM) holds the referencing program's type.
|
|
537
|
+
const PROGRAM_TYPE_CODES = { P: "*PGM", V: "*SRVPGM", M: "*MODULE", S: "*SQLPKG", Q: "*QRYDFN" };
|
|
538
|
+
export function shapeProgramReference(row) {
|
|
539
|
+
const usageCode = num(row, "WHFUSG");
|
|
540
|
+
const typeCode = str(row, "WHSPKG");
|
|
541
|
+
return compact({
|
|
542
|
+
programLibrary: str(row, "WHLIB") || "",
|
|
543
|
+
program: str(row, "WHPNAM") || "",
|
|
544
|
+
programType: typeCode ? PROGRAM_TYPE_CODES[typeCode] || typeCode : undefined,
|
|
545
|
+
programText: str(row, "WHTEXT"),
|
|
546
|
+
objectLibrary: str(row, "WHLNAM"),
|
|
547
|
+
object: str(row, "WHFNAM"),
|
|
548
|
+
objectType: str(row, "WHOTYP"),
|
|
549
|
+
fileUsage: usageCode !== undefined && usageCode > 0
|
|
550
|
+
? FILE_USAGE_BITS.filter(([bit]) => (usageCode & bit) !== 0).map(([, label]) => label)
|
|
551
|
+
: undefined,
|
|
552
|
+
recordFormat: str(row, "WHRFNM"),
|
|
553
|
+
formatLevel: str(row, "WHRFSN")
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
// ---- tables.describe ----
|
|
557
|
+
async function describeTable(conn, args) {
|
|
558
|
+
const library = requireSqlName(args?.library, "library");
|
|
559
|
+
const table = requireSqlName(args?.table, "table");
|
|
560
|
+
const limit = clampInteger(args?.limit, 1, 8000, 1000);
|
|
561
|
+
const lib = Tools.sqlString(library);
|
|
562
|
+
const tbl = Tools.sqlString(table);
|
|
563
|
+
const tableRows = await conn.runSQL(`select * from QSYS2.SYSTABLES where (TABLE_SCHEMA = ${lib} or SYSTEM_TABLE_SCHEMA = ${lib}) ` +
|
|
564
|
+
`and (TABLE_NAME = ${tbl} or SYSTEM_TABLE_NAME = ${tbl}) fetch first 1 row only`);
|
|
565
|
+
const tableRow = tableRows[0];
|
|
566
|
+
if (!tableRow)
|
|
567
|
+
return toolError(`Table ${library}/${table} not found`, { library, table });
|
|
568
|
+
const schema = str(tableRow, "TABLE_SCHEMA") || library;
|
|
569
|
+
const name = str(tableRow, "TABLE_NAME") || table;
|
|
570
|
+
const tableFilter = (alias) => `${alias}TABLE_SCHEMA = ${Tools.sqlString(schema)} and ${alias}TABLE_NAME = ${Tools.sqlString(name)}`;
|
|
571
|
+
const columnRows = await conn.runSQL(`select * from QSYS2.SYSCOLUMNS where ${tableFilter("")} order by ORDINAL_POSITION fetch first ${limit} rows only`);
|
|
572
|
+
const payload = compact({
|
|
573
|
+
schema,
|
|
574
|
+
name,
|
|
575
|
+
systemSchema: str(tableRow, "SYSTEM_TABLE_SCHEMA"),
|
|
576
|
+
systemName: str(tableRow, "SYSTEM_TABLE_NAME"),
|
|
577
|
+
tableType: str(tableRow, "TABLE_TYPE"),
|
|
578
|
+
fileType: str(tableRow, "FILE_TYPE"),
|
|
579
|
+
text: str(tableRow, "TABLE_TEXT"),
|
|
580
|
+
rowLength: num(tableRow, "ROW_LENGTH"),
|
|
581
|
+
columnCount: num(tableRow, "COLUMN_COUNT"),
|
|
582
|
+
columns: columnRows.map(shapeColumnRow)
|
|
583
|
+
});
|
|
584
|
+
const warnings = [];
|
|
585
|
+
if (args?.includeKeys !== false) {
|
|
586
|
+
try {
|
|
587
|
+
const rows = await conn.runSQL(`select C.CONSTRAINT_SCHEMA, C.CONSTRAINT_NAME, C.CONSTRAINT_TYPE, K.COLUMN_NAME, K.ORDINAL_POSITION ` +
|
|
588
|
+
`from QSYS2.SYSCST C left join QSYS2.SYSKEYCST K ` +
|
|
589
|
+
`on K.CONSTRAINT_SCHEMA = C.CONSTRAINT_SCHEMA and K.CONSTRAINT_NAME = C.CONSTRAINT_NAME ` +
|
|
590
|
+
`where ${tableFilter("C.")} ` +
|
|
591
|
+
`order by C.CONSTRAINT_NAME, K.ORDINAL_POSITION fetch first ${limit} rows only`);
|
|
592
|
+
payload.constraints = groupRows(rows, row => `${str(row, "CONSTRAINT_SCHEMA")}.${str(row, "CONSTRAINT_NAME")}`, (first, group) => ({
|
|
593
|
+
schema: str(first, "CONSTRAINT_SCHEMA"),
|
|
594
|
+
name: str(first, "CONSTRAINT_NAME"),
|
|
595
|
+
type: str(first, "CONSTRAINT_TYPE"),
|
|
596
|
+
columns: group.map(row => str(row, "COLUMN_NAME")).filter(Boolean)
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
warnings.push(`Constraints unavailable: ${String(error?.message || error)}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (args?.includeIndexes !== false) {
|
|
604
|
+
try {
|
|
605
|
+
const rows = await conn.runSQL(`select I.INDEX_SCHEMA, I.INDEX_NAME, I.SYSTEM_INDEX_NAME, I.IS_UNIQUE, K.COLUMN_NAME, K.ORDINAL_POSITION, K.ORDERING ` +
|
|
606
|
+
`from QSYS2.SYSINDEXES I left join QSYS2.SYSKEYS K on K.INDEX_SCHEMA = I.INDEX_SCHEMA and K.INDEX_NAME = I.INDEX_NAME ` +
|
|
607
|
+
`where ${tableFilter("I.")} ` +
|
|
608
|
+
`order by I.INDEX_NAME, K.ORDINAL_POSITION fetch first ${limit} rows only`);
|
|
609
|
+
payload.indexes = groupRows(rows, row => `${str(row, "INDEX_SCHEMA")}.${str(row, "INDEX_NAME")}`, (first, group) => compact({
|
|
610
|
+
schema: str(first, "INDEX_SCHEMA"),
|
|
611
|
+
name: str(first, "INDEX_NAME"),
|
|
612
|
+
systemName: str(first, "SYSTEM_INDEX_NAME"),
|
|
613
|
+
unique: str(first, "IS_UNIQUE"),
|
|
614
|
+
keys: group
|
|
615
|
+
.filter(row => str(row, "COLUMN_NAME"))
|
|
616
|
+
.map(row => compact({ column: str(row, "COLUMN_NAME"), ordering: str(row, "ORDERING") }))
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
warnings.push(`Indexes unavailable: ${String(error?.message || error)}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (columnRows.length >= limit)
|
|
624
|
+
warnings.push(`Columns truncated at ${limit}; raise limit.`);
|
|
625
|
+
if (warnings.length > 0)
|
|
626
|
+
payload.warnings = warnings;
|
|
627
|
+
return json(payload);
|
|
628
|
+
}
|
|
629
|
+
function shapeColumnRow(row) {
|
|
630
|
+
const dataType = str(row, "DATA_TYPE");
|
|
631
|
+
return compact({
|
|
632
|
+
name: str(row, "COLUMN_NAME"),
|
|
633
|
+
systemName: str(row, "SYSTEM_COLUMN_NAME"),
|
|
634
|
+
position: num(row, "ORDINAL_POSITION"),
|
|
635
|
+
type: formatSqlType(dataType, num(row, "LENGTH", "CHARACTER_MAXIMUM_LENGTH"), num(row, "NUMERIC_PRECISION"), num(row, "NUMERIC_SCALE")),
|
|
636
|
+
dataType,
|
|
637
|
+
length: num(row, "LENGTH"),
|
|
638
|
+
scale: num(row, "NUMERIC_SCALE"),
|
|
639
|
+
nullable: yesNo(str(row, "IS_NULLABLE")),
|
|
640
|
+
default: str(row, "COLUMN_DEFAULT"),
|
|
641
|
+
identity: yesNo(str(row, "IS_IDENTITY")),
|
|
642
|
+
ccsid: num(row, "CCSID"),
|
|
643
|
+
text: str(row, "COLUMN_TEXT"),
|
|
644
|
+
heading: str(row, "COLUMN_HEADING")
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
// ---- routines.describe ----
|
|
648
|
+
async function describeRoutine(conn, args) {
|
|
649
|
+
const library = requireSqlName(args?.library, "library");
|
|
650
|
+
const routine = requireSqlName(args?.routine, "routine");
|
|
651
|
+
const routineType = args?.routineType ? String(args.routineType).trim().toUpperCase() : undefined;
|
|
652
|
+
if (routineType && routineType !== "PROCEDURE" && routineType !== "FUNCTION")
|
|
653
|
+
throw new Error("routineType must be PROCEDURE or FUNCTION");
|
|
654
|
+
const limit = clampInteger(args?.limit, 1, 500, 50);
|
|
655
|
+
const lib = Tools.sqlString(library);
|
|
656
|
+
const name = Tools.sqlString(routine);
|
|
657
|
+
const routines = await conn.runSQL(`select * from QSYS2.SYSROUTINES where ROUTINE_SCHEMA = ${lib} and (ROUTINE_NAME = ${name} or SPECIFIC_NAME = ${name})` +
|
|
658
|
+
(routineType ? ` and ROUTINE_TYPE = ${Tools.sqlString(routineType)}` : "") +
|
|
659
|
+
` order by SPECIFIC_NAME fetch first ${limit} rows only`);
|
|
660
|
+
if (routines.length === 0)
|
|
661
|
+
return toolError(`Routine ${library}.${routine} not found`, { library, routine, routineType });
|
|
662
|
+
const specifics = [...new Set(routines.map(row => str(row, "SPECIFIC_NAME")).filter(Boolean))];
|
|
663
|
+
const specificSchema = str(routines[0], "SPECIFIC_SCHEMA") || library;
|
|
664
|
+
const parmRows = specifics.length === 0 ? [] : await conn.runSQL(`select * from QSYS2.SYSPARMS where SPECIFIC_SCHEMA = ${Tools.sqlString(specificSchema)} ` +
|
|
665
|
+
`and SPECIFIC_NAME in (${specifics.map(value => Tools.sqlString(value)).join(", ")}) ` +
|
|
666
|
+
`order by SPECIFIC_NAME, ORDINAL_POSITION fetch first ${Math.min(limit * 1000, 20000)} rows only`);
|
|
667
|
+
return json({
|
|
668
|
+
library,
|
|
669
|
+
routine,
|
|
670
|
+
routines: routines.map(row => shapeRoutine(row, parmRows.filter(parm => str(parm, "SPECIFIC_NAME") === str(row, "SPECIFIC_NAME")), Boolean(args?.includeDefinition)))
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
export function shapeRoutine(row, parms, includeDefinition) {
|
|
674
|
+
const schema = str(row, "ROUTINE_SCHEMA") || "";
|
|
675
|
+
const name = str(row, "ROUTINE_NAME") || "";
|
|
676
|
+
const type = str(row, "ROUTINE_TYPE");
|
|
677
|
+
const functionType = str(row, "FUNCTION_TYPE");
|
|
678
|
+
const parameters = parms
|
|
679
|
+
.filter(parm => (str(parm, "ROW_TYPE") || "P") === "P")
|
|
680
|
+
.map(parm => compact({
|
|
681
|
+
position: num(parm, "ORDINAL_POSITION"),
|
|
682
|
+
mode: str(parm, "PARAMETER_MODE"),
|
|
683
|
+
name: str(parm, "PARAMETER_NAME"),
|
|
684
|
+
type: parmType(parm),
|
|
685
|
+
nullable: yesNo(str(parm, "IS_NULLABLE")),
|
|
686
|
+
default: str(parm, "DEFAULT"),
|
|
687
|
+
ccsid: num(parm, "CCSID"),
|
|
688
|
+
text: str(parm, "LONG_COMMENT")
|
|
689
|
+
}));
|
|
690
|
+
const results = parms
|
|
691
|
+
.filter(parm => str(parm, "ROW_TYPE") === "R" || str(parm, "ROW_TYPE") === "C")
|
|
692
|
+
.map(parm => compact({ name: str(parm, "PARAMETER_NAME"), type: parmType(parm) }));
|
|
693
|
+
const parameterText = parameters
|
|
694
|
+
.map(p => [type === "PROCEDURE" ? p.mode : undefined, p.name, p.type].filter(Boolean).join(" "))
|
|
695
|
+
.join(", ");
|
|
696
|
+
let returns = "";
|
|
697
|
+
if (type === "FUNCTION" && results.length > 0) {
|
|
698
|
+
returns = functionType === "T"
|
|
699
|
+
? ` RETURNS TABLE (${results.map(r => [r.name, r.type].filter(Boolean).join(" ")).join(", ")})`
|
|
700
|
+
: ` RETURNS ${results[results.length - 1].type}`;
|
|
701
|
+
}
|
|
702
|
+
let definition = includeDefinition ? str(row, "ROUTINE_DEFINITION") : undefined;
|
|
703
|
+
if (definition && definition.length > 65536)
|
|
704
|
+
definition = `${definition.slice(0, 65536)}\n... (truncated)`;
|
|
705
|
+
return compact({
|
|
706
|
+
schema,
|
|
707
|
+
name,
|
|
708
|
+
specificName: str(row, "SPECIFIC_NAME"),
|
|
709
|
+
type,
|
|
710
|
+
functionType,
|
|
711
|
+
signature: `${schema}.${name}(${parameterText})${returns}`,
|
|
712
|
+
body: str(row, "ROUTINE_BODY"),
|
|
713
|
+
language: str(row, "EXTERNAL_LANGUAGE"),
|
|
714
|
+
externalName: str(row, "EXTERNAL_NAME"),
|
|
715
|
+
parameterStyle: str(row, "PARAMETER_STYLE"),
|
|
716
|
+
deterministic: yesNo(str(row, "IS_DETERMINISTIC")),
|
|
717
|
+
sqlDataAccess: str(row, "SQL_DATA_ACCESS"),
|
|
718
|
+
resultSets: num(row, "RESULT_SETS", "MAX_DYNAMIC_RESULT_SETS"),
|
|
719
|
+
created: str(row, "ROUTINE_CREATED"),
|
|
720
|
+
altered: str(row, "LAST_ALTERED"),
|
|
721
|
+
text: str(row, "LONG_COMMENT"),
|
|
722
|
+
parameters,
|
|
723
|
+
...(results.length > 0 ? { returns: results } : {}),
|
|
724
|
+
...(definition ? { definition } : {})
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
function parmType(parm) {
|
|
728
|
+
return formatSqlType(str(parm, "DATA_TYPE"), num(parm, "CHARACTER_MAXIMUM_LENGTH", "LENGTH"), num(parm, "NUMERIC_PRECISION"), num(parm, "NUMERIC_SCALE"));
|
|
729
|
+
}
|
|
730
|
+
// ---- shared helpers ----
|
|
731
|
+
const LENGTH_TYPES = new Set(["CHAR", "CHARACTER", "VARCHAR", "CHARACTER VARYING", "GRAPHIC", "VARGRAPHIC", "BINARY", "VARBINARY", "CLOB", "BLOB", "DBCLOB", "NCHAR", "NVARCHAR"]);
|
|
732
|
+
const DECIMAL_TYPES = new Set(["DECIMAL", "NUMERIC", "DEC"]);
|
|
733
|
+
export function formatSqlType(dataType, length, precision, scale) {
|
|
734
|
+
if (!dataType)
|
|
735
|
+
return undefined;
|
|
736
|
+
const upper = dataType.toUpperCase();
|
|
737
|
+
if (DECIMAL_TYPES.has(upper)) {
|
|
738
|
+
const digits = precision ?? length;
|
|
739
|
+
return digits !== undefined ? `${upper}(${digits}, ${scale ?? 0})` : upper;
|
|
740
|
+
}
|
|
741
|
+
if (LENGTH_TYPES.has(upper) && length !== undefined)
|
|
742
|
+
return `${upper}(${length})`;
|
|
743
|
+
return upper;
|
|
744
|
+
}
|
|
745
|
+
function requireSqlName(value, label) {
|
|
746
|
+
const name = String(value || "").trim().toUpperCase();
|
|
747
|
+
if (!name)
|
|
748
|
+
throw new Error(`${label} is required`);
|
|
749
|
+
if (!/^[A-Z_#@$][A-Z0-9_#@$]{0,127}$/.test(name))
|
|
750
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
751
|
+
return name;
|
|
752
|
+
}
|
|
753
|
+
function str(row, ...keys) {
|
|
754
|
+
const value = pickFirst(row, keys);
|
|
755
|
+
if (value === undefined || value === null)
|
|
756
|
+
return undefined;
|
|
757
|
+
const text = String(value).trim();
|
|
758
|
+
return text.length > 0 && text !== "-" ? text : undefined;
|
|
759
|
+
}
|
|
760
|
+
function num(row, ...keys) {
|
|
761
|
+
const text = str(row, ...keys);
|
|
762
|
+
if (text === undefined)
|
|
763
|
+
return undefined;
|
|
764
|
+
const value = Number(text);
|
|
765
|
+
return Number.isFinite(value) ? value : undefined;
|
|
766
|
+
}
|
|
767
|
+
function yesNo(value) {
|
|
768
|
+
if (value === undefined)
|
|
769
|
+
return undefined;
|
|
770
|
+
const upper = value.toUpperCase();
|
|
771
|
+
if (upper === "Y" || upper === "YES")
|
|
772
|
+
return true;
|
|
773
|
+
if (upper === "N" || upper === "NO")
|
|
774
|
+
return false;
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
function compact(obj) {
|
|
778
|
+
for (const key of Object.keys(obj))
|
|
779
|
+
if (obj[key] === undefined)
|
|
780
|
+
delete obj[key];
|
|
781
|
+
return obj;
|
|
782
|
+
}
|
|
783
|
+
function compactOrUndefined(obj) {
|
|
784
|
+
const result = compact(obj);
|
|
785
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
786
|
+
}
|
|
787
|
+
/** Every non-empty column, trimmed, so values this module does not map are still visible. */
|
|
788
|
+
function allColumns(row) {
|
|
789
|
+
const out = {};
|
|
790
|
+
for (const [key, value] of Object.entries(row)) {
|
|
791
|
+
if (value === undefined || value === null)
|
|
792
|
+
continue;
|
|
793
|
+
const text = String(value).trim();
|
|
794
|
+
if (text && text !== "-")
|
|
795
|
+
out[key] = text;
|
|
796
|
+
}
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
function groupRows(rows, keyOf, build) {
|
|
800
|
+
const groups = new Map();
|
|
801
|
+
for (const row of rows) {
|
|
802
|
+
const key = keyOf(row);
|
|
803
|
+
const list = groups.get(key);
|
|
804
|
+
if (list)
|
|
805
|
+
list.push(row);
|
|
806
|
+
else
|
|
807
|
+
groups.set(key, [row]);
|
|
808
|
+
}
|
|
809
|
+
return [...groups.values()].map(group => build(group[0], group));
|
|
810
|
+
}
|
|
811
|
+
export const inspectModule = { tools, handle };
|