From 7ecad8008d886815e00bed1695c9e84320f41990 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 14 Jan 2024 16:53:19 +0100 Subject: [PATCH 01/18] Enhance getObjectList name filtering Signed-off-by: Seb Julliand --- src/api/IBMiContent.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 5443046f1..aff9d2139 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -447,7 +447,24 @@ export default class IBMiContent { throw new Error(`Library ${library} does not exist.`); } - const object = (filters.object && filters.object !== `*` ? filters.object.toUpperCase() : `*ALL`); + const objects = filters.object?.split(',') + .map(o => o.trim().toLocaleUpperCase()) + .filter(Tools.distinct) + .map(pattern => { + if (pattern.startsWith('*')) { + return (name: string) => name.endsWith(pattern.substring(1)); + } + else if (pattern.endsWith('*')) { + return (name: string) => name.startsWith(pattern.substring(0, pattern.length - 1)); + } + else { + return (name: string) => name === pattern; + } + }) + || []; + const filterNames = objects.length > 1 ? (file: IBMiFile) => objects.some(test => test(file.name)) : undefined; + + const object = filters.object && !filterNames && filters.object !== `*` ? filters.object.toUpperCase() : `*ALL`; const sourceFilesOnly = (filters.types && filters.types.includes(`*SRCPF`)); const tempLib = this.config.tempLibrary; @@ -473,6 +490,7 @@ export default class IBMiContent { text: String(object.PHTXT), count: Number(object.PHNOMB), } as IBMiFile)) + .filter(object => !filterNames || filterNames(object)) .sort((a, b) => a.library.localeCompare(b.library) || a.name.localeCompare(b.name)); } else { const objectTypes = (filters.types && filters.types.length ? filters.types.map(type => type.toUpperCase()).join(` `) : `*ALL`); @@ -494,6 +512,7 @@ export default class IBMiContent { attribute: String(object.ODOBAT), text: String(object.ODOBTX) } as IBMiFile)) + .filter(object => !filterNames || filterNames(object)) .sort((a, b) => { if (a.library.localeCompare(b.library) != 0) { return a.library.localeCompare(b.library) From dc4bfbe7811caba180b3a6adf7f1c2194fcc02f6 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 14 Jan 2024 17:02:08 +0100 Subject: [PATCH 02/18] Updated filter editor Object field description Signed-off-by: Seb Julliand --- src/webviews/filters/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 0efa8266c..3b7162126 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -41,7 +41,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, const page = await new CustomUI() .addInput(`name`, `Filter name`, `The filter name should be unique.`, { default: filter.name }) .addInput(`library`, `Library`, `Library name. Cannot be generic name with an asterisk.`, { default: filter.library }) - .addInput(`object`, `Object`, `Object name. Can be generic name with an asterisk. For example: *, or Q*.`, { default: filter.object }) + .addInput(`object`, `Objects`, `Object names. Comma-separated list of object names. Name can starts or ends with an asterisk (i.e. select names that end with or start with). Examples: *, Q* or *SRC.`, { default: filter.object }) .addInput(`types`, `Object type filter`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) .addInput(`member`, `Member`, `Member name. Can be multi-generic value. Examples: *CL or CL*ABC*. A single * will return all members.`, { default: filter.member }) .addInput(`memberType`, `Member type`, `Member type. Can be multi-generic value. Examples: RPG* or SQL*LE. A single * will return all member types.`, { default: filter.memberType || `*` }) From a1f929ceab8cf00ff8a24821b843b4b9bc1502c7 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 14 Jan 2024 17:08:13 +0100 Subject: [PATCH 03/18] Sort and clean filter's Object field on saving Signed-off-by: Seb Julliand --- src/api/IBMiContent.ts | 4 +--- src/webviews/filters/index.ts | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index aff9d2139..13184edc4 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -447,9 +447,7 @@ export default class IBMiContent { throw new Error(`Library ${library} does not exist.`); } - const objects = filters.object?.split(',') - .map(o => o.trim().toLocaleUpperCase()) - .filter(Tools.distinct) + const objects = filters.object?.split(',') .map(pattern => { if (pattern.startsWith('*')) { return (name: string) => name.endsWith(pattern.substring(1)); diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 3b7162126..00abb3cbc 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -1,5 +1,6 @@ import { ConnectionConfiguration } from "../../api/Configuration"; import { CustomUI } from "../../api/CustomUI"; +import { Tools } from "../../api/Tools"; import { instance } from "../../instantiate"; export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, copy = false) { @@ -64,6 +65,12 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, data[key] = String(data[key]).split(`,`).map(item => item.trim().toUpperCase()).filter(item => item !== ``); break; case `object`: + data[key] = (String(data[key].trim()) || `*`) + .split(',') + .map(o => o.trim().toLocaleUpperCase()) + .filter(Tools.distinct) + .join(","); + break; case `member`: case `memberType`: data[key] = String(data[key].trim()) || `*`; From a2496724a2d5fa27cd9d9fbcde0f2a2808bd69d1 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Sun, 14 Jan 2024 19:00:50 +0100 Subject: [PATCH 04/18] Handle name filter with only one "ends with" case Signed-off-by: Seb Julliand --- src/api/IBMiContent.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 13184edc4..c7422e739 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -447,9 +447,11 @@ export default class IBMiContent { throw new Error(`Library ${library} does not exist.`); } - const objects = filters.object?.split(',') + let hasEndsWith = false; + const objects = filters.object?.split(',') .map(pattern => { if (pattern.startsWith('*')) { + hasEndsWith = true; return (name: string) => name.endsWith(pattern.substring(1)); } else if (pattern.endsWith('*')) { @@ -460,7 +462,8 @@ export default class IBMiContent { } }) || []; - const filterNames = objects.length > 1 ? (file: IBMiFile) => objects.some(test => test(file.name)) : undefined; + + const filterNames = objects.length && (objects.length > 1 || hasEndsWith) ? (file: IBMiFile) => objects.some(test => test(file.name)) : undefined; const object = filters.object && !filterNames && filters.object !== `*` ? filters.object.toUpperCase() : `*ALL`; const sourceFilesOnly = (filters.types && filters.types.includes(`*SRCPF`)); From 322ce5f0b16f99816379b2ee9c67fc766a072a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Julliand?= Date: Tue, 16 Jan 2024 13:59:31 +0100 Subject: [PATCH 05/18] Better Objects field description. Co-authored-by: Christian Jorgensen --- src/webviews/filters/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 00abb3cbc..eb38cdfaf 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -42,7 +42,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, const page = await new CustomUI() .addInput(`name`, `Filter name`, `The filter name should be unique.`, { default: filter.name }) .addInput(`library`, `Library`, `Library name. Cannot be generic name with an asterisk.`, { default: filter.library }) - .addInput(`object`, `Objects`, `Object names. Comma-separated list of object names. Name can starts or ends with an asterisk (i.e. select names that end with or start with). Examples: *, Q* or *SRC.`, { default: filter.object }) + .addInput(`object`, `Objects`, `Object names. Comma-separated list of object names. Can be multi-generic values. Examples: *, Q* or *CL*SRC*. A single `*` will return all objects.`, { default: filter.object }) .addInput(`types`, `Object type filter`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) .addInput(`member`, `Member`, `Member name. Can be multi-generic value. Examples: *CL or CL*ABC*. A single * will return all members.`, { default: filter.member }) .addInput(`memberType`, `Member type`, `Member type. Can be multi-generic value. Examples: RPG* or SQL*LE. A single * will return all member types.`, { default: filter.memberType || `*` }) From 4de25bde52afa10000a8d1b3f68eb3d6004b2180 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Tue, 23 Jan 2024 00:04:45 +0100 Subject: [PATCH 06/18] Preemptively clean SQL data output Otherwise there is an additional garbled row returned when running multiple queries at once. Signed-off-by: Seb Julliand --- src/api/Tools.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/api/Tools.ts b/src/api/Tools.ts index 6a85261fb..d18b5e018 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -33,7 +33,16 @@ export namespace Tools { let figuredLengths = false; let iiErrorMessage = false; - let data = output.split(`\n`); + const data = output.split(`\n`).filter(line => { + const trimmed = line.trim(); + return trimmed !== `DB2>` && + !trimmed.startsWith(`DB20`) && // Notice messages + trimmed !== `?>`; + }); + + if(!data[data.length-1]){ + data.pop(); + } let headers: DB2Headers[]; @@ -45,9 +54,6 @@ export namespace Tools { const trimmed = line.trim(); if (trimmed.length === 0 && iiErrorMessage) iiErrorMessage = false; if (trimmed.length === 0 || index === data.length - 1) return; - if (trimmed === `DB2>`) return; - if (trimmed.startsWith(`DB20`)) return; // Notice messages - if (trimmed === `?>`) return; if (trimmed === `**** CLI ERROR *****`) { iiErrorMessage = true; From 8948f8d1a68cc2cdc857c0d3df52ba5ec91de171 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 24 Jan 2024 15:45:20 +0100 Subject: [PATCH 07/18] Enhanced Object Browser filtering Signed-off-by: Seb Julliand --- package-lock.json | 17 ++ package.json | 3 +- src/api/Configuration.ts | 6 +- src/api/Filter.ts | 52 +++++ src/api/IBMiContent.ts | 355 +++++++++++++++++----------------- src/sandbox.ts | 2 + src/typings.ts | 10 +- src/views/objectBrowser.ts | 72 +++++-- src/webviews/filters/index.ts | 21 +- 9 files changed, 322 insertions(+), 216 deletions(-) create mode 100644 src/api/Filter.ts diff --git a/package-lock.json b/package-lock.json index 780b0a153..e058c4237 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@types/tmp": "^0.2.3", "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", "csv": "^6.2.1", + "escape-string-regexp": "^5.0.0", "ignore": "^5.1.9", "node-ssh": "^13.1.0", "tar": "^6.1.13", @@ -1960,6 +1961,17 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint": { "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", @@ -5375,6 +5387,11 @@ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==" + }, "eslint": { "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", diff --git a/package.json b/package.json index e3439cc9e..3a2350e61 100644 --- a/package.json +++ b/package.json @@ -2538,7 +2538,8 @@ "tar": "^6.1.13", "tmp": "^0.2.1", "vscode-diff": "^2.0.2", - "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz" + "crc-32": "https://cdn.sheetjs.com/crc-32-latest/crc-32-latest.tgz", + "escape-string-regexp": "^5.0.0" }, "extensionDependencies": [ "barrettotte.ibmi-languages", diff --git a/src/api/Configuration.ts b/src/api/Configuration.ts index e9e653806..1b6f31194 100644 --- a/src/api/Configuration.ts +++ b/src/api/Configuration.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { DeploymentMethod } from '../typings'; +import { FilterType } from './Filter'; export type SourceDateMode = "edit" | "diff"; export type DefaultOpenMode = "browse" | "edit"; @@ -61,11 +62,12 @@ export namespace ConnectionConfiguration { defaultDeploymentMethod: DeploymentMethod | ''; protectedPaths: string[]; [name: string]: any; - } + } export interface ObjectFilters { name: string - library: string + filterType: FilterType + library: string object: string types: string[] member: string diff --git a/src/api/Filter.ts b/src/api/Filter.ts new file mode 100644 index 000000000..4daea76fa --- /dev/null +++ b/src/api/Filter.ts @@ -0,0 +1,52 @@ +import escapeStringRegexp from 'escape-string-regexp'; + +type Filter = { + test: (text: string) => boolean + noFilter: boolean +} + +const toRegexp = (regex: string) => new RegExp(regex, "i"); + +export type FilterType = 'simple' | 'regex'; + +export function parseFilter(filterString?: string, type?: FilterType): Filter { + const predicates: RegExp[] = []; + if (filterString) { + switch (type) { + case 'regex': + if (!/^\^?\.\*\$?$/.test(filterString) && escapeStringRegexp(filterString).indexOf("\\") > -1) { //regexp must not be relevant: not '.*' and an actual regexp (nothing escaped when escaping -> not a regexp) + predicates.push(toRegexp(filterString)); + } + break; + default: + const filters = filterString.split(','); + if (!filters.some(filter => /^\*(?:ALL)?$/.test(filter)) && (filters.length > 1 || filters[0].includes('*'))) { //*, *ALL or a single value with no '*' is not a filter + predicates.push(...filters + .map(filter => escapeStringRegexp(filter)) + .map(filter => toRegexp(`^${filter.replaceAll('\\*', '.*')}$`))); //* has been escaped, hence the '\\*' + } + } + } + + if (predicates.length) { + return { + test: (text) => predicates.some(regExp => regExp.test(text)), + noFilter: false + } + } + else { + return { + test: () => true, + noFilter: true + } + } +} + +/** + * Return filterString if it is a single, generic name filter (e.g. QSYS*) + * @param filterString + * @returns filterString if it is a single generic name or undefined otherwise + */ +export function singleGenericName(filterString?: string) { + return filterString && !filterString.includes(',') && filterString.indexOf('*') === filterString.length - 1 ? filterString : undefined; +} \ No newline at end of file diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index c7422e739..8868d2f3b 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -5,8 +5,9 @@ import tmp from 'tmp'; import util from 'util'; import { window } from 'vscode'; import { ObjectTypes } from '../filesystems/qsys/Objects'; -import { CommandResult, IBMiError, IBMiFile, IBMiMember, IBMiObject, IFSFile, QsysPath } from '../typings'; +import { CommandResult, IBMiError, IBMiMember, IBMiObject, IFSFile, QsysPath } from '../typings'; import { ConnectionConfiguration } from './Configuration'; +import { FilterType, parseFilter, singleGenericName } from './Filter'; import { default as IBMi } from './IBMi'; import { Tools } from './Tools'; const tmpFile = util.promisify(tmp.file); @@ -19,7 +20,7 @@ type Authority = "*ADD" | "*DLT" | "*EXECUTE" | "*READ" | "*UPD" | "*NONE" | "*A export type SortOrder = `name` | `type`; export type SortOptions = { - order: "name" | "date" | "?" + order: "name" | "date" ascending?: boolean } @@ -339,6 +340,49 @@ export default class IBMiContent { } + /** + * Prepare a table in QTEMP using any number of preparation queries and return its content. + * @param prepareQueries : SQL statements that should create a table in QTEMP + * @param table : the name of the table expected to be found in QTEMP + * @returns : the table's content + */ + async getQTempTable(prepareQueries: string[], table: string): Promise { + let temporaryFile: string | undefined; + if (this.config.enableSQL) { + prepareQueries.push(`Select * From QTEMP.${table}`); + } + else { + temporaryFile = this.getTempRemote(table); + prepareQueries.push(`CALL QSYS2.QCMDEXC('QSYS/CPYTOIMPF FROMFILE(QTEMP/${table}) TOSTMF(''${temporaryFile}'') MBROPT(*REPLACE) STMFCCSID(1208) RCDDLM(*CRLF) DTAFMT(*DLM) RMVBLANK(*TRAILING) ADDCOLNAM(*SQL) FLDDLM('','') DECPNT(*PERIOD)')`); + } + + try { + const fullQuery = prepareQueries.map(query => query.endsWith(';') ? query : `${query};`).join("\n"); + const result = await this.runSQL(fullQuery); + if (temporaryFile) { + return parse(await this.downloadStreamfile(temporaryFile), { + columns: true, + skip_empty_lines: true, + cast: true, + onRecord(record) { + for (const key of Object.keys(record)) { + record[key] = record[key] === ` ` ? `` : record[key]; + } + return record; + } + }); + } + else { + return result; + } + } + finally { + if (this.config.autoClearTempData && temporaryFile) { + await this.ibmi.sendCommand({ command: `rm -f ${temporaryFile}` }); + } + } + } + /** * Get list of libraries with description and attribute * @param libraries Array of libraries to retrieve @@ -360,16 +404,10 @@ export default class IBMiContent { `; results = await this.runSQL(statement); } else { - await this.ibmi.runCommand({ - command: `DSPOBJD OBJ(QSYS/*ALL) OBJTYPE(*LIB) DETAIL(*TEXTATR) OUTPUT(*OUTFILE) OUTFILE(${tempLib}/${TempName})`, - noLibList: true - }); - results = await this.getTable(tempLib, TempName, TempName, true); - + results = await this.getQTempTable([`CALL QSYS2.QCMDEXC('DSPOBJD OBJ(QSYS/*ALL) OBJTYPE(*LIB) DETAIL(*TEXTATR) OUTPUT(*OUTFILE) OUTFILE(QTEMP/LIBLIST)`], "LIBLIST"); if (results.length === 1 && !results[0].ODOBNM?.toString().trim()) { return []; } - results = results.filter(object => libraries.includes(this.ibmi.sysNameInLocal(String(object.ODOBNM)))); }; @@ -436,212 +474,168 @@ export default class IBMiContent { return badLibs; } + async getLibraries(filters: { library: string; filterType?: FilterType }) { + return this.getObjectList({ library: "QSYS", object: filters.library, types: ["*LIB"] }); + } + /** * @param filters * @param sortOrder * @returns an array of IBMiFile */ - async getObjectList(filters: { library: string; object?: string; types?: string[]; }, sortOrder?: SortOrder): Promise { + async getObjectList(filters: { library: string; object?: string; types?: string[]; filterType?: FilterType }, sortOrder?: SortOrder): Promise { const library = filters.library.toUpperCase(); if (!await this.checkObject({ library: "QSYS", name: library, type: "*LIB" })) { throw new Error(`Library ${library} does not exist.`); } - let hasEndsWith = false; - const objects = filters.object?.split(',') - .map(pattern => { - if (pattern.startsWith('*')) { - hasEndsWith = true; - return (name: string) => name.endsWith(pattern.substring(1)); - } - else if (pattern.endsWith('*')) { - return (name: string) => name.startsWith(pattern.substring(0, pattern.length - 1)); - } - else { - return (name: string) => name === pattern; - } - }) - || []; + const singleEntry = singleGenericName(filters.object); + const nameFilter = parseFilter(filters.object, filters.filterType); + const object = filters.object && (nameFilter.noFilter || singleEntry) && filters.object !== `*` ? filters.object.toUpperCase() : `*ALL`; - const filterNames = objects.length && (objects.length > 1 || hasEndsWith) ? (file: IBMiFile) => objects.some(test => test(file.name)) : undefined; + const typeFilter = filters.types && filters.types.length > 1 ? (t: string) => filters.types?.includes(t) : undefined; + const type = filters.types && filters.types.length === 1 && filters.types[0] !== '*' ? filters.types[0] : '*ALL'; - const object = filters.object && !filterNames && filters.object !== `*` ? filters.object.toUpperCase() : `*ALL`; - const sourceFilesOnly = (filters.types && filters.types.includes(`*SRCPF`)); + const sourceFilesOnly = filters.types && filters.types.length === 1 && filters.types.includes(`*SRCPF`); + const withSourceFiles = ['*ALL', '*SRCPF'].includes(type); - const tempLib = this.config.tempLibrary; - const tempName = Tools.makeid(); + const queries: string[] = []; - if (sourceFilesOnly) { - await this.ibmi.runCommand({ - command: `DSPFD FILE(${library}/${object}) TYPE(*ATR) FILEATR(*PF) OUTPUT(*OUTFILE) OUTFILE(${tempLib}/${tempName})`, - noLibList: true - }); + if (!sourceFilesOnly) { + queries.push(`CALL QSYS2.QCMDEXC('DSPOBJD OBJ(${library}/${object}) OBJTYPE(${type}) OUTPUT(*OUTFILE) OUTFILE(QTEMP/CODE4IOBJD)')`); + } - const results = await this.getTable(tempLib, tempName, tempName, true); - if (results.length === 1 && !results[0].PHFILE?.toString().trim()) { - return []; - } + if (withSourceFiles) { + queries.push(`CALL QSYS2.QCMDEXC('DSPFD FILE(${library}/${object}) TYPE(*ATR) FILEATR(*PF) OUTPUT(*OUTFILE) OUTFILE(QTEMP/CODE4IFD)')`); + } - return results.filter(object => object.PHDTAT === `S`) - .map(object => ({ - library, - name: this.ibmi.sysNameInLocal(String(object.PHFILE)), - type: `*FILE`, - attribute: String(object.PHFILA), - text: String(object.PHTXT), - count: Number(object.PHNOMB), - } as IBMiFile)) - .filter(object => !filterNames || filterNames(object)) - .sort((a, b) => a.library.localeCompare(b.library) || a.name.localeCompare(b.name)); - } else { - const objectTypes = (filters.types && filters.types.length ? filters.types.map(type => type.toUpperCase()).join(` `) : `*ALL`); + let createOBJLIST; + if (sourceFilesOnly) { + //DSPFD only + createOBJLIST = `Select PHFILE as NAME, ` + + `'*FILE' As TYPE, ` + + `PHFILA As ATTRIBUTE, ` + + `PHTXT As TEXT, ` + + `1 As IS_SOURCE, ` + + `PHNOMB As NB_MBR ` + + `From QTEMP.CODE4IFD Where PHDTAT = 'S'`; + } else if (!withSourceFiles) { + //DSPOBJD only + createOBJLIST = `Select ODOBNM as NAME, ` + + `ODOBTP As TYPE, ` + + `ODOBAT As ATTRIBUTE, ` + + `ODOBTX As TEXT, ` + + `0 As IS_SOURCE ` + + `From QTEMP.CODE4IOBJD`; + } + else { + //Both DSPOBJD and DSPFD + createOBJLIST = `Select ODOBNM as NAME, ` + + `ODOBTP As TYPE, ` + + `ODOBAT As ATTRIBUTE, ` + + `ODOBTX As TEXT, ` + + `Case When PHDTAT = 'S' Then 1 Else 0 End As IS_SOURCE, ` + + `PHNOMB As NB_MBR ` + + `From QTEMP.CODE4IOBJD ` + + `Left Join QTEMP.CODE4IFD On PHFILE = ODOBNM And PHDTAT = 'S'`; + } - await this.ibmi.runCommand({ - command: `DSPOBJD OBJ(${library}/${object}) OBJTYPE(${objectTypes}) OUTPUT(*OUTFILE) OUTFILE(${tempLib}/${tempName})`, - noLibList: true + queries.push(`Create Table QTEMP.OBJLIST As (${createOBJLIST}) With DATA`); + + const objects = (await this.getQTempTable(queries, "OBJLIST")); + return objects.map(object => ({ + library, + name: this.ibmi.sysNameInLocal(String(object.NAME)), + type: String(object.TYPE), + attribute: String(object.ATTRIBUTE), + text: String(object.TEXT), + memberCount: object.NB_MBR !== undefined ? Number(object.NB_MBR) : undefined, + sourceFile: Boolean(object.IS_SOURCE) + } as IBMiObject)) + .filter(object => !typeFilter || typeFilter(object.type)) + .filter(object => nameFilter.test(object.name)) + .sort((a, b) => { + if (a.library.localeCompare(b.library) != 0) { + return a.library.localeCompare(b.library) + } + else if (sortOrder === `name`) { + return a.name.localeCompare(b.name) + } + else { + return ((ObjectTypes.get(a.type) || 0) - (ObjectTypes.get(b.type) || 0)) || a.name.localeCompare(b.name); + } }); - const results = await this.getTable(tempLib, tempName, tempName, true); - - if (results.length === 1 && !results[0].ODOBNM?.toString().trim()) { - return []; - } - - return results.map(object => ({ - library, - name: this.ibmi.sysNameInLocal(String(object.ODOBNM)), - type: String(object.ODOBTP), - attribute: String(object.ODOBAT), - text: String(object.ODOBTX) - } as IBMiFile)) - .filter(object => !filterNames || filterNames(object)) - .sort((a, b) => { - if (a.library.localeCompare(b.library) != 0) { - return a.library.localeCompare(b.library) - } - else if (sortOrder === `name`) { - return a.name.localeCompare(b.name) - } - else { - return ((ObjectTypes.get(a.type) || 0) - (ObjectTypes.get(b.type) || 0)) || a.name.localeCompare(b.name); - } - }); - } } /** - * @param lib - * @param spf - * @param mbr - * @returns an array of IBMiMember + * + * @param filter: the criterias used to list the members + * @returns */ - async getMemberList(lib: string, spf: string, mbr: string = `*`, ext: string = `*`, sort: SortOptions = { order: "name" }): Promise { - sort.order = sort.order === '?' ? 'name' : sort.order; - - const library = lib.toUpperCase(); - const sourceFile = spf.toUpperCase(); - let member = (mbr !== `*` ? mbr.toUpperCase() : null); - let memberExt = (ext !== `*` ? ext.toUpperCase() : null); - - let results: Tools.DB2Row[]; - - if (this.config.enableSQL) { - if (member) { - member = member.replace(/[*]/g, `%`); - } - - if (memberExt) { - memberExt = memberExt.replace(/[*]/g, `%`); - } - - const statement = ` + async getMemberList(filter: { library: string, sourceFile: string, members?: string, extensions?: string, sort?: SortOptions, filterType?: FilterType }): Promise { + const sort = filter.sort || { order: 'name' }; + const library = filter.library.toUpperCase(); + const sourceFile = filter.sourceFile.toUpperCase(); + + const memberFilter = parseFilter(filter.members, filter.filterType); + const singleMember = memberFilter.noFilter && filter.members && !filter.members.includes(",") ? filter.members.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; + + const memberExtensionFilter = parseFilter(filter.extensions, filter.filterType); + const singleMemberExtension = memberExtensionFilter.noFilter && filter.extensions && !filter.extensions.includes(",") ? filter.extensions.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; + + const statement = + `With MEMBERS As ( SELECT - b.avgrowsize as MBMXRL, - a.iasp_number as MBASP, - cast(a.system_table_name as char(10) for bit data) AS MBFILE, - cast(b.system_table_member as char(10) for bit data) as MBNAME, - coalesce(cast(b.source_type as varchar(10) for bit data), '') as MBSEU2, - coalesce(b.partition_text, '') as MBMTXT, - b.NUMBER_ROWS as MBNRCD, + rtrim(cast(a.system_table_schema as char(10) for bit data)) as LIBRARY, + b.avgrowsize as RECORD_LENGTH, + a.iasp_number as ASP, + rtrim(cast(a.system_table_name as char(10) for bit data)) AS SOURCE_FILE, + rtrim(cast(b.system_table_member as char(10) for bit data)) as NAME, + coalesce(rtrim(cast(b.source_type as varchar(10) for bit data)), '') as TYPE, + coalesce(rtrim(b.partition_text), '') as TEXT, + b.NUMBER_ROWS as LINES, extract(epoch from (b.CREATE_TIMESTAMP))*1000 as CREATED, extract(epoch from (b.LAST_SOURCE_UPDATE_TIMESTAMP))*1000 as CHANGED FROM qsys2.systables AS a JOIN qsys2.syspartitionstat AS b ON b.table_schema = a.table_schema AND b.table_name = a.table_name - WHERE - cast(a.system_table_schema as char(10) for bit data) = '${library}' - ${sourceFile !== `*ALL` ? `AND cast(a.system_table_name as char(10) for bit data) = '${sourceFile}'` : ``} - ${member ? `AND rtrim(cast(b.system_table_member as char(10) for bit data)) like '${member}'` : ``} - ${memberExt ? `AND rtrim(coalesce(cast(b.source_type as varchar(10) for bit data), '')) like '${memberExt}'` : ``} - `; - results = await this.runSQL(statement); - } else { - const tempLib = this.config.tempLibrary; - const TempName = Tools.makeid(); + ) + Select * From MEMBERS + Where LIBRARY = '${library}' + ${sourceFile !== `*ALL` ? `And SOURCE_FILE = '${sourceFile}'` : ``} + ${singleMember ? `And NAME Like '${singleMember}'` : ''} + ${singleMemberExtension ? `And TYPE Like '${singleMemberExtension}'` : ''} + Order By ${sort.order === 'name' ? 'NAME' : 'CHANGED'} ${!sort.ascending ? 'DESC' : 'ASC'}`; - await this.ibmi.runCommand({ - command: `DSPFD FILE(${library}/${sourceFile}) TYPE(*MBR) OUTPUT(*OUTFILE) OUTFILE(${tempLib}/${TempName})`, - noLibList: true - }); - results = await this.getTable(tempLib, TempName, TempName, true); - if (results.length === 1 && String(results[0].MBNAME).trim() === ``) { - return []; - } - - if (member || memberExt) { - let pattern: RegExp | undefined, patternExt: RegExp | undefined; - if (member) { - pattern = new RegExp(`^` + member.replace(/[*]/g, `.*`).replace(/[$]/g, `\\$`) + `$`); - } - if (memberExt) { - patternExt = new RegExp(`^` + memberExt.replace(/[*]/g, `.*`).replace(/[$]/g, `\\$`) + `$`); - } - - results = results.filter(row => ( - (!pattern || pattern.test(String(row.MBNAME))) && - (!patternExt || patternExt.test(String(row.MBSEU2))))) - } - - results.forEach(element => { - element.CREATED = this.getDspfdDate(String(element.MBCCEN), String(element.MBCDAT), String(element.MBCTIM)).valueOf(); - element.CHANGED = this.getDspfdDate(String(element.MBMRCN), String(element.MBMRDT), String(element.MBMRTM)).valueOf(); - }); - } - - if (results.length === 0) { - return []; - } - - results = results.sort((a, b) => String(a.MBNAME).localeCompare(String(b.MBNAME))); - - const asp = this.ibmi.aspInfo[Number(results[0].MBASP)]; - - let sorter: (r1: IBMiMember, r2: IBMiMember) => number; - if (sort.order === 'name') { - sorter = (r1, r2) => r1.name.localeCompare(r2.name); + let results: Tools.DB2Row[]; + if (this.config.enableSQL) { + results = await this.runSQL(statement); } else { - sorter = (r1, r2) => r1.changed!.valueOf() - r2.changed!.valueOf(); + results = await this.getQTempTable([`Create Table QTEMP.MEMBERSLST As (${statement}) With DATA`], "MEMBERSLST"); } - const members = results.map(result => ({ - asp: asp, - library: library, - file: String(result.MBFILE), - name: String(result.MBNAME), - extension: String(result.MBSEU2), - recordLength: Number(result.MBMXRL) - 12, - text: `${result.MBMTXT || ``}${sourceFile === `*ALL` ? ` (${result.MBFILE})` : ``}`.trim(), - lines: Number(result.MBNRCD), - created: new Date(result.CREATED ? Number(result.CREATED) : 0), - changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) - } as IBMiMember)).sort(sorter); - - if (sort.ascending === false) { - members.reverse(); + if(results.length){ + const asp = this.ibmi.aspInfo[Number(results[0].ASP)]; + return results.map(result => ({ + asp, + library, + file: String(result.SOURCE_FILE), + name: String(result.NAME), + extension: String(result.TYPE), + recordLength: Number(result.RECORD_LENGTH) - 12, + text: `${result.TEXT || ``}${sourceFile === `*ALL` ? ` (${result.SOURCE_FILE})` : ``}`.trim(), + lines: Number(result.LINES), + created: new Date(result.CREATED ? Number(result.CREATED) : 0), + changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) + } as IBMiMember)) + .filter(member => memberFilter.test(member.name)) + .filter(member => memberExtensionFilter.test(member.name)); + } + else{ + return []; } - - return members; } /** @@ -650,7 +644,6 @@ export default class IBMiContent { * @return an array of IFSFile */ async getFileList(remotePath: string, sort: SortOptions = { order: "name" }, onListError?: (errors: string[]) => void): Promise { - sort.order = sort.order === '?' ? 'name' : sort.order; const { 'stat': STAT } = this.ibmi.remoteFeatures; const { 'sort': SORT } = this.ibmi.remoteFeatures; diff --git a/src/sandbox.ts b/src/sandbox.ts index 3383dec35..e7f9cc091 100644 --- a/src/sandbox.ts +++ b/src/sandbox.ts @@ -187,6 +187,7 @@ async function initialSetup(username: string) { config.objectFilters.push( { name: "Sandbox Sources", + filterType: 'simple', library: username, object: "*", types: [ @@ -198,6 +199,7 @@ async function initialSetup(username: string) { }, { name: "Sandbox Object Filters", + filterType: 'simple', library: username, object: "*", types: [ diff --git a/src/typings.ts b/src/typings.ts index ef14976d1..63b03384b 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -98,11 +98,9 @@ export interface QsysPath { export interface IBMiObject extends QsysPath { type: string, text: string, - attribute?: string -} - -export interface IBMiFile extends IBMiObject { - count?: number + sourceFile?: boolean + attribute?: string, + memberCount?: number } export interface IBMiMember { @@ -186,7 +184,7 @@ export interface ObjectItem extends FilteredItem, WithPath { } export interface SourcePhysicalFileItem extends FilteredItem, WithPath { - sourceFile: IBMiFile + sourceFile: IBMiObject } export interface MemberItem extends FilteredItem, WithPath { diff --git a/src/views/objectBrowser.ts b/src/views/objectBrowser.ts index fc6c754e8..ce26514d9 100644 --- a/src/views/objectBrowser.ts +++ b/src/views/objectBrowser.ts @@ -3,6 +3,7 @@ import os from "os"; import util from "util"; import vscode from "vscode"; import { ConnectionConfiguration, DefaultOpenMode, GlobalConfiguration } from "../api/Configuration"; +import { parseFilter } from "../api/Filter"; import { MemberParts } from "../api/IBMi"; import { SortOptions, SortOrder } from "../api/IBMiContent"; import { Search } from "../api/Search"; @@ -11,7 +12,7 @@ import { Tools } from "../api/Tools"; import { getMemberUri } from "../filesystems/qsys/QSysFs"; import { instance, setSearchResults } from "../instantiate"; import { t } from "../locale"; -import { BrowserItem, BrowserItemParameters, CommandResult, FilteredItem, FocusOptions, IBMiFile, IBMiMember, IBMiObject, MemberItem, ObjectItem, SourcePhysicalFileItem } from "../typings"; +import { BrowserItem, BrowserItemParameters, CommandResult, FilteredItem, FocusOptions, IBMiMember, IBMiObject, MemberItem, ObjectItem, SourcePhysicalFileItem } from "../typings"; import { editFilter } from "../webviews/filters"; const writeFileAsync = util.promisify(fs.writeFile); @@ -46,7 +47,7 @@ const objectIcons = { '': `circle-large-outline` } -function isProtected(filter: ConnectionConfiguration.ObjectFilters){ +function isProtected(filter: ConnectionConfiguration.ObjectFilters) { return filter.protected || getContent().isProtectedPath(filter.library); } @@ -61,7 +62,7 @@ class ObjectBrowserItem extends BrowserItem { reveal(options?: FocusOptions) { return vscode.commands.executeCommand(`code-for-ibmi.revealInObjectBrowser`, this, options); - } + } } class ObjectBrowser implements vscode.TreeDataProvider { @@ -166,10 +167,16 @@ class ObjectBrowserFilterItem extends ObjectBrowserItem { } async getChildren(): Promise { - return (await getContent().getObjectList(this.filter, objectSortOrder())) - .map(object => { - return object.attribute?.toLocaleUpperCase() === `*PHY` ? new ObjectBrowserSourcePhysicalFileItem(this, object) : new ObjectBrowserObjectItem(this, object); - }); + const libraryFilter = parseFilter(this.filter.library); + if (libraryFilter.noFilter) { + return await listObjects(this); + } + else { + return (await getContent().getLibraries(this.filter)) + .map(object => { + return object.sourceFile ? new ObjectBrowserSourcePhysicalFileItem(this, object) : new ObjectBrowserObjectItem(this, object); + }); + } } } @@ -177,7 +184,7 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements S readonly sort: SortOptions = { order: "name", ascending: true }; readonly path: string; - constructor(parent: ObjectBrowserFilterItem, readonly sourceFile: IBMiFile) { + constructor(parent: ObjectBrowserFilterItem, readonly sourceFile: IBMiObject) { super(parent.filter, correctCase(sourceFile.name), { parent, icon: `file-directory`, state: vscode.TreeItemCollapsibleState.Collapsed }); this.contextValue = `SPF${isProtected(this.filter) ? `_readonly` : ``}`; @@ -209,7 +216,14 @@ class ObjectBrowserSourcePhysicalFileItem extends ObjectBrowserItem implements S }, `*UPD`); try { - const members = await content.getMemberList(this.sourceFile.library, this.sourceFile.name, this.filter.member, this.filter.memberType, this.sort); + const members = await content.getMemberList({ + library: this.sourceFile.library, + sourceFile: this.sourceFile.name, + members: this.filter.member, + extensions: this.filter.memberType, + filterType: this.filter.filterType, + sort: this.sort + }); await storeMemberList(this.path, members.map(member => `${member.name}.${member.extension}`)); @@ -244,7 +258,8 @@ class ObjectBrowserObjectItem extends ObjectBrowserItem implements ObjectItem { constructor(parent: ObjectBrowserFilterItem, readonly object: IBMiObject) { const type = object.type.startsWith(`*`) ? object.type.substring(1) : object.type; const icon = Object.entries(objectIcons).find(([key]) => key === type.toUpperCase())?.[1] || objectIcons[``]; - super(parent.filter, correctCase(`${object.name}.${type}`), { icon, parent }); + const isLibrary = type === 'LIB'; + super(parent.filter, correctCase(`${object.name}.${type}`), { icon, parent, state: isLibrary ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None }); this.path = [object.library, object.name].join(`/`); this.updateDescription(); @@ -257,16 +272,24 @@ class ObjectBrowserObjectItem extends ObjectBrowserItem implements ObjectItem { fragment: object.attribute }); - this.command = { - command: `vscode.open`, - title: `Open`, - arguments: [this.resourceUri] - }; + if (!isLibrary) { + this.command = { + command: `vscode.open`, + title: `Open`, + arguments: [this.resourceUri] + }; + } } updateDescription() { this.description = this.object.text.trim() + (this.object.attribute ? ` (${this.object.attribute})` : ``); } + + async getChildren() { + const objectFilter = Object.assign({}, this.filter); + objectFilter.library = this.object.name; + return await listObjects(this, objectFilter); + } } class ObjectBrowserMemberItem extends ObjectBrowserItem implements MemberItem { @@ -346,13 +369,14 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { if (regex && parsedFilter) { const filter = { name: `Filter ${objectFilters.length + 1}`, + filterType: 'simple', library: `QSYS`, object: `${parsedFilter.lib}*`, types: [`*LIB`], member: `*`, memberType: `*`, protected: false - } + } as ConnectionConfiguration.ObjectFilters; objectFilters.push(filter); } else { regex = FILTER_REGEX.exec(newFilter.toUpperCase()); @@ -360,13 +384,14 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { if (regex && parsedFilter) { const filter = { name: `Filter ${objectFilters.length + 1}`, + filterType: 'simple', library: parsedFilter.lib || `QGPL`, object: parsedFilter.obj || `*`, types: [parsedFilter.objType || `*SRCPF`], member: parsedFilter.mbr || `*`, memberType: parsedFilter.mbrType || `*`, protected: false - } + } as ConnectionConfiguration.ObjectFilters; objectFilters.push(filter); } } @@ -835,6 +860,7 @@ export function initializeObjectBrowser(context: vscode.ExtensionContext) { filters.push({ name: newLibrary, + filterType: 'simple', library: newLibrary, object: `*ALL`, types: [`*ALL`], @@ -1119,8 +1145,7 @@ async function doSearchInSourceFile(searchTerm: string, path: string, filter: Co message: t(`objectBrowser.doSearchInSourceFile.progressMessage`, path) }); - const members = await content.getMemberList(pathParts[0], pathParts[1], filter?.member); - + const members = await content.getMemberList({ library: pathParts[0], sourceFile: pathParts[1], members: filter?.member }); if (members.length > 0) { // NOTE: if more messages are added, lower the timeout interval const timeoutInternal = 9000; @@ -1193,4 +1218,11 @@ async function doSearchInSourceFile(searchTerm: string, path: string, filter: Co } catch (e) { vscode.window.showErrorMessage(t(`objectBrowser.doSearchInSourceFile.errorMessage`, e)); } -} \ No newline at end of file +} + +async function listObjects(item: ObjectBrowserFilterItem, filter?: ConnectionConfiguration.ObjectFilters) { + return (await getContent().getObjectList(filter || item.filter, objectSortOrder())) + .map(object => { + return object.sourceFile ? new ObjectBrowserSourcePhysicalFileItem(item, object) : new ObjectBrowserObjectItem(item, object); + }); +} diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index eb38cdfaf..2b6e37d64 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -14,7 +14,8 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, if (copy) { filter = { name: `${filter.name} - copy`, - library: filter.library, + filterType: 'simple', + library: filter.library, object: filter.object, types: [...filter.types], member: filter.member, @@ -28,6 +29,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, // Otherwise, set the default values filter = { name: `Filter ${objectFilters.length + 1}`, + filterType: 'simple', library: `QGPL`, object: `*`, types: [`*SRCPF`], @@ -41,11 +43,15 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, const page = await new CustomUI() .addInput(`name`, `Filter name`, `The filter name should be unique.`, { default: filter.name }) - .addInput(`library`, `Library`, `Library name. Cannot be generic name with an asterisk.`, { default: filter.library }) - .addInput(`object`, `Objects`, `Object names. Comma-separated list of object names. Can be multi-generic values. Examples: *, Q* or *CL*SRC*. A single `*` will return all objects.`, { default: filter.object }) - .addInput(`types`, `Object type filter`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) - .addInput(`member`, `Member`, `Member name. Can be multi-generic value. Examples: *CL or CL*ABC*. A single * will return all members.`, { default: filter.member }) - .addInput(`memberType`, `Member type`, `Member type. Can be multi-generic value. Examples: RPG* or SQL*LE. A single * will return all member types.`, { default: filter.memberType || `*` }) + .addSelect(`filterType`, `Filtering type`, [ + { value: 'simple', description: 'Simple', text: `A comma-separated list of multi-generic values. Examples: *, Q* or *CL*SRC*. A single *, *ALL or blank will return everything.`, selected: filter.filterType === "simple" }, + { value: 'regex', description: 'Regex', text: `Use a single RegEx for filtering.`, selected: filter.filterType === "regex" } + ], `Select the filtering strategy to apply for filtering names.
Checkout https://regex101.com to get started with RegExs.`) + .addInput(`library`, `Libraries`, `Library names filter.`, { default: filter.library }) + .addInput(`object`, `Objects`, `Object names filter.`, { default: filter.object }) + .addInput(`types`, `Object types`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) + .addInput(`member`, `Members`, `Member names filter.`, { default: filter.member }) + .addInput(`memberType`, `Member type`, `Member types filter.`, { default: filter.memberType }) .addCheckbox(`protected`, `Protected`, `Make this filter protected, preventing modifications and source members from being saved.`, filter.protected) .addButtons({ id: `save`, label: `Save settings` }) .loadPage(`Filter: ${newFilter ? `New` : filter.name}`); @@ -59,6 +65,8 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, //In case we need to play with the data switch (key) { case `name`: + case `filterType`: + case `library`: data[key] = String(data[key]).trim(); break; case `types`: @@ -72,6 +80,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, .join(","); break; case `member`: + case `member`: case `memberType`: data[key] = String(data[key].trim()) || `*`; break; From 16ee211c2a4374575a57828a136c00d2ff4c8035 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 24 Jan 2024 16:04:11 +0100 Subject: [PATCH 08/18] Fixed formatting Signed-off-by: Seb Julliand --- src/webviews/filters/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 2b6e37d64..39f125047 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -49,7 +49,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, ], `Select the filtering strategy to apply for filtering names.
Checkout https://regex101.com to get started with RegExs.`) .addInput(`library`, `Libraries`, `Library names filter.`, { default: filter.library }) .addInput(`object`, `Objects`, `Object names filter.`, { default: filter.object }) - .addInput(`types`, `Object types`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) + .addInput(`types`, `Object types`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) .addInput(`member`, `Members`, `Member names filter.`, { default: filter.member }) .addInput(`memberType`, `Member type`, `Member types filter.`, { default: filter.memberType }) .addCheckbox(`protected`, `Protected`, `Make this filter protected, preventing modifications and source members from being saved.`, filter.protected) From c2490ffe3b7a9ad53780cbc7c7dadc1c7c840da1 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 24 Jan 2024 16:04:33 +0100 Subject: [PATCH 09/18] Fixed getLibraries not taking regexp Signed-off-by: Seb Julliand --- src/api/IBMiContent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 8868d2f3b..65d6598f5 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -475,7 +475,7 @@ export default class IBMiContent { } async getLibraries(filters: { library: string; filterType?: FilterType }) { - return this.getObjectList({ library: "QSYS", object: filters.library, types: ["*LIB"] }); + return this.getObjectList({ library: "QSYS", object: filters.library, types: ["*LIB"], filterType:filters.filterType }); } /** From cf0cd8c4bec29b144a6f4815a9971a5ea5307cd0 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 24 Jan 2024 16:04:52 +0100 Subject: [PATCH 10/18] Added tests for getLibraries/Objects/Members filters Signed-off-by: Seb Julliand --- src/testing/content.ts | 126 +++++++++++++++++++++++++++-------------- src/testing/filter.ts | 90 +++++++++++++++++++++++++++++ src/testing/index.ts | 16 ++---- 3 files changed, 181 insertions(+), 51 deletions(-) create mode 100644 src/testing/filter.ts diff --git a/src/testing/content.ts b/src/testing/content.ts index 5135b1989..efe31451c 100644 --- a/src/testing/content.ts +++ b/src/testing/content.ts @@ -13,7 +13,7 @@ export const ContentSuite: TestSuite = { { name: `Test memberResolve`, test: async () => { const content = instance.getContent(); - + const member = await content?.memberResolve(`MATH`, [ { library: `QSYSINC`, name: `MIH` }, // Doesn't exist here { library: `QSYSINC`, name: `H` } // Does exist @@ -29,37 +29,41 @@ export const ContentSuite: TestSuite = { }); } }, - - - {name: `Test memberResolve (with invalid ASP)`, test: async () => { - const content = instance.getContent(); - - const member = await content?.memberResolve(`MATH`, [ - {library: `QSYSINC`, name: `MIH`}, // Doesn't exist here - {library: `QSYSINC`, name: `H`, asp: `myasp`} // Does exist, but not in the ASP - ]); - - assert.deepStrictEqual(member, { - asp: undefined, - library: `QSYSINC`, - file: `H`, - name: `MATH`, - extension: `MBR`, - basename: `MATH.MBR` - }); - }}, - - {name: `Test memberResolve with bad name`, test: async () => { - const content = instance.getContent(); - - const member = await content?.memberResolve(`BOOOP`, [ - {library: `QSYSINC`, name: `MIH`}, // Doesn't exist here - {library: `NOEXIST`, name: `SUP`}, // Doesn't exist here - {library: `QSYSINC`, name: `H`} // Doesn't exist here - ]); - - assert.deepStrictEqual(member, undefined); - }}, + + + { + name: `Test memberResolve (with invalid ASP)`, test: async () => { + const content = instance.getContent(); + + const member = await content?.memberResolve(`MATH`, [ + { library: `QSYSINC`, name: `MIH` }, // Doesn't exist here + { library: `QSYSINC`, name: `H`, asp: `myasp` } // Does exist, but not in the ASP + ]); + + assert.deepStrictEqual(member, { + asp: undefined, + library: `QSYSINC`, + file: `H`, + name: `MATH`, + extension: `MBR`, + basename: `MATH.MBR` + }); + } + }, + + { + name: `Test memberResolve with bad name`, test: async () => { + const content = instance.getContent(); + + const member = await content?.memberResolve(`BOOOP`, [ + { library: `QSYSINC`, name: `MIH` }, // Doesn't exist here + { library: `NOEXIST`, name: `SUP` }, // Doesn't exist here + { library: `QSYSINC`, name: `H` } // Doesn't exist here + ]); + + assert.deepStrictEqual(member, undefined); + } + }, { name: `Test memberResolve with bad name`, test: async () => { @@ -396,7 +400,6 @@ export const ContentSuite: TestSuite = { assert.strictEqual(containsNonFiles, false); } }, - { name: `Test getObjectList (source files only, named filter)`, test: async () => { const content = instance.getContent(); @@ -409,16 +412,46 @@ export const ContentSuite: TestSuite = { assert.strictEqual(objects[0].text, `DATA BASE FILE FOR C INCLUDES FOR MI`); } }, + { + name: `getLibraries (simple filters)`, test: async () => { + const content = instance.getContent(); + + const qsysLibraries = await content?.getLibraries({ library: "QSYS*" }) + assert.notStrictEqual(qsysLibraries?.length, 0); + assert.strictEqual(qsysLibraries?.every(l => l.name.startsWith("QSYS")), true); + const includeSYSLibraries = await content?.getLibraries({ library: "*SYS*" }); + assert.notStrictEqual(includeSYSLibraries?.length, 0); + assert.strictEqual(includeSYSLibraries?.every(l => l.name.includes("SYS")), true); + } + }, + { + name: `getLibraries (regexp filters)`, test: async () => { + const content = instance.getContent(); + + const qsysLibraries = await content?.getLibraries({ library: "^.*SYS[^0-9]*$", filterType: "regex" }) + assert.notStrictEqual(qsysLibraries?.length, 0); + assert.strictEqual(qsysLibraries?.every(l => /^.*SYS[^0-9]*$/.test(l.name)), true); + } + }, + { + name: `getObjectList (advanced filtering)`, test: async () => { + const content = instance.getContent(); + const objects = await content?.getObjectList({ library: `QSYSINC`, object:"L*OU*" }); + + assert.notStrictEqual(objects?.length, 0); + assert.strictEqual(objects?.map(o => o.name).every(n => n.startsWith("L") && n.includes("OU")), true); + } + }, { name: `getMemberList (SQL, no filter)`, test: async () => { const content = instance.getContent(); - let members = await content?.getMemberList(`qsysinc`, `mih`, `*inxen`); + let members = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih`, members: `*inxen` }); assert.strictEqual(members?.length, 3); - members = await content?.getMemberList(`qsysinc`, `mih`); + members = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih` }); const actbpgm = members?.find(mbr => mbr.name === `ACTBPGM`); @@ -438,13 +471,11 @@ export const ContentSuite: TestSuite = { assert.strictEqual(config!.enableSQL, true, `SQL must be enabled for this test`); // First we fetch the members in SQL mode - const membersA = await content?.getMemberList(`qsysinc`, `mih`); - + const membersA = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih` }); config!.enableSQL = false; // Then we fetch the members without SQL - const membersB = await content?.getMemberList(`qsysinc`, `mih`); - + const membersB = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih` }); // Reset the config config!.enableSQL = true; @@ -460,12 +491,12 @@ export const ContentSuite: TestSuite = { assert.strictEqual(config!.enableSQL, true, `SQL must be enabled for this test`); // First we fetch the members in SQL mode - const membersA = await content?.getMemberList(`qsysinc`, `mih`, `C*`); + const membersA = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih`, members: 'C*' }); config!.enableSQL = false; // Then we fetch the members without SQL - const membersB = await content?.getMemberList(`qsysinc`, `mih`, `C*`); + const membersB = await content?.getMemberList({ library: `qsysinc`, sourceFile: `mih`, members: 'C*' }); // Reset the config config!.enableSQL = true; @@ -473,5 +504,18 @@ export const ContentSuite: TestSuite = { assert.deepStrictEqual(membersA, membersB); } }, + { + name: `getMemberList (advanced filtering)`, test: async () => { + const content = instance.getContent(); + + const members = await content?.getMemberList({ library: `QSYSINC`, sourceFile: `QRPGLESRC`, members: 'SYS*,I*,*EX' }); + assert.notStrictEqual(members?.length, 0) + assert.strictEqual(members!.map(m => m.name).every(n => n.startsWith('SYS') || n.startsWith('I') || n.endsWith('EX')), true); + + const membersRegex = await content?.getMemberList({ library: `QSYSINC`, sourceFile: `QRPGLESRC`, members: '^QSY(?!RTV).*$', filterType: "regex" }); + assert.notStrictEqual(membersRegex?.length, 0); + assert.strictEqual(membersRegex!.map(m => m.name).every(n => n.startsWith('QSY') && !n.includes('RTV')), true); + } + }, ] }; diff --git a/src/testing/filter.ts b/src/testing/filter.ts new file mode 100644 index 000000000..cae896e81 --- /dev/null +++ b/src/testing/filter.ts @@ -0,0 +1,90 @@ +import assert from "assert"; +import { TestSuite } from "."; +import { parseFilter, singleGenericName } from "../api/Filter"; + +const QSYSINCS = ["CMRPG", "DSQCOMMR", "ECACHCMD", "ECARTCMD", "ECHGPRF1", "ECHGRCV1", "ECHKPWD1", "ECLRMST", "ECRTPRF1", "EDBOPNDB", "EDCVARY", "EDLTKR", "EDLTPRF1", "EDLTPRF2", "EDLTRCV1", "EIM", "EIMGEPH", "EJOBNTFY", "EKICONR", "EMHDFTPG", "EMOOPTEP", "ENPSEP", "EOGDOCH", "EOK", "EOKDRSH1", "EOKDRSP", "EOKDRVF", "EPADSEL", "EPDTRCJB", "EPQMAPXT", "EPQXFORM", "EPWFSEP", "EQQQRYGV", "EQQQRYSV", "ERRNO", "ERSTPRF1", "ERWSCI", "ESCWCHT", "ESETMST", "ESOEXTPT", "ESPBLSEP", "ESPDQRCD", "ESPDRVXT", "ESPTRNXT", "ESPXPTS", "ESYDRAPP", "ESYRGAPP", "ESYUPDCA", "ESYUPDCU", "ETASTGEX", "ETATAPMG", "ETEPGMST", "ETEPSEPH", "ETGDEVEX", "ETNCMTRB", "ETOCSVRE", "ETRNKSF", "EUIAFEX", "EUIALCL", "EUIALEX", "EUICSEX", "EUICTEX", "EUIFKCL", "EUIGPEX", "EUIILEX", "EUIMICL", "EUITAEX", "EVLDPWD1", "EWCPRSEP", "EWCPWRD", "EZDAEP", "EZHQEP", "EZRCEP", "EZSCEP", "EZSOEP", "FCNTL", "ICONV", "IFS", "JNI", "PTHREAD", "QALRTVA", "QANE", "QBNCHGPD", "QBNLMODI", "QBNLPGMI", "QBNLSPGM", "QBNRMODI", "QBNRPII", "QBNRSPGM", "QC3CCI", "QCAPCMD", "QCAVFY", "QCDRCMDD", "QCDRCMDI", "QCLRPGMI", "QCST", "QCSTCFG", "QCSTCFG1", "QCSTCHT", "QCSTCRG1", "QCSTCRG3", "QCSTCRG4", "QCSTCTL", "QCSTCTL1", "QCSTCTL2", "QCSTDD", "QDBJRNL", "QDBLDBR", "QDBRJBRL", "QDBRPLAY", "QDBRRCDL", "QDBRTVFD", "QDBRTVSN", "QDBST", "QDCCCFGD", "QDCLCFGD", "QDCRCFGS", "QDCRCTLD", "QDCRDEVD", "QDCRLIND", "QDCRNWSD", "QDFRPRTA", "QDFRTVFD", "QDMLOPNF", "QDMRTVFO", "QEDCHGIN", "QEDRTVCI", "QESCPTFO", "QESRSRVA", "QEZCHBKL", "QEZCHBKS", "QEZLSGNU", "QEZOLBKL", "QEZRTBKD", "QEZRTBKH", "QEZRTBKO", "QEZRTBKS", "QFPADAP1", "QFPADOLD", "QFPADOLS", "QFPADOLU", "QFPADRNI", "QFPADRUA", "QFPRLNWS", "QFPRRNWS", "QFPZAAPI", "QFVLSTA", "QFVLSTNL", "QFVRTVCD", "QGLDPAPI", "QGLDUAPI", "QGY", "QGYFNDF", "QGYGTLE", "QGYOLAFP", "QGYOLJBL", "QGYOLJOB", "QGYOLMSG", "QGYOLOBJ", "QGYOLSPL", "QGYRATLO", "QGYRHRCM", "QGYRPRTA", "QGYRPRTL", "QGYRTVSJ", "QHF", "QHFLSTFS", "QHFRDDR", "QIMGAPII", "QITDRSTS", "QJOJRNENT", "QJORJIDI", "QJOSJRNE", "QJOURNAL", "QKRBSPNEGO", "QLEAWI", "QLG", "QLGLCL", "QLGRLNGI", "QLGRTVCD", "QLGRTVCI", "QLGRTVCT", "QLGRTVLI", "QLGRTVSS", "QLGSORT", "QLGSRTIO", "QLIJRNL", "QLIRLIBD", "QLP", "QLPINSLP", "QLPLPRDS", "QLPRAGR", "QLYWRTBI", "QLZA", "QLZAADDK", "QLZADDLI", "QLZAGENK", "QLZARTV", "QLZARTVK", "QMHCTLJL", "QMHLJOBL", "QMHLSTM", "QMHOLHST", "QMHQCDQ", "QMHQJRNL", "QMHQRDQD", "QMHRCVM", "QMHRCVPM", "QMHRDQM", "QMHRMFAT", "QMHRMQAT", "QMHRSNEM", "QMHRTVM", "QMHRTVRQ", "QMR", "QMRAP1", "QNMRCVDT", "QNMRGFN", "QNMRGTI", "QNMRRGF", "QOGRTVOE", "QOKDSPDP", "QOKSCHD", "QOLQLIND", "QOLRECV", "QOLSEND", "QOLSETF", "QP0LFLOP", "QP0LROR", "QP0LRRO", "QP0LSCAN", "QP0LSTDI", "QP0MSRTVSO", "QPASTRPT", "QPDETCPP", "QPDETCVT", "QPDETPOL", "QPDETRPD", "QPDETRTV", "QPDETSND", "QPDETWCH", "QPDSRVPG", "QPMAAPI", "QPMDCPRM", "QPMLPFRD", "QPMLPMGT", "QPQ", "QPQAPME", "QPQMAP", "QPQOLPM", "QPQRAFPI", "QPQRPME", "QPTRTVPO", "QPZCPYSV", "QPZCRTFX", "QPZGENNM", "QPZGROUP", "QPZLOGFX", "QPZLSTFX", "QPZRTVFX", "QQQQRY", "QRCVDTAQ", "QRZRRSI", "QRZSCHE", "QSCCHGCT", "QSCJOINT", "QSCRWCHI", "QSCRWCHL", "QSCRXMLI", "QSCSWCH", "QSNAPI", "QSOTLSA", "QSPBOPNC", "QSPBSEPP", "QSPEXTWI", "QSPGETSP", "QSPMOVJB", "QSPMOVSP", "QSPOLJBQ", "QSPOLOTQ", "QSPRILSP", "QSPRJOBQ", "QSPROUTQ", "QSPRWTRI", "QSPSETWI", "QSPSNDWM", "QSPSPLI", "QSQCHKS", "QSQGNDDL", "QSQPRCED", "QSR", "QSRLIB01", "QSRLSAVF", "QSRRSTO", "QSRSAVO", "QSXFTRPB", "QSXSRVPL", "QSY", "QSYDIGID", "QSYEIMAPI", "QSYJRNL", "QSYLATLO", "QSYLAUTU", "QSYLOBJA", "QSYLOBJP", "QSYLUSRA", "QSYOLUC", "QSYOLVLE", "QSYRAUTU", "QSYREG", "QSYRTVAI", "QSYRTVSA", "QSYRTVSE", "QSYRTVUA", "QSYRUPWD", "QSYRUSRA", "QSYRUSRI", "QSYSUPWD", "QSYUSRIN", "QSYVLDL", "QSZCRTPD", "QSZCRTPL", "QSZPKGPO", "QSZRTVPR", "QSZSLTPR", "QSZSPTPR", "QTACJMA", "QTACTLDV", "QTAFROBJ", "QTARCGYL", "QTARCTGF", "QTARCTGI", "QTARDCAP", "QTARDINF", "QTARDSTS", "QTARJMA", "QTARTLBL", "QTASCTGF", "QTECRTVS", "QTEDBGS", "QTEDBGSI", "QTEDMPV", "QTERTVPV", "QTES", "QTHMCTLT", "QTMMSNDM", "QTMSCRTSNM", "QTNADDCR", "QTNCHGCO", "QTNRCMTI", "QTNXADTP", "QTOBUPDT", "QTOCC4IF", "QTOCCVTI", "QTOCLPPJ", "QTOCNETSTS", "QTOCPPPAPI", "QTOOSPF1", "QTOQMONAPI", "QTQICONV", "QTRXRLRL", "QTRXRLSA", "QTRXRLSL", "QTVOPNVT", "QTWAIDSP", "QTWCHKSP", "QUHRHLPT", "QUS", "QUSADDUI", "QUSCUSAT", "QUSEC", "QUSGEN", "QUSLFLD", "QUSLJOB", "QUSLMBR", "QUSLOBJ", "QUSLRCD", "QUSLSPL", "QUSREG", "QUSRJOBI", "QUSRMBRD", "QUSROBJD", "QUSRSPLA", "QUSRUIAT", "QUSRUSAT", "QVOIRCLD", "QVOIRCLG", "QVTRMSTG", "QWCADJTM", "QWCATTR", "QWCCHGJP", "QWCCHGPL", "QWCCHGTN", "QWCCVTDT", "QWCJBITP", "QWCJRNL", "QWCLASBS", "QWCLOBJL", "QWCLSCDE", "QWCOLTHD", "QWCRCLSI", "QWCRDTAA", "QWCRIPLA", "QWCRJBLK", "QWCRLCKI", "QWCRLRQI", "QWCRNETA", "QWCRSSTS", "QWCRSVAL", "QWCRTVCA", "QWCRTVTM", "QWCRTVTZ", "QWDCSBSE", "QWDLSBSE", "QWDLSJBQ", "QWDRJOBD", "QWDRSBSD", "QWPZ", "QWPZTAFP", "QWSRTVOI", "QWTCHGJB", "QWTRMVJL", "QWTRTVPX", "QWTRTVTA", "QWTSETPX", "QWVOLACT", "QWVOLAGP", "QWVRCSTK", "QXDADBBK", "QXDAEDRS", "QYASPOL", "QYASRDI", "QYASRDMS", "QYASRTVDDD", "QYASSDMO", "QYCDCUSG", "QYCDRCUI", "QYCUCERTI", "QYDOCOMMON", "QYDORTVR", "QYPERPEX", "QYPSCOLL", "QYPSSRVS", "QZCACLT", "QZD", "QZDMMDTA", "QZIPUTIL", "QZLS", "QZLSCHSI", "QZLSLSTI", "QZLSOLST", "QZMF", "QZMFASRV", "QZNFNFSO", "QZNFRTVE", "SCHED", "SIGNAL", "SQL", "SQLCLI", "SQLENV", "SQLFP", "SQLSCDS", "SQLUDF", "SYSIPC", "SYSSEM", "SYSSTAT", "SYSTYPES", "TIME", "TRGBUF", "UNISTD"]; + +export const FilterSuite: TestSuite = { + name: `Filter API tests`, + tests: [ + { + name: `Simple 'ends with'`, test: async () => { + const filter = parseFilter("*cmd", 'simple'); + const filtered = QSYSINCS.filter(t => filter.test(t)); + assert.strictEqual(filtered.length, 3); + assert.strictEqual(filtered.filter(t => t.endsWith("CMD")).length, filtered.length); + } + }, + { + name: `Simple 'starts with'`, test: async () => { + const filter = parseFilter("sql*", 'simple'); + const filtered = QSYSINCS.filter(t => filter.test(t)); + assert.strictEqual(filtered.length, 6); + assert.strictEqual(filtered.filter(t => t.startsWith("SQL")).length, filtered.length); + } + }, + { + name: `Simple 'contains'`, test: async () => { + const filter = parseFilter("*USR*", 'simple'); + const filtered = QSYSINCS.filter(t => filter.test(t)); + assert.strictEqual(filtered.length, 11); + assert.strictEqual(filtered.filter(t => t.includes("USR")).length, filtered.length); + } + }, + { + name: `Multiple simples`, test: async () => { + const filter = parseFilter("SQL*,*CMD,*USR*", 'simple'); + const filtered = QSYSINCS.filter(t => filter.test(t)); + assert.strictEqual(filtered.length, 20); + assert.strictEqual(filtered.filter(t => t.startsWith("SQL") || t.endsWith("CMD") || t.includes("USR")).length, filtered.length); + } + }, + { + name: `RegExp`, test: async () => { + const filter = parseFilter("^[^E].*CHG.*$", 'regex'); + const filtered = QSYSINCS.filter(t => filter.test(t)); + assert.strictEqual(filtered.length, 8); + assert.strictEqual(filtered.filter(t => !t.startsWith("E") && t.indexOf("CHG")).length, filtered.length); + } + }, + { + name: `Is case insensitive`, test: async () => { + const lowerCaseFilter = parseFilter("sql*", 'simple'); + const upperCaseFilter = parseFilter("SQL*", 'simple'); + const mixedCaseFilter = parseFilter("SqL*", 'simple'); + const lowerCaseFiltered = QSYSINCS.filter(t => lowerCaseFilter.test(t)) + const upperCaseFiltered = QSYSINCS.filter(t => upperCaseFilter.test(t)) + const mixedCaseFiltered = QSYSINCS.filter(t => mixedCaseFilter.test(t)) + + assert.strictEqual(lowerCaseFiltered.length, 6); + assert.strictEqual(upperCaseFiltered.length, 6); + assert.strictEqual(mixedCaseFiltered.length, 6); + + assert.strictEqual(upperCaseFiltered.every(t => lowerCaseFiltered.includes(t)), true); + assert.strictEqual(lowerCaseFiltered.every(t => mixedCaseFiltered.includes(t)), true); + } + }, + { + name: `Is relevant`, test: async () => { + const notAFilter = parseFilter("QSYSINC", 'simple'); + const notAFilterEither = parseFilter("QSYSINC", 'regex'); + const aFilter = parseFilter("*QSYS*", 'simple'); + + assert.strictEqual(notAFilter.noFilter, true); + assert.strictEqual(notAFilterEither.noFilter, true); + assert.strictEqual(aFilter.noFilter, false); + }, + }, + { + name: `Single generic name`, test: async () => { + const generic = singleGenericName("SQL*"); + const notGeneric = singleGenericName("*SQL"); + const notGenericEither = singleGenericName("SQL*,QSYS*"); + + assert.strictEqual(generic, "SQL*"); + assert.strictEqual(notGeneric, undefined); + assert.strictEqual(notGenericEither, undefined); + } + } + ] +}; diff --git a/src/testing/index.ts b/src/testing/index.ts index 2129da4b9..307913fdc 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -1,21 +1,17 @@ import { env } from "process"; import vscode from "vscode"; import { instance } from "../instantiate"; -import { ConnectionSuite } from "./connection"; import { ContentSuite } from "./content"; -import { DeployToolsSuite } from "./deployTools"; -import { ILEErrorSuite } from "./ileErrors"; import { TestSuitesTreeProvider } from "./testCasesTree"; -import { ToolsSuite } from "./tools"; -import { ActionSuite } from "./action"; const suites: TestSuite[] = [ - ActionSuite, - ConnectionSuite, + //ActionSuite, + //ConnectionSuite, ContentSuite, - DeployToolsSuite, - ToolsSuite, - ILEErrorSuite + //DeployToolsSuite, + //ToolsSuite, + //ILEErrorSuite, + //FilterSuite ] export type TestSuite = { From 14706b174350f3000604b4f8b26f2569e5505fba Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Wed, 24 Jan 2024 17:12:56 +0100 Subject: [PATCH 11/18] Added test for getQTempTable Signed-off-by: Seb Julliand --- src/testing/content.ts | 48 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/testing/content.ts b/src/testing/content.ts index efe31451c..00d3bfa79 100644 --- a/src/testing/content.ts +++ b/src/testing/content.ts @@ -437,7 +437,7 @@ export const ContentSuite: TestSuite = { { name: `getObjectList (advanced filtering)`, test: async () => { const content = instance.getContent(); - const objects = await content?.getObjectList({ library: `QSYSINC`, object:"L*OU*" }); + const objects = await content?.getObjectList({ library: `QSYSINC`, object: "L*OU*" }); assert.notStrictEqual(objects?.length, 0); assert.strictEqual(objects?.map(o => o.name).every(n => n.startsWith("L") && n.includes("OU")), true); @@ -517,5 +517,51 @@ export const ContentSuite: TestSuite = { assert.strictEqual(membersRegex!.map(m => m.name).every(n => n.startsWith('QSY') && !n.includes('RTV')), true); } }, + { + name: `Test getQtempTable`, test: async () => { + const content = instance.getContent(); + const config = instance.getConfig(); + + const queries = [ + `CALL QSYS2.QCMDEXC('DSPOBJD OBJ(QSYSINC/*ALL) OBJTYPE(*ALL) OUTPUT(*OUTFILE) OUTFILE(QTEMP/DSPOBJD)')`, + `Create Table QTEMP.OBJECTS As ( + Select ODLBNM as LIBRARY, + ODOBNM as NAME, + ODOBAT as ATTRIBUTE, + ODOBTP as TYPE, + Coalesce(ODOBTX, '') as TEXT + From QTEMP.DSPOBJD + ) With Data` + ]; + + const sqlEnabled = config?.enableSQL; + if(sqlEnabled){ + config.enableSQL = false; + } + + const nosqlContent = await content?.getQTempTable(queries, "OBJECTS"); + const objects = nosqlContent?.map(row => ({ + library: row.LIBRARY, + name: row.NAME, + attribute: row.ATTRIBUTE, + type: row.TYPE, + text: row.TEXT, + })); + + assert.notStrictEqual(objects?.length, 0); + assert.strictEqual(objects?.every(obj => obj.library === "QSYSINC"), true); + + const qrpglesrc = objects.find(obj => obj.name === "QRPGLESRC"); + assert.notStrictEqual(qrpglesrc, undefined); + assert.strictEqual(qrpglesrc?.attribute === "PF", true); + assert.strictEqual(qrpglesrc?.type === "*FILE", true); + + if(sqlEnabled){ + config.enableSQL = true; + const sqlContent = await content?.getQTempTable(queries, "OBJECTS"); + assert.deepStrictEqual(nosqlContent, sqlContent); + } + } + } ] }; From 8887227ca3bb471be72cacbd33a31a148cc08ebf Mon Sep 17 00:00:00 2001 From: Christian Jorgensen Date: Wed, 24 Jan 2024 21:50:41 +0100 Subject: [PATCH 12/18] Fix delimited SQL command string --- src/api/IBMiContent.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 65d6598f5..7261e6042 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -282,9 +282,9 @@ export default class IBMiContent { /** * Download the contents of a table. - * @param library - * @param file - * @param member Will default to file provided + * @param library + * @param file + * @param member Will default to file provided * @param deleteTable Will delete the table after download */ async getTable(library: string, file: string, member: string, deleteTable?: boolean): Promise { @@ -404,7 +404,7 @@ export default class IBMiContent { `; results = await this.runSQL(statement); } else { - results = await this.getQTempTable([`CALL QSYS2.QCMDEXC('DSPOBJD OBJ(QSYS/*ALL) OBJTYPE(*LIB) DETAIL(*TEXTATR) OUTPUT(*OUTFILE) OUTFILE(QTEMP/LIBLIST)`], "LIBLIST"); + results = await this.getQTempTable([`CALL QSYS2.QCMDEXC('DSPOBJD OBJ(QSYS/*ALL) OBJTYPE(*LIB) DETAIL(*TEXTATR) OUTPUT(*OUTFILE) OUTFILE(QTEMP/LIBLIST)')`], "LIBLIST"); if (results.length === 1 && !results[0].ODOBNM?.toString().trim()) { return []; } @@ -479,9 +479,9 @@ export default class IBMiContent { } /** - * @param filters + * @param filters * @param sortOrder - * @returns an array of IBMiFile + * @returns an array of IBMiFile */ async getObjectList(filters: { library: string; object?: string; types?: string[]; filterType?: FilterType }, sortOrder?: SortOrder): Promise { const library = filters.library.toUpperCase(); @@ -568,9 +568,9 @@ export default class IBMiContent { } /** - * + * * @param filter: the criterias used to list the members - * @returns + * @returns */ async getMemberList(filter: { library: string, sourceFile: string, members?: string, extensions?: string, sort?: SortOptions, filterType?: FilterType }): Promise { const sort = filter.sort || { order: 'name' }; @@ -579,7 +579,7 @@ export default class IBMiContent { const memberFilter = parseFilter(filter.members, filter.filterType); const singleMember = memberFilter.noFilter && filter.members && !filter.members.includes(",") ? filter.members.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; - + const memberExtensionFilter = parseFilter(filter.extensions, filter.filterType); const singleMemberExtension = memberExtensionFilter.noFilter && filter.extensions && !filter.extensions.includes(",") ? filter.extensions.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; @@ -640,7 +640,7 @@ export default class IBMiContent { /** * Get list of items in a path - * @param remotePath + * @param remotePath * @return an array of IFSFile */ async getFileList(remotePath: string, sort: SortOptions = { order: "name" }, onListError?: (errors: string[]) => void): Promise { @@ -850,7 +850,7 @@ export default class IBMiContent { /** * Return `true` if `remotePath` denotes a directory - * + * * @param remotePath: a remote IFS path */ async isDirectory(remotePath: string) { @@ -874,7 +874,7 @@ export default class IBMiContent { if (path.startsWith('/')) { //IFS path return this.config.protectedPaths.some(p => path.startsWith(p)); } - else { //QSYS path + else { //QSYS path const qsysObject = Tools.parseQSysPath(path); return this.config.protectedPaths.includes(qsysObject.library.toLocaleUpperCase()); } From 61e6ba0bb32a5608fd8896c5213cb9e716c507bc Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 25 Jan 2024 11:21:16 +0100 Subject: [PATCH 13/18] Do not try to generate a regexp for '*' filters Signed-off-by: Seb Julliand --- src/api/Filter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Filter.ts b/src/api/Filter.ts index 4daea76fa..a358c8478 100644 --- a/src/api/Filter.ts +++ b/src/api/Filter.ts @@ -14,7 +14,7 @@ export function parseFilter(filterString?: string, type?: FilterType): Filter { if (filterString) { switch (type) { case 'regex': - if (!/^\^?\.\*\$?$/.test(filterString) && escapeStringRegexp(filterString).indexOf("\\") > -1) { //regexp must not be relevant: not '.*' and an actual regexp (nothing escaped when escaping -> not a regexp) + if (!/^\^?\.?\*\$?$/.test(filterString) && escapeStringRegexp(filterString).indexOf("\\") > -1) { //regexp must not be relevant: not '.*' and an actual regexp (nothing escaped when escaping -> not a regexp) predicates.push(toRegexp(filterString)); } break; From 673e6fbc737cb79b5e58a34b25d8003e828a9e2e Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 25 Jan 2024 11:21:37 +0100 Subject: [PATCH 14/18] Fixed regexp filtering and members extension filtering Signed-off-by: Seb Julliand --- src/api/IBMiContent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index 7261e6042..e005cd95d 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -489,7 +489,7 @@ export default class IBMiContent { throw new Error(`Library ${library} does not exist.`); } - const singleEntry = singleGenericName(filters.object); + const singleEntry = filters.filterType !== 'regex' ? singleGenericName(filters.object) : undefined; const nameFilter = parseFilter(filters.object, filters.filterType); const object = filters.object && (nameFilter.noFilter || singleEntry) && filters.object !== `*` ? filters.object.toUpperCase() : `*ALL`; @@ -631,7 +631,7 @@ export default class IBMiContent { changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) } as IBMiMember)) .filter(member => memberFilter.test(member.name)) - .filter(member => memberExtensionFilter.test(member.name)); + .filter(member => memberExtensionFilter.test(member.extension)); } else{ return []; From 86f7117ee717066cbe2edd06c0efedecc14697d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Julliand?= Date: Thu, 25 Jan 2024 13:19:51 +0100 Subject: [PATCH 15/18] Clarified filter usage on object types Co-authored-by: Christian Jorgensen --- src/webviews/filters/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 39f125047..5cacbf10e 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -46,7 +46,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, .addSelect(`filterType`, `Filtering type`, [ { value: 'simple', description: 'Simple', text: `A comma-separated list of multi-generic values. Examples: *, Q* or *CL*SRC*. A single *, *ALL or blank will return everything.`, selected: filter.filterType === "simple" }, { value: 'regex', description: 'Regex', text: `Use a single RegEx for filtering.`, selected: filter.filterType === "regex" } - ], `Select the filtering strategy to apply for filtering names.
Checkout https://regex101.com to get started with RegExs.`) + ], `Select the filtering strategy to apply for filtering names (not object types).
Checkout https://regex101.com to get started with RegExs.`) .addInput(`library`, `Libraries`, `Library names filter.`, { default: filter.library }) .addInput(`object`, `Objects`, `Object names filter.`, { default: filter.object }) .addInput(`types`, `Object types`, `A comma delimited list of object types. For example *ALL, or *PGM, *SRVPGM. *SRCPF is a special type which will return only source files.`, { default: filter.types.join(`, `) }) From 4145606777b872afc46ff5ed4f9101df14d5ac1a Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 25 Jan 2024 13:24:32 +0100 Subject: [PATCH 16/18] Do not uppercase object filter when using Regex Signed-off-by: Seb Julliand --- src/webviews/filters/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webviews/filters/index.ts b/src/webviews/filters/index.ts index 5cacbf10e..422abcaa3 100644 --- a/src/webviews/filters/index.ts +++ b/src/webviews/filters/index.ts @@ -61,7 +61,8 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, const data = page.data; for (const key in data) { - + const useRegexFilters = data.filterType === "regex"; + //In case we need to play with the data switch (key) { case `name`: @@ -75,7 +76,7 @@ export async function editFilter(filter?: ConnectionConfiguration.ObjectFilters, case `object`: data[key] = (String(data[key].trim()) || `*`) .split(',') - .map(o => o.trim().toLocaleUpperCase()) + .map(o => useRegexFilters ? o : o.toLocaleUpperCase()) .filter(Tools.distinct) .join(","); break; From df3a360572ce9181321a2c287a97c168f60bc435 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Thu, 25 Jan 2024 18:09:00 +0100 Subject: [PATCH 17/18] Put back all the tests because we love them Signed-off-by: Seb Julliand --- src/testing/index.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/testing/index.ts b/src/testing/index.ts index 307913fdc..862219b04 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -1,17 +1,23 @@ import { env } from "process"; import vscode from "vscode"; import { instance } from "../instantiate"; +import { ActionSuite } from "./action"; +import { ConnectionSuite } from "./connection"; import { ContentSuite } from "./content"; +import { DeployToolsSuite } from "./deployTools"; +import { FilterSuite } from "./filter"; +import { ILEErrorSuite } from "./ileErrors"; import { TestSuitesTreeProvider } from "./testCasesTree"; +import { ToolsSuite } from "./tools"; const suites: TestSuite[] = [ - //ActionSuite, - //ConnectionSuite, + ActionSuite, + ConnectionSuite, ContentSuite, - //DeployToolsSuite, - //ToolsSuite, - //ILEErrorSuite, - //FilterSuite + DeployToolsSuite, + ToolsSuite, + ILEErrorSuite, + FilterSuite ] export type TestSuite = { From 2e3e23d3b9cc8c158346fe301f609ccc05ccd9a2 Mon Sep 17 00:00:00 2001 From: Seb Julliand Date: Mon, 29 Jan 2024 16:26:41 +0100 Subject: [PATCH 18/18] Always use SQL to read from QTEMP Signed-off-by: Seb Julliand --- src/api/IBMiContent.ts | 10 ++-------- src/testing/content.ts | 11 ----------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index e005cd95d..9bbed0cbc 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -347,14 +347,8 @@ export default class IBMiContent { * @returns : the table's content */ async getQTempTable(prepareQueries: string[], table: string): Promise { - let temporaryFile: string | undefined; - if (this.config.enableSQL) { - prepareQueries.push(`Select * From QTEMP.${table}`); - } - else { - temporaryFile = this.getTempRemote(table); - prepareQueries.push(`CALL QSYS2.QCMDEXC('QSYS/CPYTOIMPF FROMFILE(QTEMP/${table}) TOSTMF(''${temporaryFile}'') MBROPT(*REPLACE) STMFCCSID(1208) RCDDLM(*CRLF) DTAFMT(*DLM) RMVBLANK(*TRAILING) ADDCOLNAM(*SQL) FLDDLM('','') DECPNT(*PERIOD)')`); - } + let temporaryFile: string | undefined; + prepareQueries.push(`Select * From QTEMP.${table}`); try { const fullQuery = prepareQueries.map(query => query.endsWith(';') ? query : `${query};`).join("\n"); diff --git a/src/testing/content.ts b/src/testing/content.ts index 00d3bfa79..ab70fadda 100644 --- a/src/testing/content.ts +++ b/src/testing/content.ts @@ -520,7 +520,6 @@ export const ContentSuite: TestSuite = { { name: `Test getQtempTable`, test: async () => { const content = instance.getContent(); - const config = instance.getConfig(); const queries = [ `CALL QSYS2.QCMDEXC('DSPOBJD OBJ(QSYSINC/*ALL) OBJTYPE(*ALL) OUTPUT(*OUTFILE) OUTFILE(QTEMP/DSPOBJD)')`, @@ -534,10 +533,6 @@ export const ContentSuite: TestSuite = { ) With Data` ]; - const sqlEnabled = config?.enableSQL; - if(sqlEnabled){ - config.enableSQL = false; - } const nosqlContent = await content?.getQTempTable(queries, "OBJECTS"); const objects = nosqlContent?.map(row => ({ @@ -555,12 +550,6 @@ export const ContentSuite: TestSuite = { assert.notStrictEqual(qrpglesrc, undefined); assert.strictEqual(qrpglesrc?.attribute === "PF", true); assert.strictEqual(qrpglesrc?.type === "*FILE", true); - - if(sqlEnabled){ - config.enableSQL = true; - const sqlContent = await content?.getQTempTable(queries, "OBJECTS"); - assert.deepStrictEqual(nosqlContent, sqlContent); - } } } ]