@marktiderman/genesis-core 1.5.1 → 1.7.0
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/README.md +112 -0
- package/dist/chunk-ADD2RIGA.js +108 -0
- package/dist/chunk-ADD2RIGA.js.map +1 -0
- package/dist/chunk-KXVHDK3Q.js +71 -0
- package/dist/chunk-KXVHDK3Q.js.map +1 -0
- package/dist/{chunk-F3BE7NGM.js → chunk-OBUBP4BY.js} +83 -47
- package/dist/chunk-OBUBP4BY.js.map +1 -0
- package/dist/{chunk-6Q477CHB.js → chunk-QFR6YJ3R.js} +7 -4
- package/dist/chunk-QFR6YJ3R.js.map +1 -0
- package/dist/chunk-V3GDAELU.js +3 -0
- package/dist/{chunk-HT43MPKW.js.map → chunk-V3GDAELU.js.map} +1 -1
- package/dist/{chunk-3JSFTLDV.js → chunk-Y3SCYMNN.js} +6 -6
- package/dist/{chunk-3JSFTLDV.js.map → chunk-Y3SCYMNN.js.map} +1 -1
- package/dist/{chunk-E5VYFAQO.js → chunk-Y475Z7BM.js} +202 -11
- package/dist/chunk-Y475Z7BM.js.map +1 -0
- package/dist/{chunk-ZK5C3OOT.js → chunk-ZKMAU6NN.js} +12 -3
- package/dist/chunk-ZKMAU6NN.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -2
- package/dist/hooks/index.js +9 -7
- package/dist/hooks/mutation-mode.d.ts +2 -2
- package/dist/hooks/mutation-mode.js +1 -1
- package/dist/hooks/related-keys.d.ts +2 -2
- package/dist/hooks/use-capabilities.d.ts +1 -1
- package/dist/hooks/use-infinite-resource.d.ts +50 -0
- package/dist/hooks/use-infinite-resource.js +6 -0
- package/dist/hooks/use-infinite-resource.js.map +1 -0
- package/dist/hooks/use-live.d.ts +27 -0
- package/dist/hooks/use-live.js +5 -0
- package/dist/hooks/use-live.js.map +1 -0
- package/dist/hooks/use-one.d.ts +9 -2
- package/dist/hooks/use-one.js +2 -1
- package/dist/hooks/use-resource-form.d.ts +2 -2
- package/dist/hooks/use-resource-form.js +6 -5
- package/dist/hooks/use-resource.d.ts +14 -2
- package/dist/hooks/use-resource.js +4 -3
- package/dist/index.d.ts +6 -4
- package/dist/index.js +10 -8
- package/dist/navigation.d.ts +108 -0
- package/dist/navigation.js +101 -0
- package/dist/navigation.js.map +1 -0
- package/dist/provider/index.d.ts +54 -6
- package/dist/provider/index.js +1 -1
- package/dist/{read-outcome-kgNhfuST.d.ts → read-outcome-C_9HbXvb.d.ts} +1 -1
- package/dist/schema/index.d.ts +4 -4
- package/dist/slots.d.ts +60 -0
- package/dist/slots.js +12 -0
- package/dist/slots.js.map +1 -0
- package/dist/{types-5cuflvha.d.ts → types-C7FJtFXV.d.ts} +12 -0
- package/dist/{types-DDOmG9hj.d.ts → types-CS9UpxQS.d.ts} +62 -4
- package/package.json +9 -1
- package/dist/chunk-6Q477CHB.js.map +0 -1
- package/dist/chunk-E5VYFAQO.js.map +0 -1
- package/dist/chunk-F3BE7NGM.js.map +0 -1
- package/dist/chunk-HT43MPKW.js +0 -3
- package/dist/chunk-ZK5C3OOT.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"use-infinite-resource.js"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { QueryKey } from '@tanstack/react-query';
|
|
2
|
+
import { o as SubscribeParams, l as ResourceChangeEvent } from '../types-CS9UpxQS.js';
|
|
3
|
+
import '../types-C7FJtFXV.js';
|
|
4
|
+
import '../form-types.js';
|
|
5
|
+
|
|
6
|
+
/** How long a burst of change events is gathered into one refetch. */
|
|
7
|
+
declare const DEFAULT_LIVE_DEBOUNCE_MS = 250;
|
|
8
|
+
/** `live` as an object, to tune it. `live: true` takes the defaults. */
|
|
9
|
+
interface LiveOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Events arriving within this many milliseconds of each other cause one
|
|
12
|
+
* refetch, not one each. Default {@link DEFAULT_LIVE_DEBOUNCE_MS}. A
|
|
13
|
+
* steady stream still refetches at least every `4 × debounceMs`.
|
|
14
|
+
*/
|
|
15
|
+
debounceMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Subscribes while mounted and `enabled`, and invalidates `queryKey` (a
|
|
19
|
+
* prefix) once per burst of events that `accept` lets through. While the
|
|
20
|
+
* page is hidden the queries are only marked stale; they refetch when it is
|
|
21
|
+
* shown again (TanStack Query's `refetchOnWindowFocus`) or next used.
|
|
22
|
+
*
|
|
23
|
+
* @internal Used by `useResource` and `useOne`'s `live` option.
|
|
24
|
+
*/
|
|
25
|
+
declare function useLiveInvalidation(resource: string, live: boolean | LiveOptions | undefined, enabled: boolean, params: SubscribeParams, queryKey: QueryKey, accept?: (event: ResourceChangeEvent) => boolean): void;
|
|
26
|
+
|
|
27
|
+
export { DEFAULT_LIVE_DEBOUNCE_MS, type LiveOptions, useLiveInvalidation };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"use-live.js"}
|
package/dist/hooks/use-one.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { UseQueryResult } from '@tanstack/react-query';
|
|
2
|
-
import { B as BaseRecord, I as Identifier } from '../types-
|
|
3
|
-
import '
|
|
2
|
+
import { B as BaseRecord, I as Identifier } from '../types-CS9UpxQS.js';
|
|
3
|
+
import { LiveOptions } from './use-live.js';
|
|
4
|
+
import '../types-C7FJtFXV.js';
|
|
4
5
|
import '../form-types.js';
|
|
5
6
|
|
|
6
7
|
interface UseOneOptions {
|
|
@@ -8,6 +9,12 @@ interface UseOneOptions {
|
|
|
8
9
|
select?: string;
|
|
9
10
|
/** Defaults to `!!id` — won't fetch when id is falsy. */
|
|
10
11
|
enabled?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Keep the record current while mounted: refetch it when the provider
|
|
14
|
+
* reports a change to it (`DataProvider.subscribe`). See
|
|
15
|
+
* `UseResourceOptions.live`. Off by default.
|
|
16
|
+
*/
|
|
17
|
+
live?: boolean | LiveOptions;
|
|
11
18
|
}
|
|
12
19
|
/**
|
|
13
20
|
* Fetch a single record by id, powered by TanStack Query.
|
package/dist/hooks/use-one.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { UseFormReturn } from 'react-hook-form';
|
|
2
2
|
import { ResourceFormFieldDef } from '../form-types.js';
|
|
3
|
-
import { j as InvalidationKey } from '../types-
|
|
3
|
+
import { j as InvalidationKey } from '../types-CS9UpxQS.js';
|
|
4
4
|
import { MutationModeOptions } from './mutation-mode.js';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-C7FJtFXV.js';
|
|
6
6
|
import '@tanstack/react-query';
|
|
7
7
|
|
|
8
8
|
interface UseResourceFormOptions extends MutationModeOptions {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
export { buildArrayItemDefaults, useResourceForm } from '../chunk-
|
|
2
|
-
import '../chunk-XQO4OH2N.js';
|
|
1
|
+
export { buildArrayItemDefaults, useResourceForm } from '../chunk-Y3SCYMNN.js';
|
|
3
2
|
import '../chunk-5NHR6ZXS.js';
|
|
3
|
+
import '../chunk-ZKMAU6NN.js';
|
|
4
|
+
import '../chunk-KXVHDK3Q.js';
|
|
5
|
+
import '../chunk-XQO4OH2N.js';
|
|
4
6
|
import '../chunk-RYEXWQFD.js';
|
|
5
|
-
import '../chunk-
|
|
6
|
-
import '../chunk-7NUL32W7.js';
|
|
7
|
+
import '../chunk-OBUBP4BY.js';
|
|
7
8
|
import '../chunk-BQOJX67X.js';
|
|
8
|
-
import '../chunk-ZK5C3OOT.js';
|
|
9
9
|
import '../chunk-GGSXSI6Z.js';
|
|
10
10
|
import '../chunk-ZYJUEJ4Z.js';
|
|
11
|
+
import '../chunk-7NUL32W7.js';
|
|
11
12
|
//# sourceMappingURL=use-resource-form.js.map
|
|
12
13
|
//# sourceMappingURL=use-resource-form.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import { n as SortParam, F as FilterParam, j as InvalidationKey, k as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-CS9UpxQS.js';
|
|
3
3
|
import { MutationModeOptions } from './mutation-mode.js';
|
|
4
|
-
import '
|
|
4
|
+
import { LiveOptions } from './use-live.js';
|
|
5
|
+
import '../types-C7FJtFXV.js';
|
|
5
6
|
import '../form-types.js';
|
|
6
7
|
|
|
7
8
|
interface UseResourceOptions<T> extends MutationModeOptions {
|
|
@@ -40,6 +41,17 @@ interface UseResourceOptions<T> extends MutationModeOptions {
|
|
|
40
41
|
* rather than flashing a skeleton. Default false.
|
|
41
42
|
*/
|
|
42
43
|
keepPreviousData?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Keep the list current while mounted: subscribe to the provider's change
|
|
46
|
+
* events (`DataProvider.subscribe`) and refetch this resource's cached
|
|
47
|
+
* reads (lists and records) when it reports one, once per burst of
|
|
48
|
+
* events. Events are hints: rows always come from the provider's normal
|
|
49
|
+
* read. Pending optimistic and undoable writes stay applied over the
|
|
50
|
+
* refetch. Needs a provider that serves `subscribe` for the resource
|
|
51
|
+
* (`capabilities(resource).realtime`); otherwise it does nothing. Off by
|
|
52
|
+
* default.
|
|
53
|
+
*/
|
|
54
|
+
live?: boolean | LiveOptions;
|
|
43
55
|
/**
|
|
44
56
|
* After the write commits, with the provider's row. In `optimistic` and
|
|
45
57
|
* `undoable` modes that is after `create` / `update` / `remove` have
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { useResource } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import '../chunk-
|
|
1
|
+
export { useResource } from '../chunk-QFR6YJ3R.js';
|
|
2
|
+
import '../chunk-KXVHDK3Q.js';
|
|
3
|
+
import '../chunk-OBUBP4BY.js';
|
|
4
4
|
import '../chunk-BQOJX67X.js';
|
|
5
5
|
import '../chunk-GGSXSI6Z.js';
|
|
6
6
|
import '../chunk-ZYJUEJ4Z.js';
|
|
7
|
+
import '../chunk-7NUL32W7.js';
|
|
7
8
|
//# sourceMappingURL=use-resource.js.map
|
|
8
9
|
//# sourceMappingURL=use-resource.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
export { A as ArrayFilter, B as BaseRecord, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, D as DataProvider, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, I as Identifier, i as ImportParams, j as InvalidationKey, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as RowFailure, S as ScalarFilter,
|
|
2
|
-
export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from './read-outcome-
|
|
3
|
-
export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, PostgrestFieldErrors, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode } from './provider/index.js';
|
|
1
|
+
export { A as ArrayFilter, B as BaseRecord, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, D as DataProvider, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, I as Identifier, i as ImportParams, j as InvalidationKey, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as ResourceChangeEvent, m as RowFailure, S as ScalarFilter, n as SortParam, o as SubscribeParams, U as UpdateParams, p as UpdateResult, W as WriteEvent } from './types-CS9UpxQS.js';
|
|
2
|
+
export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from './read-outcome-C_9HbXvb.js';
|
|
3
|
+
export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, PostgrestFieldErrors, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseRealtimeChannel, SupabaseRealtimePayload, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode } from './provider/index.js';
|
|
4
4
|
export { S as StorageAdapter } from './types-DKcxHXxq.js';
|
|
5
5
|
export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './storage/index.js';
|
|
6
6
|
export { UseResourceOptions, UseResourceReturn, useResource } from './hooks/use-resource.js';
|
|
7
7
|
export { UseOneOptions, useOne } from './hooks/use-one.js';
|
|
8
|
+
export { InfinitePageParam, UseInfiniteResourceOptions, UseInfiniteResourceReturn, useInfiniteResource } from './hooks/use-infinite-resource.js';
|
|
8
9
|
export { useCapabilities } from './hooks/use-capabilities.js';
|
|
10
|
+
export { DEFAULT_LIVE_DEBOUNCE_MS, LiveOptions } from './hooks/use-live.js';
|
|
9
11
|
export { UseResourceFormOptions, UseResourceFormReturn, buildArrayItemDefaults, useResourceForm } from './hooks/use-resource-form.js';
|
|
10
12
|
export { UseWizardReturn, WizardStep, useWizard } from './hooks/use-wizard.js';
|
|
11
13
|
export { DEFAULT_UNDO_DELAY_MS, MutationMode, MutationModeOptions, UndoableWrite, pendingWriteCount } from './hooks/mutation-mode.js';
|
|
12
14
|
export { DEFAULT_UNSAVED_CHANGES_MESSAGE, UseUnsavedChangesGuardOptions, UseUnsavedChangesGuardReturn, useUnsavedChangesGuard } from './hooks/use-unsaved-changes-guard.js';
|
|
13
15
|
export { FieldType, ResourceFormFieldDef, SelectOption, FieldType as WidgetHint } from './form-types.js';
|
|
14
|
-
export { A as AuditEntry, D as DataRow, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, e as ResourceContract, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from './types-
|
|
16
|
+
export { A as AuditEntry, D as DataRow, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, e as ResourceContract, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from './types-C7FJtFXV.js';
|
|
15
17
|
export { decodeRecordKey, encodeRecordKey, keyFields } from './schema/index.js';
|
|
16
18
|
export { titleCase } from './utils.js';
|
|
17
19
|
import 'react';
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from './chunk-
|
|
1
|
+
export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from './chunk-Y475Z7BM.js';
|
|
2
2
|
export { decodeRecordKey, encodeRecordKey, keyFields } from './chunk-KV2HW4G4.js';
|
|
3
3
|
export { DataReadError, isDataReadError, readFailureMessage } from './chunk-HUNGCTRA.js';
|
|
4
4
|
import './chunk-XCX2TRBL.js';
|
|
5
5
|
import './chunk-65SXNHWE.js';
|
|
6
|
-
import './chunk-
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
6
|
+
import './chunk-V3GDAELU.js';
|
|
7
|
+
export { useInfiniteResource } from './chunk-ADD2RIGA.js';
|
|
8
|
+
export { buildArrayItemDefaults, useResourceForm } from './chunk-Y3SCYMNN.js';
|
|
9
|
+
export { DataWriteError, isDataWriteError } from './chunk-5NHR6ZXS.js';
|
|
10
|
+
export { useOne } from './chunk-ZKMAU6NN.js';
|
|
11
|
+
export { useResource } from './chunk-QFR6YJ3R.js';
|
|
12
|
+
export { DEFAULT_LIVE_DEBOUNCE_MS } from './chunk-KXVHDK3Q.js';
|
|
9
13
|
export { DEFAULT_UNSAVED_CHANGES_MESSAGE, useUnsavedChangesGuard } from './chunk-XQO4OH2N.js';
|
|
10
14
|
export { useWizard } from './chunk-7KOLYF27.js';
|
|
11
|
-
export { DataWriteError, isDataWriteError } from './chunk-5NHR6ZXS.js';
|
|
12
15
|
export { titleCase } from './chunk-RYEXWQFD.js';
|
|
13
|
-
export { DEFAULT_UNDO_DELAY_MS, pendingWriteCount } from './chunk-
|
|
14
|
-
import './chunk-7NUL32W7.js';
|
|
16
|
+
export { DEFAULT_UNDO_DELAY_MS, pendingWriteCount } from './chunk-OBUBP4BY.js';
|
|
15
17
|
export { useCapabilities } from './chunk-BQOJX67X.js';
|
|
16
|
-
export { useOne } from './chunk-ZK5C3OOT.js';
|
|
17
18
|
export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
|
|
18
19
|
export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './chunk-ZYJUEJ4Z.js';
|
|
20
|
+
import './chunk-7NUL32W7.js';
|
|
19
21
|
//# sourceMappingURL=index.js.map
|
|
20
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router-free, DOM-free navigation: the logical navigation tree and the
|
|
3
|
+
* active-path matcher, shared by the web app shell and native shells.
|
|
4
|
+
* `resolveNavigation` never reads browser history or `window`.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Options for {@link isPathActive}.
|
|
8
|
+
*
|
|
9
|
+
* @stability Beta
|
|
10
|
+
*/
|
|
11
|
+
interface IsPathActiveOptions {
|
|
12
|
+
/** Require an exact path match instead of a prefix match (like `NavLink end`). */
|
|
13
|
+
end?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Default active-path matcher, mirroring React Router's `NavLink` (prefix
|
|
17
|
+
* match, with `/` treated as exact). Consumers can override via an `isActive`
|
|
18
|
+
* prop where components accept one.
|
|
19
|
+
*
|
|
20
|
+
* `activePath` may carry a query and hash (`pathname + search + hash`). An
|
|
21
|
+
* item whose `to` has neither compares against the path part only, exactly as
|
|
22
|
+
* before. An item that opts in by putting a query or hash in its `to`
|
|
23
|
+
* (`/admin?tab=users`, `/settings#profile`) is active only when the path
|
|
24
|
+
* matches exactly AND every query param it names has the same value in
|
|
25
|
+
* `activePath` (extra params such as `&page=2` are ignored) AND, if it names a
|
|
26
|
+
* hash, the hash is equal.
|
|
27
|
+
*
|
|
28
|
+
* @stability Beta
|
|
29
|
+
*/
|
|
30
|
+
declare function isPathActive(to: string, activePath?: string, options?: IsPathActiveOptions): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* One node of a logical navigation hierarchy. `path` is a route pattern whose
|
|
33
|
+
* segments are either static (`clients`) or named params (`$clientId`). A
|
|
34
|
+
* node's place in the tree is its LOGICAL parent, which need not share its URL
|
|
35
|
+
* prefix — `/clients/$clientId` can sit under `/dashboard`.
|
|
36
|
+
*
|
|
37
|
+
* @stability Beta
|
|
38
|
+
*/
|
|
39
|
+
interface NavigationNode<T> {
|
|
40
|
+
id: string;
|
|
41
|
+
path: string;
|
|
42
|
+
label: string;
|
|
43
|
+
/** Contextual nav items to show while this node is current. */
|
|
44
|
+
items: T[];
|
|
45
|
+
children?: NavigationNode<T>[];
|
|
46
|
+
/**
|
|
47
|
+
* Ask the app shell to open its desktop sidebar collapsed (icon rail) while
|
|
48
|
+
* this node, or a descendant that doesn't set its own value, is current.
|
|
49
|
+
* The nearest node in the resolved trail that sets it wins, so
|
|
50
|
+
* `defaultCollapsed: false` on a child re-opens it under a collapsed
|
|
51
|
+
* parent. Only a default: once the user has toggled the sidebar, their
|
|
52
|
+
* choice wins everywhere. `AppShell` reads it; `resolveNavigation` copies
|
|
53
|
+
* it through untouched.
|
|
54
|
+
*
|
|
55
|
+
* @stability Beta
|
|
56
|
+
*/
|
|
57
|
+
defaultCollapsed?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* How the app shell shows this node's `items` (its child pages, e.g. a
|
|
60
|
+
* form's Questions / Logic / Responses / Settings). `"tabs"` (across the
|
|
61
|
+
* top of the content) or `"sidebar"` (nested under the active item) turn
|
|
62
|
+
* shell sub-navigation on, which also gives phones a back item plus the
|
|
63
|
+
* children in the bottom bar. `"section"` is the legacy rendering (the
|
|
64
|
+
* shell's default) and `"off"` hides them. The nearest node in the trail
|
|
65
|
+
* that sets it wins over the shell's `options.subNav`.
|
|
66
|
+
*
|
|
67
|
+
* @stability Beta
|
|
68
|
+
*/
|
|
69
|
+
subNav?: "section" | "tabs" | "sidebar" | "off";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Result of {@link resolveNavigation}. `current`, `parent` and every `trail`
|
|
73
|
+
* entry are shallow copies of their tree nodes with `path` interpolated from
|
|
74
|
+
* `params` (a param the URL did not supply stays as `$name`).
|
|
75
|
+
*
|
|
76
|
+
* @stability Beta
|
|
77
|
+
*/
|
|
78
|
+
interface ResolvedNavigation<T> {
|
|
79
|
+
/** The best-matching node — the root when nothing else matches. */
|
|
80
|
+
current: NavigationNode<T>;
|
|
81
|
+
/** `current`'s logical parent, for an "up" / back-to-parent link. */
|
|
82
|
+
parent?: NavigationNode<T>;
|
|
83
|
+
/** `current.items`. */
|
|
84
|
+
items: T[];
|
|
85
|
+
/** Root → `current`, inclusive. */
|
|
86
|
+
trail: NavigationNode<T>[];
|
|
87
|
+
/** Params captured by `current.path`, URL-decoded. */
|
|
88
|
+
params: Record<string, string>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolve where `pathname` sits in a logical navigation tree. Pure: it never
|
|
92
|
+
* reads browser history or `window`.
|
|
93
|
+
*
|
|
94
|
+
* A node matches when its pattern matches a leading run of WHOLE path
|
|
95
|
+
* segments (`/my` matches `/my/tasks`, never `/my-other`). Among matches the
|
|
96
|
+
* winner is decided by positional specificity: at the first position where
|
|
97
|
+
* one pattern is static and the other a `$param`, the static one wins (so
|
|
98
|
+
* `/clients/new` beats `/clients/$clientId` whatever the sibling order, and
|
|
99
|
+
* `/my` beats `/$space/tasks` for `/my/tasks`). Otherwise the pattern matching
|
|
100
|
+
* more segments wins, then the deeper tree node.
|
|
101
|
+
* Non-matching nodes are still searched, so a match keeps its logical
|
|
102
|
+
* ancestors even when their URLs differ.
|
|
103
|
+
*
|
|
104
|
+
* @stability Beta
|
|
105
|
+
*/
|
|
106
|
+
declare function resolveNavigation<T>(tree: NavigationNode<T>, pathname: string): ResolvedNavigation<T>;
|
|
107
|
+
|
|
108
|
+
export { type IsPathActiveOptions, type NavigationNode, type ResolvedNavigation, isPathActive, resolveNavigation };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/navigation.ts
|
|
2
|
+
function splitHref(href) {
|
|
3
|
+
const hashAt = href.indexOf("#");
|
|
4
|
+
const hash = hashAt === -1 ? "" : href.slice(hashAt + 1);
|
|
5
|
+
const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);
|
|
6
|
+
const searchAt = beforeHash.indexOf("?");
|
|
7
|
+
const rawPath = searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt);
|
|
8
|
+
return {
|
|
9
|
+
path: rawPath.length > 1 ? rawPath.replace(/\/+$/, "") || "/" : rawPath,
|
|
10
|
+
search: searchAt === -1 ? "" : beforeHash.slice(searchAt + 1),
|
|
11
|
+
hash
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function isPathActive(to, activePath, options) {
|
|
15
|
+
if (activePath == null) return false;
|
|
16
|
+
if (to === activePath) return true;
|
|
17
|
+
const target = splitHref(to);
|
|
18
|
+
const current = splitHref(activePath);
|
|
19
|
+
if (!target.search && !target.hash) {
|
|
20
|
+
if (target.path === current.path) return true;
|
|
21
|
+
if (options?.end) return false;
|
|
22
|
+
return target.path !== "/" && current.path.startsWith(target.path + "/");
|
|
23
|
+
}
|
|
24
|
+
if (target.path !== current.path) return false;
|
|
25
|
+
if (target.hash && target.hash !== current.hash) return false;
|
|
26
|
+
if (target.search) {
|
|
27
|
+
const want = new URLSearchParams(target.search);
|
|
28
|
+
const have = new URLSearchParams(current.search);
|
|
29
|
+
for (const key of new Set(want.keys())) {
|
|
30
|
+
const wanted = want.getAll(key);
|
|
31
|
+
const had = have.getAll(key);
|
|
32
|
+
if (!wanted.every((value) => had.includes(value))) return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function pathSegments(path) {
|
|
38
|
+
return splitHref(path).path.split("/").filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
function decodeSegment(segment) {
|
|
41
|
+
try {
|
|
42
|
+
return decodeURIComponent(segment);
|
|
43
|
+
} catch {
|
|
44
|
+
return segment;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveNavigation(tree, pathname) {
|
|
48
|
+
const segments = pathSegments(pathname).map(decodeSegment);
|
|
49
|
+
const matches = (pattern) => pattern.length <= segments.length && pattern.every(
|
|
50
|
+
(part, i) => part.startsWith("$") || decodeSegment(part) === segments[i]
|
|
51
|
+
);
|
|
52
|
+
const compare = (a, b) => {
|
|
53
|
+
const shared = Math.min(a.pattern.length, b.pattern.length);
|
|
54
|
+
for (let i = 0; i < shared; i++) {
|
|
55
|
+
const aParam = a.pattern[i].startsWith("$");
|
|
56
|
+
const bParam = b.pattern[i].startsWith("$");
|
|
57
|
+
if (aParam !== bParam) return aParam ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
if (a.pattern.length !== b.pattern.length) {
|
|
60
|
+
return a.pattern.length - b.pattern.length;
|
|
61
|
+
}
|
|
62
|
+
return a.trail.length - b.trail.length;
|
|
63
|
+
};
|
|
64
|
+
let best;
|
|
65
|
+
const visit = (node, ancestors) => {
|
|
66
|
+
const trail2 = [...ancestors, node];
|
|
67
|
+
const pattern = pathSegments(node.path);
|
|
68
|
+
if (matches(pattern)) {
|
|
69
|
+
const candidate = { trail: trail2, pattern };
|
|
70
|
+
if (!best || compare(candidate, best) > 0) best = candidate;
|
|
71
|
+
}
|
|
72
|
+
for (const child of node.children ?? []) visit(child, trail2);
|
|
73
|
+
};
|
|
74
|
+
visit(tree, []);
|
|
75
|
+
const winner = best ?? { trail: [tree], pattern: [] };
|
|
76
|
+
const params = {};
|
|
77
|
+
winner.pattern.forEach((part, i) => {
|
|
78
|
+
if (part.startsWith("$")) params[part.slice(1)] = segments[i];
|
|
79
|
+
});
|
|
80
|
+
const interpolate = (node) => ({
|
|
81
|
+
...node,
|
|
82
|
+
path: node.path.split("/").map((part) => {
|
|
83
|
+
if (!part.startsWith("$")) return part;
|
|
84
|
+
const value = params[part.slice(1)];
|
|
85
|
+
return value == null ? part : encodeURIComponent(value);
|
|
86
|
+
}).join("/")
|
|
87
|
+
});
|
|
88
|
+
const trail = winner.trail.map(interpolate);
|
|
89
|
+
const current = trail[trail.length - 1];
|
|
90
|
+
return {
|
|
91
|
+
current,
|
|
92
|
+
parent: trail.length > 1 ? trail[trail.length - 2] : void 0,
|
|
93
|
+
items: current.items,
|
|
94
|
+
trail,
|
|
95
|
+
params
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { isPathActive, resolveNavigation };
|
|
100
|
+
//# sourceMappingURL=navigation.js.map
|
|
101
|
+
//# sourceMappingURL=navigation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/navigation.ts"],"names":["trail"],"mappings":"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","file":"navigation.js","sourcesContent":["/**\n * Router-free, DOM-free navigation: the logical navigation tree and the\n * active-path matcher, shared by the web app shell and native shells.\n * `resolveNavigation` never reads browser history or `window`.\n */\n\n/**\n * Split a href into its path (trailing slash dropped, except for `/`), query\n * (no `?`) and hash (no `#`).\n */\nfunction splitHref(href: string): { path: string; search: string; hash: string } {\n const hashAt = href.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : href.slice(hashAt + 1);\n const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const searchAt = beforeHash.indexOf(\"?\");\n const rawPath = searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt);\n return {\n path: rawPath.length > 1 ? rawPath.replace(/\\/+$/, \"\") || \"/\" : rawPath,\n search: searchAt === -1 ? \"\" : beforeHash.slice(searchAt + 1),\n hash,\n };\n}\n\n/**\n * Options for {@link isPathActive}.\n *\n * @stability Beta\n */\nexport interface IsPathActiveOptions {\n /** Require an exact path match instead of a prefix match (like `NavLink end`). */\n end?: boolean;\n}\n\n/**\n * Default active-path matcher, mirroring React Router's `NavLink` (prefix\n * match, with `/` treated as exact). Consumers can override via an `isActive`\n * prop where components accept one.\n *\n * `activePath` may carry a query and hash (`pathname + search + hash`). An\n * item whose `to` has neither compares against the path part only, exactly as\n * before. An item that opts in by putting a query or hash in its `to`\n * (`/admin?tab=users`, `/settings#profile`) is active only when the path\n * matches exactly AND every query param it names has the same value in\n * `activePath` (extra params such as `&page=2` are ignored) AND, if it names a\n * hash, the hash is equal.\n *\n * @stability Beta\n */\nexport function isPathActive(\n to: string,\n activePath?: string,\n options?: IsPathActiveOptions,\n): boolean {\n if (activePath == null) return false;\n if (to === activePath) return true;\n const target = splitHref(to);\n const current = splitHref(activePath);\n\n if (!target.search && !target.hash) {\n if (target.path === current.path) return true;\n if (options?.end) return false;\n return target.path !== \"/\" && current.path.startsWith(target.path + \"/\");\n }\n\n if (target.path !== current.path) return false;\n if (target.hash && target.hash !== current.hash) return false;\n if (target.search) {\n const want = new URLSearchParams(target.search);\n const have = new URLSearchParams(current.search);\n for (const key of new Set(want.keys())) {\n const wanted = want.getAll(key);\n const had = have.getAll(key);\n if (!wanted.every((value) => had.includes(value))) return false;\n }\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Hierarchical navigation resolver\n// ---------------------------------------------------------------------------\n\n/**\n * One node of a logical navigation hierarchy. `path` is a route pattern whose\n * segments are either static (`clients`) or named params (`$clientId`). A\n * node's place in the tree is its LOGICAL parent, which need not share its URL\n * prefix — `/clients/$clientId` can sit under `/dashboard`.\n *\n * @stability Beta\n */\nexport interface NavigationNode<T> {\n id: string;\n path: string;\n label: string;\n /** Contextual nav items to show while this node is current. */\n items: T[];\n children?: NavigationNode<T>[];\n /**\n * Ask the app shell to open its desktop sidebar collapsed (icon rail) while\n * this node, or a descendant that doesn't set its own value, is current.\n * The nearest node in the resolved trail that sets it wins, so\n * `defaultCollapsed: false` on a child re-opens it under a collapsed\n * parent. Only a default: once the user has toggled the sidebar, their\n * choice wins everywhere. `AppShell` reads it; `resolveNavigation` copies\n * it through untouched.\n *\n * @stability Beta\n */\n defaultCollapsed?: boolean;\n /**\n * How the app shell shows this node's `items` (its child pages, e.g. a\n * form's Questions / Logic / Responses / Settings). `\"tabs\"` (across the\n * top of the content) or `\"sidebar\"` (nested under the active item) turn\n * shell sub-navigation on, which also gives phones a back item plus the\n * children in the bottom bar. `\"section\"` is the legacy rendering (the\n * shell's default) and `\"off\"` hides them. The nearest node in the trail\n * that sets it wins over the shell's `options.subNav`.\n *\n * @stability Beta\n */\n subNav?: \"section\" | \"tabs\" | \"sidebar\" | \"off\";\n}\n\n/**\n * Result of {@link resolveNavigation}. `current`, `parent` and every `trail`\n * entry are shallow copies of their tree nodes with `path` interpolated from\n * `params` (a param the URL did not supply stays as `$name`).\n *\n * @stability Beta\n */\nexport interface ResolvedNavigation<T> {\n /** The best-matching node — the root when nothing else matches. */\n current: NavigationNode<T>;\n /** `current`'s logical parent, for an \"up\" / back-to-parent link. */\n parent?: NavigationNode<T>;\n /** `current.items`. */\n items: T[];\n /** Root → `current`, inclusive. */\n trail: NavigationNode<T>[];\n /** Params captured by `current.path`, URL-decoded. */\n params: Record<string, string>;\n}\n\nfunction pathSegments(path: string): string[] {\n return splitHref(path).path.split(\"/\").filter(Boolean);\n}\n\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\n/**\n * Resolve where `pathname` sits in a logical navigation tree. Pure: it never\n * reads browser history or `window`.\n *\n * A node matches when its pattern matches a leading run of WHOLE path\n * segments (`/my` matches `/my/tasks`, never `/my-other`). Among matches the\n * winner is decided by positional specificity: at the first position where\n * one pattern is static and the other a `$param`, the static one wins (so\n * `/clients/new` beats `/clients/$clientId` whatever the sibling order, and\n * `/my` beats `/$space/tasks` for `/my/tasks`). Otherwise the pattern matching\n * more segments wins, then the deeper tree node.\n * Non-matching nodes are still searched, so a match keeps its logical\n * ancestors even when their URLs differ.\n *\n * @stability Beta\n */\nexport function resolveNavigation<T>(\n tree: NavigationNode<T>,\n pathname: string,\n): ResolvedNavigation<T> {\n // Compare decoded forms on both sides, so `/caf%C3%A9` matches a `/café`\n // node (and an encoded pattern matches a decoded pathname).\n const segments = pathSegments(pathname).map(decodeSegment);\n\n type Match = { trail: NavigationNode<T>[]; pattern: string[] };\n const matches = (pattern: string[]) =>\n pattern.length <= segments.length &&\n pattern.every(\n (part, i) => part.startsWith(\"$\") || decodeSegment(part) === segments[i],\n );\n\n // > 0 when `a` should win over `b`.\n // Positional specificity, as TanStack Router and React Router rank routes:\n // walking left to right, the first position where one pattern is static and\n // the other a `$param` decides. Only then does the longer match win.\n const compare = (a: Match, b: Match): number => {\n const shared = Math.min(a.pattern.length, b.pattern.length);\n for (let i = 0; i < shared; i++) {\n const aParam = a.pattern[i]!.startsWith(\"$\");\n const bParam = b.pattern[i]!.startsWith(\"$\");\n if (aParam !== bParam) return aParam ? -1 : 1;\n }\n if (a.pattern.length !== b.pattern.length) {\n return a.pattern.length - b.pattern.length;\n }\n return a.trail.length - b.trail.length;\n };\n\n let best: Match | undefined;\n const visit = (node: NavigationNode<T>, ancestors: NavigationNode<T>[]) => {\n const trail = [...ancestors, node];\n const pattern = pathSegments(node.path);\n if (matches(pattern)) {\n const candidate = { trail, pattern };\n if (!best || compare(candidate, best) > 0) best = candidate;\n }\n for (const child of node.children ?? []) visit(child, trail);\n };\n visit(tree, []);\n\n const winner: Match = best ?? { trail: [tree], pattern: [] };\n const params: Record<string, string> = {};\n winner.pattern.forEach((part, i) => {\n if (part.startsWith(\"$\")) params[part.slice(1)] = segments[i]!;\n });\n\n const interpolate = (node: NavigationNode<T>): NavigationNode<T> => ({\n ...node,\n path: node.path\n .split(\"/\")\n .map((part) => {\n if (!part.startsWith(\"$\")) return part;\n const value = params[part.slice(1)];\n return value == null ? part : encodeURIComponent(value);\n })\n .join(\"/\"),\n });\n\n const trail = winner.trail.map(interpolate);\n const current = trail[trail.length - 1]!;\n return {\n current,\n parent: trail.length > 1 ? trail[trail.length - 2] : undefined,\n items: current.items,\n trail,\n params,\n };\n}\n"]}
|
package/dist/provider/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { D as DataProvider, I as Identifier, B as BaseRecord, j as InvalidationKey, W as WriteEvent } from '../types-
|
|
2
|
-
export { A as ArrayFilter, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, i as ImportParams, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as RowFailure, S as ScalarFilter,
|
|
3
|
-
import { D as DataReadError, R as ReadOutcome, b as DataWriteError, W as WriteOutcome } from '../read-outcome-
|
|
4
|
-
export { a as DataReadErrorOptions, c as DataWriteErrorOptions, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-
|
|
1
|
+
import { D as DataProvider, I as Identifier, B as BaseRecord, j as InvalidationKey, W as WriteEvent } from '../types-CS9UpxQS.js';
|
|
2
|
+
export { A as ArrayFilter, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, i as ImportParams, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as ResourceChangeEvent, m as RowFailure, S as ScalarFilter, n as SortParam, o as SubscribeParams, U as UpdateParams, p as UpdateResult } from '../types-CS9UpxQS.js';
|
|
3
|
+
import { D as DataReadError, R as ReadOutcome, b as DataWriteError, W as WriteOutcome } from '../read-outcome-C_9HbXvb.js';
|
|
4
|
+
export { a as DataReadErrorOptions, c as DataWriteErrorOptions, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-C_9HbXvb.js';
|
|
5
5
|
import * as react from 'react';
|
|
6
6
|
import { ReactNode } from 'react';
|
|
7
7
|
import { QueryClient } from '@tanstack/react-query';
|
|
8
8
|
import { S as StorageAdapter } from '../types-DKcxHXxq.js';
|
|
9
|
-
import '../types-
|
|
9
|
+
import '../types-C7FJtFXV.js';
|
|
10
10
|
import '../form-types.js';
|
|
11
11
|
|
|
12
12
|
interface MockProviderOptions {
|
|
@@ -37,6 +37,7 @@ interface SupabaseQueryBuilder {
|
|
|
37
37
|
or(filters: string): SupabaseQueryBuilder;
|
|
38
38
|
order(column: string, options?: {
|
|
39
39
|
ascending?: boolean;
|
|
40
|
+
nullsFirst?: boolean;
|
|
40
41
|
}): SupabaseQueryBuilder;
|
|
41
42
|
range(from: number, to: number): SupabaseQueryBuilder;
|
|
42
43
|
single(): SupabaseQueryBuilder;
|
|
@@ -55,6 +56,35 @@ interface SupabaseQueryBuilder {
|
|
|
55
56
|
*/
|
|
56
57
|
interface SupabaseClient {
|
|
57
58
|
from(table: string): SupabaseQueryBuilder;
|
|
59
|
+
/** Supabase Realtime; needed only for `subscribe` (see `SupabaseProviderOptions.realtime`). */
|
|
60
|
+
channel?(name: string): SupabaseRealtimeChannel;
|
|
61
|
+
/** Releases a channel made by `channel()`. */
|
|
62
|
+
removeChannel?(channel: SupabaseRealtimeChannel): unknown;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One `postgres_changes` message, as Supabase Realtime sends it. Only the
|
|
66
|
+
* fields the provider reads; a real payload has more.
|
|
67
|
+
*/
|
|
68
|
+
interface SupabaseRealtimePayload {
|
|
69
|
+
eventType: "INSERT" | "UPDATE" | "DELETE";
|
|
70
|
+
/** The row after an insert or update (empty for a delete). */
|
|
71
|
+
new?: Record<string, unknown>;
|
|
72
|
+
/** For a delete or update: the old row's key (all of it with `REPLICA IDENTITY FULL`). */
|
|
73
|
+
old?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The part of a Supabase `RealtimeChannel` the provider uses. A real
|
|
77
|
+
* `@supabase/supabase-js` channel satisfies it.
|
|
78
|
+
*/
|
|
79
|
+
interface SupabaseRealtimeChannel {
|
|
80
|
+
on(type: "postgres_changes", filter: {
|
|
81
|
+
event: "*" | "INSERT" | "UPDATE" | "DELETE";
|
|
82
|
+
schema: string;
|
|
83
|
+
table: string;
|
|
84
|
+
filter?: string;
|
|
85
|
+
}, callback: (payload: SupabaseRealtimePayload) => void): SupabaseRealtimeChannel;
|
|
86
|
+
subscribe(callback?: (status: string, error?: unknown) => void): SupabaseRealtimeChannel;
|
|
87
|
+
unsubscribe?(): unknown;
|
|
58
88
|
}
|
|
59
89
|
/**
|
|
60
90
|
* What `createSupabaseProvider` actually requires from its `client` argument
|
|
@@ -72,6 +102,9 @@ interface SupabaseClient {
|
|
|
72
102
|
*/
|
|
73
103
|
interface SupabaseClientInput {
|
|
74
104
|
from(table: string): unknown;
|
|
105
|
+
/** Supabase Realtime, for `subscribe`. Any real client's `channel` fits. */
|
|
106
|
+
channel?(name: string): unknown;
|
|
107
|
+
removeChannel?(channel: unknown): unknown;
|
|
75
108
|
}
|
|
76
109
|
/** What a write hook is told about the write it is part of. */
|
|
77
110
|
interface SupabaseWriteContext {
|
|
@@ -138,6 +171,14 @@ interface SupabaseResourceConfig {
|
|
|
138
171
|
* `useResource` and `useResourceForm` invalidate after their writes.
|
|
139
172
|
*/
|
|
140
173
|
invalidates?: InvalidationKey[] | ((event: WriteEvent) => InvalidationKey[]);
|
|
174
|
+
/**
|
|
175
|
+
* Serve `subscribe` for this resource through Supabase Realtime
|
|
176
|
+
* (`postgres_changes`), so `live` lists and records refresh when the table
|
|
177
|
+
* changes. The table must be in the `supabase_realtime` publication, with
|
|
178
|
+
* RLS on. `false` turns it off even when `SupabaseProviderOptions.realtime`
|
|
179
|
+
* covers the resource.
|
|
180
|
+
*/
|
|
181
|
+
realtime?: boolean;
|
|
141
182
|
}
|
|
142
183
|
/** Options for `createSupabaseProvider`. All optional; none changes reads. */
|
|
143
184
|
interface SupabaseProviderOptions {
|
|
@@ -175,6 +216,13 @@ interface SupabaseProviderOptions {
|
|
|
175
216
|
resource: string;
|
|
176
217
|
operation: "getList" | "getOne" | "getMany";
|
|
177
218
|
}) => void | Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Which resources `subscribe` serves through Supabase Realtime: `true` for
|
|
221
|
+
* all, a list of names, or a test. Off by default; a resource's own
|
|
222
|
+
* `resources[name].realtime` wins. Needs a client with `channel()` (any
|
|
223
|
+
* `@supabase/supabase-js` client).
|
|
224
|
+
*/
|
|
225
|
+
realtime?: boolean | readonly string[] | ((resource: string) => boolean);
|
|
178
226
|
}
|
|
179
227
|
/**
|
|
180
228
|
* The Supabase data provider. Reads and writes go through the client you
|
|
@@ -310,4 +358,4 @@ type GenesisProviderProps = {
|
|
|
310
358
|
*/
|
|
311
359
|
declare function GenesisProvider({ children, provider, mock, storage, queryClient: queryClientProp, }: GenesisProviderProps): react.JSX.Element;
|
|
312
360
|
|
|
313
|
-
export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataReadError, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type PostgrestFieldErrors, ReadOutcome, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
|
|
361
|
+
export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataReadError, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type PostgrestFieldErrors, ReadOutcome, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseRealtimeChannel, type SupabaseRealtimePayload, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
|
package/dist/provider/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from '../chunk-
|
|
1
|
+
export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from '../chunk-Y475Z7BM.js';
|
|
2
2
|
export { DataReadError, isDataReadError, readFailureMessage } from '../chunk-HUNGCTRA.js';
|
|
3
3
|
export { DataWriteError, isDataWriteError } from '../chunk-5NHR6ZXS.js';
|
|
4
4
|
import '../chunk-RYEXWQFD.js';
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { e as ResourceContract, D as DataRow } from '../types-
|
|
2
|
-
export { A as AuditEntry, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from '../types-
|
|
3
|
-
import { I as Identifier } from '../types-
|
|
4
|
-
export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-
|
|
1
|
+
import { e as ResourceContract, D as DataRow } from '../types-C7FJtFXV.js';
|
|
2
|
+
export { A as AuditEntry, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from '../types-C7FJtFXV.js';
|
|
3
|
+
import { I as Identifier } from '../types-CS9UpxQS.js';
|
|
4
|
+
export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-C_9HbXvb.js';
|
|
5
5
|
export { FieldType as WidgetHint } from '../form-types.js';
|
|
6
6
|
|
|
7
7
|
/** The key of a contract as a list, whichever form `key` was declared in. */
|