@nocobase/app-template-default 1.0.0-beta.89 → 1.0.0-beta.90

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 (38) hide show
  1. package/.agents/rules/shell.md +4 -0
  2. package/LICENSE.txt +129 -0
  3. package/client/lib/navigation-transform.ts +29 -1
  4. package/client/locales/system/en-US.ts +4 -0
  5. package/client/locales/system/zh-CN.ts +4 -0
  6. package/client/pages/home/index.tsx +66 -51
  7. package/client/pages/settings/database-explorer/en-US.ts +33 -0
  8. package/client/pages/settings/database-explorer/index.tsx +358 -0
  9. package/client/pages/settings/database-explorer/routes.ts +14 -0
  10. package/client/pages/settings/database-explorer/zh-CN.ts +30 -0
  11. package/client/plugins.ts +8 -1
  12. package/client/routes.ts +2 -1
  13. package/config.example.yml +17 -11
  14. package/index.html +8 -11
  15. package/package.json +26 -26
  16. package/server/config/auth.ts +1 -1
  17. package/server/config/index.ts +3 -0
  18. package/server/config/jobs.ts +16 -8
  19. package/server/config/queue.ts +14 -13
  20. package/server/config/realtime.ts +4 -0
  21. package/tests/client/client-routes.test.ts +5 -3
  22. package/tests/client/database-explorer-integration.test.tsx +30 -0
  23. package/tests/client/database-explorer.test.tsx +152 -0
  24. package/tests/client/foundation-integration.test.ts +42 -29
  25. package/tests/client/home.test.tsx +26 -85
  26. package/tests/client/layout-i18n.test.tsx +83 -2
  27. package/tests/client/settings-navigation.test.ts +20 -0
  28. package/tests/helpers/authenticated-client.tsx +7 -0
  29. package/tests/project/bootstrap-loading.test.ts +60 -0
  30. package/tests/server/app-server.test.ts +18 -12
  31. package/tests/server/auth-email-config.test.ts +21 -0
  32. package/tests/server/backends-config.test.ts +118 -0
  33. package/tests/server/concurrency.test.ts +1 -1
  34. package/tests/server/config.test.ts +22 -14
  35. package/tests/server/lifecycle.test.ts +2 -2
  36. package/tests/server/realtime-config.test.ts +114 -0
  37. package/client/pages/home/capabilities.ts +0 -62
  38. package/client/pages/home/prompt-dialog.tsx +0 -122
@@ -0,0 +1,358 @@
1
+ import { ApiClientError, useApiClient } from '@nocobase/app-client';
2
+ import { DatabaseExplorerClient } from '@nocobase/app-plugin-database-explorer/client';
3
+ import { useTranslation } from '@nocobase/i18n/client';
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { Link, useSearchParams } from 'react-router';
12
+ import { PageContainer } from '#components/page-container';
13
+ import { PageHeader } from '#components/page-header';
14
+ import { Button } from '#components/ui/button';
15
+ import { Input } from '#components/ui/input';
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from '#components/ui/select';
23
+ import {
24
+ Table,
25
+ TableBody,
26
+ TableCell,
27
+ TableHead,
28
+ TableHeader,
29
+ TableRow,
30
+ } from '#components/ui/table';
31
+
32
+ /** A keyed read boundary prevents a late response from replacing a newer selection. */
33
+ function Read<T>({
34
+ load,
35
+ children,
36
+ }: {
37
+ load: () => Promise<T>;
38
+ children: (data: T) => ReactNode;
39
+ }) {
40
+ const { t } = useTranslation();
41
+ const [attempt, setAttempt] = useState(0);
42
+ const [result, setResult] = useState<{ data: T } | { error: unknown }>();
43
+ useEffect(() => {
44
+ let active = true;
45
+ load().then(
46
+ (data) => {
47
+ if (active) setResult({ data });
48
+ },
49
+ (error: unknown) => {
50
+ if (active) setResult({ error });
51
+ },
52
+ );
53
+ return () => {
54
+ active = false;
55
+ };
56
+ }, [load, attempt]);
57
+ if (!result)
58
+ return (
59
+ <p role='status' className='p-4 text-sm text-muted-foreground'>
60
+ {t('databaseExplorer.loading')}
61
+ </p>
62
+ );
63
+ if ('error' in result) {
64
+ const status =
65
+ result.error instanceof ApiClientError ? result.error.status : undefined;
66
+ const key =
67
+ status === 401 ? 'expired' : status === 403 ? 'forbidden' : 'failed';
68
+ return (
69
+ <div role='alert' className='space-y-3 p-4'>
70
+ <p>{t(`databaseExplorer.${key}`)}</p>
71
+ {status === 401 ? (
72
+ <Link to='/login'>{t('databaseExplorer.signIn')}</Link>
73
+ ) : status !== 403 ? (
74
+ <Button
75
+ variant='outline'
76
+ onClick={() => {
77
+ setResult(undefined);
78
+ setAttempt((value) => value + 1);
79
+ }}
80
+ >
81
+ {t('databaseExplorer.retry')}
82
+ </Button>
83
+ ) : null}
84
+ </div>
85
+ );
86
+ }
87
+ return children(result.data);
88
+ }
89
+
90
+ export default function DatabaseExplorerPage() {
91
+ const { t } = useTranslation();
92
+ const api = useApiClient();
93
+ const client = useMemo(() => new DatabaseExplorerClient(api), [api]);
94
+ const load = useCallback(() => client.connections(), [client]);
95
+ const [params, setParams] = useSearchParams();
96
+ return (
97
+ <PageContainer className='space-y-6'>
98
+ <PageHeader
99
+ title={t('databaseExplorer.title')}
100
+ description={t('databaseExplorer.description')}
101
+ />
102
+ <Read load={load}>
103
+ {({ items, default: defaultConnection }) => {
104
+ if (!items.length)
105
+ return <p>{t('databaseExplorer.emptyConnections')}</p>;
106
+ const connection =
107
+ params.get('connection') ?? defaultConnection ?? items[0].name;
108
+ return (
109
+ <div className='space-y-6'>
110
+ <div className='max-w-sm space-y-2'>
111
+ <label
112
+ htmlFor='database-connection'
113
+ className='text-sm font-medium'
114
+ >
115
+ {t('databaseExplorer.connections')}
116
+ </label>
117
+ <Select
118
+ value={connection}
119
+ onValueChange={(value) => {
120
+ if (value) setParams({ connection: value });
121
+ }}
122
+ >
123
+ <SelectTrigger id='database-connection' className='w-full'>
124
+ <SelectValue />
125
+ </SelectTrigger>
126
+ <SelectContent>
127
+ {items.map((item) => (
128
+ <SelectItem key={item.name} value={item.name}>
129
+ {item.name} · {item.dialect}
130
+ {item.isDefault
131
+ ? ` · ${t('databaseExplorer.defaultConnection')}`
132
+ : ''}
133
+ </SelectItem>
134
+ ))}
135
+ </SelectContent>
136
+ </Select>
137
+ </div>
138
+ <ConnectionTables
139
+ key={connection}
140
+ client={client}
141
+ connection={connection}
142
+ />
143
+ </div>
144
+ );
145
+ }}
146
+ </Read>
147
+ </PageContainer>
148
+ );
149
+ }
150
+
151
+ function ConnectionTables({
152
+ client,
153
+ connection,
154
+ }: {
155
+ client: DatabaseExplorerClient;
156
+ connection: string;
157
+ }) {
158
+ const { t } = useTranslation();
159
+ const [params, setParams] = useSearchParams();
160
+ const selected = params.get('table');
161
+ const query = params.get('q') ?? '';
162
+ const load = useCallback(
163
+ () => client.allCollections(connection),
164
+ [client, connection],
165
+ );
166
+ return (
167
+ <div className='grid min-w-0 gap-6 lg:grid-cols-[minmax(14rem,1fr)_minmax(0,3fr)]'>
168
+ <section
169
+ aria-label={t('databaseExplorer.tables')}
170
+ className='min-w-0 space-y-3'
171
+ >
172
+ <h2 className='font-heading text-base font-semibold'>
173
+ {t('databaseExplorer.tables')}
174
+ </h2>
175
+ <Input
176
+ aria-label={t('databaseExplorer.searchTables')}
177
+ placeholder={t('databaseExplorer.searchTables')}
178
+ value={query}
179
+ onChange={(event) => {
180
+ setParams(
181
+ (previous) => {
182
+ const next = new URLSearchParams(previous);
183
+ next.set('q', event.target.value);
184
+ return next;
185
+ },
186
+ { replace: true },
187
+ );
188
+ }}
189
+ />
190
+ <Read load={load}>
191
+ {({ items, truncated }) => {
192
+ const matches = items.filter((item) =>
193
+ `${item.name} ${item.tableName} ${item.title ?? ''}`
194
+ .toLocaleLowerCase()
195
+ .includes(query.toLocaleLowerCase()),
196
+ );
197
+ return (
198
+ <>
199
+ {truncated ? (
200
+ <p role='status' className='text-sm text-muted-foreground'>
201
+ {t('databaseExplorer.truncated')}
202
+ </p>
203
+ ) : null}
204
+ {matches.length ? (
205
+ <ul className='max-h-96 space-y-1 overflow-auto rounded-lg border p-2'>
206
+ {matches.map((item) => (
207
+ <li key={item.name}>
208
+ <Button
209
+ variant={
210
+ selected === item.name ? 'secondary' : 'ghost'
211
+ }
212
+ className='h-auto w-full justify-start whitespace-normal break-all text-left'
213
+ aria-pressed={selected === item.name}
214
+ onClick={() => {
215
+ setParams((previous) => {
216
+ const next = new URLSearchParams(previous);
217
+ next.set('connection', connection);
218
+ next.set('table', item.name);
219
+ return next;
220
+ });
221
+ }}
222
+ >
223
+ {item.title
224
+ ? `${item.title} (${item.name})`
225
+ : item.name}
226
+ </Button>
227
+ </li>
228
+ ))}
229
+ </ul>
230
+ ) : (
231
+ <p className='text-sm text-muted-foreground'>
232
+ {t('databaseExplorer.emptyTables')}
233
+ </p>
234
+ )}
235
+ </>
236
+ );
237
+ }}
238
+ </Read>
239
+ </section>
240
+ <section
241
+ aria-label={t('databaseExplorer.fields')}
242
+ className='min-w-0 space-y-3'
243
+ >
244
+ {selected ? (
245
+ <CollectionFields
246
+ key={selected}
247
+ client={client}
248
+ connection={connection}
249
+ collection={selected}
250
+ />
251
+ ) : (
252
+ <p className='rounded-lg border border-dashed p-6 text-sm text-muted-foreground'>
253
+ {t('databaseExplorer.selectTable')}
254
+ </p>
255
+ )}
256
+ </section>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ function CollectionFields({
262
+ client,
263
+ connection,
264
+ collection,
265
+ }: {
266
+ client: DatabaseExplorerClient;
267
+ connection: string;
268
+ collection: string;
269
+ }) {
270
+ const { t } = useTranslation();
271
+ const [query, setQuery] = useState('');
272
+ const load = useCallback(
273
+ () => client.collection(connection, collection),
274
+ [client, connection, collection],
275
+ );
276
+ return (
277
+ <>
278
+ <h2 className='break-all font-heading text-base font-semibold'>
279
+ {collection}
280
+ </h2>
281
+ <Input
282
+ aria-label={t('databaseExplorer.searchFields')}
283
+ placeholder={t('databaseExplorer.searchFields')}
284
+ value={query}
285
+ onChange={(event) => setQuery(event.target.value)}
286
+ />
287
+ <Read load={load}>
288
+ {({ collection: artifact }) => {
289
+ const fields = (artifact.collection.fields ?? []).filter((field) =>
290
+ `${field.name} ${field.title ?? ''}`
291
+ .toLocaleLowerCase()
292
+ .includes(query.toLocaleLowerCase()),
293
+ );
294
+ return (
295
+ <>
296
+ {artifact.warnings.length ? (
297
+ <p role='status' className='text-sm text-muted-foreground'>
298
+ {t('databaseExplorer.warnings')}
299
+ </p>
300
+ ) : null}
301
+ {fields.length ? (
302
+ <div className='overflow-hidden rounded-lg border'>
303
+ <Table>
304
+ <TableHeader>
305
+ <TableRow>
306
+ {[
307
+ 'name',
308
+ 'type',
309
+ 'nullable',
310
+ 'primaryKey',
311
+ 'target',
312
+ ].map((key) => (
313
+ <TableHead key={key}>
314
+ {t(`databaseExplorer.${key}`)}
315
+ </TableHead>
316
+ ))}
317
+ </TableRow>
318
+ </TableHeader>
319
+ <TableBody>
320
+ {fields.map((field) => (
321
+ <TableRow key={field.name}>
322
+ <TableCell className='font-medium'>
323
+ {field.name}
324
+ {field.title ? (
325
+ <span className='block text-muted-foreground'>
326
+ {field.title}
327
+ </span>
328
+ ) : null}
329
+ </TableCell>
330
+ <TableCell>{field.type}</TableCell>
331
+ <TableCell>
332
+ {t(
333
+ `databaseExplorer.${field.nullable === undefined ? 'unknown' : field.nullable ? 'yes' : 'no'}`,
334
+ )}
335
+ </TableCell>
336
+ <TableCell>
337
+ {t(
338
+ `databaseExplorer.${field.primaryKey ? 'yes' : 'no'}`,
339
+ )}
340
+ </TableCell>
341
+ <TableCell>{field.target ?? '—'}</TableCell>
342
+ </TableRow>
343
+ ))}
344
+ </TableBody>
345
+ </Table>
346
+ </div>
347
+ ) : (
348
+ <p className='text-sm text-muted-foreground'>
349
+ {t('databaseExplorer.emptyFields')}
350
+ </p>
351
+ )}
352
+ </>
353
+ );
354
+ }}
355
+ </Read>
356
+ </>
357
+ );
358
+ }
@@ -0,0 +1,14 @@
1
+ import { defineAppRoutes } from '@nocobase/app-client/plugins';
2
+ import { DATABASE_EXPLORER_ACCESS } from '@nocobase/app-plugin-database-explorer/client';
3
+
4
+ export default defineAppRoutes([
5
+ {
6
+ parent: 'settingsSystem',
7
+ name: 'databaseExplorer',
8
+ path: '/database-explorer',
9
+ auth: 'required',
10
+ authz: DATABASE_EXPLORER_ACCESS,
11
+ breadcrumb: { title: 'databaseExplorer.title' },
12
+ componentLoader: () => import('./index.js'),
13
+ },
14
+ ]);
@@ -0,0 +1,30 @@
1
+ export default {
2
+ title: '数据库浏览器',
3
+ description: '查看应用的数据库连接、数据表和字段。',
4
+ connections: '数据库连接',
5
+ defaultConnection: '默认',
6
+ tables: '数据表',
7
+ searchTables: '搜索数据表',
8
+ fields: '字段',
9
+ searchFields: '搜索字段',
10
+ name: '名称',
11
+ type: '类型',
12
+ nullable: '允许为空',
13
+ primaryKey: '主键',
14
+ target: '关联表',
15
+ yes: '是',
16
+ no: '否',
17
+ unknown: '未指定',
18
+ emptyConnections: '尚未配置数据库连接。',
19
+ emptyTables: '没有找到数据表。',
20
+ emptyFields: '没有找到字段。',
21
+ selectTable: '选择数据表以查看字段。',
22
+ loading: '正在加载…',
23
+ failed: '无法加载数据库信息。',
24
+ forbidden: '你没有浏览此数据库的权限。',
25
+ expired: '登录已过期,请重新登录后继续。',
26
+ signIn: '登录',
27
+ retry: '重试',
28
+ truncated: '当前仅加载了部分数据表,请缩小数据库范围后查看其余数据表。',
29
+ warnings: '部分字段未能完整解析,显示的结构可能不完整。',
30
+ };
package/client/plugins.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import foundation from '@nocobase/app-plugin-foundation/client';
2
+ import applicationManifest from '../package.json' with { type: 'json' };
2
3
  import defaultAccess from '@nocobase/app-plugin-authz-default-access/client';
3
4
  import sharingRules from '@nocobase/app-plugin-authz-sharing-rules/client';
4
5
  import restrictionRules from '@nocobase/app-plugin-authz-restriction-rules/client';
@@ -35,7 +36,13 @@ const clientPlugins: AppClientPlugins = defineClientPlugins([
35
36
  file(),
36
37
  scheduler(),
37
38
  mail(),
38
- foundation({ settingsGroup: true }),
39
+ // The manifest is what the capability guide lists: every plugin this application declares.
40
+ foundation({
41
+ settingsGroup: true,
42
+ manifest: applicationManifest,
43
+ guideSettingsPath: '/settings',
44
+ navigation: false,
45
+ }),
39
46
  ]);
40
47
 
41
48
  export default clientPlugins;
package/client/routes.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import databaseExplorerRoutes from './pages/settings/database-explorer/routes.js';
1
2
  import { createAppClientConfig } from '@nocobase/app-client';
2
3
  import { readAppClientRuntimeConfig } from '@nocobase/app-client/runtime';
3
4
  import { includeNotificationTemplateRoutes } from './pages/settings/notification-routes.js';
@@ -75,7 +76,7 @@ export function createApplicationRoutes(
75
76
  ): readonly AppClientRouteContribution[] {
76
77
  const config = createAppClientConfig({ rawConfig });
77
78
  return includeNotificationTemplateRoutes(
78
- [appRoutes, settingsShellRoutes],
79
+ [appRoutes, settingsShellRoutes, databaseExplorerRoutes],
79
80
  config.get<boolean>('notificationTemplates.invitationConfigured') === true,
80
81
  );
81
82
  }
@@ -22,13 +22,19 @@
22
22
  # max: 600
23
23
  # window: 1m
24
24
 
25
- # Realtime events are transient and local to this process. Clients refetch after subscription acknowledgement.
25
+ # Realtime defaults to inMemory when broker is omitted. An explicit broker requires adapter.
26
+ # For local-only selection: realtime: { broker: { adapter: inMemory } }.
27
+ # For multiple processes, install @nocobase/app-plugin-realtime-redis and register its
28
+ # @nocobase/app-plugin-realtime-redis/server definition in server/plugins.ts on every instance.
29
+ # Realtime events are transient; clients refetch after subscription acknowledgement.
26
30
  # realtime:
27
- # adapter: inMemory # Use redis for multiple application processes.
28
- # redis:
31
+ # broker:
32
+ # adapter: redis
33
+ # setupTimeoutMs: 5000
29
34
  # connection:
30
35
  # host: 127.0.0.1
31
36
  # port: 6379
37
+ # commandTimeout: 5000
32
38
  # namespace: my-app # Defaults to the application name; keep applications isolated.
33
39
  # authorizationTimeoutMs: 5000
34
40
  # sessionCheckIntervalMs: 30000
@@ -116,8 +122,7 @@ auth:
116
122
  # Set app.publicOrigin before explicitly enabling automatic password recovery.
117
123
  # passwordReset:
118
124
  # enabled: true
119
- # email:
120
- # mailer: system # omit to use mailers.default
125
+ # mailer: system # omit to preserve existing default selection
121
126
  # Existing inline auth.email and AUTH_EMAIL_* remain supported for compatibility.
122
127
  emailAndPassword:
123
128
  enabled: true
@@ -190,12 +195,13 @@ snowflake:
190
195
  # Upgrading deployments must set adapter: redis explicitly; templates no longer supply Redis defaults.
191
196
  # jobs:
192
197
  # default: redis
193
- # redis:
194
- # adapter: redis
195
- # connection:
196
- # host: 127.0.0.1
197
- # port: 6379
198
- # # namespace: my-app # defaults to the application name; set it when deployments share one Redis
198
+ # backends:
199
+ # redis:
200
+ # adapter: redis
201
+ # connection:
202
+ # host: 127.0.0.1
203
+ # port: 6379
204
+ # # namespace: my-app # defaults to the application name; set it when deployments share one Redis
199
205
 
200
206
  # The jobs configuration the Scheduler plugin's schedules run on, by name. Left out, they follow jobs.default.
201
207
  # scheduler:
package/index.html CHANGED
@@ -32,9 +32,6 @@
32
32
  transform-origin: 32px 32px;
33
33
  animation: app-bootstrap-spin 1.1s linear infinite;
34
34
  }
35
- .app-bootstrap-loading span {
36
- color: oklch(0.556 0 0);
37
- }
38
35
 
39
36
  html,
40
37
  body {
@@ -60,11 +57,6 @@
60
57
  }
61
58
  }
62
59
 
63
- @media (prefers-color-scheme: dark) {
64
- .app-bootstrap-loading span {
65
- color: oklch(0.708 0 0);
66
- }
67
- }
68
60
  @media (prefers-reduced-motion: reduce) {
69
61
  .app-bootstrap-loading .nocobase-loading-orbit {
70
62
  animation: none;
@@ -76,10 +68,16 @@
76
68
  <body>
77
69
  <noscript>You need to enable JavaScript to run this app.</noscript>
78
70
  <div id="root">
71
+ <!--
72
+ The language is unknown until the client starts, so this placeholder shows no text. A screen reader still
73
+ perceives it as an indeterminate progress bar, a role it announces in its own language, named after the
74
+ brand in <title>, which reads the same in every language. The client's localized loading, a polite status,
75
+ replaces it once the language is ready, so nothing is announced twice.
76
+ -->
79
77
  <div
80
78
  class="app-bootstrap-loading"
81
- role="status"
82
- aria-label="Loading application"
79
+ role="progressbar"
80
+ aria-label="NocoBase"
83
81
  >
84
82
  <svg
85
83
  data-slot="loading-indicator"
@@ -114,7 +112,6 @@
114
112
  </g>
115
113
  </g>
116
114
  </svg>
117
- <span>Loading application…</span>
118
115
  </div>
119
116
  </div>
120
117
  <script type="module" src="/client/index.tsx"></script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nocobase/app-template-default",
3
3
  "displayName": "Default Template",
4
- "version": "1.0.0-beta.89",
4
+ "version": "1.0.0-beta.90",
5
5
  "type": "module",
6
6
  "prettier": "@nocobase/dev-config/prettier",
7
7
  "engines": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "nocobase": {
11
11
  "templateKind": "app",
12
- "defaultTemplateVersion": "1.0.0-beta.89"
12
+ "defaultTemplateVersion": "1.0.0-beta.90"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@ai-sdk/react": "4.0.131",
@@ -76,7 +76,7 @@
76
76
  "@noble/hashes": "2.3.0",
77
77
  "@nocobase/app-client": "^4.0.0-beta.9",
78
78
  "@nocobase/app-plugin-template-print": "0.0.2-beta.2",
79
- "@nocobase/app-skills": "0.1.0-beta.60",
79
+ "@nocobase/app-skills": "0.1.0-beta.61",
80
80
  "@nocobase/app-testing": "0.1.0-beta.8",
81
81
  "@nocobase/app-websocket": "^0.1.0-beta.1",
82
82
  "@nocobase/dev-config": "0.1.0-beta.25",
@@ -143,40 +143,40 @@
143
143
  "better-auth": "1.7.5",
144
144
  "hono": "^4.13.2",
145
145
  "@nocobase/concurrency": "^0.1.0-beta.1",
146
- "@nocobase/app-plugin-concurrency": "^0.1.0-beta.2",
147
- "@nocobase/app-plugin-agents": "^2.0.0-beta.6",
146
+ "@nocobase/app-plugin-concurrency": "^0.1.0-beta.3",
147
+ "@nocobase/app-plugin-agents": "^2.0.0-beta.7",
148
148
  "@nocobase/app-plugin-api-keys": "^1.0.0-beta.21",
149
- "@nocobase/app-plugin-authentication": "^2.0.0-beta.19",
150
- "@nocobase/app-plugin-authorization": "^1.0.0-beta.36",
149
+ "@nocobase/app-plugin-authentication": "^2.0.0-beta.20",
150
+ "@nocobase/app-plugin-authorization": "^1.0.0-beta.37",
151
151
  "@nocobase/app-plugin-database-explorer": "^1.0.0-beta.19",
152
- "@nocobase/app-plugin-file": "^1.0.0-beta.29",
153
- "@nocobase/app-plugin-foundation": "^0.1.0-beta.16",
154
- "@nocobase/app-plugin-i18n": "^1.0.0-beta.21",
155
- "@nocobase/app-plugin-knowledge": "^0.1.0-beta.17",
156
- "@nocobase/app-plugin-mail": "^1.0.0-beta.21",
157
- "@nocobase/app-plugin-notification": "^2.0.0-beta.5",
158
- "@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.2",
159
- "@nocobase/app-plugin-notification-providers": "^1.0.0-beta.18",
160
- "@nocobase/app-plugin-scheduler": "^1.0.0-beta.23",
161
- "@nocobase/app-plugin-users": "^2.0.0-beta.16",
162
- "@nocobase/app-server": "^5.0.0-beta.1",
152
+ "@nocobase/app-plugin-file": "^1.0.0-beta.30",
153
+ "@nocobase/app-plugin-foundation": "^0.1.0-beta.17",
154
+ "@nocobase/app-plugin-i18n": "^1.0.0-beta.22",
155
+ "@nocobase/app-plugin-knowledge": "^0.1.0-beta.18",
156
+ "@nocobase/app-plugin-mail": "^1.0.0-beta.22",
157
+ "@nocobase/app-plugin-notification": "^2.0.0-beta.6",
158
+ "@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.3",
159
+ "@nocobase/app-plugin-notification-providers": "^1.0.0-beta.19",
160
+ "@nocobase/app-plugin-scheduler": "^1.0.0-beta.24",
161
+ "@nocobase/app-plugin-users": "^2.0.0-beta.17",
162
+ "@nocobase/app-server": "^5.0.0-beta.2",
163
163
  "@nocobase/config": "^0.1.0-beta.3",
164
- "@nocobase/db": "^1.0.0-beta.25",
164
+ "@nocobase/db": "^1.0.0-beta.26",
165
165
  "@nocobase/db-sqlite": "^0.1.0-beta.6",
166
166
  "@nocobase/service-provider": "^0.0.2-beta.2",
167
167
  "@nocobase/snowflake": "^1.0.0-beta.4",
168
- "@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.19",
169
- "@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.18",
170
- "@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.17",
168
+ "@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.20",
169
+ "@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.19",
170
+ "@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.18",
171
171
  "@nocobase/authorization": "^1.0.0-beta.16",
172
- "@nocobase/queue": "^1.0.0-beta.10",
173
- "@nocobase/jobs": "^1.0.0-beta.5",
172
+ "@nocobase/queue": "^1.0.0-beta.11",
173
+ "@nocobase/jobs": "^1.0.0-beta.6",
174
174
  "@nocobase/lifecycle": "^0.1.0-beta.4",
175
- "@nocobase/i18n": "^2.0.0-beta.1",
175
+ "@nocobase/i18n": "^2.0.0-beta.2",
176
176
  "@nocobase/caching": "^1.0.0-beta.4",
177
177
  "@nocobase/repository-input": "^0.1.0-beta.3",
178
178
  "@nocobase/app-cli": "^1.0.0-beta.32",
179
- "@nocobase/realtime": "^1.0.0-beta.1"
179
+ "@nocobase/realtime": "^1.0.0-beta.2"
180
180
  },
181
181
  "description": "NocoBase 3 application template with the standard product capabilities.",
182
182
  "imports": {
@@ -12,7 +12,7 @@ import { bearer, deviceAuthorization, username } from 'better-auth/plugins';
12
12
  */
13
13
  const CLI_CLIENT_IDS: readonly string[] = ['nocobase-cli'];
14
14
 
15
- // Use mailers for shared sending; defineAuthConfig retains AUTH_EMAIL_* for legacy inline senders.
15
+ // Select a named sender with passwordReset.mailer; defineAuthConfig retains AUTH_EMAIL_* for legacy inline senders.
16
16
  // Automatic recovery requires passwordReset.enabled: true; leave the switch absent for custom callback compatibility.
17
17
  const auth: AppConfigFactory<AuthConfig> = defineAuthConfig({
18
18
  defaults: {
@@ -18,6 +18,7 @@ import queue from './queue.js';
18
18
  import jobs from './jobs.js';
19
19
  import scheduler from './scheduler.js';
20
20
  import caching from './caching.js';
21
+ import realtime from './realtime.js';
21
22
  import i18n from './i18n.js';
22
23
  import app from './app.js';
23
24
  import api from './api.js';
@@ -43,6 +44,7 @@ const defaultConfigs: AppConfigFactory<{
43
44
  jobs: ReturnType<typeof jobs>;
44
45
  scheduler: ReturnType<typeof scheduler>;
45
46
  caching: ReturnType<typeof caching>;
47
+ realtime: ReturnType<typeof realtime>;
46
48
  i18n: ReturnType<typeof i18n>;
47
49
  app: ReturnType<typeof app>;
48
50
  api: ReturnType<typeof api>;
@@ -67,6 +69,7 @@ const defaultConfigs: AppConfigFactory<{
67
69
  jobs,
68
70
  scheduler,
69
71
  caching,
72
+ realtime,
70
73
  i18n,
71
74
  app,
72
75
  api,