@blocksdiy/blocks-client-sdk 1.18.2 → 1.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Entity.d.ts +37 -11
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +52 -15
- package/dist/EntityCache.d.ts +25 -1
- package/dist/EntityCache.d.ts.map +1 -1
- package/dist/EntityCache.js +132 -13
- package/dist/EntityQuery.d.ts +66 -0
- package/dist/EntityQuery.d.ts.map +1 -0
- package/dist/EntityQuery.js +46 -0
- package/dist/ReactClientSdk.d.ts +30 -18
- package/dist/ReactClientSdk.d.ts.map +1 -1
- package/dist/ReactClientSdk.jsx +63 -26
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +3 -3
package/dist/Entity.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EntityPage, type EntityQuery } from "./EntityQuery.js";
|
|
1
2
|
/**
|
|
2
3
|
* Base entity properties automatically included in all entity instances
|
|
3
4
|
* These system fields are managed by the backend and available on all entities
|
|
@@ -122,12 +123,14 @@ export declare class Entity<EC extends EntityConfig = EntityConfig> {
|
|
|
122
123
|
*/
|
|
123
124
|
findOne(filters: Record<string, any>): Promise<EntityType<EC> | null>;
|
|
124
125
|
/**
|
|
125
|
-
* Retrieves
|
|
126
|
+
* Retrieves the entities that match a query
|
|
126
127
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
128
|
+
* Pass an {@link EntityQuery}: `where` picks the rows, `orderBy` orders them, and `page.size`
|
|
129
|
+
* caps the result at one page. Without `page`, the server's default row limit applies — use
|
|
130
|
+
* {@link findPage} to read past it. Legacy key/value filters (`{ category: "electronics" }`)
|
|
131
|
+
* are still accepted and keep their meaning; new code should use a query.
|
|
129
132
|
*
|
|
130
|
-
* @param {any} [
|
|
133
|
+
* @param {EntityQuery<EC> | Record<string, any>} [query] - The query (or legacy filters)
|
|
131
134
|
* @returns {Promise<Array<EntityType<EC>>>} Array of matching entities
|
|
132
135
|
* @example
|
|
133
136
|
* ```ts
|
|
@@ -135,16 +138,39 @@ export declare class Entity<EC extends EntityConfig = EntityConfig> {
|
|
|
135
138
|
*
|
|
136
139
|
* const productEntity = new Entity(ProductEntity);
|
|
137
140
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
* const products = await productEntity.findMany({
|
|
142
|
+
* where: { and: [
|
|
143
|
+
* { column: 'category', value: 'electronics' },
|
|
144
|
+
* { column: 'price', operator: '<', value: 100 },
|
|
145
|
+
* ]},
|
|
146
|
+
* orderBy: [{ column: 'price', direction: 'asc' }],
|
|
144
147
|
* });
|
|
145
148
|
* ```
|
|
146
149
|
*/
|
|
147
|
-
findMany(
|
|
150
|
+
findMany(query?: EntityQuery<EC> | Record<string, any>): Promise<any[]>;
|
|
151
|
+
/**
|
|
152
|
+
* Retrieves one page of the entities that match a query
|
|
153
|
+
*
|
|
154
|
+
* Pages are cursor based: pass `null` (or nothing) for the first page, then the returned
|
|
155
|
+
* `nextCursor` for the next one; `nextCursor` is `null` after the last page. Pass the same
|
|
156
|
+
* query for every page of a sequence. `page.size` sets the rows per page (server default when
|
|
157
|
+
* omitted). Only tables can be paged: the server rejects a cursor on views and on the users table.
|
|
158
|
+
*
|
|
159
|
+
* @param {EntityQuery<EC>} query - The query to page through
|
|
160
|
+
* @param {string | null} [cursor] - The `nextCursor` of the previous page; `null` for the first
|
|
161
|
+
* @returns {Promise<EntityPage<EC>>} The page's rows and the next page's cursor
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* const query = { where: { column: 'status', value: 'open' }, page: { size: 100 } };
|
|
165
|
+
* let cursor = null;
|
|
166
|
+
* do {
|
|
167
|
+
* const { rows, nextCursor } = await taskEntity.findPage(query, cursor);
|
|
168
|
+
* rows.forEach(processTask);
|
|
169
|
+
* cursor = nextCursor;
|
|
170
|
+
* } while (cursor);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
findPage(query?: EntityQuery<EC>, cursor?: string | null): Promise<EntityPage<EC>>;
|
|
148
174
|
/**
|
|
149
175
|
* Retrieves which agents created and last changed an entity
|
|
150
176
|
*
|
package/dist/Entity.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../src/Entity.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../src/Entity.ts"],"names":[],"mappings":"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"}
|
package/dist/Entity.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DataApiService } from "@blocksdiy/blocks-client-api";
|
|
2
|
+
import { isEntityQuery, toQueryOptions } from "./EntityQuery.js";
|
|
2
3
|
const DATA_OPERATIONS_CHUNK_SIZE = 500;
|
|
3
4
|
const arrayToChunks = (array, chunkSize) => {
|
|
4
5
|
const chunks = [];
|
|
@@ -61,12 +62,14 @@ export class Entity {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
|
-
* Retrieves
|
|
65
|
+
* Retrieves the entities that match a query
|
|
65
66
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
67
|
+
* Pass an {@link EntityQuery}: `where` picks the rows, `orderBy` orders them, and `page.size`
|
|
68
|
+
* caps the result at one page. Without `page`, the server's default row limit applies — use
|
|
69
|
+
* {@link findPage} to read past it. Legacy key/value filters (`{ category: "electronics" }`)
|
|
70
|
+
* are still accepted and keep their meaning; new code should use a query.
|
|
68
71
|
*
|
|
69
|
-
* @param {any} [
|
|
72
|
+
* @param {EntityQuery<EC> | Record<string, any>} [query] - The query (or legacy filters)
|
|
70
73
|
* @returns {Promise<Array<EntityType<EC>>>} Array of matching entities
|
|
71
74
|
* @example
|
|
72
75
|
* ```ts
|
|
@@ -74,21 +77,55 @@ export class Entity {
|
|
|
74
77
|
*
|
|
75
78
|
* const productEntity = new Entity(ProductEntity);
|
|
76
79
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* const products = await productEntity.findMany({
|
|
81
|
+
* where: { and: [
|
|
82
|
+
* { column: 'category', value: 'electronics' },
|
|
83
|
+
* { column: 'price', operator: '<', value: 100 },
|
|
84
|
+
* ]},
|
|
85
|
+
* orderBy: [{ column: 'price', direction: 'asc' }],
|
|
83
86
|
* });
|
|
84
87
|
* ```
|
|
85
88
|
*/
|
|
86
|
-
|
|
89
|
+
// The return type stays `any[]` as it always was: narrowing it could turn existing app code
|
|
90
|
+
// into type errors on its next compile.
|
|
91
|
+
async findMany(query) {
|
|
92
|
+
try {
|
|
93
|
+
const { rows } = isEntityQuery(query)
|
|
94
|
+
? await this.dataApiService.queryTableDataV2(this.tableBlockId, toQueryOptions(query))
|
|
95
|
+
: await this.dataApiService.getTableDataV2(this.tableBlockId, query);
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.warn(error);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Retrieves one page of the entities that match a query
|
|
105
|
+
*
|
|
106
|
+
* Pages are cursor based: pass `null` (or nothing) for the first page, then the returned
|
|
107
|
+
* `nextCursor` for the next one; `nextCursor` is `null` after the last page. Pass the same
|
|
108
|
+
* query for every page of a sequence. `page.size` sets the rows per page (server default when
|
|
109
|
+
* omitted). Only tables can be paged: the server rejects a cursor on views and on the users table.
|
|
110
|
+
*
|
|
111
|
+
* @param {EntityQuery<EC>} query - The query to page through
|
|
112
|
+
* @param {string | null} [cursor] - The `nextCursor` of the previous page; `null` for the first
|
|
113
|
+
* @returns {Promise<EntityPage<EC>>} The page's rows and the next page's cursor
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* const query = { where: { column: 'status', value: 'open' }, page: { size: 100 } };
|
|
117
|
+
* let cursor = null;
|
|
118
|
+
* do {
|
|
119
|
+
* const { rows, nextCursor } = await taskEntity.findPage(query, cursor);
|
|
120
|
+
* rows.forEach(processTask);
|
|
121
|
+
* cursor = nextCursor;
|
|
122
|
+
* } while (cursor);
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async findPage(query = {}, cursor = null) {
|
|
87
126
|
try {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
const instances = Array.isArray(response) ? response : response ? [response] : [];
|
|
91
|
-
return instances;
|
|
127
|
+
const { rows, nextCursor } = await this.dataApiService.queryTableDataV2(this.tableBlockId, toQueryOptions(query, cursor ?? ""));
|
|
128
|
+
return { rows: rows, nextCursor };
|
|
92
129
|
}
|
|
93
130
|
catch (error) {
|
|
94
131
|
console.warn(error);
|
package/dist/EntityCache.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryClient, QueryKey } from "@tanstack/react-query";
|
|
2
|
+
import type { EntityQuery } from "./EntityQuery.js";
|
|
2
3
|
export declare const ENTITIES_QUERY_KEY = "entities";
|
|
3
4
|
/**
|
|
4
5
|
* Minimal shape of a server row required for cache reconciliation.
|
|
@@ -36,14 +37,34 @@ export declare const sameId: (a: unknown, b: unknown) => boolean;
|
|
|
36
37
|
* is more trustworthy than an undecidable comparison).
|
|
37
38
|
*/
|
|
38
39
|
export declare const isIncomingRowNewer: (existing: unknown, incoming: EntityCacheRow) => boolean;
|
|
40
|
+
export declare const entityPagesKey: (tableBlockId: string, query: unknown) => QueryKey;
|
|
39
41
|
export declare const entityAttributionKey: (tableBlockId: string, rowId: string | number) => QueryKey;
|
|
40
42
|
export declare const isAttributionQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
|
|
43
|
+
export declare const isPagedListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
|
|
44
|
+
/** Any list of rows: a plain list or a paged one. */
|
|
41
45
|
export declare const isListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
|
|
42
46
|
export declare const isUnfilteredListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
|
|
47
|
+
/** Lists whose membership only the server can decide: filtered/queried lists and every paged list. */
|
|
43
48
|
export declare const isFilteredListQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
|
|
44
49
|
export declare const isOneQueryKey: (queryKey: QueryKey, tableBlockId: string) => boolean;
|
|
45
50
|
/** Returns the id of a one-query keyed strictly by { id }, undefined for any other filter shape. */
|
|
46
51
|
export declare const getOneQueryKeyId: (queryKey: QueryKey) => string | number | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The page param of a paged list's first page once its loaded pages were collapsed into one
|
|
54
|
+
* (see {@link collapsePagedListForRefetch}): re-read `size` rows starting at `cursor`.
|
|
55
|
+
*/
|
|
56
|
+
export interface EntityCollapsedPageParam {
|
|
57
|
+
cursor: string | null;
|
|
58
|
+
size: number;
|
|
59
|
+
}
|
|
60
|
+
/** A paged list's page param: a cursor (`null` for the first page), or a collapsed first page. */
|
|
61
|
+
export type EntityPageParam = string | null | EntityCollapsedPageParam;
|
|
62
|
+
export declare const isCollapsedPageParam: (value: unknown) => value is EntityCollapsedPageParam;
|
|
63
|
+
/**
|
|
64
|
+
* Fetches one page of a paged list. A collapsed page param re-reads every row it covers in one
|
|
65
|
+
* request, keeping the query's `where` / `orderBy` but replacing its page size.
|
|
66
|
+
*/
|
|
67
|
+
export declare const fetchEntityPagedListPage: <TPage>(findPage: (query: EntityQuery<any>, cursor: string | null) => Promise<TPage>, query: EntityQuery<any>, pageParam: EntityPageParam) => Promise<TPage>;
|
|
47
68
|
/**
|
|
48
69
|
* Writes canonical server rows into every cached query of the table.
|
|
49
70
|
*
|
|
@@ -95,7 +116,9 @@ export declare const restoreEntityQueriesSnapshot: (queryClient: QueryClient, sn
|
|
|
95
116
|
* snapshotted before the mutation cannot overwrite the rows we are about to write.
|
|
96
117
|
*
|
|
97
118
|
* Fetches of queries without data (initial loads) are left running: cancelling them
|
|
98
|
-
* would strand the query in a non-fetching pending state.
|
|
119
|
+
* would strand the query in a non-fetching pending state. A cancelled "load more" of a
|
|
120
|
+
* paged list is not lost: the list is collapsed to re-read its loaded rows plus one more
|
|
121
|
+
* page, so the refetch that follows the write also delivers the page that was asked for.
|
|
99
122
|
*/
|
|
100
123
|
export declare const prepareEntityCacheWrite: (queryClient: QueryClient, tableBlockId: string) => Promise<EntityWritePreparation>;
|
|
101
124
|
/**
|
|
@@ -108,6 +131,7 @@ export declare const prepareEntityCacheWrite: (queryClient: QueryClient, tableBl
|
|
|
108
131
|
*
|
|
109
132
|
* Unfiltered lists and id-keyed one-queries are NOT invalidated - they were fully
|
|
110
133
|
* reconciled from the server response, which is what saves the refetch round trips.
|
|
134
|
+
* Paged lists are collapsed before they are invalidated, so each refetches in one request.
|
|
111
135
|
*/
|
|
112
136
|
export declare const invalidateEntityQueriesAfterWrite: (queryClient: QueryClient, tableBlockId: string, { staleKeys, filteredLists, nonIdOneQueries, deletedRowIds, }: {
|
|
113
137
|
staleKeys?: QueryKey[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityCache.d.ts","sourceRoot":"","sources":["../src/EntityCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,WAAW,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEvE,eAAO,MAAM,kBAAkB,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"EntityCache.d.ts","sourceRoot":"","sources":["../src/EntityCache.ts"],"names":[],"mappings":"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"}
|
package/dist/EntityCache.js
CHANGED
|
@@ -2,6 +2,7 @@ import { hashKey } from "@tanstack/react-query";
|
|
|
2
2
|
export const ENTITIES_QUERY_KEY = "entities";
|
|
3
3
|
const ONE_SEGMENT = "one";
|
|
4
4
|
const ATTRIBUTION_SEGMENT = "attribution";
|
|
5
|
+
const PAGES_SEGMENT = "pages";
|
|
5
6
|
export const entityTableKey = (tableBlockId) => [ENTITIES_QUERY_KEY, tableBlockId];
|
|
6
7
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
8
|
const isValidRow = (value) => isRecord(value) && (typeof value.id === "string" || typeof value.id === "number");
|
|
@@ -44,7 +45,16 @@ export const isIncomingRowNewer = (existing, incoming) => {
|
|
|
44
45
|
* One queries: [ENTITIES_QUERY_KEY, tableBlockId, "one", filters] (length 4)
|
|
45
46
|
* Attribution queries: [ENTITIES_QUERY_KEY, tableBlockId, "attribution", rowId] (length 4) — never
|
|
46
47
|
* written to from rows (they hold no row), only invalidated on every write to the table.
|
|
48
|
+
* Paged list queries: [ENTITIES_QUERY_KEY, tableBlockId, "pages", query] (length 4) — infinite
|
|
49
|
+
* queries (`{ pages: [{ rows, nextCursor }], pageParams }`); maintained like filtered lists,
|
|
50
|
+
* and collapsed into one page before every refetch (see `collapsePagedListForRefetch`).
|
|
47
51
|
*/
|
|
52
|
+
export const entityPagesKey = (tableBlockId, query) => [
|
|
53
|
+
ENTITIES_QUERY_KEY,
|
|
54
|
+
tableBlockId,
|
|
55
|
+
PAGES_SEGMENT,
|
|
56
|
+
query,
|
|
57
|
+
];
|
|
48
58
|
export const entityAttributionKey = (tableBlockId, rowId) => [
|
|
49
59
|
ENTITIES_QUERY_KEY,
|
|
50
60
|
tableBlockId,
|
|
@@ -53,9 +63,13 @@ export const entityAttributionKey = (tableBlockId, rowId) => [
|
|
|
53
63
|
];
|
|
54
64
|
export const isAttributionQueryKey = (queryKey, tableBlockId) => isTableKey(queryKey, tableBlockId) && queryKey.length === 4 && queryKey[2] === ATTRIBUTION_SEGMENT;
|
|
55
65
|
const isTableKey = (queryKey, tableBlockId) => Array.isArray(queryKey) && queryKey[0] === ENTITIES_QUERY_KEY && queryKey[1] === tableBlockId;
|
|
56
|
-
|
|
57
|
-
export const
|
|
58
|
-
|
|
66
|
+
const isPlainListQueryKey = (queryKey, tableBlockId) => isTableKey(queryKey, tableBlockId) && queryKey.length === 3 && queryKey[2] !== ONE_SEGMENT;
|
|
67
|
+
export const isPagedListQueryKey = (queryKey, tableBlockId) => isTableKey(queryKey, tableBlockId) && queryKey.length === 4 && queryKey[2] === PAGES_SEGMENT;
|
|
68
|
+
/** Any list of rows: a plain list or a paged one. */
|
|
69
|
+
export const isListQueryKey = (queryKey, tableBlockId) => isPlainListQueryKey(queryKey, tableBlockId) || isPagedListQueryKey(queryKey, tableBlockId);
|
|
70
|
+
export const isUnfilteredListQueryKey = (queryKey, tableBlockId) => isPlainListQueryKey(queryKey, tableBlockId) && queryKey[2] == null;
|
|
71
|
+
/** Lists whose membership only the server can decide: filtered/queried lists and every paged list. */
|
|
72
|
+
export const isFilteredListQueryKey = (queryKey, tableBlockId) => isListQueryKey(queryKey, tableBlockId) && !isUnfilteredListQueryKey(queryKey, tableBlockId);
|
|
59
73
|
export const isOneQueryKey = (queryKey, tableBlockId) => isTableKey(queryKey, tableBlockId) && queryKey.length === 4 && queryKey[2] === ONE_SEGMENT;
|
|
60
74
|
/** Returns the id of a one-query keyed strictly by { id }, undefined for any other filter shape. */
|
|
61
75
|
export const getOneQueryKeyId = (queryKey) => {
|
|
@@ -70,12 +84,85 @@ export const getOneQueryKeyId = (queryKey) => {
|
|
|
70
84
|
const id = filters.id;
|
|
71
85
|
return typeof id === "string" || typeof id === "number" ? id : undefined;
|
|
72
86
|
};
|
|
87
|
+
const isPagedListData = (data) => typeof data === "object" &&
|
|
88
|
+
data !== null &&
|
|
89
|
+
Array.isArray(data.pages) &&
|
|
90
|
+
data.pages.every((page) => Array.isArray(page?.rows));
|
|
91
|
+
export const isCollapsedPageParam = (value) => isRecord(value) && typeof value.size === "number" && (value.cursor === null || typeof value.cursor === "string");
|
|
92
|
+
/**
|
|
93
|
+
* Fetches one page of a paged list. A collapsed page param re-reads every row it covers in one
|
|
94
|
+
* request, keeping the query's `where` / `orderBy` but replacing its page size.
|
|
95
|
+
*/
|
|
96
|
+
export const fetchEntityPagedListPage = (findPage, query, pageParam) => isCollapsedPageParam(pageParam)
|
|
97
|
+
? findPage({ ...query, page: { size: pageParam.size } }, pageParam.cursor)
|
|
98
|
+
: findPage(query, pageParam);
|
|
99
|
+
/** The `page.size` of a paged list's query key, undefined when it has none. */
|
|
100
|
+
const pagedListPageSize = (queryKey) => {
|
|
101
|
+
const query = queryKey[3];
|
|
102
|
+
const size = isRecord(query) && isRecord(query.page) ? query.page.size : undefined;
|
|
103
|
+
return typeof size === "number" && size > 0 ? size : undefined;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Folds a paged list's loaded pages into a single page whose param re-reads all of their rows
|
|
107
|
+
* (plus `extraRows`) in one request. React Query refetches an infinite query page by page,
|
|
108
|
+
* one request after another from `pageParams[0]`, as many pages as it holds — so a list
|
|
109
|
+
* paged 20 deep would cost 20 sequential requests on every refetch. Collapsed, the refetch is
|
|
110
|
+
* one request, and the rows, their order and the next cursor stay exactly as rendered.
|
|
111
|
+
*
|
|
112
|
+
* `extraRows` grows the re-read past what is loaded: a "load more" cancelled by a cache write
|
|
113
|
+
* is folded into the refetch instead of being lost.
|
|
114
|
+
*/
|
|
115
|
+
const collapsePagedListForRefetch = (queryClient, queryKey, { extraRows = 0 } = {}) => {
|
|
116
|
+
const pageSize = pagedListPageSize(queryKey);
|
|
117
|
+
if (pageSize === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
queryClient.setQueryData(queryKey, (oldData) => {
|
|
121
|
+
if (!isPagedListData(oldData) || oldData.pages.length === 0) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (oldData.pages.length === 1 && extraRows === 0) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const firstParam = oldData.pageParams[0];
|
|
128
|
+
const cursor = isCollapsedPageParam(firstParam)
|
|
129
|
+
? firstParam.cursor
|
|
130
|
+
: typeof firstParam === "string"
|
|
131
|
+
? firstParam
|
|
132
|
+
: null;
|
|
133
|
+
const rows = oldData.pages.flatMap((page) => page.rows);
|
|
134
|
+
const collapsedParam = {
|
|
135
|
+
cursor,
|
|
136
|
+
size: Math.max(rows.length, pageSize) + extraRows,
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
pages: [{ rows, nextCursor: oldData.pages[oldData.pages.length - 1].nextCursor }],
|
|
140
|
+
pageParams: [collapsedParam],
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Applies a rows transform to list data: a plain rows array, or each page of a paged list.
|
|
146
|
+
* `undefined` from the transform (and for any other data shape) leaves the cache untouched.
|
|
147
|
+
*/
|
|
73
148
|
const updateListData = (oldData, transform) => {
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
return undefined;
|
|
149
|
+
if (Array.isArray(oldData)) {
|
|
150
|
+
return transform(oldData);
|
|
77
151
|
}
|
|
78
|
-
|
|
152
|
+
if (isPagedListData(oldData)) {
|
|
153
|
+
let changed = false;
|
|
154
|
+
const pages = oldData.pages.map((page) => {
|
|
155
|
+
const rows = transform(page.rows);
|
|
156
|
+
if (rows === undefined) {
|
|
157
|
+
return page;
|
|
158
|
+
}
|
|
159
|
+
changed = true;
|
|
160
|
+
return { ...page, rows };
|
|
161
|
+
});
|
|
162
|
+
return changed ? { ...oldData, pages } : undefined;
|
|
163
|
+
}
|
|
164
|
+
// Bail out instead of corrupting non-list data.
|
|
165
|
+
return undefined;
|
|
79
166
|
};
|
|
80
167
|
/**
|
|
81
168
|
* Writes canonical server rows into every cached query of the table.
|
|
@@ -239,21 +326,31 @@ export const restoreEntityQueriesSnapshot = (queryClient, snapshot) => {
|
|
|
239
326
|
* snapshotted before the mutation cannot overwrite the rows we are about to write.
|
|
240
327
|
*
|
|
241
328
|
* Fetches of queries without data (initial loads) are left running: cancelling them
|
|
242
|
-
* would strand the query in a non-fetching pending state.
|
|
329
|
+
* would strand the query in a non-fetching pending state. A cancelled "load more" of a
|
|
330
|
+
* paged list is not lost: the list is collapsed to re-read its loaded rows plus one more
|
|
331
|
+
* page, so the refetch that follows the write also delivers the page that was asked for.
|
|
243
332
|
*/
|
|
244
333
|
export const prepareEntityCacheWrite = async (queryClient, tableBlockId) => {
|
|
245
334
|
const tableKey = entityTableKey(tableBlockId);
|
|
246
|
-
const
|
|
247
|
-
.getQueryCache()
|
|
248
|
-
.findAll({
|
|
335
|
+
const staleQueries = queryClient.getQueryCache().findAll({
|
|
249
336
|
queryKey: tableKey,
|
|
250
337
|
predicate: (query) => query.state.fetchStatus === "fetching" || query.state.isInvalidated,
|
|
251
|
-
})
|
|
338
|
+
});
|
|
339
|
+
const staleKeys = staleQueries.map((query) => query.queryKey);
|
|
340
|
+
// Read before cancelling: the cancel reverts the query's state, fetch metadata included.
|
|
341
|
+
const loadingMoreKeys = staleQueries
|
|
342
|
+
.filter((query) => query.state.fetchStatus === "fetching" &&
|
|
343
|
+
query.state.data !== undefined &&
|
|
344
|
+
query.state.fetchMeta?.fetchMore?.direction === "forward" &&
|
|
345
|
+
isPagedListQueryKey(query.queryKey, tableBlockId))
|
|
252
346
|
.map((query) => query.queryKey);
|
|
253
347
|
await queryClient.cancelQueries({
|
|
254
348
|
queryKey: tableKey,
|
|
255
349
|
predicate: (query) => query.state.data !== undefined,
|
|
256
350
|
});
|
|
351
|
+
loadingMoreKeys.forEach((queryKey) => {
|
|
352
|
+
collapsePagedListForRefetch(queryClient, queryKey, { extraRows: pagedListPageSize(queryKey) ?? 0 });
|
|
353
|
+
});
|
|
257
354
|
return { staleKeys };
|
|
258
355
|
};
|
|
259
356
|
/**
|
|
@@ -266,10 +363,11 @@ export const prepareEntityCacheWrite = async (queryClient, tableBlockId) => {
|
|
|
266
363
|
*
|
|
267
364
|
* Unfiltered lists and id-keyed one-queries are NOT invalidated - they were fully
|
|
268
365
|
* reconciled from the server response, which is what saves the refetch round trips.
|
|
366
|
+
* Paged lists are collapsed before they are invalidated, so each refetches in one request.
|
|
269
367
|
*/
|
|
270
368
|
export const invalidateEntityQueriesAfterWrite = (queryClient, tableBlockId, { staleKeys = [], filteredLists = false, nonIdOneQueries = false, deletedRowIds, }) => {
|
|
271
369
|
const staleHashes = new Set(staleKeys.map((queryKey) => hashKey(queryKey)));
|
|
272
|
-
|
|
370
|
+
const toInvalidate = queryClient.getQueryCache().findAll({
|
|
273
371
|
queryKey: entityTableKey(tableBlockId),
|
|
274
372
|
predicate: (query) => {
|
|
275
373
|
if (staleHashes.has(hashKey(query.queryKey))) {
|
|
@@ -295,9 +393,30 @@ export const invalidateEntityQueriesAfterWrite = (queryClient, tableBlockId, { s
|
|
|
295
393
|
return false;
|
|
296
394
|
},
|
|
297
395
|
});
|
|
396
|
+
if (toInvalidate.length === 0) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Collapse first: the predicate above reads query state that the collapse rewrites.
|
|
400
|
+
toInvalidate.forEach((query) => {
|
|
401
|
+
if (isPagedListQueryKey(query.queryKey, tableBlockId)) {
|
|
402
|
+
collapsePagedListForRefetch(queryClient, query.queryKey);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
const invalidateHashes = new Set(toInvalidate.map((query) => query.queryHash));
|
|
406
|
+
void queryClient.invalidateQueries({
|
|
407
|
+
queryKey: entityTableKey(tableBlockId),
|
|
408
|
+
predicate: (query) => invalidateHashes.has(query.queryHash),
|
|
409
|
+
});
|
|
298
410
|
};
|
|
299
411
|
/** Full-table invalidation - the pre-optimistic behavior, kept as the safety fallback. */
|
|
300
412
|
export const invalidateEntityTable = (queryClient, tableBlockId) => {
|
|
413
|
+
queryClient
|
|
414
|
+
.getQueryCache()
|
|
415
|
+
.findAll({
|
|
416
|
+
queryKey: entityTableKey(tableBlockId),
|
|
417
|
+
predicate: (query) => isPagedListQueryKey(query.queryKey, tableBlockId),
|
|
418
|
+
})
|
|
419
|
+
.forEach((query) => collapsePagedListForRefetch(queryClient, query.queryKey));
|
|
301
420
|
void queryClient.invalidateQueries({ queryKey: entityTableKey(tableBlockId) });
|
|
302
421
|
};
|
|
303
422
|
/**
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EntityConfig, EntityType } from "./Entity.js";
|
|
2
|
+
/** A column of the entity: one of its fields, system fields included (`id`, `createdAt`, ...). */
|
|
3
|
+
export type EntityColumn<E extends EntityConfig> = Extract<keyof EntityType<E>, string>;
|
|
4
|
+
export type EntityWhereOperator = "=" | "!=" | ">" | ">=" | "<" | "<=" | "in" | "not in" | "is null" | "is not null";
|
|
5
|
+
/**
|
|
6
|
+
* A where clause: one condition, or `and` / `or` of nested clauses. `operator` defaults to `"="`;
|
|
7
|
+
* `in` / `not in` take an array `value`; `is null` / `is not null` take no value. `not: true`
|
|
8
|
+
* negates the clause. `expression` is a server-validated SQL-like condition over column names,
|
|
9
|
+
* for comparisons a plain condition cannot express.
|
|
10
|
+
*/
|
|
11
|
+
export type EntityWhereClause<E extends EntityConfig = EntityConfig> = {
|
|
12
|
+
column: EntityColumn<E>;
|
|
13
|
+
operator?: EntityWhereOperator;
|
|
14
|
+
value?: unknown;
|
|
15
|
+
not?: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
and: EntityWhereClause<E>[];
|
|
18
|
+
not?: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
or: EntityWhereClause<E>[];
|
|
21
|
+
not?: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
expression: string;
|
|
24
|
+
not?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export interface EntityOrderBy<E extends EntityConfig = EntityConfig> {
|
|
27
|
+
column: EntityColumn<E>;
|
|
28
|
+
direction?: "asc" | "desc";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Query for `findMany` / `findPage` / `useEntityGetAll`.
|
|
32
|
+
*
|
|
33
|
+
* - `where`: which rows (omit for all rows).
|
|
34
|
+
* - `orderBy`: row order; `id` is always the final tie-breaker, so the order is stable.
|
|
35
|
+
* - `page.size`: rows per page. With `useEntityGetAll` it turns on paging (`fetchNextPage`);
|
|
36
|
+
* with `findMany` it caps the result at the first page.
|
|
37
|
+
*/
|
|
38
|
+
export interface EntityQuery<E extends EntityConfig = EntityConfig> {
|
|
39
|
+
where?: EntityWhereClause<E>;
|
|
40
|
+
orderBy?: EntityOrderBy<E>[];
|
|
41
|
+
page?: {
|
|
42
|
+
size: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** One page of rows and the cursor of the next page (`null` after the last page). */
|
|
46
|
+
export interface EntityPage<E extends EntityConfig = EntityConfig> {
|
|
47
|
+
rows: EntityType<E>[];
|
|
48
|
+
nextCursor: string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether an argument is an {@link EntityQuery} rather than legacy key/value filters.
|
|
52
|
+
*
|
|
53
|
+
* Legacy filters (`{ status: "todo" }`, `{ id: [1, 2] }`) are equality / IN matches on column
|
|
54
|
+
* values, which are primitives or arrays of primitives. A query uses only the keys `where`,
|
|
55
|
+
* `orderBy` and `page`, each holding a structure no legacy filter value can have (a clause
|
|
56
|
+
* object, an array of `{ column }` objects, `{ size }`), so every existing call keeps its
|
|
57
|
+
* meaning.
|
|
58
|
+
*
|
|
59
|
+
* A query key may also be left empty (`undefined`, `null`, or `orderBy: []`) as long as at least
|
|
60
|
+
* one other key holds a real query structure — otherwise a conditionally built query would be
|
|
61
|
+
* misread as filters on columns named `where` / `orderBy` / `page` and return the whole table.
|
|
62
|
+
*/
|
|
63
|
+
export declare const isEntityQuery: (value: unknown) => value is EntityQuery<any>;
|
|
64
|
+
/** The data-service query options for an {@link EntityQuery}. */
|
|
65
|
+
export declare const toQueryOptions: (query: EntityQuery<any>, cursor?: string) => Record<string, unknown>;
|
|
66
|
+
//# sourceMappingURL=EntityQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityQuery.d.ts","sourceRoot":"","sources":["../src/EntityQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEzD,kGAAkG;AAClG,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,YAAY,IAAI,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAExF,MAAM,MAAM,mBAAmB,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAErH;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,IAC/D;IAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3F;IAAE,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9C;IAAE,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7C;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1C,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY;IAClE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IACxB,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY;IAChE,KAAK,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB;AAED,qFAAqF;AACrF,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY;IAC/D,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAuBD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,WAAW,CAAC,GAAG,CAatE,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,cAAc,GAAI,OAAO,WAAW,CAAC,GAAG,CAAC,EAAE,SAAS,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAK9F,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
|
+
const QUERY_KEY_CHECKS = {
|
|
3
|
+
where: (value) => isPlainObject(value) &&
|
|
4
|
+
(typeof value.column === "string" ||
|
|
5
|
+
typeof value.expression === "string" ||
|
|
6
|
+
Array.isArray(value.and) ||
|
|
7
|
+
Array.isArray(value.or)),
|
|
8
|
+
orderBy: (value) => Array.isArray(value) &&
|
|
9
|
+
value.length > 0 &&
|
|
10
|
+
value.every((item) => isPlainObject(item) && typeof item.column === "string"),
|
|
11
|
+
page: (value) => isPlainObject(value) && typeof value.size === "number",
|
|
12
|
+
};
|
|
13
|
+
/** A query key left empty by a conditional (`where: status ? {...} : undefined`, `orderBy: []`). */
|
|
14
|
+
const isEmptyQueryValue = (key, value) => value === undefined || value === null || (key === "orderBy" && Array.isArray(value) && value.length === 0);
|
|
15
|
+
/**
|
|
16
|
+
* Whether an argument is an {@link EntityQuery} rather than legacy key/value filters.
|
|
17
|
+
*
|
|
18
|
+
* Legacy filters (`{ status: "todo" }`, `{ id: [1, 2] }`) are equality / IN matches on column
|
|
19
|
+
* values, which are primitives or arrays of primitives. A query uses only the keys `where`,
|
|
20
|
+
* `orderBy` and `page`, each holding a structure no legacy filter value can have (a clause
|
|
21
|
+
* object, an array of `{ column }` objects, `{ size }`), so every existing call keeps its
|
|
22
|
+
* meaning.
|
|
23
|
+
*
|
|
24
|
+
* A query key may also be left empty (`undefined`, `null`, or `orderBy: []`) as long as at least
|
|
25
|
+
* one other key holds a real query structure — otherwise a conditionally built query would be
|
|
26
|
+
* misread as filters on columns named `where` / `orderBy` / `page` and return the whole table.
|
|
27
|
+
*/
|
|
28
|
+
export const isEntityQuery = (value) => {
|
|
29
|
+
if (!isPlainObject(value)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const keys = Object.keys(value);
|
|
33
|
+
if (!keys.every((key) => key in QUERY_KEY_CHECKS)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const queryKeys = keys;
|
|
37
|
+
return (queryKeys.some((key) => QUERY_KEY_CHECKS[key](value[key])) &&
|
|
38
|
+
queryKeys.every((key) => QUERY_KEY_CHECKS[key](value[key]) || isEmptyQueryValue(key, value[key])));
|
|
39
|
+
};
|
|
40
|
+
/** The data-service query options for an {@link EntityQuery}. */
|
|
41
|
+
export const toQueryOptions = (query, cursor) => ({
|
|
42
|
+
...(query.where ? { where: query.where } : {}),
|
|
43
|
+
...(query.orderBy?.length ? { orderBy: query.orderBy } : {}),
|
|
44
|
+
...(query.page ? { limit: query.page.size } : {}),
|
|
45
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
46
|
+
});
|
package/dist/ReactClientSdk.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AgentConfig } from "./Agent.js";
|
|
|
3
3
|
import type { AgentChatConfig } from "./AgentChat.js";
|
|
4
4
|
import { ClientSdk, UpdateUserOptions } from "./ClientSdk.js";
|
|
5
5
|
import type { EntityAttribution, EntityConfig, EntityType, EntityTypeOnlyMutable } from "./Entity.js";
|
|
6
|
+
import { type EntityQuery } from "./EntityQuery.js";
|
|
6
7
|
import type { Integration } from "./Integration.js";
|
|
7
8
|
import type { PageConfig } from "./Page.js";
|
|
8
9
|
export declare class ReactClientSdk extends ClientSdk {
|
|
@@ -67,24 +68,23 @@ export declare const ClientProvider: ({ client, children, themeMode, setThemeMod
|
|
|
67
68
|
*/
|
|
68
69
|
export declare const useClient: () => ReactClientSdk;
|
|
69
70
|
/**
|
|
70
|
-
* Hook to fetch
|
|
71
|
+
* Hook to fetch the entities that match a query
|
|
71
72
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* - instanceType: TypeScript type defining the entity's structure
|
|
73
|
+
* Pass an {@link EntityQuery} as the 2nd argument: `where` picks the rows, `orderBy` orders
|
|
74
|
+
* them, and `page.size` turns on paging — `data` then holds every page loaded so far and
|
|
75
|
+
* `fetchNextPage()` loads the next one. Without `page`, one request returns up to the server's
|
|
76
|
+
* default row limit. Legacy key/value filters (`{ status: "todo" }`) are still accepted and keep
|
|
77
|
+
* their meaning; new code should use a query.
|
|
78
78
|
*
|
|
79
79
|
* @template E - Entity configuration type
|
|
80
80
|
* @param {E} entityConfig - Configuration for the entity type
|
|
81
|
-
* @param {any} [
|
|
81
|
+
* @param {EntityQuery<E> | Record<string, any>} [query] - The query (or legacy filters)
|
|
82
82
|
* @param {Object} [queryOptions] - Optional React Query configuration options
|
|
83
83
|
* @param {boolean} [queryOptions.enabled] - Whether the query should automatically execute
|
|
84
|
-
* @param {EntityType<E>[]} [queryOptions.initialData] - Initial data to use for the query
|
|
84
|
+
* @param {EntityType<E>[]} [queryOptions.initialData] - Initial data to use for the query (not used when paging)
|
|
85
85
|
* @param {EntityType<E>[]} [queryOptions.placeholderData] - Placeholder data to use while loading
|
|
86
86
|
* @returns {Object} Query result with data, loading state, and error
|
|
87
|
-
* @returns {EntityType<E>[] | undefined} returns.data - The fetched entities
|
|
87
|
+
* @returns {EntityType<E>[] | undefined} returns.data - The fetched entities (every loaded page, in order, when paging)
|
|
88
88
|
* @returns {boolean} returns.isLoading - Whether the query is loading
|
|
89
89
|
* @returns {Error | null} returns.error - Any error that occurred
|
|
90
90
|
* @returns {Function} returns.refetch - Intentional no-op kept for API compatibility; returns the current state without fetching. Data is kept fresh automatically (mutation cache writes + websocket live sync), so manual refetching is unsupported.
|
|
@@ -93,23 +93,32 @@ export declare const useClient: () => ReactClientSdk;
|
|
|
93
93
|
* @returns {boolean} returns.isFetching - Whether the query is currently fetching
|
|
94
94
|
* @returns {boolean} returns.isSuccess - Whether the query was successful
|
|
95
95
|
* @returns {string} returns.status - Current status of the query: 'pending', 'error', or 'success'
|
|
96
|
+
* @returns {boolean} returns.hasNextPage - Whether another page exists (always false without `page`)
|
|
97
|
+
* @returns {Function} returns.fetchNextPage - Loads the next page; a no-op without `page`, while one is loading, or after the last page
|
|
98
|
+
* @returns {boolean} returns.isFetchingNextPage - Whether the next page is loading
|
|
96
99
|
* @example
|
|
97
100
|
* ```tsx
|
|
98
|
-
* import {
|
|
101
|
+
* import { TaskEntity } from '@/product-types';
|
|
99
102
|
*
|
|
100
|
-
* function
|
|
101
|
-
* const { data:
|
|
103
|
+
* function OpenTasks() {
|
|
104
|
+
* const { data: tasks, hasNextPage, fetchNextPage, isFetchingNextPage } = useEntityGetAll(TaskEntity, {
|
|
105
|
+
* where: { column: 'status', operator: '!=', value: 'done' },
|
|
106
|
+
* orderBy: [{ column: 'createdAt', direction: 'desc' }],
|
|
107
|
+
* page: { size: 50 },
|
|
108
|
+
* });
|
|
102
109
|
*
|
|
103
|
-
* if (isLoading) return <div>Loading...</div>;
|
|
104
110
|
* return (
|
|
105
|
-
*
|
|
106
|
-
* {
|
|
107
|
-
*
|
|
111
|
+
* <>
|
|
112
|
+
* <ul>{tasks?.map(task => <li key={task.id}>{task.title}</li>)}</ul>
|
|
113
|
+
* {hasNextPage && (
|
|
114
|
+
* <button onClick={fetchNextPage} disabled={isFetchingNextPage}>Load more</button>
|
|
115
|
+
* )}
|
|
116
|
+
* </>
|
|
108
117
|
* );
|
|
109
118
|
* }
|
|
110
119
|
* ```
|
|
111
120
|
*/
|
|
112
|
-
export declare const useEntityGetAll: <E extends EntityConfig>(entityConfig: E,
|
|
121
|
+
export declare const useEntityGetAll: <E extends EntityConfig>(entityConfig: E, query?: EntityQuery<E> | Record<string, any>, queryOptions?: {
|
|
113
122
|
enabled?: boolean;
|
|
114
123
|
initialData?: EntityType<E>[];
|
|
115
124
|
placeholderData?: EntityType<E>[];
|
|
@@ -123,6 +132,9 @@ export declare const useEntityGetAll: <E extends EntityConfig>(entityConfig: E,
|
|
|
123
132
|
isFetching: boolean;
|
|
124
133
|
isSuccess: boolean;
|
|
125
134
|
status: "pending" | "error" | "success";
|
|
135
|
+
hasNextPage: boolean;
|
|
136
|
+
fetchNextPage: () => void;
|
|
137
|
+
isFetchingNextPage: boolean;
|
|
126
138
|
};
|
|
127
139
|
/**
|
|
128
140
|
* Hook to fetch a single entity by filters
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactClientSdk.d.ts","sourceRoot":"","sources":["../src/ReactClientSdk.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReactClientSdk.d.ts","sourceRoot":"","sources":["../src/ReactClientSdk.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAenG,OAAO,EAAmB,KAAK,WAAW,EAAiB,MAAM,eAAe,CAAC;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEzC,qBAAa,cAAe,SAAQ,SAAS;CAAG;AA+ChD,oBAAY,iBAAiB;IAC3B,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;CACpC;AA+ID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,cAAc,GAAI,gDAK5B;IACD,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvC,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,KAAK,IAAI,CAAC;CAChE,gCAqEA,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS,sBAOrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,YAAY,EACpD,cAAc,CAAC,EACf,QAAQ,WAAW,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5C,eAAc;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,eAAe,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;CAC9B,KACL;IACD,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,CAAC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACxC,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;CA+D7B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,eAAe,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACpE,cAAc,EAAE,EAChB,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,eAAc;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;CAChC,KACL;IACD,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;CA+BzC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,oBAAoB,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACzE,cAAc,EAAE,EAChB,OAAO,MAAM,GAAG,SAAS,EACzB,eAAc;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,KACvC;IACD,IAAI,EAAE,iBAAiB,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CAUpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,eAAe,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACpE,cAAc,EAAE,KACf;IACD,cAAc,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAAE,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC,CAAA;KAAE,KAAK,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3F,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CA4BrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,mBAAmB,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACxE,cAAc,EAAE,KACf;IACD,kBAAkB,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAAE,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACnG,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CA4BrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,eAAe,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACpE,cAAc,EAAE,KACf;IACD,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAA;KAAE,KAAK,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IACpH,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAyCrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,eAAe,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACpE,cAAc,EAAE,KACf;IACD,cAAc,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAsCrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,mBAAmB,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EACxE,cAAc,EAAE,KACf;IACD,kBAAkB,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE;QAAE,GAAG,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAsCrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4GG;AACH,eAAO,MAAM,gBAAgB,GAAI,EAAE,SAAS,YAAY,GAAG,YAAY,EAAE,cAAc,EAAE;;;;;;;;CAiExF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,SAAS,eAAe,GAAG,eAAe,EAAE,iBAAiB,GAAG,yCAG/F,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,QAAQ,GAAI,EAAE,SAAS,WAAW,GAAG,WAAW,EAAE,aAAa,EAAE,gCAG7E,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,UAAU,GACrB,aAAa,WAAW,GAAG,eAAe,KACzC;IAAE,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAerE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,aAAa,QAAO;IAC/B,cAAc,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAyB1B,CAAC;AAEF;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,OAAO,kCASnB,CAAC;AAcF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EACnD,UAAU,CAAC,KACV;IACD,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAMrB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,qBAAqB;;4BAIW,MAAM;;;CAGlD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,YAAY,QAAO;IAC9B,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvC,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,KAAK,IAAI,CAAC;CAIhE,CAAC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,iBAAiB,GAAI,cAAa;IAAE,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO;;gBAWtC,MAAM;cAAQ,MAAM;;;;;CAwBhE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,gBAAgB;+BAOP;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE;;;;CAmBtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,cAAc,cAG1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,EAAE,SAAS,UAAU,GAAG,UAAU,EAAE,YAAY,EAAE,2BAS/E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,EAAE,SAAS,UAAU,GAAG,UAAU,EAAE,YAAY,EAAE,WAI5E,CAAC"}
|
package/dist/ReactClientSdk.jsx
CHANGED
|
@@ -3,12 +3,13 @@ import { FatalAppError } from "@blocksdiy/blocks-client-api/errors";
|
|
|
3
3
|
import { useWebsockets, useWebsocketsAppSubscribe } from "@blocksdiy/blocks-client-api/websocketService";
|
|
4
4
|
import { websocketsService } from "@blocksdiy/blocks-client-api/websocketService";
|
|
5
5
|
import { useAiStreamChunks } from "@blocksdiy/react-common/useAiStreamChunks";
|
|
6
|
-
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
+
import { QueryClient, QueryClientProvider, useInfiniteQuery, useMutation, useQuery, useQueryClient, } from "@tanstack/react-query";
|
|
7
7
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
8
8
|
import { agentRefOf, isBlockId, matchesAgentRef } from "./AgentRef.js";
|
|
9
9
|
import { ClientSdk } from "./ClientSdk.js";
|
|
10
|
-
import { applyEntityCreateResult, applyEntityDeleteResult, applyEntityRowPatch, applyEntityRowsDeleted, beginEntityOptimisticWrite, ENTITIES_QUERY_KEY, entityAttributionKey, rollbackEntityOptimisticWrite, } from "./EntityCache.js";
|
|
10
|
+
import { applyEntityCreateResult, applyEntityDeleteResult, applyEntityRowPatch, applyEntityRowsDeleted, beginEntityOptimisticWrite, ENTITIES_QUERY_KEY, entityAttributionKey, entityPagesKey, fetchEntityPagedListPage, rollbackEntityOptimisticWrite, } from "./EntityCache.js";
|
|
11
11
|
import { createEntityLiveSync } from "./EntityLiveSync.js";
|
|
12
|
+
import { isEntityQuery } from "./EntityQuery.js";
|
|
12
13
|
export class ReactClientSdk extends ClientSdk {
|
|
13
14
|
}
|
|
14
15
|
const CURRENT_USER_QUERY_KEY = "currentUser";
|
|
@@ -246,24 +247,23 @@ export const useClient = () => {
|
|
|
246
247
|
return client;
|
|
247
248
|
};
|
|
248
249
|
/**
|
|
249
|
-
* Hook to fetch
|
|
250
|
+
* Hook to fetch the entities that match a query
|
|
250
251
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* - instanceType: TypeScript type defining the entity's structure
|
|
252
|
+
* Pass an {@link EntityQuery} as the 2nd argument: `where` picks the rows, `orderBy` orders
|
|
253
|
+
* them, and `page.size` turns on paging — `data` then holds every page loaded so far and
|
|
254
|
+
* `fetchNextPage()` loads the next one. Without `page`, one request returns up to the server's
|
|
255
|
+
* default row limit. Legacy key/value filters (`{ status: "todo" }`) are still accepted and keep
|
|
256
|
+
* their meaning; new code should use a query.
|
|
257
257
|
*
|
|
258
258
|
* @template E - Entity configuration type
|
|
259
259
|
* @param {E} entityConfig - Configuration for the entity type
|
|
260
|
-
* @param {any} [
|
|
260
|
+
* @param {EntityQuery<E> | Record<string, any>} [query] - The query (or legacy filters)
|
|
261
261
|
* @param {Object} [queryOptions] - Optional React Query configuration options
|
|
262
262
|
* @param {boolean} [queryOptions.enabled] - Whether the query should automatically execute
|
|
263
|
-
* @param {EntityType<E>[]} [queryOptions.initialData] - Initial data to use for the query
|
|
263
|
+
* @param {EntityType<E>[]} [queryOptions.initialData] - Initial data to use for the query (not used when paging)
|
|
264
264
|
* @param {EntityType<E>[]} [queryOptions.placeholderData] - Placeholder data to use while loading
|
|
265
265
|
* @returns {Object} Query result with data, loading state, and error
|
|
266
|
-
* @returns {EntityType<E>[] | undefined} returns.data - The fetched entities
|
|
266
|
+
* @returns {EntityType<E>[] | undefined} returns.data - The fetched entities (every loaded page, in order, when paging)
|
|
267
267
|
* @returns {boolean} returns.isLoading - Whether the query is loading
|
|
268
268
|
* @returns {Error | null} returns.error - Any error that occurred
|
|
269
269
|
* @returns {Function} returns.refetch - Intentional no-op kept for API compatibility; returns the current state without fetching. Data is kept fresh automatically (mutation cache writes + websocket live sync), so manual refetching is unsupported.
|
|
@@ -272,32 +272,66 @@ export const useClient = () => {
|
|
|
272
272
|
* @returns {boolean} returns.isFetching - Whether the query is currently fetching
|
|
273
273
|
* @returns {boolean} returns.isSuccess - Whether the query was successful
|
|
274
274
|
* @returns {string} returns.status - Current status of the query: 'pending', 'error', or 'success'
|
|
275
|
+
* @returns {boolean} returns.hasNextPage - Whether another page exists (always false without `page`)
|
|
276
|
+
* @returns {Function} returns.fetchNextPage - Loads the next page; a no-op without `page`, while one is loading, or after the last page
|
|
277
|
+
* @returns {boolean} returns.isFetchingNextPage - Whether the next page is loading
|
|
275
278
|
* @example
|
|
276
279
|
* ```tsx
|
|
277
|
-
* import {
|
|
280
|
+
* import { TaskEntity } from '@/product-types';
|
|
278
281
|
*
|
|
279
|
-
* function
|
|
280
|
-
* const { data:
|
|
282
|
+
* function OpenTasks() {
|
|
283
|
+
* const { data: tasks, hasNextPage, fetchNextPage, isFetchingNextPage } = useEntityGetAll(TaskEntity, {
|
|
284
|
+
* where: { column: 'status', operator: '!=', value: 'done' },
|
|
285
|
+
* orderBy: [{ column: 'createdAt', direction: 'desc' }],
|
|
286
|
+
* page: { size: 50 },
|
|
287
|
+
* });
|
|
281
288
|
*
|
|
282
|
-
* if (isLoading) return <div>Loading...</div>;
|
|
283
289
|
* return (
|
|
284
|
-
*
|
|
285
|
-
* {
|
|
286
|
-
*
|
|
290
|
+
* <>
|
|
291
|
+
* <ul>{tasks?.map(task => <li key={task.id}>{task.title}</li>)}</ul>
|
|
292
|
+
* {hasNextPage && (
|
|
293
|
+
* <button onClick={fetchNextPage} disabled={isFetchingNextPage}>Load more</button>
|
|
294
|
+
* )}
|
|
295
|
+
* </>
|
|
287
296
|
* );
|
|
288
297
|
* }
|
|
289
298
|
* ```
|
|
290
299
|
*/
|
|
291
|
-
export const useEntityGetAll = (entityConfig,
|
|
292
|
-
const { enabled, initialData, placeholderData } = queryOptions;
|
|
300
|
+
export const useEntityGetAll = (entityConfig, query, queryOptions = {}) => {
|
|
301
|
+
const { enabled = true, initialData, placeholderData } = queryOptions;
|
|
293
302
|
const client = useClient();
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
const pagedQuery = isEntityQuery(query) && query.page ? query : undefined;
|
|
304
|
+
// Both hooks are always called (rules of hooks); exactly one is enabled. The plain list keeps
|
|
305
|
+
// the key every existing call has always used, so its cache entry and live sync are unchanged.
|
|
306
|
+
const list = useQuery({
|
|
307
|
+
queryKey: [ENTITIES_QUERY_KEY, entityConfig.tableBlockId, query],
|
|
308
|
+
queryFn: () => client.entity(entityConfig).findMany(query),
|
|
309
|
+
enabled: enabled && !pagedQuery,
|
|
310
|
+
initialData: pagedQuery ? undefined : initialData,
|
|
299
311
|
placeholderData,
|
|
300
312
|
});
|
|
313
|
+
const pages = useInfiniteQuery({
|
|
314
|
+
queryKey: entityPagesKey(entityConfig.tableBlockId, pagedQuery ?? null),
|
|
315
|
+
// A collapsed first page re-reads every loaded row in one request (see EntityCache).
|
|
316
|
+
queryFn: ({ pageParam }) => fetchEntityPagedListPage((pageQuery, cursor) => client.entity(entityConfig).findPage(pageQuery, cursor), pagedQuery ?? {}, pageParam),
|
|
317
|
+
initialPageParam: null,
|
|
318
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
319
|
+
enabled: enabled && !!pagedQuery,
|
|
320
|
+
placeholderData: placeholderData
|
|
321
|
+
? { pages: [{ rows: placeholderData, nextCursor: null }], pageParams: [null] }
|
|
322
|
+
: undefined,
|
|
323
|
+
});
|
|
324
|
+
const pagedData = useMemo(() => pages.data?.pages.flatMap((page) => page.rows), [pages.data]);
|
|
325
|
+
const active = pagedQuery ? pages : list;
|
|
326
|
+
const data = (pagedQuery ? pagedData : list.data);
|
|
327
|
+
const { isLoading, error, isError, isFetched, isFetching, isSuccess, status } = active;
|
|
328
|
+
const hasNextPage = !!pagedQuery && pages.hasNextPage;
|
|
329
|
+
const { fetchNextPage: fetchNextPageQuery, isFetchingNextPage } = pages;
|
|
330
|
+
const fetchNextPage = useCallback(() => {
|
|
331
|
+
if (hasNextPage && !isFetchingNextPage) {
|
|
332
|
+
void fetchNextPageQuery();
|
|
333
|
+
}
|
|
334
|
+
}, [hasNextPage, isFetchingNextPage, fetchNextPageQuery]);
|
|
301
335
|
return {
|
|
302
336
|
data,
|
|
303
337
|
isLoading,
|
|
@@ -310,6 +344,9 @@ export const useEntityGetAll = (entityConfig, filters, queryOptions = {}) => {
|
|
|
310
344
|
isFetching,
|
|
311
345
|
isSuccess,
|
|
312
346
|
status,
|
|
347
|
+
hasNextPage,
|
|
348
|
+
fetchNextPage,
|
|
349
|
+
isFetchingNextPage: !!pagedQuery && isFetchingNextPage,
|
|
313
350
|
};
|
|
314
351
|
};
|
|
315
352
|
/**
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocksdiy/blocks-client-sdk",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Blocks client sdk",
|
|
6
6
|
"keywords": [],
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@tanstack/react-query": "^5.101.0",
|
|
50
|
-
"@blocksdiy/blocks-client-api": "1.
|
|
51
|
-
"@blocksdiy/react-common": "1.50.
|
|
50
|
+
"@blocksdiy/blocks-client-api": "1.25.1",
|
|
51
|
+
"@blocksdiy/react-common": "1.50.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"tsc-alias": "^1.8.10",
|