@svadmin/refine-adapter 0.12.15 → 0.14.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @svadmin/refine-adapter
2
2
 
3
- > Bridge any [@refinedev](https://github.com/refinedev/refine) data provider to svadmin
3
+ > Bridge [@refinedev](https://github.com/refinedev/refine) data and framework providers to svadmin
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@svadmin/refine-adapter?color=ff3e00&label=npm)](https://www.npmjs.com/package/@svadmin/refine-adapter)
6
6
 
@@ -66,6 +66,36 @@ Then pass `dataProvider` to `<AdminApp>` or `setDataProvider()` as usual.
66
66
 
67
67
  Optional methods are only wired if the underlying Refine provider implements them.
68
68
 
69
+ ## Other Refine providers
70
+
71
+ ```typescript
72
+ import {
73
+ createRefineAuthAdapter,
74
+ createRefineAccessControlAdapter,
75
+ createRefineNotificationAdapter,
76
+ createRefineLiveAdapter,
77
+ createRefineRouterAdapter,
78
+ createRefineI18nAdapter,
79
+ } from '@svadmin/refine-adapter';
80
+
81
+ const authProvider = createRefineAuthAdapter(refineAuthProvider);
82
+ const accessControlProvider = createRefineAccessControlAdapter(refineAccessControlProvider);
83
+ const notificationProvider = createRefineNotificationAdapter(refineNotificationProvider);
84
+ const liveProvider = createRefineLiveAdapter(refineLiveProvider);
85
+ const routerProvider = createRefineRouterAdapter(refineRouterProvider);
86
+ const i18nProvider = createRefineI18nAdapter(refineI18nProvider);
87
+ ```
88
+
89
+ Router adapters resolve Refine's function-generator bindings at call time. I18n
90
+ adapters normalize translations to strings and map `changeLocale` to
91
+ `setLocale`.
92
+
93
+ ## UI boundary
94
+
95
+ Like Refine, this package keeps the core headless. It does not ship Table,
96
+ Upload, DatePicker, Modal/Drawer, or form components. Those stay in the UI layer
97
+ or host application, while Refine-style provider contracts stay in core.
98
+
69
99
  ## Which Svadmin Providers Use This?
70
100
 
71
101
  The following `@svadmin/*` packages internally use `@svadmin/refine-adapter` to bridge their corresponding `@refinedev/*` provider:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/refine-adapter",
3
- "version": "0.12.15",
3
+ "version": "0.14.1",
4
4
  "description": "Universal Refine DataProvider adapter for svadmin",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "peerDependencies": {
23
- "@svadmin/core": "^0.67.0"
23
+ "@svadmin/core": "^0.69.2"
24
24
  },
25
25
  "license": "MIT",
26
26
  "repository": {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Refine AccessControlProvider → svadmin AccessControlProvider.
3
+ *
4
+ * Browser checks remain UI hints; the backend must enforce the same policy.
5
+ */
6
+ import type { AccessControlProvider, CanParams, CanResult } from '@svadmin/core';
7
+ import { decodeCanResult } from '@svadmin/core/contracts';
8
+ import { definedOptions } from '@svadmin/core/options';
9
+
10
+ type RefineCan = (params: { resource: string; action: string; params?: CanParams['params']; meta?: Record<string, unknown> }) => Promise<unknown>;
11
+ type RefineAccessSource = {
12
+ can?: RefineCan;
13
+ canMany?: (params: readonly { resource: string; action: string; params?: CanParams['params']; meta?: Record<string, unknown> }[]) => Promise<unknown>;
14
+ options?: unknown;
15
+ };
16
+
17
+ function normalizeParams(params: CanParams) {
18
+ return {
19
+ resource: params.resource,
20
+ action: params.action,
21
+ ...(params.params === undefined ? {} : { params: params.params }),
22
+ ...(params.meta === undefined ? {} : { meta: params.meta }),
23
+ };
24
+ }
25
+
26
+ /** Register the adapter with `setAccessControlProvider` after creation. */
27
+ export function createRefineAccessControlAdapter(source: unknown): AccessControlProvider {
28
+ if (typeof source !== 'object' || source === null || typeof (source as RefineAccessSource).can !== 'function') {
29
+ throw new TypeError('createRefineAccessControlAdapter: expected a can() method');
30
+ }
31
+ const input = source as RefineAccessSource;
32
+ const can = input.can as RefineCan;
33
+ const canMany = input.canMany;
34
+ const options = typeof input.options === 'object' && input.options !== null
35
+ ? input.options as NonNullable<AccessControlProvider['options']> : undefined;
36
+ return {
37
+ can: async (params: CanParams): Promise<CanResult> => decodeCanResult(await can.call(input, normalizeParams(params))),
38
+ ...(typeof canMany === 'function' ? {
39
+ canMany: async (params: readonly CanParams[]): Promise<CanResult[]> => {
40
+ const results = await canMany.call(input, params.map(normalizeParams));
41
+ if (!Array.isArray(results) || results.length !== params.length) {
42
+ throw new TypeError('Invalid access control batch response');
43
+ }
44
+ return results.map((result) => decodeCanResult(result));
45
+ },
46
+ } : {}),
47
+ ...definedOptions({ options }),
48
+ };
49
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Refine AuthProvider → svadmin AuthProvider.
3
+ *
4
+ * Refine's auth contract is structurally close to svadmin's, so this adapter only
5
+ * normalizes results through the same validated decoders used by first-party
6
+ * providers. Optional methods are forwarded only when the source implements them.
7
+ */
8
+ import type {
9
+ AuthActionResult,
10
+ AuthErrorResult,
11
+ AuthProvider,
12
+ CheckResult,
13
+ Identity,
14
+ } from '@svadmin/core';
15
+ import {
16
+ decodeAuthAction,
17
+ decodeAuthCheck,
18
+ decodeAuthErrorResult,
19
+ decodeIdentity,
20
+ } from '@svadmin/core/contracts';
21
+
22
+ type RefineAuthSource = Partial<Record<
23
+ | 'login' | 'logout' | 'check' | 'getIdentity' | 'getPermissions'
24
+ | 'register' | 'forgotPassword' | 'updatePassword' | 'onError',
25
+ (...args: never[]) => unknown
26
+ >>;
27
+
28
+ function isObject(value: unknown): value is Record<string, unknown> {
29
+ return typeof value === 'object' && value !== null;
30
+ }
31
+
32
+ function method<T extends (...args: never[]) => unknown>(source: RefineAuthSource, name: keyof RefineAuthSource): T | undefined {
33
+ const value = source[name];
34
+ return typeof value === 'function' ? value as T : undefined;
35
+ }
36
+
37
+ function actionResult(value: unknown): AuthActionResult {
38
+ if (isObject(value) && value['success'] === true) {
39
+ return decodeAuthAction({
40
+ success: true,
41
+ ...(typeof value['redirectTo'] === 'string' ? { redirectTo: value['redirectTo'] } : {}),
42
+ });
43
+ }
44
+ const rawError = isObject(value) ? value['error'] : undefined;
45
+ const error = isObject(rawError)
46
+ ? {
47
+ message: typeof rawError['message'] === 'string' ? rawError['message'] : 'Authentication failed',
48
+ ...(typeof rawError['name'] === 'string' ? { name: rawError['name'] } : {}),
49
+ }
50
+ : { message: 'Authentication failed' };
51
+ return decodeAuthAction({ success: false, error });
52
+ }
53
+
54
+ function call<A extends unknown[], R>(source: RefineAuthSource, name: keyof RefineAuthSource, args: A): Promise<R> {
55
+ const fn = method<(...input: A) => Promise<R> | R>(source, name);
56
+ if (!fn) throw new TypeError(`Refine AuthProvider is missing "${String(name)}"`);
57
+ return Promise.resolve(fn.apply(source, args));
58
+ }
59
+
60
+ /** Validate that the source exposes the methods svadmin requires before mounting it. */
61
+ export function createRefineAuthAdapter(source: unknown): AuthProvider {
62
+ if (!isObject(source) || typeof source['login'] !== 'function' ||
63
+ typeof source['logout'] !== 'function' || typeof source['check'] !== 'function' ||
64
+ typeof source['getIdentity'] !== 'function') {
65
+ throw new TypeError('createRefineAuthAdapter: expected login, logout, check, and getIdentity');
66
+ }
67
+ const input = source as RefineAuthSource;
68
+ const register = method(input, 'register');
69
+ const forgotPassword = method(input, 'forgotPassword');
70
+ const updatePassword = method(input, 'updatePassword');
71
+ const getPermissions = method(input, 'getPermissions');
72
+ const onError = method(input, 'onError');
73
+
74
+ return {
75
+ login: async (params): Promise<AuthActionResult> => actionResult(await call(input, 'login', [params])),
76
+ logout: async (params): Promise<AuthActionResult> => actionResult(await call(input, 'logout', [params ?? {}])),
77
+ check: async (params): Promise<CheckResult> => decodeAuthCheck(await call(input, 'check', [params ?? {}])),
78
+ getIdentity: async (): Promise<Identity | null> => decodeIdentity(await call(input, 'getIdentity', [])),
79
+ ...(register ? { register: async (params: Record<string, unknown>) => actionResult(await call(input, 'register', [params])) } : {}),
80
+ ...(forgotPassword ? { forgotPassword: async (params: Record<string, unknown>) => actionResult(await call(input, 'forgotPassword', [params])) } : {}),
81
+ ...(updatePassword ? { updatePassword: async (params: Record<string, unknown>) => actionResult(await call(input, 'updatePassword', [params])) } : {}),
82
+ ...(getPermissions ? { getPermissions: async (params?: Record<string, unknown>) => call(input, 'getPermissions', [params]) } : {}),
83
+ ...(onError ? { onError: async (error: unknown): Promise<AuthErrorResult> => decodeAuthErrorResult(await call(input, 'onError', [error])) } : {}),
84
+ };
85
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Refine I18nProvider → svadmin I18nProvider.
3
+ *
4
+ * Refine's provider is imperative and may return non-string translations.
5
+ * svadmin keeps translation returns strictly string-shaped at the boundary.
6
+ */
7
+ import type { I18nProvider } from '@svadmin/core';
8
+
9
+ type RefineI18nSource = {
10
+ translate?: (key: string, options?: unknown, defaultMessage?: string) => unknown;
11
+ changeLocale?: (locale: string, options?: unknown) => unknown;
12
+ getLocale?: () => unknown;
13
+ };
14
+
15
+ function isObject(value: unknown): value is Record<PropertyKey, unknown> {
16
+ return typeof value === 'object' && value !== null;
17
+ }
18
+
19
+ export function createRefineI18nAdapter(source: unknown): I18nProvider {
20
+ if (!isObject(source)) {
21
+ throw new TypeError('[svadmin] createRefineI18nAdapter: expected a Refine I18nProvider object');
22
+ }
23
+ const i18n = source as RefineI18nSource;
24
+ if (typeof i18n.translate !== 'function'
25
+ || typeof i18n.changeLocale !== 'function'
26
+ || typeof i18n.getLocale !== 'function') {
27
+ throw new TypeError('createRefineI18nAdapter: expected translate, changeLocale and getLocale');
28
+ }
29
+
30
+ return {
31
+ translate(key, params) {
32
+ const value = i18n.translate?.(key, params, key);
33
+ if (typeof value !== 'string') throw new TypeError('Refine I18nProvider translate() must return a string');
34
+ return value;
35
+ },
36
+ getLocale() {
37
+ const value = i18n.getLocale?.();
38
+ if (typeof value !== 'string') throw new TypeError('Refine I18nProvider getLocale() must return a string');
39
+ return value;
40
+ },
41
+ setLocale(locale) {
42
+ void i18n.changeLocale?.(locale);
43
+ },
44
+ };
45
+ }
package/src/index.ts CHANGED
@@ -95,3 +95,10 @@ export function createRefineAdapter(refineProvider: unknown): SvadminDataProvide
95
95
 
96
96
  return withValidatedResponses(transport);
97
97
  }
98
+
99
+ export { createRefineAuthAdapter } from './auth';
100
+ export { createRefineAccessControlAdapter } from './access-control';
101
+ export { createRefineNotificationAdapter } from './notification';
102
+ export { createRefineLiveAdapter } from './live';
103
+ export { createRefineRouterAdapter } from './router';
104
+ export { createRefineI18nAdapter } from './i18n';
package/src/live.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Refine LiveProvider → svadmin LiveProvider.
3
+ *
4
+ * Refine emits `created | updated | deleted` while svadmin uses
5
+ * `INSERT | UPDATE | DELETE`. Channels map to svadmin resources.
6
+ */
7
+ import type { LiveEvent, LiveProvider } from '@svadmin/core';
8
+ import { snapshotLiveEvent } from '@svadmin/core/contracts';
9
+
10
+ type RefineEventType = 'created' | 'updated' | 'deleted' | string;
11
+ type RefineLiveEvent = {
12
+ type: RefineEventType;
13
+ channel: string;
14
+ payload?: unknown;
15
+ date?: Date | string;
16
+ };
17
+ type RefineLiveSource = {
18
+ subscribe: (params: {
19
+ channel: string;
20
+ types?: RefineEventType[];
21
+ params?: Record<string, unknown>;
22
+ callback: (event: RefineLiveEvent) => void;
23
+ onError?: (error: unknown) => void;
24
+ }) => unknown;
25
+ unsubscribe?: (params: { channel: string; params?: Record<string, unknown> }) => void;
26
+ publish?: (event: RefineLiveEvent) => unknown;
27
+ };
28
+
29
+ const toEventType: Record<string, LiveEvent['type']> = {
30
+ created: 'INSERT', updated: 'UPDATE', deleted: 'DELETE',
31
+ INSERT: 'INSERT', UPDATE: 'UPDATE', DELETE: 'DELETE',
32
+ };
33
+ const fromEventType: Record<LiveEvent['type'], RefineEventType> = {
34
+ INSERT: 'created', UPDATE: 'updated', DELETE: 'deleted',
35
+ };
36
+
37
+ function toLiveEvent(event: RefineLiveEvent, resource: string): LiveEvent | undefined {
38
+ const type = toEventType[String(event.type)];
39
+ if (!type) return undefined;
40
+ const payload = typeof event.payload === 'object' && event.payload !== null
41
+ ? event.payload as Record<string, unknown>
42
+ : {};
43
+ return snapshotLiveEvent({ type, resource, payload });
44
+ }
45
+
46
+ export function createRefineLiveAdapter(source: unknown): LiveProvider {
47
+ if (typeof source !== 'object' || source === null || typeof (source as RefineLiveSource).subscribe !== 'function') {
48
+ throw new TypeError('createRefineLiveAdapter: expected a subscribe() method');
49
+ }
50
+ const input = source as RefineLiveSource;
51
+ return {
52
+ subscribe: ({ resource, liveParams, callback }) => {
53
+ const dispose = input.subscribe.call(input, {
54
+ channel: resource,
55
+ ...(liveParams === undefined ? {} : { params: liveParams }),
56
+ callback: (event) => {
57
+ if (typeof event !== 'object' || event === null) return;
58
+ const decoded = toLiveEvent(event, resource);
59
+ if (decoded) callback(decoded);
60
+ },
61
+ });
62
+ return typeof dispose === 'function' ? dispose as () => void : () => {};
63
+ },
64
+ ...(typeof input.unsubscribe === 'function' ? {
65
+ unsubscribe: (params: { resource: string; liveParams?: Record<string, unknown> }) => {
66
+ input.unsubscribe?.call(input, {
67
+ channel: params.resource,
68
+ ...(params.liveParams === undefined ? {} : { params: params.liveParams }),
69
+ });
70
+ },
71
+ } : {}),
72
+ ...(typeof input.publish === 'function' ? {
73
+ publish: (event: LiveEvent) => {
74
+ const checked = snapshotLiveEvent(event);
75
+ if (!checked) throw new TypeError('Invalid live event');
76
+ void input.publish?.call(input, {
77
+ channel: checked.resource,
78
+ type: fromEventType[checked.type],
79
+ payload: checked.payload,
80
+ date: new Date(),
81
+ });
82
+ },
83
+ } : {}),
84
+ };
85
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Refine NotificationProvider → svadmin NotificationProvider.
3
+ *
4
+ * Refine allows extra fields such as `undoable`; svadmin keeps the checked
5
+ * message envelope and ignores provider-specific extras.
6
+ */
7
+ import type { NotificationProvider } from '@svadmin/core';
8
+
9
+ type RefineNotificationSource = {
10
+ open?: (params: { type: string; message: string; description?: string; key?: string }) => unknown;
11
+ close?: (key: string) => unknown;
12
+ };
13
+
14
+ const types = new Set(['success', 'error', 'warning', 'info']);
15
+
16
+ export function createRefineNotificationAdapter(source: unknown): NotificationProvider {
17
+ if (typeof source !== 'object' || source === null ||
18
+ typeof (source as RefineNotificationSource).open !== 'function' ||
19
+ typeof (source as RefineNotificationSource).close !== 'function') {
20
+ throw new TypeError('createRefineNotificationAdapter: expected open() and close()');
21
+ }
22
+ const input = source as Required<RefineNotificationSource>;
23
+ return {
24
+ open: (params) => {
25
+ if (!types.has(params.type)) throw new TypeError('Invalid notification type');
26
+ input.open.call(source, params);
27
+ },
28
+ close: (key) => { input.close.call(source, key); },
29
+ };
30
+ }
@@ -57,3 +57,17 @@ declare module '@refinedev/supabase' {
57
57
  const defaultExport: (...args: unknown[]) => unknown;
58
58
  export default defaultExport;
59
59
  }
60
+
61
+ declare module '@refinedev/parse' {
62
+ export const createDataProvider: (...args: unknown[]) => unknown;
63
+ export const dataProvider: (...args: unknown[]) => unknown;
64
+ const defaultExport: (...args: unknown[]) => unknown;
65
+ export default defaultExport;
66
+ }
67
+
68
+ declare module '@refinedev/strapi-graphql' {
69
+ export const createDataProvider: (...args: unknown[]) => unknown;
70
+ export const dataProvider: (...args: unknown[]) => unknown;
71
+ const defaultExport: (...args: unknown[]) => unknown;
72
+ export default defaultExport;
73
+ }
package/src/router.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Refine RouterProvider → svadmin RouterProvider.
3
+ *
4
+ * Refine exposes router bindings as function generators so UI packages can use
5
+ * hooks inside `go`, `back` and `parse`. svadmin consumes plain functions, so
6
+ * this adapter resolves the generators once per call and normalizes the result.
7
+ */
8
+ import type { RouterProvider } from '@svadmin/core';
9
+
10
+ type RefineGoConfig = {
11
+ to?: string;
12
+ query?: Record<string, unknown>;
13
+ hash?: string;
14
+ type?: 'push' | 'replace' | 'path';
15
+ };
16
+
17
+ type RefineParsed = {
18
+ resource?: { name?: string; identifier?: string } | string;
19
+ action?: string;
20
+ id?: string | number;
21
+ params?: Record<string, unknown>;
22
+ pathname?: string;
23
+ };
24
+
25
+ type RefineRouterSource = {
26
+ go?: () => ((config: RefineGoConfig) => unknown);
27
+ back?: () => (() => unknown);
28
+ parse?: () => (() => RefineParsed);
29
+ };
30
+
31
+ function isObject(value: unknown): value is Record<PropertyKey, unknown> {
32
+ return typeof value === 'object' && value !== null;
33
+ }
34
+
35
+ function queryRecord(query: Record<string, string> | undefined): Record<string, unknown> | undefined {
36
+ if (!query) return undefined;
37
+ return Object.fromEntries(Object.entries(query).map(([key, value]) => [key, value]));
38
+ }
39
+
40
+ function resourceName(resource: RefineParsed['resource']): string | undefined {
41
+ if (typeof resource === 'string') return resource;
42
+ return resource?.identifier ?? resource?.name;
43
+ }
44
+
45
+ export function createRefineRouterAdapter(source: unknown): RouterProvider {
46
+ if (!isObject(source)) {
47
+ throw new TypeError('[svadmin] createRefineRouterAdapter: expected a Refine RouterProvider object');
48
+ }
49
+ const router = source as RefineRouterSource;
50
+
51
+ return {
52
+ go({ to, query, hash, type }) {
53
+ if (!router.go) return false;
54
+ const go = router.go();
55
+ if (typeof go !== 'function') throw new TypeError('Refine RouterProvider go() must return a function');
56
+ const normalizedType = type === 'replace' ? 'replace' : 'push';
57
+ const queryValue = queryRecord(query);
58
+ const result = go({
59
+ ...(to === undefined ? {} : { to }),
60
+ ...(queryValue === undefined ? {} : { query: queryValue }),
61
+ ...(hash === undefined ? {} : { hash }),
62
+ type: normalizedType,
63
+ });
64
+ if (typeof result === 'string') return false;
65
+ return;
66
+ },
67
+ back() {
68
+ if (!router.back) return;
69
+ const back = router.back();
70
+ if (typeof back !== 'function') throw new TypeError('Refine RouterProvider back() must return a function');
71
+ back();
72
+ },
73
+ parse() {
74
+ if (!router.parse) return { params: {}, pathname: '/' };
75
+ const parse = router.parse();
76
+ if (typeof parse !== 'function') throw new TypeError('Refine RouterProvider parse() must return a function');
77
+ const parsed = parse();
78
+ const params = Object.fromEntries(Object.entries(parsed.params ?? {}).map(([key, value]) => [key, String(value)]));
79
+ const resource = resourceName(parsed.resource);
80
+ return {
81
+ ...(resource === undefined ? {} : { resource }),
82
+ ...(parsed.action === undefined ? {} : { action: String(parsed.action) }),
83
+ ...(parsed.id === undefined ? {} : { id: String(parsed.id) }),
84
+ params,
85
+ pathname: typeof parsed.pathname === 'string' ? parsed.pathname : '/',
86
+ };
87
+ },
88
+ };
89
+ }