@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
|
@@ -2,6 +2,22 @@ import { Logger } from '@nestjs/common';
|
|
|
2
2
|
import type { Model } from 'mongoose';
|
|
3
3
|
|
|
4
4
|
import { getStringIds } from '../helpers/id.helper.js';
|
|
5
|
+
import {
|
|
6
|
+
includeTombstones,
|
|
7
|
+
mongoChangeWindowMs,
|
|
8
|
+
mongoServerNow,
|
|
9
|
+
syncOfSchema,
|
|
10
|
+
tombstoneWrite,
|
|
11
|
+
} from './mongo/sync-schema.plugin.js';
|
|
12
|
+
import type { SyncDescriptor } from './model-descriptor.interface.js';
|
|
13
|
+
import {
|
|
14
|
+
assertClientId,
|
|
15
|
+
insertWithServerId,
|
|
16
|
+
suppliedId,
|
|
17
|
+
syncInsertRecord,
|
|
18
|
+
tombstoneClearedFields,
|
|
19
|
+
} from './policies/sync.policy.js';
|
|
20
|
+
import { SYNC_CLOCK_FIELD, SYNC_DELETED_FIELD } from './sync-model.js';
|
|
5
21
|
import {
|
|
6
22
|
assertAccessReason,
|
|
7
23
|
assertArrayField,
|
|
@@ -10,10 +26,13 @@ import {
|
|
|
10
26
|
assertSelectFields,
|
|
11
27
|
} from './store-guards.js';
|
|
12
28
|
import type {
|
|
29
|
+
ChangeClock,
|
|
13
30
|
DataStore,
|
|
14
31
|
DataStoreUpdate,
|
|
32
|
+
DeleteOptions,
|
|
15
33
|
FindOptions,
|
|
16
34
|
PushOptions,
|
|
35
|
+
ReadOptions,
|
|
17
36
|
StoreCapabilities,
|
|
18
37
|
StoredRecord,
|
|
19
38
|
UpdateOptions,
|
|
@@ -42,18 +61,34 @@ export const MONGOOSE_STORE_CAPABILITIES: Readonly<StoreCapabilities> = Object.f
|
|
|
42
61
|
* are the Mongo-operator form — except that the neutral `id` is addressed as `_id`.
|
|
43
62
|
*/
|
|
44
63
|
export class MongooseDataStore<T = Record<string, any>> implements DataStore<T> {
|
|
45
|
-
readonly capabilities
|
|
64
|
+
readonly capabilities: Readonly<StoreCapabilities>;
|
|
46
65
|
readonly driver = 'mongodb';
|
|
47
66
|
protected readonly logger = new Logger(MongooseDataStore.name);
|
|
67
|
+
/** The model's sync declaration (`@PersistedModel({ sync })`), read from its schema */
|
|
68
|
+
protected readonly sync?: SyncDescriptor;
|
|
48
69
|
|
|
49
|
-
constructor(protected readonly model: Model<any>) {
|
|
70
|
+
constructor(protected readonly model: Model<any>) {
|
|
71
|
+
this.sync = syncOfSchema(model?.schema);
|
|
72
|
+
this.capabilities = this.sync
|
|
73
|
+
? Object.freeze({ ...MONGOOSE_STORE_CAPABILITIES, changeOrder: 'clock', idType: this.sync.idFormat })
|
|
74
|
+
: MONGOOSE_STORE_CAPABILITIES;
|
|
75
|
+
}
|
|
50
76
|
|
|
51
77
|
get modelName(): string {
|
|
52
78
|
return this.model.modelName;
|
|
53
79
|
}
|
|
54
80
|
|
|
55
|
-
|
|
56
|
-
|
|
81
|
+
/** The server clock and the window W of a synchronized model (its change order is the clock, plan §3.4) */
|
|
82
|
+
async changeClock(): Promise<ChangeClock> {
|
|
83
|
+
if (!this.sync) {
|
|
84
|
+
throw new Error(`changeClock(): ${this.modelName} is not a synchronized model (@PersistedModel({ sync }))`);
|
|
85
|
+
}
|
|
86
|
+
const [now, windowMs] = await Promise.all([mongoServerNow(this.model.db), mongoChangeWindowMs(this.model.db)]);
|
|
87
|
+
return { now, windowMs };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async count(where?: Where<T>, options: ReadOptions = {}): Promise<number> {
|
|
91
|
+
return this.tombstones(this.model.countDocuments(toMongoFilter(where)), options).exec();
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
async sum(field: string, where?: Where<T>): Promise<{ count: number; sum: number }> {
|
|
@@ -69,28 +104,43 @@ export class MongooseDataStore<T = Record<string, any>> implements DataStore<T>
|
|
|
69
104
|
return { count: row?.count ?? 0, sum: row?.sum ?? 0 };
|
|
70
105
|
}
|
|
71
106
|
|
|
72
|
-
async deleteById(id: string): Promise<null | StoredRecord<T>> {
|
|
73
|
-
|
|
107
|
+
async deleteById(id: string, options: DeleteOptions = {}): Promise<null | StoredRecord<T>> {
|
|
108
|
+
if (this.sync && !options.hardDelete) {
|
|
109
|
+
return this.tombstone({ _id: id }, options);
|
|
110
|
+
}
|
|
111
|
+
return toRecord<T>(await this.tombstones(this.model.findByIdAndDelete(id), options).lean().exec());
|
|
74
112
|
}
|
|
75
113
|
|
|
76
|
-
async deleteMany(where: Where<T
|
|
77
|
-
|
|
114
|
+
async deleteMany(where: Where<T>, options: DeleteOptions = {}): Promise<number> {
|
|
115
|
+
if (this.sync && !options.hardDelete) {
|
|
116
|
+
const query = tombstoneWrite(this.model.updateMany(toMongoFilter(where), this.tombstoneUpdate() as any));
|
|
117
|
+
const result = await this.tombstones(query, options).exec();
|
|
118
|
+
return result.matchedCount;
|
|
119
|
+
}
|
|
120
|
+
const result = await this.tombstones(this.model.deleteMany(toMongoFilter(where)), options).exec();
|
|
78
121
|
return result.deletedCount;
|
|
79
122
|
}
|
|
80
123
|
|
|
81
|
-
async deleteOne(where: Where<T
|
|
82
|
-
|
|
124
|
+
async deleteOne(where: Where<T>, options: DeleteOptions = {}): Promise<null | StoredRecord<T>> {
|
|
125
|
+
if (this.sync && !options.hardDelete) {
|
|
126
|
+
return this.tombstone(toMongoFilter(where), options);
|
|
127
|
+
}
|
|
128
|
+
return toRecord<T>(
|
|
129
|
+
await this.tombstones(this.model.findOneAndDelete(toMongoFilter(where)), options)
|
|
130
|
+
.lean()
|
|
131
|
+
.exec(),
|
|
132
|
+
);
|
|
83
133
|
}
|
|
84
134
|
|
|
85
|
-
async distinct(field: string, where?: Where<T
|
|
86
|
-
return this.model.distinct(field, toMongoFilter(where)).exec();
|
|
135
|
+
async distinct(field: string, where?: Where<T>, options: ReadOptions = {}): Promise<unknown[]> {
|
|
136
|
+
return this.tombstones(this.model.distinct(field, toMongoFilter(where)), options).exec();
|
|
87
137
|
}
|
|
88
138
|
|
|
89
139
|
async find(where?: Where<T>, options: FindOptions = {}): Promise<StoredRecord<T>[]> {
|
|
90
140
|
const sample = options.sample === undefined ? undefined : assertSampleSize(options.sample);
|
|
91
|
-
let query = this.model.find(toMongoFilter(where));
|
|
141
|
+
let query = this.tombstones(this.model.find(toMongoFilter(where)), options);
|
|
92
142
|
if (options.select || options.exclude) query = query.select(toProjection(options));
|
|
93
|
-
if (options.sort) query = query.sort(options.sort);
|
|
143
|
+
if (options.sort) query = query.sort(toMongoSort(options.sort));
|
|
94
144
|
if (options.skip !== undefined) query = query.skip(options.skip);
|
|
95
145
|
if (options.limit !== undefined) query = query.limit(options.limit);
|
|
96
146
|
if (options.collation) query = query.collation(options.collation as any);
|
|
@@ -101,21 +151,27 @@ export class MongooseDataStore<T = Record<string, any>> implements DataStore<T>
|
|
|
101
151
|
return sample === undefined ? records : randomSample(records, sample);
|
|
102
152
|
}
|
|
103
153
|
|
|
104
|
-
async findById(
|
|
105
|
-
|
|
154
|
+
async findById(
|
|
155
|
+
id: string,
|
|
156
|
+
options: Pick<FindOptions, 'exclude' | 'includeTombstones' | 'select'> = {},
|
|
157
|
+
): Promise<null | StoredRecord<T>> {
|
|
158
|
+
let query = this.tombstones(this.model.findById(id), options);
|
|
106
159
|
if (options.select || options.exclude) query = query.select(toProjection(options));
|
|
107
160
|
return toRecord<T>(await query.lean().exec());
|
|
108
161
|
}
|
|
109
162
|
|
|
110
163
|
async findOne(where: Where<T>, options: Omit<FindOptions, 'limit' | 'skip'> = {}): Promise<null | StoredRecord<T>> {
|
|
111
|
-
let query = this.model.findOne(toMongoFilter(where));
|
|
164
|
+
let query = this.tombstones(this.model.findOne(toMongoFilter(where)), options);
|
|
112
165
|
if (options.select || options.exclude) query = query.select(toProjection(options));
|
|
113
|
-
if (options.sort) query = query.sort(options.sort);
|
|
166
|
+
if (options.sort) query = query.sort(toMongoSort(options.sort));
|
|
114
167
|
if (options.collation) query = query.collation(options.collation as any);
|
|
115
168
|
return toRecord<T>(await query.lean().exec());
|
|
116
169
|
}
|
|
117
170
|
|
|
118
171
|
async insert(record: Partial<T>): Promise<StoredRecord<T>> {
|
|
172
|
+
if (this.sync) {
|
|
173
|
+
return this.insertSynchronized(record as Record<string, any>);
|
|
174
|
+
}
|
|
119
175
|
// `create()` runs the save hooks, so tenant stamping, audit fields and password hashing apply
|
|
120
176
|
const created = await this.model.create(record);
|
|
121
177
|
return toRecord<T>(created.toObject());
|
|
@@ -161,30 +217,95 @@ export class MongooseDataStore<T = Record<string, any>> implements DataStore<T>
|
|
|
161
217
|
update: DataStoreUpdate<T>,
|
|
162
218
|
options: UpdateOptions = {},
|
|
163
219
|
): Promise<null | StoredRecord<T>> {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.lean()
|
|
170
|
-
.exec();
|
|
171
|
-
return toRecord<T>(updated);
|
|
220
|
+
const query = this.model.findByIdAndUpdate(id, update as any, {
|
|
221
|
+
returnDocument: 'after',
|
|
222
|
+
...(options.timestamps === false ? { timestamps: false } : {}),
|
|
223
|
+
});
|
|
224
|
+
return toRecord<T>(await this.tombstones(query, options).lean().exec());
|
|
172
225
|
}
|
|
173
226
|
|
|
174
|
-
async updateMany(where: Where<T>, update: DataStoreUpdate<T
|
|
175
|
-
const
|
|
227
|
+
async updateMany(where: Where<T>, update: DataStoreUpdate<T>, options: UpdateOptions = {}): Promise<number> {
|
|
228
|
+
const query = this.model.updateMany(
|
|
229
|
+
toMongoFilter(where),
|
|
230
|
+
update as any,
|
|
231
|
+
options.timestamps === false ? { timestamps: false } : {},
|
|
232
|
+
);
|
|
233
|
+
const result = await this.tombstones(query, options).exec();
|
|
176
234
|
return result.matchedCount;
|
|
177
235
|
}
|
|
178
236
|
|
|
179
|
-
async updateOne(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
237
|
+
async updateOne(
|
|
238
|
+
where: Where<T>,
|
|
239
|
+
update: DataStoreUpdate<T>,
|
|
240
|
+
options: UpdateOptions = {},
|
|
241
|
+
): Promise<null | StoredRecord<T>> {
|
|
242
|
+
const query = this.model.findOneAndUpdate(toMongoFilter(where), update as any, {
|
|
243
|
+
returnDocument: 'after',
|
|
244
|
+
...(options.timestamps === false ? { timestamps: false } : {}),
|
|
245
|
+
});
|
|
246
|
+
return toRecord<T>(await this.tombstones(query, options).lean().exec());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// -- synchronized models ------------------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
/** A query that also sees tombstones when the caller asked for them (synchronized models only) */
|
|
252
|
+
protected tombstones<Q extends object>(query: Q, options: { includeTombstones?: boolean }): Q {
|
|
253
|
+
return this.sync && options.includeTombstones ? includeTombstones(query) : query;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The insert of a synchronized model: system fields set by the store, a client id in the model's format kept, a
|
|
258
|
+
* server id generated anew when it is taken (plan §3.3, E6). `create()` runs every save hook, as for any model.
|
|
259
|
+
*/
|
|
260
|
+
protected async insertSynchronized(record: Record<string, any>): Promise<StoredRecord<T>> {
|
|
261
|
+
const { _id: _ignored, id: _alsoIgnored, ...doc } = syncInsertRecord(record);
|
|
262
|
+
const supplied = suppliedId(record);
|
|
263
|
+
if (supplied !== undefined && supplied !== null) {
|
|
264
|
+
const created = await this.model.create({
|
|
265
|
+
...doc,
|
|
266
|
+
_id: assertClientId(supplied, this.sync.idFormat, this.modelName),
|
|
267
|
+
});
|
|
268
|
+
return toRecord<T>(created.toObject());
|
|
269
|
+
}
|
|
270
|
+
const created = await insertWithServerId(
|
|
271
|
+
this.sync.idFormat,
|
|
272
|
+
(id) => this.model.create({ ...doc, _id: id }),
|
|
273
|
+
(error) => isDuplicateIdError(error),
|
|
274
|
+
);
|
|
275
|
+
return toRecord<T>(created.toObject());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The update that turns a record into a tombstone: content cleared, `deletedAt` from the server clock */
|
|
279
|
+
protected tombstoneUpdate(): Record<string, unknown> {
|
|
280
|
+
const fields = Object.keys(this.model.schema.paths).map((path) => path.split('.', 1)[0]);
|
|
281
|
+
const cleared = tombstoneClearedFields(new Set(fields), this.sync);
|
|
282
|
+
return {
|
|
283
|
+
$currentDate: { [SYNC_CLOCK_FIELD]: true, [SYNC_DELETED_FIELD]: true },
|
|
284
|
+
...(cleared.length ? { $unset: Object.fromEntries(cleared.map((field) => [field, 1])) } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Turn the first record matching `filter` into a tombstone; returns it as it was before */
|
|
289
|
+
protected async tombstone(filter: Record<string, any>, options: DeleteOptions): Promise<null | StoredRecord<T>> {
|
|
290
|
+
const query = tombstoneWrite(
|
|
291
|
+
this.model.findOneAndUpdate(filter, this.tombstoneUpdate() as any, { returnDocument: 'before' }),
|
|
292
|
+
);
|
|
293
|
+
return toRecord<T>(await this.tombstones(query, options).lean().exec());
|
|
185
294
|
}
|
|
186
295
|
}
|
|
187
296
|
|
|
297
|
+
/** A duplicate-key error raised by the `_id` index — the case a server-generated id is replaced for */
|
|
298
|
+
export function isDuplicateIdError(error: unknown): boolean {
|
|
299
|
+
const candidate = error as { code?: unknown; keyPattern?: Record<string, unknown>; message?: unknown };
|
|
300
|
+
if (candidate?.code !== 11000) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (candidate.keyPattern) {
|
|
304
|
+
return Object.keys(candidate.keyPattern).length === 1 && '_id' in candidate.keyPattern;
|
|
305
|
+
}
|
|
306
|
+
return typeof candidate.message === 'string' && /index: _id_ /.test(candidate.message);
|
|
307
|
+
}
|
|
308
|
+
|
|
188
309
|
/** The neutral `id` is `_id` in MongoDB — at the top level and inside `$and` / `$or` / `$nor`. */
|
|
189
310
|
export function toMongoFilter(where: undefined | Where<any>): Record<string, any> {
|
|
190
311
|
if (!where) {
|
|
@@ -203,13 +324,28 @@ export function toMongoFilter(where: undefined | Where<any>): Record<string, any
|
|
|
203
324
|
return filter;
|
|
204
325
|
}
|
|
205
326
|
|
|
327
|
+
/** The neutral `id` is `_id` in a sort too */
|
|
328
|
+
function toMongoSort(sort: Record<string, -1 | 1>): Record<string, -1 | 1> {
|
|
329
|
+
return Object.fromEntries(
|
|
330
|
+
Object.entries(sort).map(([field, direction]) => [field === 'id' ? '_id' : field, direction]),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
206
334
|
/** A lean document as a neutral record: the id as a string under `id`, no `_id`, no version key. */
|
|
207
335
|
function toRecord<T>(doc: unknown): StoredRecord<T> {
|
|
208
336
|
if (!doc) {
|
|
209
337
|
return null;
|
|
210
338
|
}
|
|
211
339
|
const { __v: _version, _id, ...fields } = doc as Record<string, any>;
|
|
212
|
-
return { ...fields, id: _id === undefined ? fields.id :
|
|
340
|
+
return { ...fields, id: _id === undefined ? fields.id : storedIdString(_id) } as StoredRecord<T>;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** An id as the neutral string: a BSON UUID (a uuidv7 model's id) in its hyphenated form, anything else as before */
|
|
344
|
+
function storedIdString(id: unknown): string {
|
|
345
|
+
if (id && typeof id === 'object' && (id as { sub_type?: unknown }).sub_type === 4) {
|
|
346
|
+
return String(id);
|
|
347
|
+
}
|
|
348
|
+
return getStringIds(id);
|
|
213
349
|
}
|
|
214
350
|
|
|
215
351
|
function toProjection(options: Pick<FindOptions, 'exclude' | 'select'>): string {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SyncModelOptions } from './sync-model.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Persistence options a model class declares in code.
|
|
3
5
|
*/
|
|
@@ -8,6 +10,13 @@ export interface PersistedModelOptions {
|
|
|
8
10
|
* @default the default source
|
|
9
11
|
*/
|
|
10
12
|
dataSource?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The model is synchronized to offline clients (`docs/offline-sync-protocol.md`): its records carry `version`,
|
|
15
|
+
* `deletedAt` and a change position, a delete leaves a tombstone, and unique indexes apply to live records only.
|
|
16
|
+
* `true` declares it with the defaults. Takes effect for a model registered with `CorePersistenceModule.forFeature()`.
|
|
17
|
+
* @default not synchronized
|
|
18
|
+
*/
|
|
19
|
+
sync?: boolean | SyncModelOptions;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
const PERSISTED_MODEL_OPTIONS = Symbol('PERSISTED_MODEL_OPTIONS');
|
|
@@ -59,3 +68,20 @@ export function getPersistedModelOptionsByName(name: string): PersistedModelOpti
|
|
|
59
68
|
}
|
|
60
69
|
return classes.length ? getPersistedModelOptions(classes[0]) : undefined;
|
|
61
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The `sync` option declared for the model registered under `name` without its class. Whether a model is synchronized
|
|
74
|
+
* changes what its table holds and what a delete does, so two decorated classes of that name that disagree are never
|
|
75
|
+
* guessed between either.
|
|
76
|
+
*/
|
|
77
|
+
export function getPersistedSyncByName(name: string): PersistedModelOptions['sync'] {
|
|
78
|
+
const classes = declaredByName.get(name) ?? [];
|
|
79
|
+
const syncs = new Set(classes.map((target) => JSON.stringify(getPersistedModelOptions(target)?.sync ?? false)));
|
|
80
|
+
if (syncs.size > 1) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`@PersistedModel: ${classes.length} classes named "${name}" declare different sync options; pass \`model\` ` +
|
|
83
|
+
'in CorePersistenceModule.forFeature() to say which one this model is',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return classes.length ? getPersistedModelOptions(classes[0])?.sync : undefined;
|
|
87
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { BadRequestException, ConflictException } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { ErrorCode } from '../../../modules/error-code/error-codes.js';
|
|
4
|
+
import { generateId, generateUuidV7, isUuidV7, isWellFormedId } from '../id-format.js';
|
|
5
|
+
import type { SyncDescriptor, SyncIdFormat } from '../model-descriptor.interface.js';
|
|
6
|
+
import { SYNC_DELETED_FIELD, SYNC_SYSTEM_FIELDS, SYNC_VERSION_FIELD } from '../sync-model.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Write rules of synchronized models, shared by every data store (offline-sync plan §3.3) — like the tenant policy,
|
|
10
|
+
* a fix here reaches every database.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** How often an insert with a SERVER-generated id is tried again when that id is taken (plan §3.3, E6) */
|
|
14
|
+
export const SERVER_ID_ATTEMPTS = 3;
|
|
15
|
+
|
|
16
|
+
/** Fields a tombstone keeps besides the declared scope fields (protocol §2) */
|
|
17
|
+
const TOMBSTONE_KEPT = new Set([
|
|
18
|
+
'_id',
|
|
19
|
+
'__v',
|
|
20
|
+
'createdAt',
|
|
21
|
+
'createdBy',
|
|
22
|
+
'id',
|
|
23
|
+
'tenantId',
|
|
24
|
+
'updatedAt',
|
|
25
|
+
'updatedBy',
|
|
26
|
+
...SYNC_SYSTEM_FIELDS,
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The record a caller inserts, as a synchronized model stores it: the system fields are the store's (`version: 1`,
|
|
31
|
+
* `deletedAt: null`), whatever the caller sent
|
|
32
|
+
*/
|
|
33
|
+
export function syncInsertRecord(record: Record<string, any>): Record<string, any> {
|
|
34
|
+
const doc = { ...record };
|
|
35
|
+
for (const field of SYNC_SYSTEM_FIELDS) {
|
|
36
|
+
delete doc[field];
|
|
37
|
+
}
|
|
38
|
+
doc[SYNC_VERSION_FIELD] = 1;
|
|
39
|
+
doc[SYNC_DELETED_FIELD] = null;
|
|
40
|
+
return doc;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The content fields a delete clears: everything but the id, the system, audit and tenant fields and the scope */
|
|
44
|
+
export function tombstoneClearedFields(fields: Iterable<string>, sync: SyncDescriptor): string[] {
|
|
45
|
+
const scope = new Set(sync.scope);
|
|
46
|
+
return [...fields].filter((field) => !TOMBSTONE_KEPT.has(field) && !scope.has(field));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A new id in the model's format */
|
|
50
|
+
export function generateSyncId(format: SyncIdFormat): string {
|
|
51
|
+
return format === 'uuidv7' ? generateUuidV7() : generateId();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether a value is an id in the model's format */
|
|
55
|
+
export function isSyncId(value: unknown, format: SyncIdFormat): boolean {
|
|
56
|
+
return format === 'uuidv7' ? isUuidV7(value) : typeof value === 'string' && isWellFormedId(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A client-supplied id, checked against the model's format (protocol §3: `INVALID_ID`). Unlike an ordinary model —
|
|
61
|
+
* which replaces a malformed id with a new one — a synchronized model refuses it: the client keeps its own id and
|
|
62
|
+
* would lose track of the record.
|
|
63
|
+
*/
|
|
64
|
+
export function assertClientId(value: unknown, format: SyncIdFormat, model: string): string {
|
|
65
|
+
const id = typeof value === 'string' ? value : value?.toString?.();
|
|
66
|
+
if (format === 'objectId' && typeof id === 'string' && isWellFormedId(id)) {
|
|
67
|
+
return id.toLowerCase();
|
|
68
|
+
}
|
|
69
|
+
if (format === 'uuidv7' && isUuidV7(id)) {
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
throw new BadRequestException(
|
|
73
|
+
`${ErrorCode.INVALID_FIELD_FORMAT} [id]: ${model} takes ${format === 'uuidv7' ? 'a lowercase uuidv7' : 'a 24-character hex ObjectId'} as id`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The id an insert names, if any (`id` or `_id`) */
|
|
78
|
+
export function suppliedId(record: Record<string, any>): unknown {
|
|
79
|
+
return record.id ?? record._id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Insert with a server-generated id, generating a fresh one when it is taken (plan §3.3, E6): server ids are
|
|
84
|
+
* predictable from one observed id, so a client could occupy the next ones; a server id was never shown to anyone, so
|
|
85
|
+
* replacing it is invisible. A client-supplied id is never replaced — its duplicate is the caller's answer.
|
|
86
|
+
*/
|
|
87
|
+
export async function insertWithServerId<R>(
|
|
88
|
+
format: SyncIdFormat,
|
|
89
|
+
attempt: (id: string) => Promise<R>,
|
|
90
|
+
isIdTaken: (error: unknown) => boolean,
|
|
91
|
+
): Promise<R> {
|
|
92
|
+
for (let tries = 1; ; tries++) {
|
|
93
|
+
try {
|
|
94
|
+
return await attempt(generateSyncId(format));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (tries >= SERVER_ID_ATTEMPTS || !isIdTaken(error)) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A conditional change (`expectedVersion`) whose record has another version: the caller resolves and retries */
|
|
104
|
+
export class VersionConflictError extends ConflictException {
|
|
105
|
+
override readonly name = 'VersionConflictError';
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
readonly model: string,
|
|
109
|
+
readonly id: string,
|
|
110
|
+
) {
|
|
111
|
+
super(`${ErrorCode.VERSION_CONFLICT}: ${model} ${id} was changed in the meantime`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether `expectedVersion` is a usable version number */
|
|
116
|
+
export function assertExpectedVersion(value: unknown): number {
|
|
117
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
118
|
+
throw new BadRequestException(`${ErrorCode.INVALID_FIELD_FORMAT} [expectedVersion]: a positive integer`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|