diff --git a/packages/compass-components/src/components/interactive-popover.spec.tsx b/packages/compass-components/src/components/interactive-popover.spec.tsx index 5d554feb294..5613fb4b2b2 100644 --- a/packages/compass-components/src/components/interactive-popover.spec.tsx +++ b/packages/compass-components/src/components/interactive-popover.spec.tsx @@ -14,6 +14,8 @@ function renderPopover( {}} trigger={({ onClick, ref, children }) => ( <> @@ -29,6 +31,7 @@ function renderPopover( + ); + }} + align="top" + hideCloseButton={true} + customFocusTrapFallback={'#inline-save-query-modal-cancel-button'} + open={open} + setOpen={setOpen} + > +
+ + + +
+
+ ); +}; + export type BulkUpdateDialogProps = { isOpen: boolean; ns: string; @@ -105,6 +236,7 @@ export type BulkUpdateDialogProps = { closeBulkUpdateDialog: () => void; updateBulkUpdatePreview: (updateText: string) => void; runBulkUpdate: () => void; + saveUpdateQuery: (name: string) => void; }; export default function BulkUpdateDialog({ @@ -119,6 +251,7 @@ export default function BulkUpdateDialog({ closeBulkUpdateDialog, updateBulkUpdatePreview, runBulkUpdate, + saveUpdateQuery, }: BulkUpdateDialogProps) { const darkMode = useDarkMode(); @@ -136,9 +269,6 @@ export default function BulkUpdateDialog({ updateBulkUpdatePreview(value); }; - const title = - count === undefined ? 'Update documents' : `Update documents (${count})`; - const annotations = useMemo(() => { if (syntaxError?.loc?.index) { return [ @@ -164,92 +294,102 @@ export default function BulkUpdateDialog({ }, [isOpen, wasOpen, updateText]); return ( - -
-
-
- -
- -
- - - - Learn more about Update syntax - - - - ({})} - annotations={annotations} + + + +
+
+
+ +
-
- {syntaxError && ( - - {syntaxError.message} - - )} - {serverError && !syntaxError && ( - - {serverError.message} - +
+ + + + Learn more about Update syntax + + + - -
-
-
- -
- {previewDocuments.map((doc: HadronDocument, index: number) => { - return ( - + ({})} + annotations={annotations} /> - ); - })} + +
+ {syntaxError && ( + + {syntaxError.message} + + )} + {serverError && !syntaxError && ( + + {serverError.message} + + )} +
+ +
+
+
+ +
+ {previewDocuments.map((doc: HadronDocument, index: number) => { + return ( + + ); + })} +
-
- +
+ +
+ +
+
+ + +
+
+
); } diff --git a/packages/compass-crud/src/components/document-list.tsx b/packages/compass-crud/src/components/document-list.tsx index d2b69fe0920..df512cadbda 100644 --- a/packages/compass-crud/src/components/document-list.tsx +++ b/packages/compass-crud/src/components/document-list.tsx @@ -77,6 +77,7 @@ export type DocumentListProps = { openBulkUpdateDialog: () => void; updateBulkUpdatePreview: (updateText: string) => void; runBulkUpdate: () => void; + saveUpdateQuery: (name: string) => void; openImportFileDialog?: (origin: 'empty-state' | 'crud-toolbar') => void; docs: Document[]; view: DocumentView; @@ -264,6 +265,10 @@ class DocumentList extends React.Component { } } + onSaveUpdateQuery(name: string) { + void this.props.store.saveUpdateQuery(name); + } + renderBulkUpdateModal() { if (!this.props.isEditable) { return; @@ -278,6 +283,7 @@ class DocumentList extends React.Component { closeBulkUpdateDialog={this.props.closeBulkUpdateDialog} updateBulkUpdatePreview={this.props.updateBulkUpdatePreview} runBulkUpdate={this.props.runBulkUpdate} + saveUpdateQuery={this.onSaveUpdateQuery.bind(this)} /> ); } @@ -533,6 +539,7 @@ DocumentList.propTypes = { openBulkUpdateDialog: PropTypes.func, updateBulkUpdatePreview: PropTypes.func, runBulkUpdate: PropTypes.func, + saveUpdateQuery: PropTypes.func, openImportFileDialog: PropTypes.func, openExportFileDialog: PropTypes.func, refreshDocuments: PropTypes.func, diff --git a/packages/compass-crud/src/stores/crud-store.spec.ts b/packages/compass-crud/src/stores/crud-store.spec.ts index aa076ebbe47..8f5fa6cbb5b 100644 --- a/packages/compass-crud/src/stores/crud-store.spec.ts +++ b/packages/compass-crud/src/stores/crud-store.spec.ts @@ -15,6 +15,7 @@ import configureStore, { import configureActions from '../actions'; import { Int32 } from 'bson'; import { mochaTestServer } from '@mongodb-js/compass-test-server'; +import { FavoriteQueryStorage } from '@mongodb-js/my-queries-storage'; chai.use(chaiAsPromised); @@ -893,7 +894,7 @@ describe('store', function () { previewAbortController: undefined, serverError: undefined, syntaxError: undefined, - updateText: '{ $set: { }}', + updateText: '{ $set: { } }', }); }); @@ -928,7 +929,7 @@ describe('store', function () { previewAbortController: undefined, serverError: undefined, syntaxError: undefined, - updateText: '{ $set: { }}', + updateText: '{ $set: { } }', }); }); }); @@ -2590,4 +2591,48 @@ describe('store', function () { } }); }); + + describe('saveUpdateQuery', function () { + const favoriteQueriesStorage: FavoriteQueryStorage = + new FavoriteQueryStorage(); + + let saveQueryStub; + let actions; + let store; + + beforeEach(function () { + saveQueryStub = sinon.stub().resolves(); + favoriteQueriesStorage.saveQuery = saveQueryStub; + + actions = configureActions(); + store = configureStore({ + localAppRegistry: localAppRegistry, + globalAppRegistry: globalAppRegistry, + dataProvider: { + dataProvider: dataService, + }, + actions: actions, + namespace: 'compass-crud.testview', + noRefreshOnConfigure: true, + favoriteQueriesStorage: favoriteQueriesStorage, + }); + }); + + it('should save the query once is submitted to save', async function () { + await store.onQueryChanged({ filter: { field: 1 } }); + await store.updateBulkUpdatePreview('{ $set: { anotherField: 2 } }'); + await store.saveUpdateQuery('my-query'); + + expect(saveQueryStub).to.have.been.calledWith({ + _name: 'my-query', + _ns: 'compass-crud.testview', + filter: { + field: 1, + }, + update: { + $set: { anotherField: 2 }, + }, + }); + }); + }); }); diff --git a/packages/compass-crud/src/stores/crud-store.ts b/packages/compass-crud/src/stores/crud-store.ts index e887186b244..96727dd6e8e 100644 --- a/packages/compass-crud/src/stores/crud-store.ts +++ b/packages/compass-crud/src/stores/crud-store.ts @@ -13,6 +13,7 @@ import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import { capMaxTimeMSAtPreferenceLimit } from 'compass-preferences-model'; import type { Stage } from '@mongodb-js/explain-plan-helper'; import { ExplainPlan } from '@mongodb-js/explain-plan-helper'; +import { FavoriteQueryStorage } from '@mongodb-js/my-queries-storage'; import { countDocuments, @@ -65,6 +66,7 @@ export type CrudActions = { closeBulkDeleteDialog(): void; runBulkDelete(): void; openDeleteQueryExportToLanguageDialog(): void; + saveUpdateQuery(name: string): Promise; }; export type DocumentView = 'List' | 'JSON' | 'Table'; @@ -331,6 +333,7 @@ type CrudStoreOptions = { dataProvider: { error?: Error; dataProvider?: DataService }; noRefreshOnConfigure?: boolean; isSearchIndexesSupported: boolean; + favoriteQueriesStorage?: FavoriteQueryStorage; }; export type InsertCSFLEState = { @@ -438,10 +441,13 @@ class CrudStoreImpl dataService!: DataService; localAppRegistry!: AppRegistry; globalAppRegistry!: AppRegistry; + favoriteQueriesStorage: FavoriteQueryStorage; constructor(options: CrudStoreOptions) { super(options); this.listenables = options.actions as any; // TODO: The types genuinely mismatch here + this.favoriteQueriesStorage = + options.favoriteQueriesStorage || new FavoriteQueryStorage(); } updateFields(fields: { autocompleteFields: { name: string }[] }) { @@ -1094,7 +1100,7 @@ class CrudStoreImpl } async openBulkUpdateDialog() { - await this.updateBulkUpdatePreview('{ $set: { }}'); + await this.updateBulkUpdatePreview('{ $set: { } }'); this.setState({ bulkUpdate: { ...this.state.bulkUpdate, @@ -1913,6 +1919,33 @@ class CrudStoreImpl 'Delete Query' ); } + + async saveUpdateQuery(name: string): Promise { + const { filter } = this.state.query; + let update; + try { + update = parseShellBSON(this.state.bulkUpdate.updateText); + } catch (err) { + // If this couldn't parse then the update button should have been + // disabled. So if we get here it is a race condition and ignoring is + // probably OK - the button will soon appear disabled to the user anyway. + return; + } + + await this.favoriteQueriesStorage.saveQuery({ + _name: name, + _ns: this.state.ns, + filter, + update, + }); + openToast('saved-favorite-update-query', { + title: '', + variant: 'success', + dismissible: true, + timeout: 6_000, + description: `${name} added to "My Queries".`, + }); + } } export type CrudStore = Store & CrudStoreImpl & { gridStore: GridStore }; @@ -1937,6 +1970,18 @@ const configureStore = (options: CrudStoreOptions & GridStoreOptions) => { localAppRegistry.on('fields-changed', store.updateFields.bind(store)); + localAppRegistry.on( + 'favorites-open-bulk-update-favorite', + (query: QueryState & { update: BSONObject }) => { + void store.onQueryChanged(query); + void store.refreshDocuments(); + void store.openBulkUpdateDialog(); + void store.updateBulkUpdatePreview( + toJSString(query.update) || '{ $set: { } }' + ); + } + ); + setLocalAppRegistry(store, options.localAppRegistry); } diff --git a/packages/compass-query-bar/src/components/query-history-button-popover.tsx b/packages/compass-query-bar/src/components/query-history-button-popover.tsx index 7ddadcdcb8e..f07fcf463f3 100644 --- a/packages/compass-query-bar/src/components/query-history-button-popover.tsx +++ b/packages/compass-query-bar/src/components/query-history-button-popover.tsx @@ -64,6 +64,10 @@ const QueryHistoryButtonPopover = ({ [onOpenPopover] ); + const closePopover = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + return ( - + ); }; diff --git a/packages/compass-query-bar/src/components/query-history/favorite-list.tsx b/packages/compass-query-bar/src/components/query-history/favorite-list.tsx index d4c17a69c10..ddfec05d46f 100644 --- a/packages/compass-query-bar/src/components/query-history/favorite-list.tsx +++ b/packages/compass-query-bar/src/components/query-history/favorite-list.tsx @@ -17,28 +17,37 @@ import { formatQuery, copyToClipboard, getQueryAttributes } from '../../utils'; import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging'; import type { BaseQuery } from '../../constants/query-properties'; import type { RootState } from '../../stores/query-bar-store'; +import { OpenBulkUpdateActionButton } from './query-item/query-item-action-buttons'; const { track } = createLoggerAndTelemetry('COMPASS-QUERY-BAR-UI'); type FavoriteActions = { onApply: (query: BaseQuery) => void; onDelete: (id: string) => void; + onUpdateFavoriteChoosen: () => void; }; const FavoriteItem = ({ query, onApply, onDelete, + onUpdateFavoriteChoosen, }: FavoriteActions & { query: FavoriteQuery; }) => { + const isUpdateQuery = useMemo(() => !!query.update, [query]); const attributes = useMemo(() => getQueryAttributes(query), [query]); const onCardClick = useCallback(() => { track('Query History Favorite Used', { id: query._id, screen: 'documents', }); + + if (isUpdateQuery) { + onUpdateFavoriteChoosen(); + } + onApply(attributes); - }, [onApply, query._id, attributes]); + }, [onApply, onUpdateFavoriteChoosen, query._id, attributes, isUpdateQuery]); const onDeleteClick = useCallback(() => { track('Query History Favorite Removed', { @@ -54,11 +63,16 @@ const FavoriteItem = ({ onClick={onCardClick} data-testid="favorite-query-list-item" header={(isHovered: boolean) => ( - - copyToClipboard(formatQuery(attributes))} - /> - + + {isHovered && ( + copyToClipboard(formatQuery(attributes))} + /> + )} + {isHovered && } + {isUpdateQuery && ( + + )} )} > @@ -71,6 +85,7 @@ const FavoriteList = ({ queries, onApply, onDelete, + onUpdateFavoriteChoosen, }: FavoriteActions & { queries: FavoriteQuery[]; }) => { @@ -83,6 +98,7 @@ const FavoriteList = ({ query={query} onApply={onApply} onDelete={onDelete} + onUpdateFavoriteChoosen={onUpdateFavoriteChoosen} /> )); return <>{content}; diff --git a/packages/compass-query-bar/src/components/query-history/index.spec.tsx b/packages/compass-query-bar/src/components/query-history/index.spec.tsx index b5bec026992..8dc2896c9dc 100644 --- a/packages/compass-query-bar/src/components/query-history/index.spec.tsx +++ b/packages/compass-query-bar/src/components/query-history/index.spec.tsx @@ -74,7 +74,7 @@ const renderQueryHistory = (basepath: string) => { const data = createStore(basepath); render( - + {}} /> ); return data; diff --git a/packages/compass-query-bar/src/components/query-history/index.tsx b/packages/compass-query-bar/src/components/query-history/index.tsx index d89304f16fd..bfa1dd21e7c 100644 --- a/packages/compass-query-bar/src/components/query-history/index.tsx +++ b/packages/compass-query-bar/src/components/query-history/index.tsx @@ -27,9 +27,13 @@ export type QueryHistoryTab = 'recent' | 'favorite'; type QueryHistoryProps = { namespace: string; + onUpdateFavoriteChoosen: () => void; }; -const QueryHistory = ({ namespace }: QueryHistoryProps) => { +const QueryHistory = ({ + namespace, + onUpdateFavoriteChoosen, +}: QueryHistoryProps) => { const [tab, setTab] = useState('recent'); useTrackOnChange( @@ -52,7 +56,9 @@ const QueryHistory = ({ namespace }: QueryHistoryProps) => { {tab === 'recent' && ( setTab('favorite')} /> )} - {tab === 'favorite' && } + {tab === 'favorite' && ( + + )}
); diff --git a/packages/compass-query-bar/src/components/query-history/query-item/query-item-action-buttons.tsx b/packages/compass-query-bar/src/components/query-history/query-item/query-item-action-buttons.tsx index a955ad4ac5b..f3613b3fa91 100644 --- a/packages/compass-query-bar/src/components/query-history/query-item/query-item-action-buttons.tsx +++ b/packages/compass-query-bar/src/components/query-history/query-item/query-item-action-buttons.tsx @@ -5,16 +5,19 @@ type ActionButtonProps = { onClick: () => void; }; +const justDelegateClick = + (onClick: () => void) => (event: React.MouseEvent) => { + event.stopPropagation(); + onClick(); + }; + export const FavoriteActionButton = ({ onClick }: ActionButtonProps) => { return ( { - event.stopPropagation(); - onClick(); - }} + onClick={justDelegateClick(onClick)} > @@ -27,10 +30,7 @@ export const CopyActionButton = ({ onClick }: ActionButtonProps) => { data-testid="query-history-button-copy-query" aria-label="Copy Query to Clipboard" title="Copy Query to Clipboard" - onClick={(event) => { - event.stopPropagation(); - onClick(); - }} + onClick={justDelegateClick(onClick)} > @@ -43,12 +43,22 @@ export const DeleteActionButton = ({ onClick }: ActionButtonProps) => { data-testid="query-history-button-delete-recent" aria-label="Delete Query from List" title="Delete Query from List" - onClick={(event) => { - event.stopPropagation(); - onClick(); - }} + onClick={justDelegateClick(onClick)} > ); }; + +export const OpenBulkUpdateActionButton = ({ onClick }: ActionButtonProps) => { + return ( + + + + ); +}; diff --git a/packages/compass-query-bar/src/components/query-option.tsx b/packages/compass-query-bar/src/components/query-option.tsx index a9a23f71ad8..921c3a44bd5 100644 --- a/packages/compass-query-bar/src/components/query-option.tsx +++ b/packages/compass-query-bar/src/components/query-option.tsx @@ -77,12 +77,14 @@ export const documentEditorLabelContainerStyles = css( } ); +type QueryBarProperty = Exclude; + type QueryOptionProps = { id: string; - name: QueryProperty; + name: QueryBarProperty; value?: string; hasError: boolean; - onChange: (name: QueryProperty, value: string) => void; + onChange: (name: QueryBarProperty, value: string) => void; placeholder?: string | HTMLElement; onApply?(): void; insights?: Signal | Signal[]; diff --git a/packages/compass-query-bar/src/constants/query-option-definition.ts b/packages/compass-query-bar/src/constants/query-option-definition.ts index 1816fccbf10..442c3f65424 100644 --- a/packages/compass-query-bar/src/constants/query-option-definition.ts +++ b/packages/compass-query-bar/src/constants/query-option-definition.ts @@ -4,7 +4,7 @@ import React from 'react'; import { usePreference } from 'compass-preferences-model'; import type { QueryProperty } from './query-properties'; -export type QueryOption = QueryProperty; +export type QueryOption = Exclude; export const OPTION_DEFINITION: { [optionName in QueryOption]: { diff --git a/packages/compass-query-bar/src/stores/query-bar-reducer.ts b/packages/compass-query-bar/src/stores/query-bar-reducer.ts index ed62b72bbeb..d971b736d9d 100644 --- a/packages/compass-query-bar/src/stores/query-bar-reducer.ts +++ b/packages/compass-query-bar/src/stores/query-bar-reducer.ts @@ -214,8 +214,19 @@ type ApplyFromHistoryAction = { query: BaseQuery; }; -export const applyFromHistory = (query: BaseQuery): ApplyFromHistoryAction => { - return { type: QueryBarActions.ApplyFromHistory, query }; +export const applyFromHistory = ( + query: BaseQuery & { update?: Document } +): QueryBarThunkAction => { + return (dispatch, getState, { localAppRegistry }) => { + dispatch({ + type: QueryBarActions.ApplyFromHistory, + query, + }); + + if (query.update) { + localAppRegistry?.emit('favorites-open-bulk-update-favorite', query); + } + }; }; type RecentQueriesFetchedAction = { diff --git a/packages/compass-query-bar/src/utils/get-query-attributes.spec.ts b/packages/compass-query-bar/src/utils/get-query-attributes.spec.ts index c2d1a042a96..a0096bd0146 100644 --- a/packages/compass-query-bar/src/utils/get-query-attributes.spec.ts +++ b/packages/compass-query-bar/src/utils/get-query-attributes.spec.ts @@ -26,6 +26,7 @@ describe('get-query-attributes', function () { sort: { name: 1 }, skip: 1, limit: 20, + update: { $set: { a: 1 } }, }) ).to.deep.equal({ filter: { @@ -34,6 +35,7 @@ describe('get-query-attributes', function () { sort: { name: 1 }, skip: 1, limit: 20, + update: { $set: { a: 1 } }, }); }); }); diff --git a/packages/compass-query-bar/src/utils/get-query-attributes.ts b/packages/compass-query-bar/src/utils/get-query-attributes.ts index a431c29cc78..515b5ee45ee 100644 --- a/packages/compass-query-bar/src/utils/get-query-attributes.ts +++ b/packages/compass-query-bar/src/utils/get-query-attributes.ts @@ -1,5 +1,5 @@ import { isEmpty, isObject } from 'lodash'; -import type { BaseQuery } from './../constants/query-properties'; +import type { FavoriteQuery } from '@mongodb-js/my-queries-storage'; export const getQueryAttributes = ({ filter, @@ -8,7 +8,8 @@ export const getQueryAttributes = ({ project, limit, skip, -}: Omit): Omit => { + update, +}: Partial): Partial => { const attributes = { filter, collation, @@ -16,6 +17,7 @@ export const getQueryAttributes = ({ project, limit, skip, + update, }; Object.keys(attributes).forEach((k) => { const key = k as keyof typeof attributes; diff --git a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx index 3e3d19efd39..bd8da27d017 100644 --- a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx +++ b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.test.tsx @@ -38,6 +38,26 @@ describe('SavedItemCard', function () { expect(screen.getByText('Last modified: now')).to.exist; }); + it('should render a card with an update query props', function () { + render( + {}} + lastModified={now} + > + ); + + expect(screen.getByText('My Awesome Update Query')).to.exist; + expect(screen.getByText('.updatemany')).to.exist; + expect(screen.getByText('sample_airbnb')).to.exist; + expect(screen.getByText('listingsAndReviews')).to.exist; + expect(screen.getByText('Last modified: now')).to.exist; + }); + it('should emit an "open" action on click / space / enter', function () { const onAction = Sinon.spy(); diff --git a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx index 28dea883615..e16d13a87a9 100644 --- a/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx +++ b/packages/compass-saved-aggregations-queries/src/components/saved-item-card.tsx @@ -189,6 +189,19 @@ export const SavedItemCard: React.FunctionComponent< defaultActionProps ); + let badge: string; + switch (type) { + case 'query': + badge = 'find'; + break; + case 'updatemany': + badge = 'updatemany'; + break; + case 'aggregation': + badge = 'aggregate'; + break; + } + const formattedDate = useFormattedDate(lastModified); const darkMode = useDarkMode(); @@ -199,7 +212,7 @@ export const SavedItemCard: React.FunctionComponent<
- .{type === 'query' ? 'find' : 'aggregate'} + .{badge}
diff --git a/packages/compass-saved-aggregations-queries/src/hooks/use-grid-filters.tsx b/packages/compass-saved-aggregations-queries/src/hooks/use-grid-filters.tsx index b5c0ff44404..dfaf04d1093 100644 --- a/packages/compass-saved-aggregations-queries/src/hooks/use-grid-filters.tsx +++ b/packages/compass-saved-aggregations-queries/src/hooks/use-grid-filters.tsx @@ -237,12 +237,14 @@ export function filterByText(items: Item[], text: string): FilterItem[] { } if (key === 'data') { - if (item.type === 'query') { + if (item.type === 'query' || item.type === 'updatemany') { return JSON.stringify({ filter: item.query.filter, }); - } else { + } else if (item.type === 'aggregation') { return item.aggregation.pipelineText; + } else { + return []; } } diff --git a/packages/compass-saved-aggregations-queries/src/stores/aggregations-queries-items.ts b/packages/compass-saved-aggregations-queries/src/stores/aggregations-queries-items.ts index c5c7060bf6e..3cf4ec9c52b 100644 --- a/packages/compass-saved-aggregations-queries/src/stores/aggregations-queries-items.ts +++ b/packages/compass-saved-aggregations-queries/src/stores/aggregations-queries-items.ts @@ -25,7 +25,7 @@ export type Item = { collection: string; } & ( | { - type: 'query'; + type: 'query' | 'updatemany'; query: FavoriteQuery; } | { @@ -125,7 +125,7 @@ const mapQueryToItem = (query: FavoriteQuery): Item => { lastModified: (query._dateModified ?? query._dateSaved).getTime(), database, collection, - type: 'query', + type: query.update ? 'updatemany' : 'query', query, }; }; diff --git a/packages/my-queries-storage/src/query-storage.spec.ts b/packages/my-queries-storage/src/query-storage.spec.ts index 71aac5de1e8..09faa6253aa 100644 --- a/packages/my-queries-storage/src/query-storage.spec.ts +++ b/packages/my-queries-storage/src/query-storage.spec.ts @@ -25,7 +25,7 @@ const queries = [ const maxAllowedRecentQueries = 30; -describe('QueryStorage', function () { +describe('RecentQueryStorage', function () { let queryHistoryStorage: RecentQueryStorage; let tmpDir: string; @@ -203,3 +203,37 @@ describe('QueryStorage', function () { }); } }); + +describe('FavoriteQueryStorage', function () { + let queryFavoriteStorage: FavoriteQueryStorage; + + let tmpDir: string; + beforeEach(async function () { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'query-storage-tests')); + queryFavoriteStorage = new FavoriteQueryStorage({ + basepath: tmpDir, + }); + }); + + afterEach(async function () { + await fs.rm(tmpDir, { recursive: true }); + }); + + it('should retrieve saved queries', async function () { + await queryFavoriteStorage.saveQuery({ + _ns: 'test.test', + _name: 'my-query', + filter: { a: 1 }, + update: { $set: { a: 2 } }, + }); + + const loaded = await queryFavoriteStorage.loadAll(); + expect(loaded.length).to.be.greaterThan(0); + + const [query] = loaded; + expect(query._name).to.equal('my-query'); + expect(query._ns).to.equal('test.test'); + expect(query.filter).to.deep.equal({ a: 1 }); + expect(query.update).to.deep.equal({ $set: { a: 2 } }); + }); +}); diff --git a/packages/my-queries-storage/src/query-storage.ts b/packages/my-queries-storage/src/query-storage.ts index b9eeaa57ff3..8851e2666f8 100644 --- a/packages/my-queries-storage/src/query-storage.ts +++ b/packages/my-queries-storage/src/query-storage.ts @@ -8,6 +8,7 @@ const queryProps = { sort: z.any().optional(), skip: z.number().optional(), limit: z.number().optional(), + update: z.any().optional(), }; const commonMetadata = { @@ -126,4 +127,20 @@ export class FavoriteQueryStorage extends QueryStorage< constructor(options: QueryStorageOptions = {}) { super(FavoriteQuerySchema, 'FavoriteQueries', options); } + + async saveQuery( + data: Omit< + z.input, + '_id' | '_lastExecuted' | '_dateModified' | '_dateSaved' + > + ): Promise { + const _id = new UUID().toString(); + const favoriteQuery = { + ...data, + _id, + _lastExecuted: new Date(), + _dateSaved: new Date(), + }; + await this.userData.write(_id, favoriteQuery); + } }