@lenne.tech/nest-server 12.1.0 → 12.2.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/.claude/rules/architecture.md +1 -0
- package/.claude/rules/better-auth.md +18 -6
- package/.claude/rules/configurable-features.md +5 -4
- package/.claude/rules/package-management.md +16 -1
- package/.claude/rules/persistence.md +26 -0
- package/.claude/rules/testing.md +102 -28
- package/CLAUDE.md +3 -2
- package/FRAMEWORK-API.md +17 -2
- package/README.md +24 -3
- package/dist/core/common/helpers/file.helper.js +3 -4
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +41 -0
- package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
- package/dist/core/common/persistence/core-persistence.module.js +12 -2
- package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
- package/dist/core/common/persistence/data-source.runtime.js +14 -19
- package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
- package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
- package/dist/core/common/persistence/id-format.d.ts +2 -0
- package/dist/core/common/persistence/id-format.js +14 -0
- package/dist/core/common/persistence/id-format.js.map +1 -1
- package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
- package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
- package/dist/core/common/persistence/model-metadata.registry.js +20 -2
- package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
- package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
- package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
- package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
- package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
- package/dist/core/common/persistence/mongoose-data-store.js +107 -31
- package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
- package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
- package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
- package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
- package/dist/core/common/persistence/persistence-runtime.registry.d.ts +5 -1
- package/dist/core/common/persistence/persistence-runtime.registry.js.map +1 -1
- package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
- package/dist/core/common/persistence/policies/sync.policy.js +77 -0
- package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
- package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
- package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.d.ts +2 -0
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +145 -4
- package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
- package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
- package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
- package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
- package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
- package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-dialect.d.ts +2 -0
- package/dist/core/common/persistence/sql/sql-dialect.js +14 -2
- package/dist/core/common/persistence/sql/sql-dialect.js.map +1 -1
- package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
- package/dist/core/common/persistence/sql/sql-errors.js +3 -1
- package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
- package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
- package/dist/core/common/persistence/sync-access.registry.js +8 -0
- package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
- package/dist/core/common/persistence/sync-model.d.ts +29 -0
- package/dist/core/common/persistence/sync-model.js +138 -0
- package/dist/core/common/persistence/sync-model.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.service.d.ts +27 -0
- package/dist/core/common/services/core-cron-jobs.service.js +269 -13
- package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
- package/dist/core/common/services/core-redis-pubsub.js +5 -5
- package/dist/core/common/services/core-redis-pubsub.js.map +1 -1
- package/dist/core/common/services/core-redis.service.d.ts +6 -0
- package/dist/core/common/services/core-redis.service.js +57 -0
- package/dist/core/common/services/core-redis.service.js.map +1 -1
- package/dist/core/common/services/crud.service.d.ts +13 -1
- package/dist/core/common/services/crud.service.js +174 -16
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-sql.helper.d.ts +1 -0
- package/dist/core/modules/better-auth/better-auth-sql.helper.js +41 -14
- package/dist/core/modules/better-auth/better-auth-sql.helper.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-sql.store.d.ts +4 -7
- package/dist/core/modules/better-auth/core-better-auth-sql.store.js +23 -16
- package/dist/core/modules/better-auth/core-better-auth-sql.store.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +12 -2
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +82 -5
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +54 -0
- package/dist/core/modules/error-code/error-codes.js +48 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/hub/hub-buffer.js +3 -4
- package/dist/core/modules/hub/hub-buffer.js.map +1 -1
- package/dist/core/modules/migrate/index.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +6 -0
- package/dist/core/modules/migrate/migration-runner.js +28 -2
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/migrate/mongo-state-store.d.ts +0 -1
- package/dist/core/modules/migrate/mongo-state-store.js +0 -5
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
- package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
- package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
- package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
- package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
- package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
- package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
- package/dist/core/modules/sync/core-sync-cursor.js +77 -0
- package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
- package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
- package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
- package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
- package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
- package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
- package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
- package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
- package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
- package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
- package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
- package/dist/core/modules/sync/core-sync-state.store.js +64 -0
- package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
- package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
- package/dist/core/modules/sync/core-sync.constants.js +23 -0
- package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
- package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
- package/dist/core/modules/sync/core-sync.controller.js +79 -0
- package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
- package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
- package/dist/core/modules/sync/core-sync.decorators.js +28 -0
- package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
- package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
- package/dist/core/modules/sync/core-sync.module.js +50 -0
- package/dist/core/modules/sync/core-sync.module.js.map +1 -0
- package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
- package/dist/core/modules/sync/core-sync.resolver.js +46 -0
- package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
- package/dist/core/modules/sync/core-sync.service.d.ts +142 -0
- package/dist/core/modules/sync/core-sync.service.js +1208 -0
- package/dist/core/modules/sync/core-sync.service.js.map +1 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
- package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
- package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +8 -3
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core.module.js +14 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +6 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
- package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
- package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
- package/dist/server/modules/sync-note/sync-note.model.js +73 -0
- package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
- package/dist/server/modules/sync-note/sync-note.module.js +21 -0
- package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
- package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
- package/dist/server/modules/sync-note/sync-note.service.js +35 -0
- package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
- package/docs/REQUEST-LIFECYCLE.md +3 -2
- package/docs/choosing-a-database.md +7 -4
- package/docs/offline-sync-protocol.md +410 -0
- package/docs/security-overrides.md +14 -0
- package/migration-guides/12.0.x-to-12.1.x.md +9 -4
- package/migration-guides/12.1.0-to-12.1.1.md +125 -0
- package/migration-guides/12.1.x-to-12.2.x.md +154 -0
- package/package.json +10 -8
- package/src/core/common/helpers/file.helper.ts +7 -5
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +3 -2
- package/src/core/common/interfaces/cron-job-config.interface.ts +7 -2
- package/src/core/common/interfaces/server-options.interface.ts +149 -0
- package/src/core/common/interfaces/service-options.interface.ts +15 -0
- package/src/core/common/persistence/core-persistence.module.ts +21 -2
- package/src/core/common/persistence/data-source.runtime.ts +22 -23
- package/src/core/common/persistence/data-store.interface.ts +65 -12
- package/src/core/common/persistence/id-format.ts +26 -0
- package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
- package/src/core/common/persistence/model-metadata.registry.ts +31 -2
- package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
- package/src/core/common/persistence/mongoose-data-store.ts +171 -35
- package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
- package/src/core/common/persistence/persistence-runtime.registry.ts +16 -5
- package/src/core/common/persistence/policies/sync.policy.ts +121 -0
- package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
- package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
- package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +194 -3
- package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
- package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
- package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
- package/src/core/common/persistence/sql/sql-dialect.ts +38 -1
- package/src/core/common/persistence/sql/sql-errors.ts +5 -1
- package/src/core/common/persistence/sql/sql-executor.interface.ts +4 -1
- package/src/core/common/persistence/sync-access.registry.ts +50 -0
- package/src/core/common/persistence/sync-model.ts +270 -0
- package/src/core/common/services/core-cron-jobs.service.ts +441 -27
- package/src/core/common/services/core-redis-pubsub.ts +8 -6
- package/src/core/common/services/core-redis.service.ts +99 -0
- package/src/core/common/services/crud.service.ts +257 -19
- package/src/core/modules/better-auth/README.md +25 -1
- package/src/core/modules/better-auth/better-auth-sql.helper.ts +56 -24
- package/src/core/modules/better-auth/better-auth.config.ts +4 -3
- package/src/core/modules/better-auth/core-better-auth-sql.store.ts +28 -21
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +122 -6
- package/src/core/modules/better-auth/core-better-auth.module.ts +2 -2
- package/src/core/modules/error-code/error-codes.ts +59 -0
- package/src/core/modules/hub/README.md +5 -2
- package/src/core/modules/hub/hub-buffer.ts +13 -7
- package/src/core/modules/migrate/README.md +20 -11
- package/src/core/modules/migrate/index.ts +1 -0
- package/src/core/modules/migrate/migration-runner.ts +68 -3
- package/src/core/modules/migrate/mongo-state-store.ts +7 -30
- package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
- package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
- package/src/core/modules/sync/README.md +188 -0
- package/src/core/modules/sync/SECURITY.md +65 -0
- package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
- package/src/core/modules/sync/core-sync-cursor.ts +117 -0
- package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
- package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
- package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
- package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
- package/src/core/modules/sync/core-sync-state.store.ts +89 -0
- package/src/core/modules/sync/core-sync.constants.ts +51 -0
- package/src/core/modules/sync/core-sync.controller.ts +68 -0
- package/src/core/modules/sync/core-sync.decorators.ts +84 -0
- package/src/core/modules/sync/core-sync.module.ts +75 -0
- package/src/core/modules/sync/core-sync.resolver.ts +35 -0
- package/src/core/modules/sync/core-sync.service.ts +1728 -0
- package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
- package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
- package/src/core/modules/tenant/README.md +1 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +16 -3
- package/src/core.module.ts +25 -0
- package/src/index.ts +21 -0
- package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
- package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
- package/src/server/modules/sync-note/sync-note.model.ts +77 -0
- package/src/server/modules/sync-note/sync-note.module.ts +16 -0
- package/src/server/modules/sync-note/sync-note.service.ts +25 -0
|
@@ -4,7 +4,7 @@ import { getMigrations } from 'better-auth/db/migration';
|
|
|
4
4
|
import { DataSourceConfigError } from '../../common/persistence/data-source.config.js';
|
|
5
5
|
import { generateId } from '../../common/persistence/id-format.js';
|
|
6
6
|
import { type IamSqlStorage, registerForeignColumns } from '../../common/persistence/persistence-runtime.registry.js';
|
|
7
|
-
import { columnName, quoteIdentifier } from '../../common/persistence/sql/sql-naming.js';
|
|
7
|
+
import { columnName, quoteIdentifier, quoteMysqlIdentifier } from '../../common/persistence/sql/sql-naming.js';
|
|
8
8
|
|
|
9
9
|
/** The table Better-Auth shares with the user model — created and owned by the framework */
|
|
10
10
|
const USERS_TABLE = 'users';
|
|
@@ -15,15 +15,38 @@ const USERS_TABLE = 'users';
|
|
|
15
15
|
*/
|
|
16
16
|
registerForeignColumns(USERS_TABLE, ['email_verified', 'image', 'name']);
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Column type of a Better-Auth field per database, for a column the framework adds before Better-Auth's migration —
|
|
20
|
+
* the type Better-Auth's own migrator would choose, so it finds nothing to report about the column afterwards
|
|
21
|
+
*/
|
|
22
|
+
const SQL_TYPES: Record<IamSqlStorage['driver'], Record<string, string>> = {
|
|
23
|
+
mariadb: {
|
|
24
|
+
boolean: 'boolean',
|
|
25
|
+
date: 'timestamp(3)',
|
|
26
|
+
json: 'json',
|
|
27
|
+
number: 'integer',
|
|
28
|
+
'number[]': 'json',
|
|
29
|
+
string: 'text',
|
|
30
|
+
'string[]': 'json',
|
|
31
|
+
},
|
|
32
|
+
mysql: {
|
|
33
|
+
boolean: 'boolean',
|
|
34
|
+
date: 'timestamp(3)',
|
|
35
|
+
json: 'json',
|
|
36
|
+
number: 'integer',
|
|
37
|
+
'number[]': 'json',
|
|
38
|
+
string: 'text',
|
|
39
|
+
'string[]': 'json',
|
|
40
|
+
},
|
|
41
|
+
postgres: {
|
|
42
|
+
boolean: 'boolean',
|
|
43
|
+
date: 'timestamptz',
|
|
44
|
+
json: 'jsonb',
|
|
45
|
+
number: 'integer',
|
|
46
|
+
'number[]': 'jsonb',
|
|
47
|
+
string: 'text',
|
|
48
|
+
'string[]': 'jsonb',
|
|
49
|
+
},
|
|
27
50
|
};
|
|
28
51
|
|
|
29
52
|
/** The tables Better-Auth's own options name (models of plugins come from the plugins) */
|
|
@@ -32,8 +55,9 @@ const CORE_MODELS = ['user', 'session', 'account', 'verification'] as const;
|
|
|
32
55
|
type Options = Record<string, any>;
|
|
33
56
|
|
|
34
57
|
/**
|
|
35
|
-
* Better-Auth on a SQL data source: its built-in Kysely adapter on the given `pg
|
|
36
|
-
* pool and builds its own Kysely instance — the core imports
|
|
58
|
+
* Better-Auth on a SQL data source: its built-in Kysely adapter on the given pool — `pg`, or on MySQL / MariaDB the
|
|
59
|
+
* `mysql2` pool's promise wrapper (Better-Auth recognises the pool and builds its own Kysely instance — the core imports
|
|
60
|
+
* no Kysely), ids in the framework's format, and every table
|
|
37
61
|
* and column named the way the framework names it.
|
|
38
62
|
*
|
|
39
63
|
* Naming is not cosmetic. Better-Auth and the user model share ONE row per user in `users`: the framework creates that
|
|
@@ -114,8 +138,10 @@ export function betterAuthOwnedTables(options: Options): string[] {
|
|
|
114
138
|
* Without an owner connection it only checks, and a pending change stops the boot like a framework table would.
|
|
115
139
|
*/
|
|
116
140
|
export async function applyBetterAuthSchema(options: Options, storage: IamSqlStorage): Promise<void> {
|
|
117
|
-
await storage.applySchema(async (pool, asOwner) => {
|
|
118
|
-
const nullable = asOwner
|
|
141
|
+
await storage.applySchema(async (pool, asOwner, query) => {
|
|
142
|
+
const nullable = asOwner
|
|
143
|
+
? await addRequiredUserColumnsAsNullable(options, query, storage.driver)
|
|
144
|
+
: new Set<string>();
|
|
119
145
|
const { runMigrations, toBeAdded, toBeCreated } = await getMigrations({
|
|
120
146
|
...options,
|
|
121
147
|
database: pool,
|
|
@@ -135,10 +161,6 @@ export async function applyBetterAuthSchema(options: Options, storage: IamSqlSto
|
|
|
135
161
|
}, betterAuthOwnedTables(options));
|
|
136
162
|
}
|
|
137
163
|
|
|
138
|
-
interface PgPool {
|
|
139
|
-
query(text: string, params?: unknown[]): Promise<{ rows: any[] }>;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
164
|
/**
|
|
143
165
|
* A field Better-Auth declares required without a default (`name`) becomes a NOT NULL column when Better-Auth adds it —
|
|
144
166
|
* and every user the FRAMEWORK creates (legacy sign-up, user service, system setup) would then fail to insert, because the
|
|
@@ -146,10 +168,19 @@ interface PgPool {
|
|
|
146
168
|
* first, nullable like every column it creates (the database does not enforce `required`, as on MongoDB); Better-Auth
|
|
147
169
|
* then finds them and adds nothing. Returns every required user column that is nullable by the framework's design.
|
|
148
170
|
*/
|
|
149
|
-
async function addRequiredUserColumnsAsNullable(
|
|
171
|
+
export async function addRequiredUserColumnsAsNullable(
|
|
172
|
+
options: Options,
|
|
173
|
+
query: IamSqlStorage['query'],
|
|
174
|
+
driver: IamSqlStorage['driver'],
|
|
175
|
+
): Promise<Set<string>> {
|
|
150
176
|
const users = (getAuthTables(options as any) as Record<string, any>).user;
|
|
151
|
-
const
|
|
152
|
-
|
|
177
|
+
const postgres = driver === 'postgres';
|
|
178
|
+
const quote = postgres ? quoteIdentifier : quoteMysqlIdentifier;
|
|
179
|
+
const types = SQL_TYPES[driver];
|
|
180
|
+
const { rows } = await query(
|
|
181
|
+
postgres
|
|
182
|
+
? 'SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = $1'
|
|
183
|
+
: 'SELECT COLUMN_NAME AS column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = $1',
|
|
153
184
|
[users.modelName],
|
|
154
185
|
);
|
|
155
186
|
const existing = new Set(rows.map((row) => row.column_name as string));
|
|
@@ -161,9 +192,10 @@ async function addRequiredUserColumnsAsNullable(options: Options, pool: PgPool):
|
|
|
161
192
|
}
|
|
162
193
|
// Every column the framework created for a field is nullable — the user model's `email` and timestamps included
|
|
163
194
|
nullable.add(column);
|
|
164
|
-
if (field.defaultValue === undefined && !existing.has(column) &&
|
|
165
|
-
|
|
166
|
-
|
|
195
|
+
if (field.defaultValue === undefined && !existing.has(column) && types[field.type]) {
|
|
196
|
+
// MySQL has no ADD COLUMN IF NOT EXISTS; the catalog read above and the schema lock make it unnecessary
|
|
197
|
+
await query(
|
|
198
|
+
`ALTER TABLE ${quote(users.modelName)} ADD COLUMN ${postgres ? 'IF NOT EXISTS ' : ''}${quote(column)} ${types[field.type]}`,
|
|
167
199
|
);
|
|
168
200
|
}
|
|
169
201
|
}
|
|
@@ -233,9 +233,10 @@ export interface CreateBetterAuthOptions {
|
|
|
233
233
|
serverEnv?: string;
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* The
|
|
237
|
-
*
|
|
238
|
-
* table and column names — see
|
|
236
|
+
* The pool of the SQL data source the `users` table lives in, as `SqlDialect.foreignPool()` hands it out: the
|
|
237
|
+
* application role's `pg` pool, or on MySQL / MariaDB its `mysql2` pool's promise wrapper. Set, Better-Auth runs its
|
|
238
|
+
* built-in Kysely adapter on it instead of MongoDB, with the framework's ids and table and column names — see
|
|
239
|
+
* `withSqlStorage()`.
|
|
239
240
|
*/
|
|
240
241
|
sqlPool?: unknown;
|
|
241
242
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataStore } from '../../common/persistence/data-store.interface.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { IamSqlStorage } from '../../common/persistence/persistence-runtime.registry.js';
|
|
3
|
+
import { quoteIdentifier, quoteMysqlIdentifier } from '../../common/persistence/sql/sql-naming.js';
|
|
3
4
|
import { RequestContext } from '../../common/services/request-context.service.js';
|
|
4
5
|
import type { BetterAuthInstance } from './better-auth.config.js';
|
|
5
6
|
|
|
@@ -28,10 +29,8 @@ export interface IamMigrationCounts {
|
|
|
28
29
|
withIamId: number;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/** What the store needs
|
|
32
|
-
export
|
|
33
|
-
query(text: string, values?: unknown[]): Promise<{ rows: any[] }>;
|
|
34
|
-
}
|
|
32
|
+
/** What the store needs to count in the database Better-Auth runs on: which one it is, and a statement runner */
|
|
33
|
+
export type IamSqlQueryable = Pick<IamSqlStorage, 'driver' | 'query'>;
|
|
35
34
|
|
|
36
35
|
/** A passkey challenge mapping as `CoreBetterAuthChallengeService` keeps it */
|
|
37
36
|
export interface IamChallengeMapping {
|
|
@@ -60,7 +59,7 @@ export class BetterAuthSqlStore {
|
|
|
60
59
|
constructor(
|
|
61
60
|
readonly users: DataStore<any>,
|
|
62
61
|
protected readonly auth: BetterAuthInstance,
|
|
63
|
-
protected readonly
|
|
62
|
+
protected readonly sql?: IamSqlQueryable,
|
|
64
63
|
) {}
|
|
65
64
|
|
|
66
65
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -144,39 +143,47 @@ export class BetterAuthSqlStore {
|
|
|
144
143
|
* The IAM migration counts, computed in the database over every user and account — the definitions of the MongoDB
|
|
145
144
|
* aggregation (`CoreBetterAuthUserMapper.getMigrationStatus()`). Never a page of rows: Better-Auth's `findMany()`
|
|
146
145
|
* stops at 100 without a limit, which capped the status at 100 migrated users. Table and column names come from
|
|
147
|
-
* Better-Auth's own schema, so they follow what `withSqlStorage()` mapped.
|
|
146
|
+
* Better-Auth's own schema, so they follow what `withSqlStorage()` mapped. The same statements on every SQL database,
|
|
147
|
+
* only the identifier quotes differ; counts are converted here because MySQL answers them as strings.
|
|
148
148
|
*/
|
|
149
149
|
async migrationCounts(pendingEmailLimit: number): Promise<IamMigrationCounts> {
|
|
150
|
-
if (!this.
|
|
151
|
-
throw new Error('BetterAuthSqlStore: migration counts need the
|
|
150
|
+
if (!this.sql) {
|
|
151
|
+
throw new Error('BetterAuthSqlStore: migration counts need the database Better-Auth runs on');
|
|
152
152
|
}
|
|
153
|
+
const quote = this.sql.driver === 'postgres' ? quoteIdentifier : quoteMysqlIdentifier;
|
|
153
154
|
const { tables } = await this.context();
|
|
154
|
-
const table = (model: string) =>
|
|
155
|
-
const column = (model: string, field: string) =>
|
|
155
|
+
const table = (model: string) => quote(tables[model].modelName);
|
|
156
|
+
const column = (model: string, field: string) => quote(tables[model].fields[field]?.fieldName ?? field);
|
|
156
157
|
const users = table('user');
|
|
157
158
|
const accounts = table('account');
|
|
158
159
|
const iamId = column('user', 'iamId');
|
|
159
160
|
const userId = column('account', 'userId');
|
|
160
161
|
const providerId = column('account', 'providerId');
|
|
161
|
-
const migrated = `EXISTS (SELECT 1 FROM ${accounts} a WHERE a.${userId} = u
|
|
162
|
+
const migrated = `EXISTS (SELECT 1 FROM ${accounts} a WHERE a.${userId} = u.${quote('id')} AND a.${providerId} = $1)`;
|
|
162
163
|
const {
|
|
163
164
|
rows: [counts],
|
|
164
|
-
} = await this.
|
|
165
|
-
`SELECT (SELECT count(*) FROM ${users})
|
|
166
|
-
(SELECT count(*) FROM ${users} WHERE ${iamId} IS NOT NULL)
|
|
167
|
-
(SELECT count(DISTINCT ${userId}) FROM ${accounts} WHERE ${providerId} = $1)
|
|
168
|
-
(SELECT count(*) FROM ${users} u WHERE u.${iamId} IS NOT NULL AND ${migrated})
|
|
165
|
+
} = await this.sql.query(
|
|
166
|
+
`SELECT (SELECT count(*) FROM ${users}) AS ${quote('total')},
|
|
167
|
+
(SELECT count(*) FROM ${users} WHERE ${iamId} IS NOT NULL) AS ${quote('withIamId')},
|
|
168
|
+
(SELECT count(DISTINCT ${userId}) FROM ${accounts} WHERE ${providerId} = $1) AS ${quote('withIamAccount')},
|
|
169
|
+
(SELECT count(*) FROM ${users} u WHERE u.${iamId} IS NOT NULL AND ${migrated}) AS ${quote('fullyMigrated')}`,
|
|
169
170
|
['credential'],
|
|
170
171
|
);
|
|
171
172
|
const pending =
|
|
172
173
|
pendingEmailLimit > 0
|
|
173
|
-
? await this.
|
|
174
|
-
`SELECT u.${column('user', 'email')} AS
|
|
175
|
-
WHERE u.${iamId} IS NULL OR NOT ${migrated} ORDER BY u
|
|
174
|
+
? await this.sql.query(
|
|
175
|
+
`SELECT u.${column('user', 'email')} AS ${quote('email')} FROM ${users} u
|
|
176
|
+
WHERE u.${iamId} IS NULL OR NOT ${migrated} ORDER BY u.${quote('id')} LIMIT $2`,
|
|
176
177
|
['credential', pendingEmailLimit],
|
|
177
178
|
)
|
|
178
179
|
: { rows: [] };
|
|
179
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
fullyMigrated: Number(counts.fullyMigrated),
|
|
182
|
+
pendingEmails: pending.rows.map((row) => row.email).filter(Boolean),
|
|
183
|
+
total: Number(counts.total),
|
|
184
|
+
withIamAccount: Number(counts.withIamAccount),
|
|
185
|
+
withIamId: Number(counts.withIamId),
|
|
186
|
+
};
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
/** A session by its token with its user; `null` when unknown — expiry is the caller's check, as on MongoDB */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable, Logger, Optional } from '@nestjs/common';
|
|
1
|
+
import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy, Optional } from '@nestjs/common';
|
|
2
2
|
import { InjectConnection } from '../../common/persistence/mongo/mongoose-compat.js';
|
|
3
3
|
import * as bcrypt from 'bcrypt';
|
|
4
4
|
import { randomBytes, scrypt, ScryptOptions } from 'crypto';
|
|
@@ -17,7 +17,9 @@ const scryptPromise = (password: string, salt: string, keylen: number, options:
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
import { RoleEnum } from '../../common/enums/role.enum.js';
|
|
20
|
+
import { revalidateWsConnectionsOf } from '../../common/helpers/graphql-ws-connection.helper.js';
|
|
20
21
|
import { maskEmail } from '../../common/helpers/logging.helper.js';
|
|
22
|
+
import { CoreRedisService } from '../../common/services/core-redis.service.js';
|
|
21
23
|
import { legacyCredentialAccountIssuer } from './core-better-auth-account-issuer.helper.js';
|
|
22
24
|
import { getBetterAuthSqlStore } from './core-better-auth-sql.registry.js';
|
|
23
25
|
import type { BetterAuthSqlStore } from './core-better-auth-sql.store.js';
|
|
@@ -139,13 +141,15 @@ function isClosedConnectionError(error: unknown): boolean {
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
@Injectable()
|
|
142
|
-
export class CoreBetterAuthUserMapper {
|
|
144
|
+
export class CoreBetterAuthUserMapper implements OnApplicationBootstrap, OnModuleDestroy {
|
|
143
145
|
private readonly logger = new Logger(CoreBetterAuthUserMapper.name);
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
148
|
* Lightweight TTL cache for user DB lookups.
|
|
147
149
|
* Key: iamId (BetterAuth user ID), Value: minimal user data (id, roles, verified).
|
|
148
150
|
* Only caches ~100 bytes per entry (no full documents). Max 500 entries = ~50KB.
|
|
151
|
+
*
|
|
152
|
+
* With `redis` configured, {@link invalidateUserCache} reaches the cache of every replica; without it, only this one.
|
|
149
153
|
*/
|
|
150
154
|
private readonly userCache = new Map<string, CachedUserData>();
|
|
151
155
|
private static readonly USER_CACHE_MAX = 500;
|
|
@@ -167,7 +171,64 @@ export class CoreBetterAuthUserMapper {
|
|
|
167
171
|
? 0
|
|
168
172
|
: 15_000;
|
|
169
173
|
|
|
170
|
-
|
|
174
|
+
/**
|
|
175
|
+
* How long a user's roles and verification state are answered from the cache, in ms. `0` switches the cache off.
|
|
176
|
+
* Defaults to {@link USER_CACHE_TTL_MS}; a subclass may set another value.
|
|
177
|
+
*/
|
|
178
|
+
protected userCacheTtlMs = CoreBetterAuthUserMapper.USER_CACHE_TTL_MS;
|
|
179
|
+
|
|
180
|
+
/** The listener on the shared subscriber connection, kept to detach it again in {@link onModuleDestroy}. */
|
|
181
|
+
private invalidationListener?: (channel: string, message: string) => void;
|
|
182
|
+
|
|
183
|
+
constructor(
|
|
184
|
+
@Optional() @InjectConnection() private readonly connection?: Connection,
|
|
185
|
+
@Optional() protected readonly redisService?: CoreRedisService,
|
|
186
|
+
) {}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Subscribe to the cache invalidations of the other replicas. Runs after every `onModuleInit`, so the Redis service is
|
|
190
|
+
* connected. A no-op without `redis`.
|
|
191
|
+
*/
|
|
192
|
+
async onApplicationBootstrap(): Promise<void> {
|
|
193
|
+
if (!this.redisService?.enabled) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const channel = this.invalidationChannel(this.redisService);
|
|
197
|
+
try {
|
|
198
|
+
// Held in a field: the subscriber connection is shared between features, so a listener that is never detached
|
|
199
|
+
// keeps clearing a destroyed mapper's cache for the life of the process.
|
|
200
|
+
this.invalidationListener = (incomingChannel: string, message: string) => {
|
|
201
|
+
if (incomingChannel === channel) {
|
|
202
|
+
this.applyInvalidation(message);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
this.redisService.getSubscriber().on('message', this.invalidationListener);
|
|
206
|
+
// Registered with the Redis service, so the subscription is established again after every reconnect — also when
|
|
207
|
+
// this first attempt fails because Redis is down at boot.
|
|
208
|
+
await this.redisService.subscribeChannel(channel);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.logger.warn(
|
|
211
|
+
`IAM user cache invalidation subscribe failed: ${(error as Error).message}. Retried when the Redis connection ` +
|
|
212
|
+
'is up; until then a role or verification change made on another replica reaches this one only when its ' +
|
|
213
|
+
'cache entry expires and at the next periodic WebSocket re-check.',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onModuleDestroy(): void {
|
|
219
|
+
if (!this.invalidationListener) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
if (this.redisService) {
|
|
224
|
+
this.redisService.getSubscriber().off('message', this.invalidationListener);
|
|
225
|
+
void this.redisService.releaseChannel(this.invalidationChannel(this.redisService)).catch(() => undefined);
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// Redis already gone — nothing to detach from
|
|
229
|
+
}
|
|
230
|
+
this.invalidationListener = undefined;
|
|
231
|
+
}
|
|
171
232
|
|
|
172
233
|
/**
|
|
173
234
|
* Maps a Better-Auth session user to a user with role capabilities
|
|
@@ -203,7 +264,7 @@ export class CoreBetterAuthUserMapper {
|
|
|
203
264
|
|
|
204
265
|
try {
|
|
205
266
|
// Check lightweight cache first (only stores id, roles, verified — ~100 bytes per entry)
|
|
206
|
-
const ttl =
|
|
267
|
+
const ttl = this.userCacheTtlMs;
|
|
207
268
|
const now = Date.now();
|
|
208
269
|
const cached = ttl > 0 ? this.userCache.get(sessionUser.id) : undefined;
|
|
209
270
|
if (cached && now < cached.expiresAt) {
|
|
@@ -235,7 +296,7 @@ export class CoreBetterAuthUserMapper {
|
|
|
235
296
|
const verified = dbUser.verified === true || sessionUser.emailVerified === true;
|
|
236
297
|
|
|
237
298
|
// Cache only the minimal data needed (not the full document)
|
|
238
|
-
if (
|
|
299
|
+
if (this.userCacheTtlMs > 0) {
|
|
239
300
|
this.cacheUserData(sessionUser.id, { id: String(dbUser._id ?? dbUser.id), roles, verified });
|
|
240
301
|
}
|
|
241
302
|
|
|
@@ -316,7 +377,7 @@ export class CoreBetterAuthUserMapper {
|
|
|
316
377
|
}
|
|
317
378
|
this.userCache.set(iamId, {
|
|
318
379
|
...data,
|
|
319
|
-
expiresAt: Date.now() +
|
|
380
|
+
expiresAt: Date.now() + this.userCacheTtlMs,
|
|
320
381
|
});
|
|
321
382
|
}
|
|
322
383
|
|
|
@@ -324,10 +385,65 @@ export class CoreBetterAuthUserMapper {
|
|
|
324
385
|
* Invalidate cached user data. Call when user roles or verified status change.
|
|
325
386
|
* Called automatically from `CoreUserService.setRoles()` and `CoreUserService.update()`.
|
|
326
387
|
*
|
|
388
|
+
* With `redis` configured, the invalidation is broadcast to every replica, which also re-checks the user's open
|
|
389
|
+
* GraphQL WebSockets — otherwise a withdrawn role would keep working there until the cache entry expires.
|
|
390
|
+
*
|
|
327
391
|
* @param iamId - The BetterAuth user ID (from session/account, not MongoDB _id)
|
|
328
392
|
*/
|
|
329
393
|
invalidateUserCache(iamId: string): void {
|
|
330
394
|
this.userCache.delete(iamId);
|
|
395
|
+
this.publishInvalidation(iamId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Apply an invalidation received from a replica (this one included — it is subscribed to its own broadcasts). Clears
|
|
400
|
+
* locally only: re-publishing would bounce the message around the cluster.
|
|
401
|
+
*/
|
|
402
|
+
protected applyInvalidation(message: string): void {
|
|
403
|
+
let iamId: unknown;
|
|
404
|
+
try {
|
|
405
|
+
iamId = JSON.parse(message)?.iamId;
|
|
406
|
+
} catch {
|
|
407
|
+
iamId = undefined;
|
|
408
|
+
}
|
|
409
|
+
if (typeof iamId !== 'string' || !iamId) {
|
|
410
|
+
this.logger.debug(`Ignoring malformed IAM user cache invalidation message: ${message}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.userCache.delete(iamId);
|
|
414
|
+
// The replica that changed the user re-checks its own sockets; these are the ones on this replica. After the cache
|
|
415
|
+
// above, so the re-check reads the user as they are now.
|
|
416
|
+
revalidateWsConnectionsOf({ iamId });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Broadcast an invalidation to the other replicas (fire-and-forget). A no-op without `redis`.
|
|
421
|
+
*
|
|
422
|
+
* A failure is logged as a warning, not swallowed: until the entry expires on the other replicas, a withdrawn role
|
|
423
|
+
* keeps working there.
|
|
424
|
+
*/
|
|
425
|
+
protected publishInvalidation(iamId: string): void {
|
|
426
|
+
const redisService = this.redisService;
|
|
427
|
+
if (!redisService?.enabled) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const warn = (error: Error) =>
|
|
431
|
+
this.logger.warn(
|
|
432
|
+
`IAM user cache invalidation could not be broadcast: ${error.message}. Other replicas keep the previous roles ` +
|
|
433
|
+
'until their cache entry expires, and their open WebSockets until the next periodic re-check.',
|
|
434
|
+
);
|
|
435
|
+
try {
|
|
436
|
+
void redisService
|
|
437
|
+
.getClient()
|
|
438
|
+
.publish(this.invalidationChannel(redisService), JSON.stringify({ iamId }))
|
|
439
|
+
.catch(warn);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
warn(error as Error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private invalidationChannel(redisService: CoreRedisService): string {
|
|
446
|
+
return redisService.key('iam-user-cache', 'invalidate');
|
|
331
447
|
}
|
|
332
448
|
|
|
333
449
|
// ===================================================================================================================
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from './better-auth.config.js';
|
|
35
35
|
import { applyBetterAuthSchema } from './better-auth-sql.helper.js';
|
|
36
36
|
import { getBetterAuthSqlStore, setBetterAuthSqlStore } from './core-better-auth-sql.registry.js';
|
|
37
|
-
import { BetterAuthSqlStore
|
|
37
|
+
import { BetterAuthSqlStore } from './core-better-auth-sql.store.js';
|
|
38
38
|
import { DefaultBetterAuthResolver } from './better-auth.resolver.js';
|
|
39
39
|
import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware.js';
|
|
40
40
|
import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.service.js';
|
|
@@ -490,7 +490,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
490
490
|
}
|
|
491
491
|
const result = instantiateBetterAuth(built);
|
|
492
492
|
if (storage) {
|
|
493
|
-
setBetterAuthSqlStore(new BetterAuthSqlStore(storage.users, result.instance, storage
|
|
493
|
+
setBetterAuthSqlStore(new BetterAuthSqlStore(storage.users, result.instance, storage));
|
|
494
494
|
}
|
|
495
495
|
return result;
|
|
496
496
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - LTNS_0400-LTNS_0499: Resource errors
|
|
23
23
|
* - LTNS_0500-LTNS_0599: File errors
|
|
24
24
|
* - LTNS_0600-LTNS_0699: AI errors
|
|
25
|
+
* - LTNS_0700-LTNS_0799: Offline sync errors
|
|
25
26
|
* - LTNS_0900-LTNS_0999: Internal errors
|
|
26
27
|
*
|
|
27
28
|
* @example
|
|
@@ -515,6 +516,15 @@ export const LtnsErrors = {
|
|
|
515
516
|
},
|
|
516
517
|
},
|
|
517
518
|
|
|
519
|
+
VERSION_CONFLICT: {
|
|
520
|
+
code: 'LTNS_0402',
|
|
521
|
+
message: 'Version conflict',
|
|
522
|
+
translations: {
|
|
523
|
+
de: 'Der Datensatz wurde inzwischen geändert.',
|
|
524
|
+
en: 'The record was changed in the meantime.',
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
|
|
518
528
|
// =====================================================
|
|
519
529
|
// File Errors (LTNS_0500-LTNS_0599)
|
|
520
530
|
// =====================================================
|
|
@@ -702,6 +712,55 @@ export const LtnsErrors = {
|
|
|
702
712
|
en: 'The AI connection has no valid base URL.',
|
|
703
713
|
},
|
|
704
714
|
},
|
|
715
|
+
|
|
716
|
+
// =====================================================
|
|
717
|
+
// Offline Sync Errors (LTNS_0700-LTNS_0799)
|
|
718
|
+
// =====================================================
|
|
719
|
+
|
|
720
|
+
SYNC_RESYNC_REQUIRED: {
|
|
721
|
+
code: 'LTNS_0700',
|
|
722
|
+
message: 'Resync required',
|
|
723
|
+
translations: {
|
|
724
|
+
de: 'Die lokalen Daten müssen vollständig neu geladen werden.',
|
|
725
|
+
en: 'The local data must be loaded again in full.',
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
SYNC_MODEL_NOT_FOUND: {
|
|
730
|
+
code: 'LTNS_0701',
|
|
731
|
+
message: 'Model is not available for sync',
|
|
732
|
+
translations: {
|
|
733
|
+
de: 'Diese Daten können nicht synchronisiert werden.',
|
|
734
|
+
en: 'This data cannot be synchronized.',
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
SYNC_INVALID_REQUEST: {
|
|
739
|
+
code: 'LTNS_0702',
|
|
740
|
+
message: 'Invalid sync request',
|
|
741
|
+
translations: {
|
|
742
|
+
de: 'Die Synchronisierungsanfrage ist ungültig.',
|
|
743
|
+
en: 'The synchronization request is invalid.',
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
SYNC_ORIGIN_REFUSED: {
|
|
748
|
+
code: 'LTNS_0703',
|
|
749
|
+
message: 'Origin not allowed for this request',
|
|
750
|
+
translations: {
|
|
751
|
+
de: 'Diese Anfrage ist von dieser Seite aus nicht erlaubt.',
|
|
752
|
+
en: 'This request is not allowed from this site.',
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
SYNC_STORE_UNAVAILABLE: {
|
|
757
|
+
code: 'LTNS_0704',
|
|
758
|
+
message: 'Sync store unavailable',
|
|
759
|
+
translations: {
|
|
760
|
+
de: 'Die Synchronisierung ist gerade nicht verfügbar. Bitte später erneut versuchen.',
|
|
761
|
+
en: 'Synchronization is unavailable right now. Please try again later.',
|
|
762
|
+
},
|
|
763
|
+
},
|
|
705
764
|
} as const satisfies IErrorRegistry;
|
|
706
765
|
|
|
707
766
|
// =====================================================
|
|
@@ -196,8 +196,11 @@ shows the merged cluster-wide view and "clear" clears it everywhere. Sequence nu
|
|
|
196
196
|
from a shared counter, so the panels' cursor-based polling keeps working across replicas.
|
|
197
197
|
|
|
198
198
|
Writes are fire-and-forget — the collectors sit in hot paths and their API stays synchronous.
|
|
199
|
-
If Redis is unreachable a read
|
|
200
|
-
must not be the thing that breaks), so
|
|
199
|
+
If Redis is unreachable a read answers with the client's cursor and no entries (a diagnostics
|
|
200
|
+
panel must not be the thing that breaks), so the panels show nothing new during an outage, and
|
|
201
|
+
the first poll after it delivers what was missed. Until 12.1.0 the read fell back to the local
|
|
202
|
+
buffer, whose sequence numbers count per replica: the next poll after the outage then repeated
|
|
203
|
+
or skipped entries.
|
|
201
204
|
|
|
202
205
|
#### Cost: Redis write volume scales with your traffic
|
|
203
206
|
|
|
@@ -48,7 +48,8 @@ const SHARED_READ_CHUNK = 64;
|
|
|
48
48
|
*
|
|
49
49
|
* **Writes are fire-and-forget.** The collectors sit in hot paths (every log line, every
|
|
50
50
|
* request) and their API towards callers must stay synchronous. A Redis failure is logged at
|
|
51
|
-
* debug level;
|
|
51
|
+
* debug level; a read then answers with the client's cursor and no entries rather than failing
|
|
52
|
+
* the panel, and the next successful poll delivers what was missed.
|
|
52
53
|
*/
|
|
53
54
|
export class HubBuffer<T extends HubBufferEntry> {
|
|
54
55
|
protected readonly listKey: string;
|
|
@@ -116,11 +117,16 @@ export class HubBuffer<T extends HubBufferEntry> {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Entries oldest→newest, optionally only those newer than `since`.
|
|
122
|
+
*
|
|
123
|
+
* When the shared read fails, the answer keeps the client's cursor and carries no entries. The
|
|
124
|
+
* local buffer is no fallback there: its seq counts on this replica's own scale, and handing out
|
|
125
|
+
* its cursor made the next successful poll repeat or skip entries (DEV-3897).
|
|
126
|
+
*/
|
|
120
127
|
async read(since?: number, limit?: number): Promise<HubBufferData<T>> {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return shared;
|
|
128
|
+
if (this.redis) {
|
|
129
|
+
return (await this.readShared(since, limit)) ?? { cursor: since ?? -1, dropped: -1, entries: [] };
|
|
124
130
|
}
|
|
125
131
|
return {
|
|
126
132
|
cursor: this.local.lastSeq,
|
|
@@ -130,7 +136,7 @@ export class HubBuffer<T extends HubBufferEntry> {
|
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
|
133
|
-
* Read the shared list, or undefined when Redis is unreachable
|
|
139
|
+
* Read the shared list, or undefined when Redis is unreachable.
|
|
134
140
|
*
|
|
135
141
|
* The list is newest-first, so the entry with seq `s` sits at index `total - 1 - s` and the
|
|
136
142
|
* entries a client with cursor `since` still needs are exactly the first `total - 1 - since`.
|
|
@@ -167,7 +173,7 @@ export class HubBuffer<T extends HubBufferEntry> {
|
|
|
167
173
|
}
|
|
168
174
|
} catch (err) {
|
|
169
175
|
// console, NOT Logger — same re-entrancy as in mirror() above
|
|
170
|
-
console.debug(`Hub buffer read failed (${this.listKey})
|
|
176
|
+
console.debug(`Hub buffer read failed (${this.listKey}): ${(err as Error).message}`);
|
|
171
177
|
return undefined;
|
|
172
178
|
}
|
|
173
179
|
}
|
|
@@ -126,17 +126,21 @@ const migrationOptions = {
|
|
|
126
126
|
migrationsDirectory: './migrations',
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
//
|
|
129
|
+
// Run all pending migrations: a MigrationRunner under the lock
|
|
130
|
+
await synchronizedUp(migrationOptions);
|
|
131
|
+
|
|
132
|
+
// Or custom logic under the same lock. `migrationSet` is the loaded state (`lastRun`, `migrations`), not something
|
|
133
|
+
// that runs migrations
|
|
130
134
|
await synchronizedMigration(migrationOptions, async (migrationSet) => {
|
|
131
135
|
// Only one instance at a time will execute this code
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
// `migrations` is absent until the first migration was recorded
|
|
137
|
+
console.log(`${migrationSet.migrations?.length ?? 0} migration(s) recorded`);
|
|
134
138
|
});
|
|
135
|
-
|
|
136
|
-
// Or use the convenience function to run all pending migrations
|
|
137
|
-
await synchronizedUp(migrationOptions);
|
|
138
139
|
```
|
|
139
140
|
|
|
141
|
+
`synchronizedUp()` threw on every call until 12.1.0 (it called `up` on the loaded state, which has none). Since 12.1.1
|
|
142
|
+
it runs the `MigrationRunner` — the same runner as `migrate up` — under the lock.
|
|
143
|
+
|
|
140
144
|
### SQL migrations (PostgreSQL data sources)
|
|
141
145
|
|
|
142
146
|
On a PostgreSQL source the framework migrates the schema itself at boot (new tables, columns, declared renames,
|
|
@@ -257,10 +261,16 @@ async function synchronizedMigration(
|
|
|
257
261
|
|
|
258
262
|
#### synchronizedUp
|
|
259
263
|
|
|
260
|
-
|
|
264
|
+
Executes all pending migrations in a synchronized manner: a `MigrationRunner` under the state store's lock. Refuses a
|
|
265
|
+
`MongoStateStore` without `lockCollectionName` instead of running unsynchronized.
|
|
261
266
|
|
|
262
267
|
```typescript
|
|
263
|
-
|
|
268
|
+
interface SynchronizedUpOptions extends MigrationOptions {
|
|
269
|
+
migrationsDirectory?: string; // default: `migrations` in the working directory, as for the `migrate` CLI
|
|
270
|
+
pattern?: RegExp; // default: `*.ts` and `*.js`, never `*.d.ts`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function synchronizedUp(opts: SynchronizedUpOptions): Promise<void>;
|
|
264
274
|
```
|
|
265
275
|
|
|
266
276
|
## How It Works
|
|
@@ -589,9 +599,8 @@ are **not** re-run after the switch, and no state rewrite is needed. If both `fo
|
|
|
589
599
|
`foo.js` are present in the migrations directory (e.g. an overlapping `outDir`), they count
|
|
590
600
|
as ONE migration: only the `.js` file is loaded and executed, and a warning is emitted.
|
|
591
601
|
|
|
592
|
-
|
|
593
|
-
`synchronizedUp()
|
|
594
|
-
keeps raw-filename identity.
|
|
602
|
+
Identity normalization applies to every path that runs migrations: the `MigrationRunner`, the CLI and, since 12.1.1,
|
|
603
|
+
`synchronizedUp()`, which runs the `MigrationRunner`.
|
|
595
604
|
|
|
596
605
|
### Missing migration files & strict mode
|
|
597
606
|
|