-
Notifications
You must be signed in to change notification settings - Fork 15
W-16448626 feat: sosl #1025
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
W-16448626 feat: sosl #1025
Changes from all commits
faf71a3
f78946a
d091736
13b8194
c0f1314
f6223f4
292192f
100d4a9
dc2e427
f5bcf4f
e1e714c
1744039
b64a10e
fd24a49
0f4649f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # summary | ||
|
|
||
| Execute a SOSL text-based search query. | ||
|
|
||
| # description | ||
|
|
||
| Specify the SOSL query at the command line with the --query flag or read the query from a file with the --file flag. | ||
|
|
||
| By default, the results are written to the terminal in human-readable format. If you specify `--result-format csv`, the output is written to one or more CSV (comma-separated values) files. The file names correspond to the Salesforce objects in the results, such as Account.csv. Both `--result-format human` and `--result-format json` display only to the terminal. | ||
|
|
||
| # examples | ||
|
|
||
| - Specify a SOSL query at the command line; the command uses your default org: | ||
|
|
||
| <%= config.bin %> <%= command.id %> --query "FIND {Anna Jones} IN Name Fields RETURNING Contact (Name, Phone)" | ||
|
|
||
| - Read the SOSL query from a file called "query.txt"; the command uses the org with alias "my-scratch": | ||
|
|
||
| <%= config.bin %> <%= command.id %> --file query.txt --target-org my-scratch | ||
|
|
||
| - Similar to the previous example, but write the results to one or more CSV files, depending on the Salesforce objects in the results: | ||
|
|
||
| <%= config.bin %> <%= command.id %> --file query.txt --target-org my-scratch --result-format csv | ||
|
|
||
| # flags.query.summary | ||
|
|
||
| SOSL query to execute. | ||
|
|
||
| # flags.result-format.summary | ||
|
|
||
| Format to display the results, or to write to disk if you specify "csv". | ||
|
|
||
| # flags.file.summary | ||
|
|
||
| File that contains the SOSL query. | ||
|
|
||
| # displayQueryRecordsRetrieved | ||
|
|
||
| Total number of records retrieved: %s. | ||
|
|
||
| # queryRunningMessage | ||
|
|
||
| Querying Data |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * Copyright (c) 2020, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
|
|
||
| import fs from 'node:fs'; | ||
| import { Messages } from '@salesforce/core'; | ||
| import type { SearchResult } from '@jsforce/jsforce-node'; | ||
| import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; | ||
| import { orgFlags, resultFormatFlag } from '../../flags.js'; | ||
| import { displaySearchResults } from '../../searchUtils.js'; | ||
|
|
||
| Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); | ||
| const messages = Messages.loadMessages('@salesforce/plugin-data', 'data.search'); | ||
|
|
||
| export class DataSearchCommand extends SfCommand<SearchResult> { | ||
| public static readonly summary = messages.getMessage('summary'); | ||
| public static readonly description = messages.getMessage('description'); | ||
| public static readonly examples = messages.getMessages('examples'); | ||
|
|
||
| public static readonly flags = { | ||
| ...orgFlags, | ||
| query: Flags.string({ | ||
| char: 'q', | ||
| summary: messages.getMessage('flags.query.summary'), | ||
| exactlyOne: ['query', 'file'], | ||
| }), | ||
| file: Flags.file({ | ||
| char: 'f', | ||
| exists: true, | ||
| summary: messages.getMessage('flags.file.summary'), | ||
| exactlyOne: ['query', 'file'], | ||
| }), | ||
| 'result-format': resultFormatFlag({ | ||
| summary: messages.getMessage('flags.result-format.summary'), | ||
| exclusive: ['json'], | ||
| }), | ||
| }; | ||
|
|
||
| public async run(): Promise<SearchResult> { | ||
| const flags = (await this.parse(DataSearchCommand)).flags; | ||
|
|
||
| try { | ||
| // --file will be present if flags.query isn't. Oclif exactlyOne isn't quite that clever | ||
| const queryString = flags.query ?? fs.readFileSync(flags.file as string, 'utf8'); | ||
| const conn = flags['target-org'].getConnection(flags['api-version']); | ||
| if (flags['result-format'] !== 'json') this.spinner.start(messages.getMessage('queryRunningMessage')); | ||
| const queryResult = await conn.search(queryString); | ||
| if (!this.jsonEnabled()) { | ||
| displaySearchResults(queryResult, flags['result-format']); | ||
| } | ||
| return queryResult; | ||
| } finally { | ||
| if (flags['result-format'] !== 'json') this.spinner.stop(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /* | ||
| * Copyright (c) 2023, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import os from 'node:os'; | ||
| import fs from 'node:fs'; | ||
| import { SearchResult } from '@jsforce/jsforce-node'; | ||
| import { SearchReporter } from './reporter.js'; | ||
|
|
||
| export class CsvSearchReporter extends SearchReporter { | ||
| public constructor(props: SearchResult) { | ||
| super(props); | ||
| } | ||
|
|
||
| public display(): void { | ||
| if (this.typeRecordsMap.size === 0) { | ||
| this.ux.log('No Records Found'); | ||
| } | ||
| this.typeRecordsMap.forEach((records, type) => { | ||
| this.ux.log(`Written to ${type}.csv`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't the empty string be really bad here? |
||
| const cols = Object.keys(records[0]).join(','); | ||
| const body = records.map((r) => Object.values(r).join(',')).join(os.EOL); | ||
|
|
||
| fs.writeFileSync(`${type}.csv`, [cols, body].join(os.EOL)); | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * Copyright (c) 2023, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import { SearchResult } from '@jsforce/jsforce-node'; | ||
| import { SearchReporter } from './reporter.js'; | ||
|
|
||
| export class HumanSearchReporter extends SearchReporter { | ||
| public constructor(props: SearchResult) { | ||
| super(props); | ||
| } | ||
|
|
||
| public display(): void { | ||
| if (this.typeRecordsMap.size === 0) { | ||
| this.ux.log('No Records Found'); | ||
| } | ||
| this.typeRecordsMap.forEach((records, type) => { | ||
| // to find the columns of the query, parse the keys of the first record | ||
| this.ux.table(records, Object.fromEntries(Object.keys(records[0]).map((k) => [k, { header: k }])), { | ||
| 'no-truncate': true, | ||
| title: type, | ||
| }); | ||
| this.ux.log(); | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* | ||
| * Copyright (c) 2023, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import { Ux } from '@salesforce/sf-plugins-core'; | ||
| import { Record, SearchResult } from '@jsforce/jsforce-node'; | ||
| import { ensureString } from '@salesforce/ts-types'; | ||
| import { omit } from '@salesforce/kit'; | ||
|
|
||
| export abstract class SearchReporter { | ||
| public typeRecordsMap: Map<string, Record[]> = new Map<string, Record[]>(); | ||
| public ux = new Ux(); | ||
| protected constructor(public result: SearchResult) { | ||
| this.result.searchRecords.map((r) => { | ||
| const type = ensureString(r.attributes?.type); | ||
| return this.typeRecordsMap.has(type) | ||
| ? // the extra info in 'attributes' causes issues when creating generic csv/table columns | ||
| this.typeRecordsMap.get(type)!.push(omit(r, 'attributes')) | ||
| : this.typeRecordsMap.set(type, [omit(r, 'attributes')]); | ||
| }); | ||
| } | ||
| public abstract display(): void; | ||
| } | ||
|
|
||
| export class JsonSearchReporter extends SearchReporter { | ||
| public constructor(props: SearchResult) { | ||
| super(props); | ||
| } | ||
|
|
||
| public display(): void { | ||
| this.ux.styledJSON(this.result); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /* | ||
| * Copyright (c) 2023, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import type { SearchResult } from '@jsforce/jsforce-node'; | ||
| import { HumanSearchReporter } from './reporters/search/humanSearchReporter.js'; | ||
| import { JsonSearchReporter } from './reporters/search/reporter.js'; | ||
| import { CsvSearchReporter } from './reporters/search/csvSearchReporter.js'; | ||
|
|
||
| export const displaySearchResults = (queryResult: SearchResult, resultFormat: 'human' | 'json' | 'csv'): void => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you wanted to do this without classes, this would be a function that decides which reporter function(ux, results) to call. then you wouldn't need all the |
||
| let reporter: HumanSearchReporter | JsonSearchReporter | CsvSearchReporter; | ||
|
|
||
| switch (resultFormat) { | ||
| case 'human': | ||
| reporter = new HumanSearchReporter(queryResult); | ||
| break; | ||
| case 'csv': | ||
| reporter = new CsvSearchReporter(queryResult); | ||
| break; | ||
| case 'json': | ||
| reporter = new JsonSearchReporter(queryResult); | ||
| break; | ||
| } | ||
| // delegate to selected reporter | ||
| reporter.display(); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it the result format or the json-enabled-ness that matters for spinners?
ex: Imagine I want
--result-format csv --jsonbecause I'm a CI script. I want to get that 0/1 and file location as json output BUT I want a csv file written. I wouldn't want a spinner.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is carried from
data query, butthis.spinnershould be respecting the--jsonflag