@lenne.tech/nest-server 12.1.1 → 12.3.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 +31 -4
- package/.claude/rules/configurable-features.md +8 -6
- package/.claude/rules/package-management.md +29 -2
- package/.claude/rules/persistence.md +27 -1
- package/.claude/rules/testing.md +80 -15
- package/.claude/rules/versioning.md +13 -2
- package/CLAUDE.md +7 -1
- package/FRAMEWORK-API.md +20 -4
- package/README.md +20 -2
- package/dist/config.env.js +2 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +49 -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 +4 -2
- 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/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.js +101 -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-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/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/ai/core-ai-mcp.controller.d.ts +3 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.constants.js +2 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
- package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.controller.js +31 -1
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.module.js +8 -1
- package/dist/core/modules/ai/core-ai.module.js.map +1 -1
- package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.resolver.js +32 -0
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +7 -0
- package/dist/core/modules/ai/index.js +6 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
- package/dist/core/modules/ai/services/core-ai.service.js +68 -29
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +3 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +72 -0
- package/dist/core/modules/error-code/error-codes.js +64 -0
- package/dist/core/modules/error-code/error-codes.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 +143 -0
- package/dist/core/modules/sync/core-sync.service.js +1237 -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 +14 -0
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
- package/dist/core/modules/tenant/core-tenant.service.js +153 -1
- package/dist/core/modules/tenant/core-tenant.service.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 +8 -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/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +9 -2
- package/dist/test/test.helper.js.map +1 -1
- package/docs/REQUEST-LIFECYCLE.md +4 -2
- package/docs/error-codes.md +14 -0
- package/docs/offline-sync-protocol.md +111 -68
- package/docs/security-overrides.md +14 -0
- package/migration-guides/12.0.x-to-12.1.x.md +0 -1
- package/migration-guides/12.1.x-to-12.2.x.md +154 -0
- package/migration-guides/12.2.x-to-12.3.x.md +440 -0
- package/package.json +23 -21
- package/src/config.env.ts +6 -2
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
- package/src/core/common/interfaces/server-options.interface.ts +229 -8
- 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 +5 -2
- 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/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 +129 -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-errors.ts +5 -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/crud.service.ts +257 -19
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
- package/src/core/modules/ai/README.md +163 -25
- package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
- package/src/core/modules/ai/core-ai.constants.ts +6 -0
- package/src/core/modules/ai/core-ai.controller.ts +36 -1
- package/src/core/modules/ai/core-ai.module.ts +12 -0
- package/src/core/modules/ai/core-ai.resolver.ts +36 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
- package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
- package/src/core/modules/ai/index.ts +18 -0
- package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
- package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
- package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
- package/src/core/modules/ai/services/core-ai.service.ts +134 -35
- package/src/core/modules/api-token/README.md +4 -2
- package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
- package/src/core/modules/better-auth/README.md +32 -1
- package/src/core/modules/better-auth/better-auth.config.ts +12 -1
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
- package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/error-code/error-codes.ts +77 -0
- 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 +198 -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 +1773 -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/INTEGRATION-CHECKLIST.md +12 -1
- package/src/core/modules/tenant/README.md +49 -3
- package/src/core/modules/tenant/core-tenant.guard.ts +26 -2
- package/src/core/modules/tenant/core-tenant.service.ts +287 -2
- 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
- package/src/test/test.helper.ts +17 -3
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { BadRequestException, Logger } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { generateUuidV7 } from '../id-format.js';
|
|
4
|
+
import type { SyncDescriptor } from '../model-descriptor.interface.js';
|
|
5
|
+
import {
|
|
6
|
+
liveFilter,
|
|
7
|
+
liveUniqueIndex,
|
|
8
|
+
SYNC_CLOCK_FIELD,
|
|
9
|
+
SYNC_DELETED_FIELD,
|
|
10
|
+
SYNC_SYSTEM_FIELDS,
|
|
11
|
+
SYNC_VERSION_FIELD,
|
|
12
|
+
} from '../sync-model.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The MongoDB side of a synchronized model (offline-sync plan §3.3, E1–E4): applied by
|
|
16
|
+
* `CorePersistenceModule.forFeature()` to the schema of a model declared with `@PersistedModel({ sync })`, and to no
|
|
17
|
+
* other schema.
|
|
18
|
+
*
|
|
19
|
+
* Every write path of Mongoose runs through these hooks, so `CrudService` (which uses Mongoose documents on MongoDB),
|
|
20
|
+
* `MongooseDataStore` and a project's direct model calls keep the same rules:
|
|
21
|
+
*
|
|
22
|
+
* - **version** — `1` on insert, `$inc` on every update, a save of a loaded document conditional on the version it
|
|
23
|
+
* was loaded with; a caller never writes it
|
|
24
|
+
* - **change position** — `syncAt` from the SERVER clock: `$currentDate` on every update (one operation), and on an
|
|
25
|
+
* insert the server clock as read through `hello.localTime` (`mongoServerNow()`), because `save()` has no
|
|
26
|
+
* server-side value and keeping `save()` keeps every other plugin's save hooks and the validation
|
|
27
|
+
* - **tombstones** — `deletedAt: null` is added to every read and to the match of every update and delete, unless
|
|
28
|
+
* the query carries `includeTombstones()`; only the store's tombstone write may set `deletedAt`
|
|
29
|
+
* - **unique indexes** apply to live records only
|
|
30
|
+
*
|
|
31
|
+
* A replacement (`replaceOne`, `findOneAndReplace`) and a pipeline update are refused: neither can carry the version
|
|
32
|
+
* increment and the server-clock stamp. A hard delete through the model (`deleteOne`, `deleteMany`) stays possible
|
|
33
|
+
* — tier 2 of the native access; the framework's delete paths write tombstones (`MongooseDataStore`, `CrudService`).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Marks a schema the plugin was applied to (once per schema, however often it is registered) */
|
|
37
|
+
export const SYNC_SCHEMA = Symbol('lt:syncSchema');
|
|
38
|
+
/** On a query: read and match tombstones too */
|
|
39
|
+
const INCLUDE_TOMBSTONES = Symbol('lt:includeTombstones');
|
|
40
|
+
/** On a query: the store's tombstone write, the one update allowed to set `deletedAt` */
|
|
41
|
+
const TOMBSTONE_WRITE = Symbol('lt:tombstoneWrite');
|
|
42
|
+
|
|
43
|
+
const logger = new Logger('SyncSchema');
|
|
44
|
+
/** Where Mongoose keeps a field-level index declaration on a path */
|
|
45
|
+
const PATH_INDEX = '_index';
|
|
46
|
+
|
|
47
|
+
/** Let a Mongoose query of a synchronized model read (or match) tombstones too. Returns the query. */
|
|
48
|
+
export function includeTombstones<Q extends object>(query: Q): Q {
|
|
49
|
+
(query as any)[INCLUDE_TOMBSTONES] = true;
|
|
50
|
+
return query;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Mark a Mongoose update query as the store's tombstone write. Returns the query. */
|
|
54
|
+
export function tombstoneWrite<Q extends object>(query: Q): Q {
|
|
55
|
+
(query as any)[TOMBSTONE_WRITE] = true;
|
|
56
|
+
return query;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether a schema (or a model's schema) belongs to a synchronized model */
|
|
60
|
+
export function isSyncSchema(schema: unknown): boolean {
|
|
61
|
+
return !!schema && typeof schema === 'object' && (schema as any)[SYNC_SCHEMA] !== undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The sync declaration a schema was prepared with */
|
|
65
|
+
export function syncOfSchema(schema: unknown): SyncDescriptor | undefined {
|
|
66
|
+
return isSyncSchema(schema) ? (schema as any)[SYNC_SCHEMA] : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prepare a Mongoose schema for a synchronized model: system paths, the id type, live-only unique indexes, the
|
|
71
|
+
* change-order index and the hooks. Idempotent per schema.
|
|
72
|
+
*/
|
|
73
|
+
export function applySyncSchema(schema: any, sync: SyncDescriptor): void {
|
|
74
|
+
if (schema[SYNC_SCHEMA]) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
schema[SYNC_SCHEMA] = sync;
|
|
78
|
+
|
|
79
|
+
if (sync.idFormat === 'uuidv7') {
|
|
80
|
+
schema.add({ _id: { default: generateUuidV7, type: 'UUID' } });
|
|
81
|
+
allowUuidRanges(schema.path('_id'));
|
|
82
|
+
}
|
|
83
|
+
schema.add({
|
|
84
|
+
[SYNC_CLOCK_FIELD]: { type: Date },
|
|
85
|
+
[SYNC_DELETED_FIELD]: { default: null, type: Date },
|
|
86
|
+
[SYNC_VERSION_FIELD]: { default: 1, type: Number },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Every unique index — field-level ones included — applies to live records only (plan §3.6)
|
|
90
|
+
const indexes: Array<[Record<string, unknown>, Record<string, unknown>?]> = schema.indexes();
|
|
91
|
+
schema.eachPath((_path: string, schemaType: any) => {
|
|
92
|
+
// A field-level index lives on the path's schema type; cleared here, it is declared again below, rewritten
|
|
93
|
+
if (schemaType[PATH_INDEX]) {
|
|
94
|
+
schemaType[PATH_INDEX] = null;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
schema.clearIndexes();
|
|
98
|
+
for (const [fields, options = {}] of indexes) {
|
|
99
|
+
const index = liveUniqueIndex({ fields: fields as Record<string, number | string>, options });
|
|
100
|
+
schema.index(index.fields, index.options);
|
|
101
|
+
}
|
|
102
|
+
// The pull's keyset, led by the tenant every pull on a tenant-scoped model is filtered by (plan §3.4)
|
|
103
|
+
schema.index(
|
|
104
|
+
schema.path('tenantId') ? { tenantId: 1, [SYNC_CLOCK_FIELD]: 1, _id: 1 } : { [SYNC_CLOCK_FIELD]: 1, _id: 1 },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
registerHooks(schema);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Let range operators (`$gt`, `$gte`, `$lt`, `$lte`) be cast on a UUID path, as Mongoose casts them on an ObjectId
|
|
112
|
+
* path. Mongoose's UUID type refuses them ("Can't use $gt with UUID."), so the keyset of a pull — `(syncAt, _id)`,
|
|
113
|
+
* continued strictly after the last record — could not be expressed for a uuidv7 model, nor a neutral
|
|
114
|
+
* `find({ id: { $gt } })`. MongoDB orders BSON UUIDs (binary subtype 4) by their bytes, the order `sort({ _id: 1 })`
|
|
115
|
+
* uses. Defined on this path only: the handlers object on the prototype is shared by every UUID path of the process.
|
|
116
|
+
*/
|
|
117
|
+
function allowUuidRanges(path: any): void {
|
|
118
|
+
if (!path?.$conditionalHandlers) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const single = function (this: any, value: unknown) {
|
|
122
|
+
return this.cast(value);
|
|
123
|
+
};
|
|
124
|
+
Object.defineProperty(path, '$conditionalHandlers', {
|
|
125
|
+
enumerable: false,
|
|
126
|
+
value: { ...path.$conditionalHandlers, $gt: single, $gte: single, $lt: single, $lte: single },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A MongoDB collection that holds data from before its model was synchronized — the migration it needs */
|
|
131
|
+
export class SyncMigrationRequiredError extends Error {
|
|
132
|
+
override readonly name = 'SyncMigrationRequiredError';
|
|
133
|
+
|
|
134
|
+
constructor(
|
|
135
|
+
readonly model: string,
|
|
136
|
+
readonly statements: string[],
|
|
137
|
+
) {
|
|
138
|
+
super(
|
|
139
|
+
`${model} is synchronized, but its collection holds data from before: run these statements in a migration, ` +
|
|
140
|
+
`then start again —\n${statements.map((statement) => ` ${statement}`).join('\n')}\n` +
|
|
141
|
+
'Until the old unique indexes are dropped, a second delete fails on them (a tombstone keeps no values), and a ' +
|
|
142
|
+
'document without a change position never reaches a client.',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Refuse to run a synchronized model on a collection that is not ready for it — the MongoDB counterpart of the SQL
|
|
149
|
+
* migrator's refusal (offline-sync plan §3.6). Two things block: a unique index that is not restricted to live
|
|
150
|
+
* records (Mongoose cannot replace it under the same name, and a tombstone's missing value collides on it), and a
|
|
151
|
+
* document without `syncAt` (written before the model was synchronized: no version, no change position). Reads the
|
|
152
|
+
* index list and one document id through the driver — an administrative read, no data leaves the process.
|
|
153
|
+
*/
|
|
154
|
+
export async function assertSyncCollectionReady(model: any): Promise<void> {
|
|
155
|
+
if (!isSyncSchema(model?.schema) || !model.collection) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let indexes: Array<{ key: Record<string, unknown>; name: string; partialFilterExpression?: any; unique?: boolean }>;
|
|
159
|
+
try {
|
|
160
|
+
indexes = await model.collection.indexes();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// A collection that does not exist yet holds nothing from before
|
|
163
|
+
if ((error as { code?: unknown })?.code === 26) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const name = model.collection.collectionName ?? model.collection.name;
|
|
169
|
+
const statements: string[] = [];
|
|
170
|
+
for (const index of indexes) {
|
|
171
|
+
if (index.unique && index.name !== '_id_' && index.partialFilterExpression?.[SYNC_DELETED_FIELD] !== null) {
|
|
172
|
+
statements.push(`db.getCollection(${JSON.stringify(name)}).dropIndex(${JSON.stringify(index.name)})`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const unstamped = await model.collection.findOne(
|
|
176
|
+
{ [SYNC_CLOCK_FIELD]: { $exists: false } },
|
|
177
|
+
{ projection: { _id: 1 } },
|
|
178
|
+
);
|
|
179
|
+
if (unstamped) {
|
|
180
|
+
statements.push(
|
|
181
|
+
`db.getCollection(${JSON.stringify(name)}).updateMany({ ${SYNC_CLOCK_FIELD}: { $exists: false } }, ` +
|
|
182
|
+
`[{ $set: { ${SYNC_VERSION_FIELD}: { $ifNull: ['$${SYNC_VERSION_FIELD}', 1] }, ${SYNC_DELETED_FIELD}: { $ifNull: ['$${SYNC_DELETED_FIELD}', null] }, ${SYNC_CLOCK_FIELD}: '$$NOW' } }])`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (statements.length) {
|
|
186
|
+
throw new SyncMigrationRequiredError(model.modelName, statements);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The server's clock (plan §3.4: app clocks differ between instances, so the insert's stamp must follow the
|
|
192
|
+
* database's). Read through `hello.localTime` and carried forward with this process's MONOTONIC clock — never its wall
|
|
193
|
+
* clock, so neither a skewed nor a stepped application clock moves the stamp. Read again every `CLOCK_REFRESH_MS`,
|
|
194
|
+
* which bounds the drift between the two clocks; the error is half a round trip plus that drift.
|
|
195
|
+
*/
|
|
196
|
+
export async function mongoServerNow(connection: any): Promise<Date> {
|
|
197
|
+
let reading = clockReadings.get(connection);
|
|
198
|
+
if (!reading) {
|
|
199
|
+
reading = await readServerClock(connection);
|
|
200
|
+
clockReadings.set(connection, reading);
|
|
201
|
+
} else if (performance.now() - reading.mono > CLOCK_REFRESH_MS && !reading.refreshing) {
|
|
202
|
+
const current = reading;
|
|
203
|
+
current.refreshing = readServerClock(connection)
|
|
204
|
+
.then((fresh) => clockReadings.set(connection, fresh))
|
|
205
|
+
.then(() => undefined)
|
|
206
|
+
.finally(() => {
|
|
207
|
+
current.refreshing = undefined;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return new Date(reading.server + (performance.now() - reading.mono));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The margin of the window W over the time a write needs from its stamp until it is visible (plan §3.4, §10; decided in
|
|
215
|
+
* DEV-3901). An insert is stamped before it is sent, so a write waiting for a pooled connection or a slow network ages
|
|
216
|
+
* by that wait; a window too small loses such a change for good, a larger one only repeats records a client already has.
|
|
217
|
+
*/
|
|
218
|
+
export const SYNC_WINDOW_MARGIN_MS = 30_000;
|
|
219
|
+
|
|
220
|
+
/** MongoDB's default `transactionLifetimeLimitSeconds`, assumed when the server does not tell */
|
|
221
|
+
const DEFAULT_TRANSACTION_LIFETIME_SECONDS = 60;
|
|
222
|
+
|
|
223
|
+
const changeWindows = new WeakMap<object, Promise<number>>();
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The window W of a pull on this connection: on a replica set or a sharded cluster a write in a project's transaction
|
|
227
|
+
* is stamped when it executes and becomes visible at commit, up to `transactionLifetimeLimitSeconds` later, so W covers
|
|
228
|
+
* that limit plus the margin; a standalone has no transactions and needs the margin only. Read once per connection. A
|
|
229
|
+
* server that cannot be asked gets the larger window — too large repeats records, too small loses changes.
|
|
230
|
+
*/
|
|
231
|
+
export function mongoChangeWindowMs(connection: any): Promise<number> {
|
|
232
|
+
let window = changeWindows.get(connection);
|
|
233
|
+
if (!window) {
|
|
234
|
+
window = readChangeWindow(connection);
|
|
235
|
+
changeWindows.set(connection, window);
|
|
236
|
+
}
|
|
237
|
+
return window;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function readChangeWindow(connection: any): Promise<number> {
|
|
241
|
+
const transactional = DEFAULT_TRANSACTION_LIFETIME_SECONDS * 1000 + SYNC_WINDOW_MARGIN_MS;
|
|
242
|
+
try {
|
|
243
|
+
const admin = connection.db.admin();
|
|
244
|
+
const hello = await admin.command({ hello: 1 });
|
|
245
|
+
if (!hello?.setName && hello?.msg !== 'isdbgrid') {
|
|
246
|
+
return SYNC_WINDOW_MARGIN_MS;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const parameter = await admin.command({ getParameter: 1, transactionLifetimeLimitSeconds: 1 });
|
|
250
|
+
const lifetime = Number(parameter?.transactionLifetimeLimitSeconds);
|
|
251
|
+
return Number.isFinite(lifetime) && lifetime > 0 ? lifetime * 1000 + SYNC_WINDOW_MARGIN_MS : transactional;
|
|
252
|
+
} catch {
|
|
253
|
+
// The application user may not read server parameters: assume the default lifetime
|
|
254
|
+
return transactional;
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
// Not asked successfully: not cached, the next pull asks again
|
|
258
|
+
changeWindows.delete(connection);
|
|
259
|
+
return transactional;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const CLOCK_REFRESH_MS = 30_000;
|
|
264
|
+
const clockReadings = new WeakMap<object, { mono: number; refreshing?: Promise<void>; server: number }>();
|
|
265
|
+
let clockWarned = false;
|
|
266
|
+
|
|
267
|
+
async function readServerClock(connection: any): Promise<{ mono: number; server: number }> {
|
|
268
|
+
try {
|
|
269
|
+
const before = performance.now();
|
|
270
|
+
const hello = await connection.db.admin().command({ hello: 1 });
|
|
271
|
+
const after = performance.now();
|
|
272
|
+
const server = hello?.localTime instanceof Date ? hello.localTime.getTime() : Number.NaN;
|
|
273
|
+
if (Number.isFinite(server)) {
|
|
274
|
+
return { mono: (before + after) / 2, server };
|
|
275
|
+
}
|
|
276
|
+
warnClock('MongoDB did not report its clock (hello.localTime)');
|
|
277
|
+
} catch (error) {
|
|
278
|
+
warnClock(`Cannot read the MongoDB server clock (${(error as Error)?.message ?? error})`);
|
|
279
|
+
}
|
|
280
|
+
return { mono: performance.now(), server: Date.now() };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function warnClock(cause: string): void {
|
|
284
|
+
if (!clockWarned) {
|
|
285
|
+
clockWarned = true;
|
|
286
|
+
logger.warn(`${cause}; inserts of synchronized models use this process's clock until it can be read`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// -- hooks ----------------------------------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function registerHooks(schema: any): void {
|
|
293
|
+
schema.pre('save', async function (this: any) {
|
|
294
|
+
const connection = this.constructor?.db;
|
|
295
|
+
if (this.isNew) {
|
|
296
|
+
this.set(SYNC_VERSION_FIELD, 1);
|
|
297
|
+
this.set(SYNC_DELETED_FIELD, null);
|
|
298
|
+
this.set(SYNC_CLOCK_FIELD, await mongoServerNow(connection));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!this.modifiedPaths().length) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
for (const field of SYNC_SYSTEM_FIELDS) {
|
|
305
|
+
if (this.isModified(field)) {
|
|
306
|
+
throw new BadRequestException(`${field} of a synchronized record is written by the store, not by the caller`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// A loaded document is saved only if nobody changed it since: the version it was loaded with is the condition
|
|
310
|
+
const loaded = this.get(SYNC_VERSION_FIELD);
|
|
311
|
+
this.$where = { ...this.$where, [SYNC_VERSION_FIELD]: loaded ?? null, ...liveFilter() };
|
|
312
|
+
this.set(SYNC_VERSION_FIELD, (typeof loaded === 'number' ? loaded : 0) + 1);
|
|
313
|
+
this.set(SYNC_CLOCK_FIELD, await mongoServerNow(connection));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
schema.pre('insertMany', async function (this: any, docs: any[]) {
|
|
317
|
+
if (!Array.isArray(docs) || !docs.length) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const now = await mongoServerNow(this.db);
|
|
321
|
+
for (const doc of docs) {
|
|
322
|
+
setField(doc, SYNC_VERSION_FIELD, 1);
|
|
323
|
+
setField(doc, SYNC_DELETED_FIELD, null);
|
|
324
|
+
setField(doc, SYNC_CLOCK_FIELD, now);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
for (const hook of ['findOneAndUpdate', 'updateOne', 'updateMany']) {
|
|
329
|
+
schema.pre(hook, function (this: any) {
|
|
330
|
+
matchLive(this);
|
|
331
|
+
this.setUpdate(syncUpdate(this.getUpdate(), this[TOMBSTONE_WRITE] === true));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const hook of ['find', 'findOne', 'countDocuments', 'distinct', 'findOneAndDelete', 'deleteOne', 'deleteMany']) {
|
|
336
|
+
schema.pre(hook, function (this: any) {
|
|
337
|
+
matchLive(this);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const hook of ['replaceOne', 'findOneAndReplace']) {
|
|
342
|
+
schema.pre(hook, function () {
|
|
343
|
+
throw new BadRequestException(
|
|
344
|
+
'A synchronized record cannot be replaced: a replacement carries neither the version increment nor the change ' +
|
|
345
|
+
'position — update it instead',
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
schema.pre('bulkWrite', async function (this: any, ops: any[]) {
|
|
351
|
+
if (!Array.isArray(ops) || !ops.length) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let now: Date | undefined;
|
|
355
|
+
for (const op of ops) {
|
|
356
|
+
if (op.insertOne) {
|
|
357
|
+
now ??= await mongoServerNow(this.db);
|
|
358
|
+
setField(op.insertOne.document, SYNC_VERSION_FIELD, 1);
|
|
359
|
+
setField(op.insertOne.document, SYNC_DELETED_FIELD, null);
|
|
360
|
+
setField(op.insertOne.document, SYNC_CLOCK_FIELD, now);
|
|
361
|
+
} else if (op.updateOne || op.updateMany) {
|
|
362
|
+
const entry = op.updateOne ?? op.updateMany;
|
|
363
|
+
entry.filter = { $and: [entry.filter ?? {}, liveFilter()] };
|
|
364
|
+
entry.update = syncUpdate(entry.update, false);
|
|
365
|
+
} else if (op.deleteOne || op.deleteMany) {
|
|
366
|
+
const entry = op.deleteOne ?? op.deleteMany;
|
|
367
|
+
entry.filter = { $and: [entry.filter ?? {}, liveFilter()] };
|
|
368
|
+
} else if (op.replaceOne) {
|
|
369
|
+
throw new BadRequestException('A synchronized record cannot be replaced — update it instead');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
schema.pre('aggregate', function (this: any) {
|
|
375
|
+
const pipeline = this.pipeline();
|
|
376
|
+
if (!this[INCLUDE_TOMBSTONES]) {
|
|
377
|
+
matchLiveInPipeline(pipeline);
|
|
378
|
+
}
|
|
379
|
+
// A join into a synchronized collection sees live records only, whatever the source asked for
|
|
380
|
+
matchLiveInJoins(pipeline, this._model);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Restrict an aggregation over a synchronized collection to its live records. A stage MongoDB requires first
|
|
386
|
+
* (`$geoNear`, `$search`, `$searchMeta`, `$vectorSearch`) keeps its place: `$geoNear` takes the live filter into its
|
|
387
|
+
* own `query`, the search stages get the `$match` right after them.
|
|
388
|
+
*/
|
|
389
|
+
export function matchLiveInPipeline(pipeline: any[]): void {
|
|
390
|
+
const first = pipeline[0];
|
|
391
|
+
if (first?.$geoNear) {
|
|
392
|
+
first.$geoNear.query = first.$geoNear.query ? { $and: [first.$geoNear.query, liveFilter()] } : liveFilter();
|
|
393
|
+
} else if (first && (first.$search || first.$searchMeta || first.$vectorSearch)) {
|
|
394
|
+
pipeline.splice(1, 0, { $match: liveFilter() });
|
|
395
|
+
} else {
|
|
396
|
+
pipeline.unshift({ $match: liveFilter() });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function matchLive(query: any): void {
|
|
401
|
+
if (!query[INCLUDE_TOMBSTONES]) {
|
|
402
|
+
// `$and`, so a caller's own `deletedAt` condition is kept rather than overwritten
|
|
403
|
+
query.and([liveFilter()]);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* A caller's update as a synchronized model writes it: the system fields removed (the store's tombstone write keeps
|
|
409
|
+
* `deletedAt`), the version incremented and the change position stamped by the server
|
|
410
|
+
*/
|
|
411
|
+
function syncUpdate(update: any, tombstone: boolean): any {
|
|
412
|
+
if (Array.isArray(update)) {
|
|
413
|
+
throw new BadRequestException(
|
|
414
|
+
'A pipeline update cannot be applied to a synchronized record: it would not carry the version increment',
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const result: Record<string, any> = {};
|
|
418
|
+
for (const [key, value] of Object.entries(update ?? {})) {
|
|
419
|
+
if (!key.startsWith('$')) {
|
|
420
|
+
if (!isSystemPath(key, tombstone)) {
|
|
421
|
+
result.$set = { ...result.$set, [key]: value };
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
426
|
+
// `$rename` names a path on both sides: neither may be a system field
|
|
427
|
+
const kept = Object.fromEntries(
|
|
428
|
+
Object.entries(value).filter(
|
|
429
|
+
([path, target]) =>
|
|
430
|
+
!isSystemPath(path, tombstone) &&
|
|
431
|
+
!(key === '$rename' && typeof target === 'string' && isSystemPath(target, false)),
|
|
432
|
+
),
|
|
433
|
+
);
|
|
434
|
+
if (Object.keys(kept).length) {
|
|
435
|
+
result[key] = { ...result[key], ...kept };
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
result[key] = value;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
result.$inc = { ...result.$inc, [SYNC_VERSION_FIELD]: 1 };
|
|
442
|
+
result.$currentDate = { ...result.$currentDate, [SYNC_CLOCK_FIELD]: true };
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function isSystemPath(path: string, tombstone: boolean): boolean {
|
|
447
|
+
const head = path.split('.', 1)[0];
|
|
448
|
+
if (tombstone && head === SYNC_DELETED_FIELD) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
return SYNC_SYSTEM_FIELDS.includes(head);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function setField(doc: any, field: string, value: unknown): void {
|
|
455
|
+
if (doc && typeof doc.set === 'function' && typeof doc.$isNew === 'boolean') {
|
|
456
|
+
doc.set(field, value);
|
|
457
|
+
} else if (doc && typeof doc === 'object') {
|
|
458
|
+
doc[field] = value;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** A join into a synchronized model's collection sees its live records only, like a direct query */
|
|
463
|
+
function matchLiveInJoins(pipeline: any[], model: any): void {
|
|
464
|
+
if (!Array.isArray(pipeline)) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
for (const stage of pipeline) {
|
|
468
|
+
if (!stage || typeof stage !== 'object') {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (stage.$lookup) {
|
|
472
|
+
if (isSyncCollection(stage.$lookup.from, model)) {
|
|
473
|
+
stage.$lookup.pipeline = [{ $match: liveFilter() }, ...(stage.$lookup.pipeline ?? [])];
|
|
474
|
+
}
|
|
475
|
+
matchLiveInJoins(stage.$lookup.pipeline, model);
|
|
476
|
+
}
|
|
477
|
+
if (stage.$unionWith) {
|
|
478
|
+
if (typeof stage.$unionWith === 'string') {
|
|
479
|
+
if (isSyncCollection(stage.$unionWith, model)) {
|
|
480
|
+
stage.$unionWith = { coll: stage.$unionWith, pipeline: [{ $match: liveFilter() }] };
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
if (isSyncCollection(stage.$unionWith.coll, model)) {
|
|
484
|
+
stage.$unionWith.pipeline = [{ $match: liveFilter() }, ...(stage.$unionWith.pipeline ?? [])];
|
|
485
|
+
}
|
|
486
|
+
matchLiveInJoins(stage.$unionWith.pipeline, model);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (stage.$graphLookup && isSyncCollection(stage.$graphLookup.from, model)) {
|
|
490
|
+
stage.$graphLookup.restrictSearchWithMatch = { ...stage.$graphLookup.restrictSearchWithMatch, ...liveFilter() };
|
|
491
|
+
}
|
|
492
|
+
if (stage.$facet && typeof stage.$facet === 'object') {
|
|
493
|
+
for (const branch of Object.values(stage.$facet)) {
|
|
494
|
+
matchLiveInJoins(branch as any[], model);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function isSyncCollection(collection: unknown, sourceModel: any): boolean {
|
|
501
|
+
if (typeof collection !== 'string' || !collection) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const models = sourceModel?.db?.models ?? {};
|
|
505
|
+
return Object.values(models).some(
|
|
506
|
+
(candidate: any) => candidate?.collection?.name === collection && isSyncSchema(candidate.schema),
|
|
507
|
+
);
|
|
508
|
+
}
|