Skip to content

Commit

Permalink
add search to query state
Browse files Browse the repository at this point in the history
  • Loading branch information
kilbot committed Dec 29, 2023
1 parent 8cfcbf9 commit 713ebdc
Show file tree
Hide file tree
Showing 15 changed files with 681 additions and 32 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"packageManager": "[email protected]",
"dependencies": {
"@orama/orama": "^1.2.11",
"@shelf/fast-natural-order-by": "^2.0.0",
"lodash": "^4.17.21",
"observable-hooks": "^4.2.3",
Expand Down
32 changes: 32 additions & 0 deletions src/collection-replication-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,38 @@ export class CollectionReplicationState<T extends RxCollection> {
return remoteIDs.filter((id) => !localIDs.includes(id));
}

/**
* Remote mutations patch/create
* I'm not 100% sure this is the right spot for these, but I'm thinking in the future
* there will have to be some kind of queuing system for when the app is offline
*/
async remotePatch(doc, data) {
try {
if (!doc.id) {
throw new Error('document does not have an id');
}
const response = await this.httpClient.patch(this.endpoint + '/' + doc.id, data);
const parsedData = this.collection.parseRestResponse(response.data);
await this.collection.upsertRefs(parsedData); // upsertRefs mutates the parsedData
await doc.incrementalPatch(parsedData);
return doc;
} catch (error) {
this.subjects.error.next(error);
}
}

async remoteCreate(data) {
try {
const response = await this.httpClient.post(this.endpoint, data);
const parsedData = this.collection.parseRestResponse(response.data);
await this.collection.upsertRefs(parsedData); // upsertRefs mutates the parsedData
const doc = await this.collection.upsert(parsedData);
return doc;
} catch (error) {
this.subjects.error.next(error);
}
}

/**
* We need to a way to pause and start the replication, eg: when the user is offline
*/
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/customers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface APIQueryParams {
/**
*
*/
const filterApiQueryParams = (params, checkpoint, batchSize) => {
const filterApiQueryParams = (params) => {
let orderby = params.orderby;

if (orderby === 'date_created') {
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as categories from './categories';
import * as customers from './customers';
import * as orders from './orders';
import * as products from './products';
import * as tags from './tags';
import * as taxRates from './tax-rates';
import * as variations from './variations';

export default {
'products/categories': categories,
customers,
orders,
products,
'products/tags': tags,
'tax-rates': taxRates,
variations,
};
2 changes: 1 addition & 1 deletion src/hooks/orders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface APIQueryParams {
/**
*
*/
const filterApiQueryParams = (params, checkpoint, batchSize) => {
const filterApiQueryParams = (params) => {
let orderby = params.orderby;

if (orderby === 'date_created' || orderby === 'date_created_gmt') {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ interface APIQueryParams {
/**
*
*/
const filterApiQueryParams = (params, checkpoint, batchSize) => {
const filterApiQueryParams = (params) => {
let orderby = params.orderby;

if (orderby === 'name') {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface APIQueryParams {
/**
*
*/
const filterApiQueryParams = (params, checkpoint, batchSize) => {
const filterApiQueryParams = (params, checkpoint) => {
const { include } = checkpoint;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/variations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const postQueryResult = (result, queryState) => {
/**
*
*/
const filterApiQueryParams = (params, checkpoint, batchSize) => {
const filterApiQueryParams = (params) => {
let orderby = params.orderby;

if (orderby === 'name') {
Expand Down
59 changes: 35 additions & 24 deletions src/manager.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import forEach from 'lodash/forEach';
import { Observable, Subject, Subscription } from 'rxjs';

import { CollectionReplicationState } from './collection-replication-state';
import allHooks from './hooks';
import { QueryReplicationState } from './query-replication-state';
import { Query } from './query-state';
import { Search } from './search-state';
import { buildUrlWithParams } from './utils';

import type { QueryParams, QueryHooks } from './query-state';
import type { QueryParams } from './query-state';
import type { RxDatabase, RxCollection } from 'rxdb';

/**
*
*/
export interface ResisterQueryConfig {
export interface RegisterQueryConfig {
queryKeys: (string | number | object)[];
collectionName: string;
initialParams?: QueryParams;
hooks?: QueryHooks;
locale?: string;
endpoint?: string;
}

Expand Down Expand Up @@ -48,7 +49,7 @@ export class Manager<TDatabase extends RxDatabase> {
> = new Map();

/**
*
* Each queryKey should have one collection replication and at least one query replication
*/
public queryKeyToReplicationsMap: Map<string, string[]> = new Map();

Expand All @@ -67,7 +68,8 @@ export class Manager<TDatabase extends RxDatabase> {

constructor(
private localDB: TDatabase,
private httpClient
private httpClient,
private locale: string
) {
/**
* Subscribe to localDB to detect if collection is reset
Expand Down Expand Up @@ -107,21 +109,22 @@ export class Manager<TDatabase extends RxDatabase> {
return this.queries.has(key);
}

registerQuery({
queryKeys,
collectionName,
initialParams,
hooks = {},
locale,
...args
}: ResisterQueryConfig) {
registerQuery({ queryKeys, collectionName, initialParams, ...args }: RegisterQueryConfig) {
const key = this.stringify(queryKeys);
const endpoint = args.endpoint || collectionName;
const hooks = allHooks[collectionName] || {};

if (key && !this.queries.has(key)) {
const collection = this.getCollection(collectionName);
if (collection) {
const query = new Query<typeof collection>({ id: key, collection, initialParams, hooks });
const searchService = new Search({ collection, locale: this.locale });
const query = new Query<typeof collection>({
id: key,
collection,
initialParams,
hooks,
searchService,
});
const collectionReplication = this.registerCollectionReplication({ collection, endpoint });
this.addQueryKeyToReplicationsMap(key, endpoint);
collectionReplication.start();
Expand All @@ -136,7 +139,10 @@ export class Manager<TDatabase extends RxDatabase> {
* - also cancel the previous query replication
*/
query.params$.subscribe((params) => {
const apiQueryParams = this.getApiQueryParams(params);
let apiQueryParams = this.getApiQueryParams(params);
if (hooks?.filterApiQueryParams) {
apiQueryParams = hooks.filterApiQueryParams(apiQueryParams, params);
}
const queryEndpoint = buildUrlWithParams(endpoint, apiQueryParams);

if (!this.replicationStates.has(queryEndpoint)) {
Expand Down Expand Up @@ -285,16 +291,21 @@ export class Manager<TDatabase extends RxDatabase> {
* - NOTE: the api query params have a different format than the query params
* - allow hooks to modify the query params
*/
getApiQueryParams(params: QueryParams) {
params = params || {};

Object.assign(params, {
uuid: undefined, // remove all uuid params?
getApiQueryParams(queryParams: QueryParams = {}) {
const params = {
orderby: queryParams?.sortBy,
order: queryParams?.sortDirection,
per_page: 10,
});
};

if (queryParams?.search && typeof queryParams?.search === 'string') {
params.search = queryParams?.search;
}

if (this.hooks?.filterApiQueryParams) {
params = this.hooks.filterApiQueryParams(params);
if (queryParams?.selector) {
forEach(queryParams.selector, (value, key) => {
params[key] = value;
});
}

return params;
Expand Down
4 changes: 3 additions & 1 deletion src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ const QueryContext = React.createContext<Manager<RxDatabase> | undefined>(undefi
interface QueryProviderProps<T extends RxDatabase> {
localDB: T;
http: any; // Replace 'any' with the actual type of your HTTP client
locale: string;
children: React.ReactNode;
}

export const QueryProvider = <T extends RxDatabase>({
localDB,
http,
children,
locale,
}: QueryProviderProps<T>) => {
const manager = React.useMemo(() => new Manager(localDB, http), [localDB, http]);
const manager = React.useMemo(() => new Manager(localDB, http, locale), [localDB, http, locale]);

/**
* Clean up the manager when the dependency changes
Expand Down
47 changes: 45 additions & 2 deletions src/query-state.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { orderBy } from '@shelf/fast-natural-order-by';
import debounce from 'lodash/debounce';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import { ObservableResource } from 'observable-hooks';
import { BehaviorSubject, Observable, Subscription, Subject } from 'rxjs';
import { map, switchMap, distinctUntilChanged } from 'rxjs/operators';

import type { Search } from './search-state';
import type { RxCollection, RxDocument } from 'rxdb';

// This type utility extracts the document type from a collection
Expand All @@ -30,6 +33,7 @@ export interface QueryConfig<T> {
collection: T;
initialParams?: QueryParams;
hooks?: QueryHooks;
searchService: Search;
}

type WhereClause = { field: string; value: any };
Expand All @@ -45,6 +49,8 @@ export class Query<T extends RxCollection> {
private hooks: QueryConfig<T>['hooks'];
private paginationEndReached = false;
private pageSize: number;
private searchService: Search;
private activeSearchSubscription: Subscription | null = null;

/**
*
Expand Down Expand Up @@ -78,12 +84,13 @@ export class Query<T extends RxCollection> {
/**
*
*/
constructor({ id, collection, initialParams = {}, hooks }: QueryConfig<T>) {
constructor({ id, collection, initialParams = {}, hooks, searchService }: QueryConfig<T>) {
this.id = id;
this.collection = collection;
this.subjects.params.next(initialParams);
this.hooks = hooks || {};
this.pageSize = 10;
this.searchService = searchService;

/**
* Keep track of what we are subscribed to
Expand Down Expand Up @@ -205,7 +212,43 @@ export class Query<T extends RxCollection> {
return this;
}

search(query: string | Record<string, any> = '') {}
search(query: string | Record<string, any> = '') {
if (typeof query === 'string' || query === null) {
// if activeSearchSubscription exists, cancel it
if (this.activeSearchSubscription) {
this.activeSearchSubscription.unsubscribe();
}

/**
* If empty, we can just remove the uuid where clause
*/
if (isEmpty(query)) {
this.whereClauses = this.whereClauses.filter((clause) => clause.field !== 'uuid');
return this.updateParams({ search: query });
}

/**
* We need to use the Orama searchDB to find matching uuids
*/
this.activeSearchSubscription = this.searchService.search$(query).subscribe((uuids) => {
// remove uuid from whereClauses
this.whereClauses = this.whereClauses.filter((clause) => clause.field !== 'uuid');

if (uuids) {
this.whereClauses.push({ field: 'uuid', value: { $in: uuids } });
}

this.updateParams({ search: query });
});

// make sure we clean up the subscription on cancel
this.subs.push(this.activeSearchSubscription);
} else {
this.updateParams({ search: query });
}
}

debouncedSearch = debounce(this.search, 250);

private updateParams(additionalParams: Partial<QueryParams> = {}): void {
// Construct the selector from where clauses
Expand Down
Loading

0 comments on commit 713ebdc

Please sign in to comment.