@venizia/ardor-admin 0.4.8-0 → 0.4.8-10

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.
Files changed (90) hide show
  1. package/README.md +9 -5
  2. package/dist/common/locales/en.d.ts +1 -0
  3. package/dist/common/locales/en.js +1 -0
  4. package/dist/common/locales/vi.d.ts +1 -0
  5. package/dist/common/locales/vi.js +1 -0
  6. package/dist/common/merge.d.ts +11 -0
  7. package/dist/common/merge.js +25 -0
  8. package/dist/common/types.d.ts +39 -6
  9. package/dist/components/application.d.ts +8 -0
  10. package/dist/components/application.js +37 -2
  11. package/dist/components/resolve.d.ts +16 -5
  12. package/dist/components/resolve.js +3 -6
  13. package/dist/features/feature.d.ts +4 -0
  14. package/dist/features/feature.js +1 -0
  15. package/dist/features/messages.js +3 -4
  16. package/dist/hooks/use-document-title.d.ts +5 -0
  17. package/dist/hooks/use-features.js +2 -2
  18. package/dist/hooks/use-notify-error.d.ts +4 -0
  19. package/dist/hooks/use-notify-error.js +4 -0
  20. package/dist/hooks/use-refresh-token.d.ts +4 -0
  21. package/dist/hooks/use-refresh-token.js +4 -0
  22. package/dist/hooks/use-request-header-locale.d.ts +9 -1
  23. package/dist/hooks/use-request-header-locale.js +4 -0
  24. package/dist/hooks/use-translate.d.ts +20 -1
  25. package/dist/hooks/use-translate.js +5 -10
  26. package/dist/providers/auth.d.ts +67 -3
  27. package/dist/providers/auth.js +95 -7
  28. package/dist/providers/count-rest-data.d.ts +8 -0
  29. package/dist/providers/count-rest-data.js +8 -0
  30. package/dist/providers/i18n.d.ts +22 -2
  31. package/dist/providers/i18n.js +28 -5
  32. package/dist/providers/list-filter.d.ts +5 -1
  33. package/dist/providers/list-filter.js +8 -9
  34. package/dist/providers/rest-data.d.ts +66 -3
  35. package/dist/providers/rest-data.js +96 -11
  36. package/package.json +14 -17
  37. package/dist/common/index.d.ts.map +0 -1
  38. package/dist/common/index.js.map +0 -1
  39. package/dist/common/locales/en.d.ts.map +0 -1
  40. package/dist/common/locales/en.js.map +0 -1
  41. package/dist/common/locales/index.d.ts.map +0 -1
  42. package/dist/common/locales/index.js.map +0 -1
  43. package/dist/common/locales/vi.d.ts.map +0 -1
  44. package/dist/common/locales/vi.js.map +0 -1
  45. package/dist/common/types.d.ts.map +0 -1
  46. package/dist/common/types.js.map +0 -1
  47. package/dist/components/application.d.ts.map +0 -1
  48. package/dist/components/application.js.map +0 -1
  49. package/dist/components/index.d.ts.map +0 -1
  50. package/dist/components/index.js.map +0 -1
  51. package/dist/components/resolve.d.ts.map +0 -1
  52. package/dist/components/resolve.js.map +0 -1
  53. package/dist/features/feature.d.ts.map +0 -1
  54. package/dist/features/feature.js.map +0 -1
  55. package/dist/features/index.d.ts.map +0 -1
  56. package/dist/features/index.js.map +0 -1
  57. package/dist/features/messages.d.ts.map +0 -1
  58. package/dist/features/messages.js.map +0 -1
  59. package/dist/hooks/index.d.ts.map +0 -1
  60. package/dist/hooks/index.js.map +0 -1
  61. package/dist/hooks/use-document-title.d.ts.map +0 -1
  62. package/dist/hooks/use-document-title.js.map +0 -1
  63. package/dist/hooks/use-features.d.ts.map +0 -1
  64. package/dist/hooks/use-features.js.map +0 -1
  65. package/dist/hooks/use-html-lang.d.ts.map +0 -1
  66. package/dist/hooks/use-html-lang.js.map +0 -1
  67. package/dist/hooks/use-notify-error.d.ts.map +0 -1
  68. package/dist/hooks/use-notify-error.js.map +0 -1
  69. package/dist/hooks/use-refresh-token.d.ts.map +0 -1
  70. package/dist/hooks/use-refresh-token.js.map +0 -1
  71. package/dist/hooks/use-request-header-locale.d.ts.map +0 -1
  72. package/dist/hooks/use-request-header-locale.js.map +0 -1
  73. package/dist/hooks/use-translate.d.ts.map +0 -1
  74. package/dist/hooks/use-translate.js.map +0 -1
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js.map +0 -1
  77. package/dist/providers/auth.d.ts.map +0 -1
  78. package/dist/providers/auth.js.map +0 -1
  79. package/dist/providers/count-rest-data.d.ts.map +0 -1
  80. package/dist/providers/count-rest-data.js.map +0 -1
  81. package/dist/providers/i18n-sources.d.ts.map +0 -1
  82. package/dist/providers/i18n-sources.js.map +0 -1
  83. package/dist/providers/i18n.d.ts.map +0 -1
  84. package/dist/providers/i18n.js.map +0 -1
  85. package/dist/providers/index.d.ts.map +0 -1
  86. package/dist/providers/index.js.map +0 -1
  87. package/dist/providers/list-filter.d.ts.map +0 -1
  88. package/dist/providers/list-filter.js.map +0 -1
  89. package/dist/providers/rest-data.d.ts.map +0 -1
  90. package/dist/providers/rest-data.js.map +0 -1
@@ -18,6 +18,7 @@ export declare class SessionClearReasons {
18
18
  /** `checkAuth` rejected: no token is stored, or the server answered with no data. */
19
19
  static readonly REJECTED = "rejected";
20
20
  }
21
+ /** One of the `SessionClearReasons` values. */
21
22
  export type TSessionClearReason = TStringConstValue<typeof SessionClearReasons>;
22
23
  /**
23
24
  * The options hold data (`paths`); behaviour is in the protected hooks around `login` and the page
@@ -28,6 +29,27 @@ export type TSessionClearReason = TStringConstValue<typeof SessionClearReasons>;
28
29
  * hook that throws, or does not settle in `sessionHookTimeout`, is logged and does not stop the call
29
30
  * that fired it. A hook does not call this provider's methods: they wait behind it. Build one provider
30
31
  * per auth service: a second one listening to the same service calls every hook again.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { type BaseArdorApplication, configuration, CoreBindings, DefaultAuthProvider, inject, provide } from '@venizia/ardor';
36
+ *
37
+ * class AppAuthProvider extends DefaultAuthProvider {
38
+ * protected override get afterLoginPath() {
39
+ * return '/products';
40
+ * }
41
+ * }
42
+ *
43
+ * @configuration()
44
+ * export class AuthConfiguration {
45
+ * constructor(@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private readonly application: BaseArdorApplication) {}
46
+ *
47
+ * @provide({ key: CoreBindings.DEFAULT_AUTH_PROVIDER })
48
+ * authProvider() {
49
+ * return this.application.instantiate(AppAuthProvider).value(this.application);
50
+ * }
51
+ * }
52
+ * ```
31
53
  */
32
54
  export declare class DefaultAuthProvider<TResource extends string = string> extends BaseProvider<IAuthProvider> {
33
55
  protected restDataProvider: IDataProvider<TResource>;
@@ -46,8 +68,8 @@ export declare class DefaultAuthProvider<TResource extends string = string> exte
46
68
  private hookQueue;
47
69
  /** Where `login` posts: `paths.signIn`, else the IGNIS auth controller's route. */
48
70
  protected get signInPath(): string;
49
- /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`. */
50
- protected get afterLoginPath(): string;
71
+ /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`; `false` stays on the page. */
72
+ protected get afterLoginPath(): string | false;
51
73
  /** The route of the login page, which `checkAuth` and a `401` redirect to. */
52
74
  protected get loginPagePath(): string;
53
75
  /** The route a `403` redirects to; the application mounts it. */
@@ -149,18 +171,60 @@ export declare class DefaultAuthProvider<TResource extends string = string> exte
149
171
  private clearSession;
150
172
  /** Announces the session in the store, when there is one. */
151
173
  private startStoredSession;
152
- /** Typed `Promise<unknown>` so that a subclass override of `login` keeps compiling. */
174
+ /**
175
+ * Runs when the session is gone, however it ended (sign-out, a `401`, a failed refresh, other
176
+ * code calling `cleanUp`): removes what the user stored under the store prefix of this
177
+ * application (`@app/store/`, or `@app/store/<id>/` with an application `id`) in `localStorage` and `sessionStorage`, so the next user does not see it.
178
+ * Override it to keep preferences, or put them under another prefix.
179
+ */
180
+ protected clearStoredData(): void;
181
+ /**
182
+ * Posts the sign-in body to `signInPath`, stores the session and resolves `{ ...response,
183
+ * redirectTo }`, where `redirectTo` is `afterLoginPath` (`false` when it is `false`). Rejects when the request fails, when the
184
+ * response carries no token or user id, or when `beforeSaveSession` throws; no session is stored
185
+ * then. Typed `Promise<unknown>` so that a subclass override of `login` keeps compiling.
186
+ */
153
187
  login(params: ILoginParams): Promise<unknown>;
188
+ /** Clears the session and resolves. */
154
189
  logout(_params?: unknown): Promise<void>;
190
+ /**
191
+ * Resolves when a token is stored and, if `paths.checkAuth` is set, the server answers with data.
192
+ * Otherwise it ends the session (reason `rejected`) and rejects with `unauthenticatedRejection`;
193
+ * it rejects with the request's error when the check request fails.
194
+ */
155
195
  checkAuth(_params?: unknown): Promise<void>;
196
+ /**
197
+ * Resolves for every error but 401 and 403. A 401 clears the session (unless
198
+ * `unauthenticatedRejection` says `logoutUser: false`) and rejects with that rejection; a 403
199
+ * rejects with `{ redirectTo: unauthorizedPagePath, logoutUser: false }`.
200
+ */
156
201
  checkError(error: unknown): Promise<void>;
202
+ /** Resolves the stored user; rejects when there is no `userId`. */
157
203
  getIdentity(_params?: unknown): Promise<any>;
204
+ /** Resolves with nothing; the roles come from `getRoles`. */
158
205
  getPermissions(_params?: unknown): Promise<void>;
206
+ /**
207
+ * Resolves the roles of the stored session. They are read from browser storage, which the user can
208
+ * edit: use them to show or hide interface only, and let the backend check every permission.
209
+ */
159
210
  getRoles(_params?: unknown): Promise<Set<string>>;
160
211
  /**
161
212
  * Refreshes the session through the network service's `refresh`, the one place a refresh is made:
162
213
  * `true` when it was refreshed, `false` when the service has no refresh to make. The `401`
163
214
  * recovery calls the same method, so one burst of 401s and this call share one refresh.
215
+ *
216
+ * @example
217
+ * ```tsx
218
+ * import { useRefreshToken } from '@venizia/ardor';
219
+ *
220
+ * // The data provider options name the refresh endpoint:
221
+ * // { url: '/api', authRecovery: { refreshTokenPath: '/auth/refresh-token' } }
222
+ * export const RefreshButton = () => {
223
+ * const refreshToken = useRefreshToken();
224
+ *
225
+ * return <button onClick={() => refreshToken()}>Refresh</button>;
226
+ * };
227
+ * ```
164
228
  */
165
229
  refreshToken(): Promise<boolean>;
166
230
  value(container: Container): IAuthProvider;
@@ -13,7 +13,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
13
13
  var DefaultAuthProvider_1;
14
14
  import { getError, inject } from '@venizia/ignis-inversion';
15
15
  import { removeDoubleSlashes } from 'ra-core';
16
- import { BaseProvider, CoreBindings, RequestMethods, } from '@venizia/ardor-kernel';
16
+ import { BaseProvider, BrowserStorage, CoreBindings, LocalStorageKeys, RequestMethods, StorageAreas, } from '@venizia/ardor-kernel';
17
17
  /** Why `onSessionCleared` was called. */
18
18
  export class SessionClearReasons {
19
19
  /** `logout` ran. */
@@ -42,6 +42,27 @@ const readStatus = (error) => {
42
42
  * hook that throws, or does not settle in `sessionHookTimeout`, is logged and does not stop the call
43
43
  * that fired it. A hook does not call this provider's methods: they wait behind it. Build one provider
44
44
  * per auth service: a second one listening to the same service calls every hook again.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { type BaseArdorApplication, configuration, CoreBindings, DefaultAuthProvider, inject, provide } from '@venizia/ardor';
49
+ *
50
+ * class AppAuthProvider extends DefaultAuthProvider {
51
+ * protected override get afterLoginPath() {
52
+ * return '/products';
53
+ * }
54
+ * }
55
+ *
56
+ * @configuration()
57
+ * export class AuthConfiguration {
58
+ * constructor(@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private readonly application: BaseArdorApplication) {}
59
+ *
60
+ * @provide({ key: CoreBindings.DEFAULT_AUTH_PROVIDER })
61
+ * authProvider() {
62
+ * return this.application.instantiate(AppAuthProvider).value(this.application);
63
+ * }
64
+ * }
65
+ * ```
45
66
  */
46
67
  let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider extends BaseProvider {
47
68
  constructor(restDataProvider, authProviderOptions, authService, permissionProvider) {
@@ -63,7 +84,7 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
63
84
  get signInPath() {
64
85
  return this.authProviderOptions.paths?.signIn ?? '/auth/sign-in';
65
86
  }
66
- /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`. */
87
+ /** Where the admin goes once `login` resolves: `paths.afterLogin`, else `/`; `false` stays on the page. */
67
88
  get afterLoginPath() {
68
89
  return this.authProviderOptions.paths?.afterLogin ?? '/';
69
90
  }
@@ -272,9 +293,17 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
272
293
  }
273
294
  /** Announces or ends the session after the service saved or cleared it. */
274
295
  syncSession() {
275
- return this.authService.getAuth()?.value
276
- ? this.startStoredSession()
277
- : this.endSession({ reason: this.clearing ?? SessionClearReasons.EXTERNAL });
296
+ if (this.authService.getAuth()?.value) {
297
+ return this.startStoredSession();
298
+ }
299
+ try {
300
+ this.clearStoredData();
301
+ }
302
+ catch (error) {
303
+ // An override that throws must not stop the session from ending.
304
+ this.logger.error('[clearStoredData] failed, the session ends anyway | error: %o', error);
305
+ }
306
+ return this.endSession({ reason: this.clearing ?? SessionClearReasons.EXTERNAL });
278
307
  }
279
308
  async clearSession(opts) {
280
309
  this.clearing = opts.reason;
@@ -294,8 +323,24 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
294
323
  }
295
324
  return this.startSession({ token, user: this.authService.getUser() });
296
325
  }
326
+ /**
327
+ * Runs when the session is gone, however it ended (sign-out, a `401`, a failed refresh, other
328
+ * code calling `cleanUp`): removes what the user stored under the store prefix of this
329
+ * application (`@app/store/`, or `@app/store/<id>/` with an application `id`) in `localStorage` and `sessionStorage`, so the next user does not see it.
330
+ * Override it to keep preferences, or put them under another prefix.
331
+ */
332
+ clearStoredData() {
333
+ const prefix = this.authService.keys.storePrefix;
334
+ BrowserStorage.removeWhere({ storage: StorageAreas.LOCAL, prefix });
335
+ BrowserStorage.removeWhere({ storage: StorageAreas.SESSION, prefix });
336
+ }
297
337
  // --- react-admin's auth provider -------------------------------------------------------------
298
- /** Typed `Promise<unknown>` so that a subclass override of `login` keeps compiling. */
338
+ /**
339
+ * Posts the sign-in body to `signInPath`, stores the session and resolves `{ ...response,
340
+ * redirectTo }`, where `redirectTo` is `afterLoginPath` (`false` when it is `false`). Rejects when the request fails, when the
341
+ * response carries no token or user id, or when `beforeSaveSession` throws; no session is stored
342
+ * then. Typed `Promise<unknown>` so that a subclass override of `login` keeps compiling.
343
+ */
299
344
  async login(params) {
300
345
  const response = await this.restDataProvider.send({
301
346
  resource: this.signInPath,
@@ -311,14 +356,21 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
311
356
  await this.startSession(stored?.value === session.token.value
312
357
  ? { token: stored, user: this.authService.getUser() }
313
358
  : { token: session.token, user: { userId: session.userId, username } });
359
+ const afterLogin = this.afterLoginPath;
314
360
  return {
315
361
  ...response,
316
- redirectTo: removeDoubleSlashes(`/${this.afterLoginPath}`),
362
+ redirectTo: afterLogin === false ? false : removeDoubleSlashes(`/${afterLogin}`),
317
363
  };
318
364
  }
365
+ /** Clears the session and resolves. */
319
366
  async logout(_params) {
320
367
  await this.clearSession({ reason: SessionClearReasons.LOGOUT });
321
368
  }
369
+ /**
370
+ * Resolves when a token is stored and, if `paths.checkAuth` is set, the server answers with data.
371
+ * Otherwise it ends the session (reason `rejected`) and rejects with `unauthenticatedRejection`;
372
+ * it rejects with the request's error when the check request fails.
373
+ */
322
374
  async checkAuth(_params) {
323
375
  const token = this.authService.getAuth();
324
376
  if (!token?.value) {
@@ -344,6 +396,11 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
344
396
  await this.startStoredSession();
345
397
  return Promise.resolve();
346
398
  }
399
+ /**
400
+ * Resolves for every error but 401 and 403. A 401 clears the session (unless
401
+ * `unauthenticatedRejection` says `logoutUser: false`) and rejects with that rejection; a 403
402
+ * rejects with `{ redirectTo: unauthorizedPagePath, logoutUser: false }`.
403
+ */
347
404
  async checkError(error) {
348
405
  const status = readStatus(error);
349
406
  if (status === 401) {
@@ -361,6 +418,7 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
361
418
  }
362
419
  return Promise.resolve();
363
420
  }
421
+ /** Resolves the stored user; rejects when there is no `userId`. */
364
422
  getIdentity(_params) {
365
423
  const user = this.authService.getUser();
366
424
  if (!user?.userId) {
@@ -368,9 +426,14 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
368
426
  }
369
427
  return Promise.resolve(user);
370
428
  }
429
+ /** Resolves with nothing; the roles come from `getRoles`. */
371
430
  getPermissions(_params) {
372
431
  return Promise.resolve();
373
432
  }
433
+ /**
434
+ * Resolves the roles of the stored session. They are read from browser storage, which the user can
435
+ * edit: use them to show or hide interface only, and let the backend check every permission.
436
+ */
374
437
  getRoles(_params) {
375
438
  return Promise.resolve(this.authService.getRoles());
376
439
  }
@@ -378,6 +441,19 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
378
441
  * Refreshes the session through the network service's `refresh`, the one place a refresh is made:
379
442
  * `true` when it was refreshed, `false` when the service has no refresh to make. The `401`
380
443
  * recovery calls the same method, so one burst of 401s and this call share one refresh.
444
+ *
445
+ * @example
446
+ * ```tsx
447
+ * import { useRefreshToken } from '@venizia/ardor';
448
+ *
449
+ * // The data provider options name the refresh endpoint:
450
+ * // { url: '/api', authRecovery: { refreshTokenPath: '/auth/refresh-token' } }
451
+ * export const RefreshButton = () => {
452
+ * const refreshToken = useRefreshToken();
453
+ *
454
+ * return <button onClick={() => refreshToken()}>Refresh</button>;
455
+ * };
456
+ * ```
381
457
  */
382
458
  async refreshToken() {
383
459
  const isRefreshed = await this.restDataProvider.getNetworkService().refresh();
@@ -387,6 +463,18 @@ let DefaultAuthProvider = DefaultAuthProvider_1 = class DefaultAuthProvider exte
387
463
  return isRefreshed;
388
464
  }
389
465
  value(container) {
466
+ // A subclass of DefaultAuthService with its own constructor must forward the application info to
467
+ // super(info): without it the service keeps the unscoped session, silently shared with every other
468
+ // application of the origin. Refuse that here, where the application info is at hand.
469
+ const id = container.get({
470
+ key: CoreBindings.APPLICATION_INFO,
471
+ isOptional: true,
472
+ })?.id;
473
+ if (id !== undefined && this.authService.keys.token !== LocalStorageKeys.scoped({ id }).token) {
474
+ throw getError({
475
+ message: `[DefaultAuthProvider] The auth service does not use the application id '${id}': a subclass of DefaultAuthService with its own constructor passes the application info to super(info)`,
476
+ });
477
+ }
390
478
  // A subclass with its own constructor rarely forwards the optional 4th argument to `super`.
391
479
  const permissionProvider = this.permissionProvider ??
392
480
  container.get({
@@ -3,7 +3,15 @@ import { type TRequestType } from '@venizia/ardor-kernel';
3
3
  import type { TExtraRequest } from '@venizia/ignis-kernel/repository';
4
4
  import type { IListFilter } from './list-filter.js';
5
5
  import { DefaultRestDataProvider } from './rest-data.js';
6
+ /**
7
+ * A `DefaultRestDataProvider` whose list reads take their `total` from the count route
8
+ * (`GET /{resource}/count` with the same `where`) instead of the `Content-Range` header.
9
+ */
6
10
  export declare class CountRestDataProvider<TResource extends string = string> extends DefaultRestDataProvider<TResource> {
11
+ /**
12
+ * Sends the list GET and the count GET together; `total` is the count's `count`, or `0` when the
13
+ * count answers none. Rejects when either request rejects.
14
+ */
7
15
  protected requestList<RecordType extends RaRecord = RaRecord>(opts: {
8
16
  resource: TResource;
9
17
  type: TRequestType;
@@ -1,6 +1,14 @@
1
1
  import { RequestMethods, RequestTypes } from '@venizia/ardor-kernel';
2
2
  import { DefaultRestDataProvider } from './rest-data.js';
3
+ /**
4
+ * A `DefaultRestDataProvider` whose list reads take their `total` from the count route
5
+ * (`GET /{resource}/count` with the same `where`) instead of the `Content-Range` header.
6
+ */
3
7
  export class CountRestDataProvider extends DefaultRestDataProvider {
8
+ /**
9
+ * Sends the list GET and the count GET together; `total` is the count's `count`, or `0` when the
10
+ * count answers none. Rejects when either request rejects.
11
+ */
4
12
  requestList(opts) {
5
13
  const { resource, queryKey, filter } = opts;
6
14
  // The base class reads the rows and keeps the route's list extras as `meta.extra`.
@@ -7,6 +7,26 @@ import { LocaleSources } from './i18n-sources.js';
7
7
  /**
8
8
  * The options hold data (`i18nSources`, `languages`); the hooks are `resolveInitialLocale` and
9
9
  * `composeMessages`, each taking one `opts` object.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { configuration, CoreBindings, englishMessages, provide, vietnameseMessages } from '@venizia/ardor';
14
+ *
15
+ * @configuration()
16
+ * export class I18nConfiguration {
17
+ * @provide({ key: CoreBindings.I18N_PROVIDER_OPTIONS })
18
+ * options() {
19
+ * return {
20
+ * languages: [{ locale: 'en', name: 'English' }, { locale: 'vi', name: 'Tieng Viet' }],
21
+ * i18nSources: {
22
+ * en: englishMessages,
23
+ * // A loader runs the first time the locale is used, such as () => import('./vi').
24
+ * vi: async () => vietnameseMessages,
25
+ * },
26
+ * };
27
+ * }
28
+ * }
29
+ * ```
10
30
  */
11
31
  export declare class DefaultI18nProvider extends BaseProvider<I18nProvider> {
12
32
  protected i18nProviderOptions: II18nProviderOptions;
@@ -15,8 +35,8 @@ export declare class DefaultI18nProvider extends BaseProvider<I18nProvider> {
15
35
  constructor(i18nProviderOptions: II18nProviderOptions, features?: Array<TFeature>);
16
36
  /**
17
37
  * The locale the admin starts in: the browser's language when it is one of `locales`, else `en`.
18
- * Read when the provider is built, not when the module is imported, and not at all outside a
19
- * browser.
38
+ * Read when the provider is built, not when the module is imported. Without a `navigator` the
39
+ * language is `en-US`.
20
40
  */
21
41
  protected resolveInitialLocale(opts: {
22
42
  locales: Array<string>;
@@ -12,15 +12,35 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  };
13
13
  var DefaultI18nProvider_1;
14
14
  import { inject } from '@venizia/ignis-inversion';
15
- import merge from 'lodash/merge.js';
16
15
  import polyglotI18nProvider from 'ra-i18n-polyglot';
17
16
  import { BaseProvider, CoreBindings } from '@venizia/ardor-kernel';
18
17
  import { englishMessages } from '../common/locales/index.js';
18
+ import { deepMerge } from '../common/merge.js';
19
19
  import { FeatureMessages } from '../features/messages.js';
20
20
  import { LazyLocales, LocaleSources } from './i18n-sources.js';
21
21
  /**
22
22
  * The options hold data (`i18nSources`, `languages`); the hooks are `resolveInitialLocale` and
23
23
  * `composeMessages`, each taking one `opts` object.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { configuration, CoreBindings, englishMessages, provide, vietnameseMessages } from '@venizia/ardor';
28
+ *
29
+ * @configuration()
30
+ * export class I18nConfiguration {
31
+ * @provide({ key: CoreBindings.I18N_PROVIDER_OPTIONS })
32
+ * options() {
33
+ * return {
34
+ * languages: [{ locale: 'en', name: 'English' }, { locale: 'vi', name: 'Tieng Viet' }],
35
+ * i18nSources: {
36
+ * en: englishMessages,
37
+ * // A loader runs the first time the locale is used, such as () => import('./vi').
38
+ * vi: async () => vietnameseMessages,
39
+ * },
40
+ * };
41
+ * }
42
+ * }
43
+ * ```
24
44
  */
25
45
  let DefaultI18nProvider = DefaultI18nProvider_1 = class DefaultI18nProvider extends BaseProvider {
26
46
  constructor(i18nProviderOptions, features = []) {
@@ -34,8 +54,8 @@ let DefaultI18nProvider = DefaultI18nProvider_1 = class DefaultI18nProvider exte
34
54
  }
35
55
  /**
36
56
  * The locale the admin starts in: the browser's language when it is one of `locales`, else `en`.
37
- * Read when the provider is built, not when the module is imported, and not at all outside a
38
- * browser.
57
+ * Read when the provider is built, not when the module is imported. Without a `navigator` the
58
+ * language is `en-US`.
39
59
  */
40
60
  resolveInitialLocale(opts) {
41
61
  const browserLanguage = typeof navigator === 'undefined' ? undefined : navigator.language;
@@ -49,8 +69,11 @@ let DefaultI18nProvider = DefaultI18nProvider_1 = class DefaultI18nProvider exte
49
69
  */
50
70
  composeMessages(opts) {
51
71
  const source = this.localeSources.readNow({ locale: opts.locale });
52
- // lodash `merge` loses the message type; every source is a (partial) set of messages.
53
- return merge({}, opts.messages, source ?? englishMessages);
72
+ // A merge of (partial) message sets is a set of messages, which a merge that takes any object
73
+ // cannot say in its type: the assertion is here, where the sources are known, not inside it.
74
+ return deepMerge({
75
+ sources: [opts.messages, source ?? englishMessages],
76
+ });
54
77
  }
55
78
  value(_container) {
56
79
  const { languages = [{ locale: 'en', name: 'English' }] } = this.i18nProviderOptions;
@@ -4,16 +4,20 @@ import { type ICustomParams } from '@venizia/ardor-kernel';
4
4
  /** The IGNIS filter a list request sends. Any other key of the list params is a filter key too. */
5
5
  export interface IListFilter {
6
6
  where: Record<string, unknown>;
7
+ /** Each entry is `'<field> <ASC|DESC>'`. */
7
8
  order?: Array<string>;
8
9
  limit?: number;
10
+ /** IGNIS's offset; `ListFilterBuilder.build` sets this one. */
9
11
  skip?: number;
10
- offset?: number;
11
12
  include?: unknown;
12
13
  fields?: unknown;
14
+ /** Not part of the filter sent: `ListFilterBuilder.build` moves it into the query string. */
13
15
  params?: Record<string, unknown>;
16
+ /** Lifts `limit` and `skip`: the list is not paginated. */
14
17
  noLimit?: boolean;
15
18
  [key: string]: unknown;
16
19
  }
20
+ /** Builds the filter, query and extras of a list request from react-admin's list params. */
17
21
  export declare class ListFilterBuilder {
18
22
  /** `filter.params` as the keys of the query string, as a copy. */
19
23
  static extractQueryParams(opts: {
@@ -1,5 +1,5 @@
1
- import omit from 'lodash/omit.js';
2
1
  import { ValueUtility } from '@venizia/ardor-kernel';
2
+ /** Builds the filter, query and extras of a list request from react-admin's list params. */
3
3
  export class ListFilterBuilder {
4
4
  /** `filter.params` as the keys of the query string, as a copy. */
5
5
  static extractQueryParams(opts) {
@@ -18,13 +18,12 @@ export class ListFilterBuilder {
18
18
  filter = { ...filterInput, where: { ...filterInput.where } };
19
19
  }
20
20
  else {
21
- filter['where'] = {
22
- ...omit(filterInput, ['include', 'params', 'noLimit', 'fields']),
23
- };
24
- filter['include'] = filterInput?.include;
25
- filter['fields'] = filterInput?.fields;
26
- filter['params'] = filterInput?.params;
27
- filter['noLimit'] = filterInput?.noLimit;
21
+ const { include, fields, params, noLimit, ...where } = filterInput ?? {};
22
+ filter['where'] = where;
23
+ filter['include'] = include;
24
+ filter['fields'] = fields;
25
+ filter['params'] = params;
26
+ filter['noLimit'] = noLimit;
28
27
  }
29
28
  if (sort?.field) {
30
29
  filter['order'] = [`${sort.field} ${sort.order}`];
@@ -38,7 +37,7 @@ export class ListFilterBuilder {
38
37
  }
39
38
  else {
40
39
  const { page = 1, perPage = 0 } = pagination ?? {};
41
- // No `perPage` is no limit. `skip` is IGNIS's offset (`offset` is its alias): one is enough.
40
+ // No `perPage` is no limit. `skip` is IGNIS's offset.
42
41
  if (perPage > 0) {
43
42
  filter['limit'] = perPage;
44
43
  filter['skip'] = (Math.max(page, 1) - 1) * perPage;
@@ -1,13 +1,42 @@
1
1
  import { type Container } from '@venizia/ignis-inversion';
2
2
  import type { CreateParams, CreateResult, DeleteManyParams, DeleteManyResult, DeleteParams, DeleteResult, GetListParams, GetListResult, GetManyParams, GetManyReferenceParams, GetManyReferenceResult, GetManyResult, GetOneParams, GetOneResult, Identifier, QueryFunctionContext, RaRecord, UpdateManyParams, UpdateManyResult, UpdateParams, UpdateResult } from 'ra-core';
3
- import { BaseProvider, DefaultNetworkRequestService, type IApplicationInfo, type INetworkResponse, type ICustomParams, type IRestDataProviderOptions, type ISendParams, type ISendResponse, type TRequestType } from '@venizia/ardor-kernel';
3
+ import { BaseProvider, type DefaultAuthService, DefaultNetworkRequestService, type IApplicationInfo, type INetworkResponse, type ICustomParams, type IRestDataProviderOptions, type ISendParams, type TRequestType } from '@venizia/ardor-kernel';
4
4
  import type { TExtraRequest } from '@venizia/ignis-kernel/repository';
5
5
  import type { IDataProvider, IRestRequestOptions } from '../common/index.js';
6
6
  import { type IListFilter } from './list-filter.js';
7
+ /**
8
+ * The react-admin data provider over an IGNIS REST API. Each method takes one `opts` object
9
+ * (`{ resource, params }`) and resolves the result react-admin expects; it rejects with the error
10
+ * the network service throws (a non-2xx response, an empty base URL, an auth failure that cannot be
11
+ * recovered). A subclass overrides the protected hooks (`createNetworkService`, `buildListFilter`,
12
+ * `requestList`).
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { type BaseArdorApplication, configuration, CoreBindings, DefaultRestDataProvider, inject, provide } from '@venizia/ardor';
17
+ *
18
+ * @configuration()
19
+ * export class DataConfiguration {
20
+ * constructor(@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private readonly application: BaseArdorApplication) {}
21
+ *
22
+ * @provide({ key: CoreBindings.REST_DATA_PROVIDER_OPTIONS })
23
+ * options() {
24
+ * return { url: '/api', noAuthPaths: ['/auth/sign-in'] };
25
+ * }
26
+ *
27
+ * @provide({ key: CoreBindings.DEFAULT_REST_DATA_PROVIDER })
28
+ * dataProvider() {
29
+ * return this.application.instantiate(DefaultRestDataProvider).value(this.application);
30
+ * }
31
+ * }
32
+ * ```
33
+ */
7
34
  export declare class DefaultRestDataProvider<TResource extends string = string> extends BaseProvider<IDataProvider<TResource>> {
8
35
  protected restDataProviderOptions: IRestDataProviderOptions;
9
36
  protected applicationInfo: IApplicationInfo;
10
37
  private cachedNetworkService?;
38
+ /** The auth service of the container, set when `value` builds the provider; pass it to the network service you return from `createNetworkService`. */
39
+ protected authService?: DefaultAuthService;
11
40
  constructor(restDataProviderOptions: IRestDataProviderOptions, applicationInfo: IApplicationInfo);
12
41
  /**
13
42
  * Builds the network service this provider sends through. Override it to return a subclass of
@@ -16,6 +45,7 @@ export declare class DefaultRestDataProvider<TResource extends string = string>
16
45
  */
17
46
  protected createNetworkService(): DefaultNetworkRequestService;
18
47
  protected get networkService(): DefaultNetworkRequestService;
48
+ /** The network service every request of this provider goes through; built on first use. */
19
49
  getNetworkService(): DefaultNetworkRequestService;
20
50
  /**
21
51
  * The ids a bulk write touched, read from the affected rows the server answers (not the ids asked
@@ -69,52 +99,85 @@ export declare class DefaultRestDataProvider<TResource extends string = string>
69
99
  filename?: string;
70
100
  contentDisposition?: string;
71
101
  }>;
102
+ /**
103
+ * `GET /{resource}` with the filter built from the list params in the query. Resolves the rows and
104
+ * `total` (the `Content-Range` total, else the page's row count), plus `meta.extra` when a route
105
+ * answered extras.
106
+ */
72
107
  getList<RecordType extends RaRecord = RaRecord>(opts: {
73
108
  resource: TResource;
74
109
  params: GetListParams & QueryFunctionContext & ICustomParams;
75
110
  }): Promise<GetListResult<RecordType>>;
111
+ /**
112
+ * `GET /{resource}/{id}`, with `params.meta.filter` as the query filter. Resolves `{ data }`.
113
+ */
76
114
  getOne<RecordType extends RaRecord = RaRecord>(opts: {
77
115
  resource: TResource;
78
116
  params: GetOneParams<RecordType> & QueryFunctionContext & ICustomParams;
79
117
  }): Promise<GetOneResult<RecordType>>;
118
+ /**
119
+ * `GET /{resource}` with the ids in `filter.where.id.inq`, merged into `params.meta.filter`.
120
+ * Resolves `{ data }`.
121
+ */
80
122
  getMany<RecordType extends RaRecord = RaRecord>(opts: {
81
123
  resource: TResource;
82
124
  params: GetManyParams<RecordType> & QueryFunctionContext & ICustomParams;
83
125
  }): Promise<GetManyResult<RecordType>>;
126
+ /**
127
+ * As `getList`, with `filter.where[target] = id`. Resolves the rows and `total`.
128
+ */
84
129
  getManyReference<RecordType extends RaRecord = RaRecord>(opts: {
85
130
  resource: TResource;
86
131
  params: GetManyReferenceParams & QueryFunctionContext & ICustomParams;
87
132
  }): Promise<GetManyReferenceResult<RecordType>>;
133
+ /** `PATCH /{resource}/{id}` with `params.data` as the body. Resolves `{ data }`. */
88
134
  update<RecordType extends RaRecord = RaRecord>(opts: {
89
135
  resource: TResource;
90
136
  params: UpdateParams;
91
137
  }): Promise<UpdateResult<RecordType>>;
138
+ /**
139
+ * `PATCH /{resource}` with `{ ...params.data, where: { id: { inq: ids } } }` as the body. Resolves
140
+ * `{ data }`, the ids of the rows the server answered (`undefined` when the answer is not a row
141
+ * array). Rejects when `ids` is empty or `params.data` has a `where` field, before any request.
142
+ */
92
143
  updateMany<RecordType extends RaRecord = RaRecord>(opts: {
93
144
  resource: TResource;
94
145
  params: UpdateManyParams;
95
146
  }): Promise<UpdateManyResult<RecordType>>;
147
+ /** `POST /{resource}` with `params.data` as the body. Resolves `{ data }`, the created record. */
96
148
  create<RecordType extends Omit<RaRecord, 'id'> = Omit<RaRecord, 'id'>, ResultRecordType extends RaRecord = RecordType & {
97
149
  id: Identifier;
98
150
  }>(opts: {
99
151
  resource: TResource;
100
152
  params: CreateParams;
101
153
  }): Promise<CreateResult<ResultRecordType>>;
154
+ /** `DELETE /{resource}/{id}`. Resolves `{ data }`, the record the server answered. */
102
155
  delete<RecordType extends RaRecord = RaRecord>(opts: {
103
156
  resource: TResource;
104
157
  params: DeleteParams<RecordType>;
105
158
  }): Promise<DeleteResult<RecordType>>;
159
+ /**
160
+ * `DELETE /{resource}` with `{ where: { id: { inq: ids } } }` as the body. Resolves `{ data }`, the
161
+ * ids of the rows the server answered (`undefined` when the answer is not a row array). Rejects
162
+ * when `ids` is empty, before any request.
163
+ */
106
164
  deleteMany<RecordType extends RaRecord = RaRecord>(opts: {
107
165
  resource: TResource;
108
166
  params: DeleteManyParams<RecordType>;
109
167
  }): Promise<DeleteManyResult<RecordType>>;
168
+ /**
169
+ * Sends `params.method` to `/{resource}` with the provider's headers and auth; `requestType`
170
+ * defaults to `RequestTypes.SEND`. Resolves the response read by that type. Rejects when
171
+ * `params.method` is missing, before any request.
172
+ */
110
173
  send<TResult = unknown>(opts: {
111
174
  resource: TResource;
112
175
  params: ISendParams;
113
- }): Promise<ISendResponse<TResult>>;
176
+ }): Promise<INetworkResponse<TResult>>;
114
177
  /**
115
178
  * react-admin's data provider contract takes positional arguments, `(resource, params)`: the one
116
179
  * place this class does not take an `opts` object. Each method hands them to the `opts` method.
117
180
  */
118
- value(_container: Container): IDataProvider<TResource>;
181
+ value(container: Container): IDataProvider<TResource>;
119
182
  }
120
183
  //# sourceMappingURL=rest-data.d.ts.map