@asteby/metacore-runtime-react 39.2.7 → 39.2.8
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/CHANGELOG.md +6 -0
- package/dist/api-context.d.ts.map +1 -1
- package/dist/api-context.js +10 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +8 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/list-include.d.ts +9 -0
- package/dist/list-include.d.ts.map +1 -0
- package/dist/list-include.js +36 -0
- package/dist/query-batch.d.ts +34 -0
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +202 -5
- package/package.json +1 -1
- package/src/api-context.tsx +10 -2
- package/src/dynamic-table.tsx +10 -2
- package/src/index.ts +8 -0
- package/src/list-include.test.ts +33 -0
- package/src/list-include.ts +34 -0
- package/src/query-batch.test.ts +44 -1
- package/src/query-batch.ts +223 -5
- package/vitest.config.ts +1 -0
- package/vitest.setup.ts +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 39.2.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 871800c: useApi().get agrupa lista, metadata y options en un POST /api/q, guarda cada id de un in: durante 30 s y DynamicTable pide include= solo de las columnas visibles.
|
|
8
|
+
|
|
3
9
|
## 39.2.7
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-context.d.ts","sourceRoot":"","sources":["../src/api-context.tsx"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"api-context.d.ts","sourceRoot":"","sources":["../src/api-context.tsx"],"names":[],"mappings":"AAIA,OAAO,KAA6C,MAAM,OAAO,CAAA;AAGjE,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACtB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;IACzE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;IACrF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAA;CAC/E;AAID,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,SAAS,CAAA;IACjB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAED,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,gBAAgB,qBAEjE;AAED,mFAAmF;AACnF,wBAAgB,MAAM,IAAI,SAAS,CAalC;AAED;;wEAEwE;AACxE,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CACzC;AAID,MAAM,WAAW,mBAAmB;IAChC,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAED,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,mBAAmB,qBAEvE;AAED,wBAAgB,gBAAgB,IAAI,WAAW,CAE9C"}
|
package/dist/api-context.js
CHANGED
|
@@ -3,7 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
// runtime-react components (DynamicTable, dialogs, action dispatcher) can
|
|
4
4
|
// talk to the backend without a bundler alias to `@/lib/api`. Hosts wrap
|
|
5
5
|
// their app in <ApiProvider value={axiosInstance}> once at the root.
|
|
6
|
-
import { createContext, useContext } from 'react';
|
|
6
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
7
|
+
import { batchGet } from './query-batch';
|
|
7
8
|
const ApiContext = createContext(null);
|
|
8
9
|
export function ApiProvider({ client, children }) {
|
|
9
10
|
return _jsx(ApiContext.Provider, { value: client, children: children });
|
|
@@ -14,7 +15,14 @@ export function useApi() {
|
|
|
14
15
|
if (!ctx) {
|
|
15
16
|
throw new Error('useApi() requires an <ApiProvider> ancestor. Hosts must inject an axios-like client via runtime-react ApiProvider.');
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
// List, metadata and options GETs share one POST /api/q. Mutations stay
|
|
19
|
+
// on the host client. The wrapper is stable while the host client is.
|
|
20
|
+
return useMemo(() => ({
|
|
21
|
+
get: (url, config) => batchGet(ctx, url, config),
|
|
22
|
+
post: (url, body, config) => ctx.post(url, body, config),
|
|
23
|
+
put: (url, body, config) => ctx.put(url, body, config),
|
|
24
|
+
delete: (url, config) => ctx.delete(url, config),
|
|
25
|
+
}), [ctx]);
|
|
18
26
|
}
|
|
19
27
|
const BranchContext = createContext({ id: undefined });
|
|
20
28
|
export function BranchProvider({ branch, children }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAwGnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;CACpC;AAQD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,aAAoB,EACpB,UAAU,EACV,UAAU,EACV,aAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,kBAA0B,EAC1C,cAAc,GACjB,EAAE,iBAAiB,+BAuiDnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -31,6 +31,7 @@ import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll';
|
|
|
31
31
|
import { DYNAMIC_TABLE_CARD_ESTIMATE_PX, DYNAMIC_TABLE_ROW_ESTIMATE_PX, resolveVirtualizeThreshold, } from './table-virtualization';
|
|
32
32
|
import { OptionsContext } from './options-context';
|
|
33
33
|
import { getSearchableColumnKeys } from './column-visibility';
|
|
34
|
+
import { visibleRelationInclude } from './list-include';
|
|
34
35
|
import { useDebouncedValue } from './use-debounced-value';
|
|
35
36
|
import { useCan, usePermissionsActive, gateTableMetadata } from './permissions-context';
|
|
36
37
|
import { useDynamicRowActions } from './dynamic-row-actions';
|
|
@@ -649,6 +650,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
649
650
|
set.add(k);
|
|
650
651
|
return Array.from(set);
|
|
651
652
|
}, [hiddenColumns, scopeHiddenColumns]);
|
|
653
|
+
const relationInclude = useMemo(() => visibleRelationInclude(metadata?.columns, effectiveHiddenColumns), [metadata, effectiveHiddenColumns]);
|
|
652
654
|
const buildFilterParams = useCallback(() => {
|
|
653
655
|
const params = {};
|
|
654
656
|
if (sorting.length > 0) {
|
|
@@ -719,6 +721,8 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
719
721
|
per_page: pagination.pageSize,
|
|
720
722
|
...buildFilterParams(),
|
|
721
723
|
};
|
|
724
|
+
if (relationInclude)
|
|
725
|
+
params.include = relationInclude;
|
|
722
726
|
const res = await api.get(endpoint || `/data/${model}`, { params });
|
|
723
727
|
setForbidden(false);
|
|
724
728
|
if (res.data.success) {
|
|
@@ -746,7 +750,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
746
750
|
finally {
|
|
747
751
|
setLoadingData(false);
|
|
748
752
|
}
|
|
749
|
-
}, [model, metadata, pagination, buildFilterParams, refreshTrigger, endpoint, currentBranch?.id, api, enableUrlSync]);
|
|
753
|
+
}, [model, metadata, pagination, buildFilterParams, refreshTrigger, endpoint, currentBranch?.id, api, enableUrlSync, relationInclude]);
|
|
750
754
|
// Columns whose metadata opts into a footer total (display_config.aggregate
|
|
751
755
|
// → styleConfig.aggregate). When empty, no footer row is rendered and no
|
|
752
756
|
// aggregate request is made.
|
|
@@ -784,6 +788,8 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
784
788
|
per_page: infPageSize,
|
|
785
789
|
...buildFilterParams(),
|
|
786
790
|
};
|
|
791
|
+
if (relationInclude)
|
|
792
|
+
params.include = relationInclude;
|
|
787
793
|
const res = (await api.get(endpoint || `/data/${model}`, {
|
|
788
794
|
params,
|
|
789
795
|
}));
|
|
@@ -823,7 +829,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
823
829
|
else
|
|
824
830
|
setLoadingData(false);
|
|
825
831
|
}
|
|
826
|
-
}, [metadata, buildFilterParams, endpoint, model, api, currentBranch?.id, enableUrlSync]);
|
|
832
|
+
}, [metadata, buildFilterParams, endpoint, model, api, currentBranch?.id, enableUrlSync, relationInclude]);
|
|
827
833
|
// Signature of everything that must reset the incremental list to page 1:
|
|
828
834
|
// the filters/search AND the sort (both live in buildFilterParams).
|
|
829
835
|
const filterSignature = useMemo(() => JSON.stringify(buildFilterParams()), [buildFilterParams]);
|
package/dist/index.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export { DynamicRelation, type DynamicRelationProps, type DynamicRelationStrings
|
|
|
74
74
|
export { DynamicRelations, resolveParentId, buildRelationFilters, isEmbedded, type DynamicRelationsProps, } from './dynamic-relations';
|
|
75
75
|
export { registerModelExtension, getModelExtension, clearModelExtensions, type ModelExtension, type ModelExtensionProps, } from './model-extension-registry';
|
|
76
76
|
export { isColumnVisibleInTable, isColumnVisibleInModal, isColumnVisibleInLineSubtable, AUDIT_SUBTABLE_COLUMN_KEYS, getSearchableColumnKeys, } from './column-visibility';
|
|
77
|
-
export { loadQueryPart, nameBatchTokens, optionsBatchToken, optionsModelFromUrl, type QueryPart, } from './query-batch';
|
|
77
|
+
export { batchGet, loadQueryPart, nameBatchTokens, narrowInToken, optionsBatchToken, optionsModelFromUrl, rememberInRows, resetQueryBatchCache, splitInToken, tokenForGet, type InListToken, type NarrowedIn, type QueryPart, } from './query-batch';
|
|
78
78
|
export { useOptionsResolver, projectOption, type ResolvedOption, type OptionsMeta, type UseOptionsResolverArgs, type UseOptionsResolverResult, } from './use-options-resolver';
|
|
79
79
|
export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, type OrgConfigBridge, } from './use-org-config-bridge';
|
|
80
80
|
export { registerValidator, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, getVisibleWhen, evaluateVisibleWhen, scopeValueFromFilterToken, buildListScopeValues, evaluateVisibleWhenForListScope, applyLineItemRowFormulas, computeLineItemTotals, toNumber, } from './dynamic-form-schema';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -74,7 +74,7 @@ export { DynamicRelation, buildRelationFilterParams, buildCreatePayload, deriveR
|
|
|
74
74
|
export { DynamicRelations, resolveParentId, buildRelationFilters, isEmbedded, } from './dynamic-relations';
|
|
75
75
|
export { registerModelExtension, getModelExtension, clearModelExtensions, } from './model-extension-registry';
|
|
76
76
|
export { isColumnVisibleInTable, isColumnVisibleInModal, isColumnVisibleInLineSubtable, AUDIT_SUBTABLE_COLUMN_KEYS, getSearchableColumnKeys, } from './column-visibility';
|
|
77
|
-
export { loadQueryPart, nameBatchTokens, optionsBatchToken, optionsModelFromUrl, } from './query-batch';
|
|
77
|
+
export { batchGet, loadQueryPart, nameBatchTokens, narrowInToken, optionsBatchToken, optionsModelFromUrl, rememberInRows, resetQueryBatchCache, splitInToken, tokenForGet, } from './query-batch';
|
|
78
78
|
export { useOptionsResolver, projectOption, } from './use-options-resolver';
|
|
79
79
|
export { setOrgConfigBridge, getOrgConfigBridge, resolveValidatorToken, } from './use-org-config-bridge';
|
|
80
80
|
export { registerValidator, getDependsOn, resolveDependsValue, getOptionsConfig, resolveOptionsSource, getVisibleWhen, evaluateVisibleWhen, scopeValueFromFilterToken, buildListScopeValues, evaluateVisibleWhenForListScope, applyLineItemRowFormulas, computeLineItemTotals, toNumber, } from './dynamic-form-schema';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ColumnDefinition } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Columns whose labels come from a relation lookup. The list asks the server
|
|
4
|
+
* to resolve only these, so hidden foreign keys do not each become a query.
|
|
5
|
+
* Returns undefined when nothing relational is visible: the server then keeps
|
|
6
|
+
* resolving everything, which is what hosts do before they send `include=`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function visibleRelationInclude(columns: ColumnDefinition[] | undefined, hidden?: string[]): string | undefined;
|
|
9
|
+
//# sourceMappingURL=list-include.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-include.d.ts","sourceRoot":"","sources":["../src/list-include.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAClC,OAAO,EAAE,gBAAgB,EAAE,GAAG,SAAS,EACvC,MAAM,GAAE,MAAM,EAAO,GACtB,MAAM,GAAG,SAAS,CAYpB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Columns whose labels come from a relation lookup. The list asks the server
|
|
3
|
+
* to resolve only these, so hidden foreign keys do not each become a query.
|
|
4
|
+
* Returns undefined when nothing relational is visible: the server then keeps
|
|
5
|
+
* resolving everything, which is what hosts do before they send `include=`.
|
|
6
|
+
*/
|
|
7
|
+
export function visibleRelationInclude(columns, hidden = []) {
|
|
8
|
+
if (!columns?.length)
|
|
9
|
+
return undefined;
|
|
10
|
+
const hide = new Set(hidden);
|
|
11
|
+
const keys = [];
|
|
12
|
+
for (const col of columns) {
|
|
13
|
+
if (!col?.key || col.hidden || hide.has(col.key))
|
|
14
|
+
continue;
|
|
15
|
+
if (col.visibility === 'modal' || col.visibility === 'list')
|
|
16
|
+
continue;
|
|
17
|
+
if (!columnNeedsRelation(col))
|
|
18
|
+
continue;
|
|
19
|
+
keys.push(col.key);
|
|
20
|
+
}
|
|
21
|
+
if (keys.length === 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
return keys.join(',');
|
|
24
|
+
}
|
|
25
|
+
function columnNeedsRelation(col) {
|
|
26
|
+
if (col.type === 'relation' || col.type === 'relation-badge-list' || col.type === 'creator' || col.type === 'user') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (col.cellStyle === 'relation' || col.cellStyle === 'reference')
|
|
30
|
+
return true;
|
|
31
|
+
if (col.ref)
|
|
32
|
+
return true;
|
|
33
|
+
if (col.itemFields?.length || col.item_fields?.length)
|
|
34
|
+
return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
package/dist/query-batch.d.ts
CHANGED
|
@@ -14,6 +14,25 @@ export interface QueryPart {
|
|
|
14
14
|
etag?: string;
|
|
15
15
|
not_modified?: boolean;
|
|
16
16
|
}
|
|
17
|
+
export interface InListToken {
|
|
18
|
+
field: string;
|
|
19
|
+
ids: string[];
|
|
20
|
+
shape: string;
|
|
21
|
+
}
|
|
22
|
+
/** A concrete `t:` `in:` list. `@` references stay whole: the ids are not known yet. */
|
|
23
|
+
export declare function splitInToken(token: string): InListToken | null;
|
|
24
|
+
export interface NarrowedIn {
|
|
25
|
+
wire: string;
|
|
26
|
+
rows: Record<string, unknown>[];
|
|
27
|
+
missingIds: string[];
|
|
28
|
+
field: string;
|
|
29
|
+
shape: string;
|
|
30
|
+
}
|
|
31
|
+
/** Drops ids already cached for this shape. An empty wire means nothing has to travel. */
|
|
32
|
+
export declare function narrowInToken(token: string, now?: number): NarrowedIn | null;
|
|
33
|
+
/** Stores one row per requested id. An id with no row is remembered as empty. */
|
|
34
|
+
export declare function rememberInRows(token: string, ids: string[], rows: unknown, now?: number): void;
|
|
35
|
+
export declare function resetQueryBatchCache(): void;
|
|
17
36
|
export declare function optionsModelFromUrl(url: string): string | null;
|
|
18
37
|
export declare function optionsBatchToken(model: string, field: string, query: string, limit: number | undefined, filter: string | undefined): string;
|
|
19
38
|
/** Names each distinct token so the server can key the response. */
|
|
@@ -23,4 +42,19 @@ export declare function nameBatchTokens(tokens: string[]): {
|
|
|
23
42
|
plan: string;
|
|
24
43
|
}[];
|
|
25
44
|
export declare function loadQueryPart(api: ApiClient, token: string): Promise<QueryPart>;
|
|
45
|
+
/**
|
|
46
|
+
* Token for a host GET that the batch can serve. Custom endpoints, aggregates
|
|
47
|
+
* and `/me` stay on the legacy GET.
|
|
48
|
+
*/
|
|
49
|
+
export declare function tokenForGet(url: string, params?: Record<string, unknown>): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* GET through the batch when the URL is a list, table metadata, modal metadata
|
|
52
|
+
* or options read. Anything the batch refuses falls back to the original GET.
|
|
53
|
+
*/
|
|
54
|
+
export declare function batchGet(api: ApiClient, url: string, config?: {
|
|
55
|
+
params?: Record<string, unknown>;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
data: any;
|
|
58
|
+
headers?: any;
|
|
59
|
+
}>;
|
|
26
60
|
//# sourceMappingURL=query-batch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-batch.d.ts","sourceRoot":"","sources":["../src/query-batch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,MAAM,WAAW,SAAS;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,OAAO,CAAA;CACzB;
|
|
1
|
+
{"version":3,"file":"query-batch.d.ts","sourceRoot":"","sources":["../src/query-batch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,MAAM,WAAW,SAAS;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,OAAO,CAAA;CACzB;AAsBD,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,EAAE,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAsB9D;AAaD,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,UAAU,GAAG,IAAI,CAsBhF;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAC1B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EAAE,EACb,IAAI,EAAE,OAAO,EACb,GAAG,SAAa,GACjB,IAAI,CAqBN;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAM3C;AAsBD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D;AAED,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAOR;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAYjG;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAsB/E;AA6DD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CA2BxF;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE;;;GAwBxG"}
|
package/dist/query-batch.js
CHANGED
|
@@ -1,9 +1,117 @@
|
|
|
1
1
|
const WINDOW_MS = 8;
|
|
2
2
|
const PART_TTL_MS = 30_000;
|
|
3
|
+
const ENTITY_TTL_MS = 30_000;
|
|
3
4
|
const partCache = new Map();
|
|
5
|
+
const entityCache = new Map();
|
|
6
|
+
/** A concrete `t:` `in:` list. `@` references stay whole: the ids are not known yet. */
|
|
7
|
+
export function splitInToken(token) {
|
|
8
|
+
const q = token.indexOf('?');
|
|
9
|
+
if (q < 0 || !token.startsWith('t:'))
|
|
10
|
+
return null;
|
|
11
|
+
const model = token.slice(2, q);
|
|
12
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(model))
|
|
13
|
+
return null;
|
|
14
|
+
const params = new URLSearchParams(token.slice(q + 1));
|
|
15
|
+
let field = '';
|
|
16
|
+
let ids = [];
|
|
17
|
+
for (const [key, value] of params.entries()) {
|
|
18
|
+
if (!value.startsWith('in:'))
|
|
19
|
+
continue;
|
|
20
|
+
if (field || value.includes('@'))
|
|
21
|
+
return null;
|
|
22
|
+
field = key;
|
|
23
|
+
ids = value
|
|
24
|
+
.slice(3)
|
|
25
|
+
.split(',')
|
|
26
|
+
.map((id) => id.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
if (!field || ids.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
const keys = [...new Set(params.keys())].sort();
|
|
32
|
+
const bits = keys.map((key) => `${key}=${key === field ? 'in:$1' : (params.get(key) ?? '')}`);
|
|
33
|
+
return { field, ids, shape: `t:${model}?${bits.join('&')}` };
|
|
34
|
+
}
|
|
35
|
+
function entityKey(scope, shape, id) {
|
|
36
|
+
return `${scope}\n${shape}\n${id}`;
|
|
37
|
+
}
|
|
38
|
+
function replaceInIds(token, field, ids) {
|
|
39
|
+
const q = token.indexOf('?');
|
|
40
|
+
const params = new URLSearchParams(token.slice(q + 1));
|
|
41
|
+
params.set(field, `in:${ids.join(',')}`);
|
|
42
|
+
return `${token.slice(0, q)}?${params.toString()}`;
|
|
43
|
+
}
|
|
44
|
+
/** Drops ids already cached for this shape. An empty wire means nothing has to travel. */
|
|
45
|
+
export function narrowInToken(token, now = Date.now()) {
|
|
46
|
+
const parsed = splitInToken(token);
|
|
47
|
+
if (!parsed)
|
|
48
|
+
return null;
|
|
49
|
+
const scope = cacheScope();
|
|
50
|
+
const rows = [];
|
|
51
|
+
const missingIds = [];
|
|
52
|
+
for (const id of parsed.ids) {
|
|
53
|
+
const hit = entityCache.get(entityKey(scope, parsed.shape, id));
|
|
54
|
+
if (!hit || now - hit.at >= ENTITY_TTL_MS) {
|
|
55
|
+
missingIds.push(id);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (hit.row)
|
|
59
|
+
rows.push(hit.row);
|
|
60
|
+
}
|
|
61
|
+
if (missingIds.length === parsed.ids.length)
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
wire: missingIds.length === 0 ? '' : replaceInIds(token, parsed.field, missingIds),
|
|
65
|
+
rows,
|
|
66
|
+
missingIds,
|
|
67
|
+
field: parsed.field,
|
|
68
|
+
shape: parsed.shape,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Stores one row per requested id. An id with no row is remembered as empty. */
|
|
72
|
+
export function rememberInRows(token, ids, rows, now = Date.now()) {
|
|
73
|
+
const parsed = splitInToken(token);
|
|
74
|
+
if (!parsed)
|
|
75
|
+
return;
|
|
76
|
+
const scope = cacheScope();
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
if (Array.isArray(rows)) {
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
if (!row || typeof row !== 'object')
|
|
81
|
+
continue;
|
|
82
|
+
const id = String(row[parsed.field] ?? '').trim();
|
|
83
|
+
if (!id)
|
|
84
|
+
continue;
|
|
85
|
+
seen.add(id);
|
|
86
|
+
entityCache.set(entityKey(scope, parsed.shape, id), {
|
|
87
|
+
row: row,
|
|
88
|
+
at: now,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const id of ids) {
|
|
93
|
+
if (seen.has(id))
|
|
94
|
+
continue;
|
|
95
|
+
entityCache.set(entityKey(scope, parsed.shape, id), { row: null, at: now });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function resetQueryBatchCache() {
|
|
99
|
+
partCache.clear();
|
|
100
|
+
entityCache.clear();
|
|
101
|
+
waiters = [];
|
|
102
|
+
if (timer != null)
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
timer = null;
|
|
105
|
+
}
|
|
4
106
|
let waiters = [];
|
|
5
107
|
let timer = null;
|
|
6
108
|
let transport = null;
|
|
109
|
+
function mergeCachedRows(cached, part) {
|
|
110
|
+
if (!cached?.length)
|
|
111
|
+
return part;
|
|
112
|
+
const fresh = Array.isArray(part.data) ? part.data : [];
|
|
113
|
+
return { ...part, data: [...cached, ...fresh] };
|
|
114
|
+
}
|
|
7
115
|
function cacheScope() {
|
|
8
116
|
if (typeof localStorage === 'undefined')
|
|
9
117
|
return '';
|
|
@@ -52,9 +160,21 @@ export function loadQueryPart(api, token) {
|
|
|
52
160
|
if (cached && Date.now() - cached.at < PART_TTL_MS && cached.part.success) {
|
|
53
161
|
return Promise.resolve(cached.part);
|
|
54
162
|
}
|
|
163
|
+
const narrowed = narrowInToken(token);
|
|
164
|
+
if (narrowed && narrowed.wire === '') {
|
|
165
|
+
return Promise.resolve({ success: true, data: narrowed.rows, meta: { total: narrowed.rows.length } });
|
|
166
|
+
}
|
|
55
167
|
return new Promise((resolve, reject) => {
|
|
56
168
|
transport = api;
|
|
57
|
-
waiters.push({
|
|
169
|
+
waiters.push({
|
|
170
|
+
token,
|
|
171
|
+
wire: narrowed?.wire ?? token,
|
|
172
|
+
resolve,
|
|
173
|
+
reject,
|
|
174
|
+
cachedRows: narrowed?.rows,
|
|
175
|
+
missingIds: narrowed?.missingIds,
|
|
176
|
+
field: narrowed?.field,
|
|
177
|
+
});
|
|
58
178
|
if (timer == null)
|
|
59
179
|
timer = setTimeout(() => void flushQueryBatch(), WINDOW_MS);
|
|
60
180
|
});
|
|
@@ -66,7 +186,7 @@ async function flushQueryBatch() {
|
|
|
66
186
|
const api = transport;
|
|
67
187
|
if (!api || batch.length === 0)
|
|
68
188
|
return;
|
|
69
|
-
const named = nameBatchTokens(batch.map((waiter) => waiter.
|
|
189
|
+
const named = nameBatchTokens(batch.map((waiter) => waiter.wire));
|
|
70
190
|
const byToken = new Map(named.map((part) => [part.token, part.name]));
|
|
71
191
|
const inm = {};
|
|
72
192
|
const scope = cacheScope();
|
|
@@ -82,7 +202,7 @@ async function flushQueryBatch() {
|
|
|
82
202
|
throw new Error('batch unavailable');
|
|
83
203
|
}
|
|
84
204
|
for (const waiter of batch) {
|
|
85
|
-
const name = byToken.get(waiter.
|
|
205
|
+
const name = byToken.get(waiter.wire);
|
|
86
206
|
const part = name ? body.parts[name] : undefined;
|
|
87
207
|
if (!part) {
|
|
88
208
|
waiter.reject(new Error('batch part missing'));
|
|
@@ -95,10 +215,14 @@ async function flushQueryBatch() {
|
|
|
95
215
|
continue;
|
|
96
216
|
}
|
|
97
217
|
}
|
|
218
|
+
const merged = mergeCachedRows(waiter.cachedRows, part);
|
|
219
|
+
if (waiter.missingIds && waiter.field) {
|
|
220
|
+
rememberInRows(waiter.token, waiter.missingIds, part.data);
|
|
221
|
+
}
|
|
98
222
|
if (part.success) {
|
|
99
|
-
partCache.set(`${scope}\n${waiter.token}`, { part, at: Date.now() });
|
|
223
|
+
partCache.set(`${scope}\n${waiter.token}`, { part: merged, at: Date.now() });
|
|
100
224
|
}
|
|
101
|
-
waiter.resolve(
|
|
225
|
+
waiter.resolve(merged);
|
|
102
226
|
}
|
|
103
227
|
}
|
|
104
228
|
catch (err) {
|
|
@@ -107,3 +231,76 @@ async function flushQueryBatch() {
|
|
|
107
231
|
waiter.reject(error);
|
|
108
232
|
}
|
|
109
233
|
}
|
|
234
|
+
function canonicalParams(params) {
|
|
235
|
+
if (!params)
|
|
236
|
+
return '';
|
|
237
|
+
const keys = Object.keys(params)
|
|
238
|
+
.filter((key) => params[key] != null && params[key] !== '')
|
|
239
|
+
.sort();
|
|
240
|
+
const search = new URLSearchParams();
|
|
241
|
+
for (const key of keys)
|
|
242
|
+
search.set(key, String(params[key]));
|
|
243
|
+
return search.toString();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Token for a host GET that the batch can serve. Custom endpoints, aggregates
|
|
247
|
+
* and `/me` stay on the legacy GET.
|
|
248
|
+
*/
|
|
249
|
+
export function tokenForGet(url, params) {
|
|
250
|
+
const [path, query = ''] = url.split('?');
|
|
251
|
+
const table = path.match(/^\/metadata\/table\/([A-Za-z][A-Za-z0-9_]*)$/);
|
|
252
|
+
if (table)
|
|
253
|
+
return `m:${table[1]}`;
|
|
254
|
+
const modal = path.match(/^\/metadata\/modal\/([A-Za-z][A-Za-z0-9_]*)$/);
|
|
255
|
+
if (modal)
|
|
256
|
+
return `mm:${modal[1]}`;
|
|
257
|
+
const list = path.match(/^\/data\/([A-Za-z][A-Za-z0-9_]*)$/);
|
|
258
|
+
if (list) {
|
|
259
|
+
const q = canonicalParams(params);
|
|
260
|
+
return q ? `t:${list[1]}?${q}` : `t:${list[1]}`;
|
|
261
|
+
}
|
|
262
|
+
const opt = path.match(/^\/options\/([A-Za-z][A-Za-z0-9_]*)$/);
|
|
263
|
+
if (!opt)
|
|
264
|
+
return null;
|
|
265
|
+
const search = new URLSearchParams(query);
|
|
266
|
+
if (params) {
|
|
267
|
+
for (const [key, value] of Object.entries(params)) {
|
|
268
|
+
if (value != null && value !== '')
|
|
269
|
+
search.set(key, String(value));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const limit = Number(search.get('limit'));
|
|
273
|
+
return optionsBatchToken(opt[1], search.get('field') || 'id', search.get('q') || search.get('search') || '', Number.isFinite(limit) && limit > 0 ? limit : undefined, search.get('filter_value') || undefined);
|
|
274
|
+
}
|
|
275
|
+
function batchForbidden(message) {
|
|
276
|
+
return /forbidden|permiso/i.test(message || '');
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* GET through the batch when the URL is a list, table metadata, modal metadata
|
|
280
|
+
* or options read. Anything the batch refuses falls back to the original GET.
|
|
281
|
+
*/
|
|
282
|
+
export async function batchGet(api, url, config) {
|
|
283
|
+
const token = tokenForGet(url, config?.params);
|
|
284
|
+
if (!token)
|
|
285
|
+
return api.get(url, config);
|
|
286
|
+
try {
|
|
287
|
+
const part = await loadQueryPart(api, token);
|
|
288
|
+
if (!part.success) {
|
|
289
|
+
if (batchForbidden(part.message)) {
|
|
290
|
+
const err = new Error(part.message || 'forbidden');
|
|
291
|
+
err.status = 403;
|
|
292
|
+
err.response = { status: 403 };
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
return api.get(url, config);
|
|
296
|
+
}
|
|
297
|
+
return { data: { success: true, data: part.data, meta: part.meta } };
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
const status = err?.status
|
|
301
|
+
?? err?.response?.status;
|
|
302
|
+
if (status === 403)
|
|
303
|
+
throw err;
|
|
304
|
+
return api.get(url, config);
|
|
305
|
+
}
|
|
306
|
+
}
|
package/package.json
CHANGED
package/src/api-context.tsx
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// runtime-react components (DynamicTable, dialogs, action dispatcher) can
|
|
3
3
|
// talk to the backend without a bundler alias to `@/lib/api`. Hosts wrap
|
|
4
4
|
// their app in <ApiProvider value={axiosInstance}> once at the root.
|
|
5
|
-
import React, { createContext, useContext } from 'react'
|
|
5
|
+
import React, { createContext, useContext, useMemo } from 'react'
|
|
6
|
+
import { batchGet } from './query-batch'
|
|
6
7
|
|
|
7
8
|
/** Minimal axios-compatible client shape consumed by runtime-react. */
|
|
8
9
|
export interface ApiClient {
|
|
@@ -29,7 +30,14 @@ export function useApi(): ApiClient {
|
|
|
29
30
|
if (!ctx) {
|
|
30
31
|
throw new Error('useApi() requires an <ApiProvider> ancestor. Hosts must inject an axios-like client via runtime-react ApiProvider.')
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
// List, metadata and options GETs share one POST /api/q. Mutations stay
|
|
34
|
+
// on the host client. The wrapper is stable while the host client is.
|
|
35
|
+
return useMemo<ApiClient>(() => ({
|
|
36
|
+
get: (url, config) => batchGet(ctx, url, config),
|
|
37
|
+
post: (url, body, config) => ctx.post(url, body, config),
|
|
38
|
+
put: (url, body, config) => ctx.put(url, body, config),
|
|
39
|
+
delete: (url, config) => ctx.delete(url, config),
|
|
40
|
+
}), [ctx])
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/** Optional branch context — hosts that support tenant branches can supply
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
import { OptionsContext } from './options-context'
|
|
79
79
|
import type { TableMetadata, ApiResponse, ColumnDefinition } from './types'
|
|
80
80
|
import { getSearchableColumnKeys } from './column-visibility'
|
|
81
|
+
import { visibleRelationInclude } from './list-include'
|
|
81
82
|
import { useDebouncedValue } from './use-debounced-value'
|
|
82
83
|
import { useCan, usePermissionsActive, gateTableMetadata } from './permissions-context'
|
|
83
84
|
import { useDynamicRowActions } from './dynamic-row-actions'
|
|
@@ -808,6 +809,11 @@ export function DynamicTable({
|
|
|
808
809
|
return Array.from(set)
|
|
809
810
|
}, [hiddenColumns, scopeHiddenColumns])
|
|
810
811
|
|
|
812
|
+
const relationInclude = useMemo(
|
|
813
|
+
() => visibleRelationInclude(metadata?.columns, effectiveHiddenColumns),
|
|
814
|
+
[metadata, effectiveHiddenColumns],
|
|
815
|
+
)
|
|
816
|
+
|
|
811
817
|
const buildFilterParams = useCallback(() => {
|
|
812
818
|
const params: Record<string, any> = {}
|
|
813
819
|
if (sorting.length > 0) {
|
|
@@ -868,6 +874,7 @@ export function DynamicTable({
|
|
|
868
874
|
per_page: pagination.pageSize,
|
|
869
875
|
...buildFilterParams(),
|
|
870
876
|
}
|
|
877
|
+
if (relationInclude) params.include = relationInclude
|
|
871
878
|
const res = await api.get(endpoint || `/data/${model}`, { params }) as { data: ApiResponse<any[]> }
|
|
872
879
|
setForbidden(false)
|
|
873
880
|
if (res.data.success) {
|
|
@@ -895,7 +902,7 @@ export function DynamicTable({
|
|
|
895
902
|
} finally {
|
|
896
903
|
setLoadingData(false)
|
|
897
904
|
}
|
|
898
|
-
}, [model, metadata, pagination, buildFilterParams, refreshTrigger, endpoint, currentBranch?.id, api, enableUrlSync])
|
|
905
|
+
}, [model, metadata, pagination, buildFilterParams, refreshTrigger, endpoint, currentBranch?.id, api, enableUrlSync, relationInclude])
|
|
899
906
|
|
|
900
907
|
// Columns whose metadata opts into a footer total (display_config.aggregate
|
|
901
908
|
// → styleConfig.aggregate). When empty, no footer row is rendered and no
|
|
@@ -936,6 +943,7 @@ export function DynamicTable({
|
|
|
936
943
|
per_page: infPageSize,
|
|
937
944
|
...buildFilterParams(),
|
|
938
945
|
}
|
|
946
|
+
if (relationInclude) params.include = relationInclude
|
|
939
947
|
const res = (await api.get(endpoint || `/data/${model}`, {
|
|
940
948
|
params,
|
|
941
949
|
})) as { data: ApiResponse<any[]> }
|
|
@@ -974,7 +982,7 @@ export function DynamicTable({
|
|
|
974
982
|
else setLoadingData(false)
|
|
975
983
|
}
|
|
976
984
|
},
|
|
977
|
-
[metadata, buildFilterParams, endpoint, model, api, currentBranch?.id, enableUrlSync],
|
|
985
|
+
[metadata, buildFilterParams, endpoint, model, api, currentBranch?.id, enableUrlSync, relationInclude],
|
|
978
986
|
)
|
|
979
987
|
|
|
980
988
|
// Signature of everything that must reset the incremental list to page 1:
|
package/src/index.ts
CHANGED
|
@@ -393,10 +393,18 @@ export {
|
|
|
393
393
|
getSearchableColumnKeys,
|
|
394
394
|
} from './column-visibility'
|
|
395
395
|
export {
|
|
396
|
+
batchGet,
|
|
396
397
|
loadQueryPart,
|
|
397
398
|
nameBatchTokens,
|
|
399
|
+
narrowInToken,
|
|
398
400
|
optionsBatchToken,
|
|
399
401
|
optionsModelFromUrl,
|
|
402
|
+
rememberInRows,
|
|
403
|
+
resetQueryBatchCache,
|
|
404
|
+
splitInToken,
|
|
405
|
+
tokenForGet,
|
|
406
|
+
type InListToken,
|
|
407
|
+
type NarrowedIn,
|
|
400
408
|
type QueryPart,
|
|
401
409
|
} from './query-batch'
|
|
402
410
|
export {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { visibleRelationInclude } from './list-include'
|
|
3
|
+
import type { ColumnDefinition } from './types'
|
|
4
|
+
|
|
5
|
+
function col(partial: Partial<ColumnDefinition> & { key: string }): ColumnDefinition {
|
|
6
|
+
return { label: partial.key, type: 'text', sortable: false, filterable: false, ...partial }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('visibleRelationInclude', () => {
|
|
10
|
+
it('omits the parameter when the table has no relations', () => {
|
|
11
|
+
expect(visibleRelationInclude([col({ key: 'name' })])).toBeUndefined()
|
|
12
|
+
expect(visibleRelationInclude(undefined)).toBeUndefined()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('lists only the visible relation columns', () => {
|
|
16
|
+
const include = visibleRelationInclude([
|
|
17
|
+
col({ key: 'name' }),
|
|
18
|
+
col({ key: 'customer_id', type: 'relation' }),
|
|
19
|
+
col({ key: 'warehouse_id', ref: 'inventory.Warehouse', hidden: true }),
|
|
20
|
+
col({ key: 'created_by', type: 'creator' }),
|
|
21
|
+
col({ key: 'items', item_fields: [{ key: 'product_id', label: 'Producto' }] }),
|
|
22
|
+
])
|
|
23
|
+
expect(include).toBe('customer_id,created_by,items')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('drops a column the view hid', () => {
|
|
27
|
+
const include = visibleRelationInclude(
|
|
28
|
+
[col({ key: 'customer_id', type: 'relation' }), col({ key: 'user_id', type: 'user' })],
|
|
29
|
+
['user_id'],
|
|
30
|
+
)
|
|
31
|
+
expect(include).toBe('customer_id')
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ColumnDefinition } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Columns whose labels come from a relation lookup. The list asks the server
|
|
5
|
+
* to resolve only these, so hidden foreign keys do not each become a query.
|
|
6
|
+
* Returns undefined when nothing relational is visible: the server then keeps
|
|
7
|
+
* resolving everything, which is what hosts do before they send `include=`.
|
|
8
|
+
*/
|
|
9
|
+
export function visibleRelationInclude(
|
|
10
|
+
columns: ColumnDefinition[] | undefined,
|
|
11
|
+
hidden: string[] = [],
|
|
12
|
+
): string | undefined {
|
|
13
|
+
if (!columns?.length) return undefined
|
|
14
|
+
const hide = new Set(hidden)
|
|
15
|
+
const keys: string[] = []
|
|
16
|
+
for (const col of columns) {
|
|
17
|
+
if (!col?.key || col.hidden || hide.has(col.key)) continue
|
|
18
|
+
if (col.visibility === 'modal' || col.visibility === 'list') continue
|
|
19
|
+
if (!columnNeedsRelation(col)) continue
|
|
20
|
+
keys.push(col.key)
|
|
21
|
+
}
|
|
22
|
+
if (keys.length === 0) return undefined
|
|
23
|
+
return keys.join(',')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function columnNeedsRelation(col: ColumnDefinition): boolean {
|
|
27
|
+
if (col.type === 'relation' || col.type === 'relation-badge-list' || col.type === 'creator' || col.type === 'user') {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
if (col.cellStyle === 'relation' || col.cellStyle === 'reference') return true
|
|
31
|
+
if (col.ref) return true
|
|
32
|
+
if (col.itemFields?.length || col.item_fields?.length) return true
|
|
33
|
+
return false
|
|
34
|
+
}
|
package/src/query-batch.test.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
nameBatchTokens,
|
|
4
|
+
narrowInToken,
|
|
5
|
+
optionsBatchToken,
|
|
6
|
+
optionsModelFromUrl,
|
|
7
|
+
rememberInRows,
|
|
8
|
+
resetQueryBatchCache,
|
|
9
|
+
splitInToken,
|
|
10
|
+
tokenForGet,
|
|
11
|
+
} from './query-batch'
|
|
3
12
|
|
|
4
13
|
describe('query batch tokens', () => {
|
|
5
14
|
it('names distinct tokens and keeps duplicates as one part', () => {
|
|
@@ -21,4 +30,38 @@ describe('query batch tokens', () => {
|
|
|
21
30
|
'o:Customer?field=id&q=lop&limit=20',
|
|
22
31
|
)
|
|
23
32
|
})
|
|
33
|
+
|
|
34
|
+
it('keeps each in: id and asks only for the ones that are missing', () => {
|
|
35
|
+
resetQueryBatchCache()
|
|
36
|
+
const token = 't:Stock?per_page=200&product_id=in:a,b,c&skip_refs=1'
|
|
37
|
+
expect(splitInToken(token)?.shape).toBe('t:Stock?per_page=200&product_id=in:$1&skip_refs=1')
|
|
38
|
+
expect(splitInToken('t:Stock?product_id=in:@p1.id')).toBeNull()
|
|
39
|
+
|
|
40
|
+
rememberInRows(token, ['a', 'b'], [{ product_id: 'a', quantity: 1 }])
|
|
41
|
+
const narrowed = narrowInToken(token)
|
|
42
|
+
expect(narrowed?.missingIds).toEqual(['c'])
|
|
43
|
+
expect(narrowed?.rows).toEqual([{ product_id: 'a', quantity: 1 }])
|
|
44
|
+
expect(narrowed?.wire).toContain('in%3Ac')
|
|
45
|
+
expect(narrowed?.wire).not.toContain('%2Cb')
|
|
46
|
+
|
|
47
|
+
rememberInRows(token, ['c'], [{ product_id: 'c', quantity: 4 }])
|
|
48
|
+
const done = narrowInToken('t:Stock?product_id=in:a,c&per_page=200&skip_refs=1')
|
|
49
|
+
expect(done?.wire).toBe('')
|
|
50
|
+
expect(done?.rows).toEqual([
|
|
51
|
+
{ product_id: 'a', quantity: 1 },
|
|
52
|
+
{ product_id: 'c', quantity: 4 },
|
|
53
|
+
])
|
|
54
|
+
resetQueryBatchCache()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('maps table reads onto batch tokens and leaves aggregates on GET', () => {
|
|
58
|
+
expect(tokenForGet('/metadata/table/Sale')).toBe('m:Sale')
|
|
59
|
+
expect(tokenForGet('/metadata/modal/Sale')).toBe('mm:Sale')
|
|
60
|
+
expect(tokenForGet('/data/Stock', { per_page: 20, page: 1, 'f_product_id': 'in:a,b' })).toBe(
|
|
61
|
+
't:Stock?f_product_id=in%3Aa%2Cb&page=1&per_page=20',
|
|
62
|
+
)
|
|
63
|
+
expect(tokenForGet('/data/Stock/aggregate', { page: 1 })).toBeNull()
|
|
64
|
+
expect(tokenForGet('/data/Stock/me')).toBeNull()
|
|
65
|
+
expect(tokenForGet('/options/Customer?field=name&q=lop')).toBe('o:Customer?field=name&q=lop')
|
|
66
|
+
})
|
|
24
67
|
})
|
package/src/query-batch.ts
CHANGED
|
@@ -18,20 +18,145 @@ export interface QueryPart {
|
|
|
18
18
|
|
|
19
19
|
const WINDOW_MS = 8
|
|
20
20
|
const PART_TTL_MS = 30_000
|
|
21
|
+
const ENTITY_TTL_MS = 30_000
|
|
21
22
|
|
|
22
23
|
type Waiter = {
|
|
23
24
|
token: string
|
|
25
|
+
wire: string
|
|
24
26
|
resolve: (part: QueryPart) => void
|
|
25
27
|
reject: (error: Error) => void
|
|
28
|
+
cachedRows?: Record<string, unknown>[]
|
|
29
|
+
missingIds?: string[]
|
|
30
|
+
field?: string
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
type CachedPart = { part: QueryPart; at: number }
|
|
34
|
+
type CachedEntity = { row: Record<string, unknown> | null; at: number }
|
|
29
35
|
|
|
30
36
|
const partCache = new Map<string, CachedPart>()
|
|
37
|
+
const entityCache = new Map<string, CachedEntity>()
|
|
38
|
+
|
|
39
|
+
export interface InListToken {
|
|
40
|
+
field: string
|
|
41
|
+
ids: string[]
|
|
42
|
+
shape: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A concrete `t:` `in:` list. `@` references stay whole: the ids are not known yet. */
|
|
46
|
+
export function splitInToken(token: string): InListToken | null {
|
|
47
|
+
const q = token.indexOf('?')
|
|
48
|
+
if (q < 0 || !token.startsWith('t:')) return null
|
|
49
|
+
const model = token.slice(2, q)
|
|
50
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(model)) return null
|
|
51
|
+
const params = new URLSearchParams(token.slice(q + 1))
|
|
52
|
+
let field = ''
|
|
53
|
+
let ids: string[] = []
|
|
54
|
+
for (const [key, value] of params.entries()) {
|
|
55
|
+
if (!value.startsWith('in:')) continue
|
|
56
|
+
if (field || value.includes('@')) return null
|
|
57
|
+
field = key
|
|
58
|
+
ids = value
|
|
59
|
+
.slice(3)
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((id) => id.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
}
|
|
64
|
+
if (!field || ids.length === 0) return null
|
|
65
|
+
const keys = [...new Set(params.keys())].sort()
|
|
66
|
+
const bits = keys.map((key) => `${key}=${key === field ? 'in:$1' : (params.get(key) ?? '')}`)
|
|
67
|
+
return { field, ids, shape: `t:${model}?${bits.join('&')}` }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function entityKey(scope: string, shape: string, id: string): string {
|
|
71
|
+
return `${scope}\n${shape}\n${id}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function replaceInIds(token: string, field: string, ids: string[]): string {
|
|
75
|
+
const q = token.indexOf('?')
|
|
76
|
+
const params = new URLSearchParams(token.slice(q + 1))
|
|
77
|
+
params.set(field, `in:${ids.join(',')}`)
|
|
78
|
+
return `${token.slice(0, q)}?${params.toString()}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface NarrowedIn {
|
|
82
|
+
wire: string
|
|
83
|
+
rows: Record<string, unknown>[]
|
|
84
|
+
missingIds: string[]
|
|
85
|
+
field: string
|
|
86
|
+
shape: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Drops ids already cached for this shape. An empty wire means nothing has to travel. */
|
|
90
|
+
export function narrowInToken(token: string, now = Date.now()): NarrowedIn | null {
|
|
91
|
+
const parsed = splitInToken(token)
|
|
92
|
+
if (!parsed) return null
|
|
93
|
+
const scope = cacheScope()
|
|
94
|
+
const rows: Record<string, unknown>[] = []
|
|
95
|
+
const missingIds: string[] = []
|
|
96
|
+
for (const id of parsed.ids) {
|
|
97
|
+
const hit = entityCache.get(entityKey(scope, parsed.shape, id))
|
|
98
|
+
if (!hit || now - hit.at >= ENTITY_TTL_MS) {
|
|
99
|
+
missingIds.push(id)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (hit.row) rows.push(hit.row)
|
|
103
|
+
}
|
|
104
|
+
if (missingIds.length === parsed.ids.length) return null
|
|
105
|
+
return {
|
|
106
|
+
wire: missingIds.length === 0 ? '' : replaceInIds(token, parsed.field, missingIds),
|
|
107
|
+
rows,
|
|
108
|
+
missingIds,
|
|
109
|
+
field: parsed.field,
|
|
110
|
+
shape: parsed.shape,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Stores one row per requested id. An id with no row is remembered as empty. */
|
|
115
|
+
export function rememberInRows(
|
|
116
|
+
token: string,
|
|
117
|
+
ids: string[],
|
|
118
|
+
rows: unknown,
|
|
119
|
+
now = Date.now(),
|
|
120
|
+
): void {
|
|
121
|
+
const parsed = splitInToken(token)
|
|
122
|
+
if (!parsed) return
|
|
123
|
+
const scope = cacheScope()
|
|
124
|
+
const seen = new Set<string>()
|
|
125
|
+
if (Array.isArray(rows)) {
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
if (!row || typeof row !== 'object') continue
|
|
128
|
+
const id = String((row as Record<string, unknown>)[parsed.field] ?? '').trim()
|
|
129
|
+
if (!id) continue
|
|
130
|
+
seen.add(id)
|
|
131
|
+
entityCache.set(entityKey(scope, parsed.shape, id), {
|
|
132
|
+
row: row as Record<string, unknown>,
|
|
133
|
+
at: now,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const id of ids) {
|
|
138
|
+
if (seen.has(id)) continue
|
|
139
|
+
entityCache.set(entityKey(scope, parsed.shape, id), { row: null, at: now })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resetQueryBatchCache(): void {
|
|
144
|
+
partCache.clear()
|
|
145
|
+
entityCache.clear()
|
|
146
|
+
waiters = []
|
|
147
|
+
if (timer != null) clearTimeout(timer)
|
|
148
|
+
timer = null
|
|
149
|
+
}
|
|
31
150
|
let waiters: Waiter[] = []
|
|
32
151
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
33
152
|
let transport: ApiClient | null = null
|
|
34
153
|
|
|
154
|
+
function mergeCachedRows(cached: Record<string, unknown>[] | undefined, part: QueryPart): QueryPart {
|
|
155
|
+
if (!cached?.length) return part
|
|
156
|
+
const fresh = Array.isArray(part.data) ? part.data : []
|
|
157
|
+
return { ...part, data: [...cached, ...fresh] }
|
|
158
|
+
}
|
|
159
|
+
|
|
35
160
|
function cacheScope(): string {
|
|
36
161
|
if (typeof localStorage === 'undefined') return ''
|
|
37
162
|
try {
|
|
@@ -83,9 +208,21 @@ export function loadQueryPart(api: ApiClient, token: string): Promise<QueryPart>
|
|
|
83
208
|
if (cached && Date.now() - cached.at < PART_TTL_MS && cached.part.success) {
|
|
84
209
|
return Promise.resolve(cached.part)
|
|
85
210
|
}
|
|
211
|
+
const narrowed = narrowInToken(token)
|
|
212
|
+
if (narrowed && narrowed.wire === '') {
|
|
213
|
+
return Promise.resolve({ success: true, data: narrowed.rows, meta: { total: narrowed.rows.length } })
|
|
214
|
+
}
|
|
86
215
|
return new Promise((resolve, reject) => {
|
|
87
216
|
transport = api
|
|
88
|
-
waiters.push({
|
|
217
|
+
waiters.push({
|
|
218
|
+
token,
|
|
219
|
+
wire: narrowed?.wire ?? token,
|
|
220
|
+
resolve,
|
|
221
|
+
reject,
|
|
222
|
+
cachedRows: narrowed?.rows,
|
|
223
|
+
missingIds: narrowed?.missingIds,
|
|
224
|
+
field: narrowed?.field,
|
|
225
|
+
})
|
|
89
226
|
if (timer == null) timer = setTimeout(() => void flushQueryBatch(), WINDOW_MS)
|
|
90
227
|
})
|
|
91
228
|
}
|
|
@@ -96,7 +233,7 @@ async function flushQueryBatch() {
|
|
|
96
233
|
waiters = []
|
|
97
234
|
const api = transport
|
|
98
235
|
if (!api || batch.length === 0) return
|
|
99
|
-
const named = nameBatchTokens(batch.map((waiter) => waiter.
|
|
236
|
+
const named = nameBatchTokens(batch.map((waiter) => waiter.wire))
|
|
100
237
|
const byToken = new Map(named.map((part) => [part.token, part.name]))
|
|
101
238
|
const inm: Record<string, string> = {}
|
|
102
239
|
const scope = cacheScope()
|
|
@@ -111,7 +248,7 @@ async function flushQueryBatch() {
|
|
|
111
248
|
throw new Error('batch unavailable')
|
|
112
249
|
}
|
|
113
250
|
for (const waiter of batch) {
|
|
114
|
-
const name = byToken.get(waiter.
|
|
251
|
+
const name = byToken.get(waiter.wire)
|
|
115
252
|
const part = name ? (body.parts[name] as QueryPart | undefined) : undefined
|
|
116
253
|
if (!part) {
|
|
117
254
|
waiter.reject(new Error('batch part missing'))
|
|
@@ -124,13 +261,94 @@ async function flushQueryBatch() {
|
|
|
124
261
|
continue
|
|
125
262
|
}
|
|
126
263
|
}
|
|
264
|
+
const merged = mergeCachedRows(waiter.cachedRows, part)
|
|
265
|
+
if (waiter.missingIds && waiter.field) {
|
|
266
|
+
rememberInRows(waiter.token, waiter.missingIds, part.data)
|
|
267
|
+
}
|
|
127
268
|
if (part.success) {
|
|
128
|
-
partCache.set(`${scope}\n${waiter.token}`, { part, at: Date.now() })
|
|
269
|
+
partCache.set(`${scope}\n${waiter.token}`, { part: merged, at: Date.now() })
|
|
129
270
|
}
|
|
130
|
-
waiter.resolve(
|
|
271
|
+
waiter.resolve(merged)
|
|
131
272
|
}
|
|
132
273
|
} catch (err) {
|
|
133
274
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
134
275
|
for (const waiter of batch) waiter.reject(error)
|
|
135
276
|
}
|
|
136
277
|
}
|
|
278
|
+
|
|
279
|
+
function canonicalParams(params?: Record<string, unknown>): string {
|
|
280
|
+
if (!params) return ''
|
|
281
|
+
const keys = Object.keys(params)
|
|
282
|
+
.filter((key) => params[key] != null && params[key] !== '')
|
|
283
|
+
.sort()
|
|
284
|
+
const search = new URLSearchParams()
|
|
285
|
+
for (const key of keys) search.set(key, String(params[key]))
|
|
286
|
+
return search.toString()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Token for a host GET that the batch can serve. Custom endpoints, aggregates
|
|
291
|
+
* and `/me` stay on the legacy GET.
|
|
292
|
+
*/
|
|
293
|
+
export function tokenForGet(url: string, params?: Record<string, unknown>): string | null {
|
|
294
|
+
const [path, query = ''] = url.split('?')
|
|
295
|
+
const table = path.match(/^\/metadata\/table\/([A-Za-z][A-Za-z0-9_]*)$/)
|
|
296
|
+
if (table) return `m:${table[1]}`
|
|
297
|
+
const modal = path.match(/^\/metadata\/modal\/([A-Za-z][A-Za-z0-9_]*)$/)
|
|
298
|
+
if (modal) return `mm:${modal[1]}`
|
|
299
|
+
const list = path.match(/^\/data\/([A-Za-z][A-Za-z0-9_]*)$/)
|
|
300
|
+
if (list) {
|
|
301
|
+
const q = canonicalParams(params)
|
|
302
|
+
return q ? `t:${list[1]}?${q}` : `t:${list[1]}`
|
|
303
|
+
}
|
|
304
|
+
const opt = path.match(/^\/options\/([A-Za-z][A-Za-z0-9_]*)$/)
|
|
305
|
+
if (!opt) return null
|
|
306
|
+
const search = new URLSearchParams(query)
|
|
307
|
+
if (params) {
|
|
308
|
+
for (const [key, value] of Object.entries(params)) {
|
|
309
|
+
if (value != null && value !== '') search.set(key, String(value))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const limit = Number(search.get('limit'))
|
|
313
|
+
return optionsBatchToken(
|
|
314
|
+
opt[1],
|
|
315
|
+
search.get('field') || 'id',
|
|
316
|
+
search.get('q') || search.get('search') || '',
|
|
317
|
+
Number.isFinite(limit) && limit > 0 ? limit : undefined,
|
|
318
|
+
search.get('filter_value') || undefined,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function batchForbidden(message?: string): boolean {
|
|
323
|
+
return /forbidden|permiso/i.test(message || '')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* GET through the batch when the URL is a list, table metadata, modal metadata
|
|
328
|
+
* or options read. Anything the batch refuses falls back to the original GET.
|
|
329
|
+
*/
|
|
330
|
+
export async function batchGet(api: ApiClient, url: string, config?: { params?: Record<string, unknown> }) {
|
|
331
|
+
const token = tokenForGet(url, config?.params)
|
|
332
|
+
if (!token) return api.get(url, config)
|
|
333
|
+
try {
|
|
334
|
+
const part = await loadQueryPart(api, token)
|
|
335
|
+
if (!part.success) {
|
|
336
|
+
if (batchForbidden(part.message)) {
|
|
337
|
+
const err = new Error(part.message || 'forbidden') as Error & {
|
|
338
|
+
status: number
|
|
339
|
+
response: { status: number }
|
|
340
|
+
}
|
|
341
|
+
err.status = 403
|
|
342
|
+
err.response = { status: 403 }
|
|
343
|
+
throw err
|
|
344
|
+
}
|
|
345
|
+
return api.get(url, config)
|
|
346
|
+
}
|
|
347
|
+
return { data: { success: true, data: part.data, meta: part.meta } }
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const status = (err as { status?: number; response?: { status?: number } })?.status
|
|
350
|
+
?? (err as { response?: { status?: number } })?.response?.status
|
|
351
|
+
if (status === 403) throw err
|
|
352
|
+
return api.get(url, config)
|
|
353
|
+
}
|
|
354
|
+
}
|
package/vitest.config.ts
CHANGED
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Node 26 exposes a global localStorage getter that returns undefined unless
|
|
2
|
+
// --localStorage-file is set, and that getter hides happy-dom's storage.
|
|
3
|
+
// Tests that persist page size or the metadata cache need a real Storage.
|
|
4
|
+
|
|
5
|
+
function installMemoryStorage(name: 'localStorage' | 'sessionStorage') {
|
|
6
|
+
const current = globalThis[name] as Storage | undefined
|
|
7
|
+
if (current && typeof current.clear === 'function' && typeof current.setItem === 'function') return
|
|
8
|
+
const store = new Map<string, string>()
|
|
9
|
+
const memory: Storage = {
|
|
10
|
+
getItem: (key) => (store.has(key) ? store.get(key)! : null),
|
|
11
|
+
setItem: (key, value) => {
|
|
12
|
+
store.set(String(key), String(value))
|
|
13
|
+
},
|
|
14
|
+
removeItem: (key) => {
|
|
15
|
+
store.delete(String(key))
|
|
16
|
+
},
|
|
17
|
+
clear: () => {
|
|
18
|
+
store.clear()
|
|
19
|
+
},
|
|
20
|
+
key: (index) => Array.from(store.keys())[index] ?? null,
|
|
21
|
+
get length() {
|
|
22
|
+
return store.size
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(globalThis, name, { value: memory, configurable: true, writable: true })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
installMemoryStorage('localStorage')
|
|
29
|
+
installMemoryStorage('sessionStorage')
|