@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,7 @@ import type { FieldDescriptor, IndexDescriptor, ModelDescriptor } from '../../mo
|
|
|
2
2
|
import { UnsupportedQueryError } from '../sql-errors.js';
|
|
3
3
|
import { columnName, quoteIdentifier, SqlLiterals, sqlLiteral } from '../sql-naming.js';
|
|
4
4
|
import { schemaHash, shortName, tableName } from '../sql-schema.shared.js';
|
|
5
|
+
import { isLiveFilter, SYNC_SEQUENCE_FIELD } from '../../sync-model.js';
|
|
5
6
|
import { compilePostgresWhere } from './postgres-where.compiler.js';
|
|
6
7
|
|
|
7
8
|
export { LOCK_MODEL, shortName, tableName } from '../sql-schema.shared.js';
|
|
@@ -23,6 +24,15 @@ export { LOCK_MODEL, shortName, tableName } from '../sql-schema.shared.js';
|
|
|
23
24
|
/** Any string item that starts with `s_` after leading whitespace, case-insensitive (`looksLikeSystemRole`) */
|
|
24
25
|
const SYSTEM_ROLE_PATH = '$[*] ? (@.type() == "string" && @ like_regex "^\\\\s*s_" flag "i")';
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Bumped whenever the change-order trigger (`lt_sync_stamp`) changes, so the migrator replaces the function and
|
|
29
|
+
* every table's trigger — the trigger's name carries the version, because a trigger has no comment the catalog query
|
|
30
|
+
* could read alongside the table
|
|
31
|
+
*/
|
|
32
|
+
export const SYNC_TRIGGER_VERSION = 'lt:sync:v1';
|
|
33
|
+
export const SYNC_STAMP_FUNCTION = 'lt_sync_stamp';
|
|
34
|
+
export const SYNC_TRIGGER = 'lt_sync_stamp_v1';
|
|
35
|
+
|
|
26
36
|
/** Bumped whenever the generated guard function or policies change, so the migrator replaces them */
|
|
27
37
|
export const RLS_VERSION = 'lt:tenant:v1';
|
|
28
38
|
export const TENANT_FILTER_FUNCTION = 'lt_tenant_filter';
|
|
@@ -41,6 +51,12 @@ export interface DesiredColumn {
|
|
|
41
51
|
renamedFrom?: string;
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
/** The objects that number a synchronized model's changes (offline-sync plan §3.4) */
|
|
55
|
+
export interface DesiredSync {
|
|
56
|
+
/** The table's sequence: `nextval()` under the model's advisory lock gives commit order */
|
|
57
|
+
sequence: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
export interface DesiredIndex {
|
|
45
61
|
/** The CREATE statement */
|
|
46
62
|
create: string;
|
|
@@ -61,6 +77,8 @@ export interface DesiredTable {
|
|
|
61
77
|
name: string;
|
|
62
78
|
/** What the model declares but the table cannot carry (text indexes, …), reported once */
|
|
63
79
|
notices: string[];
|
|
80
|
+
/** Present for a synchronized model: its change-order sequence and trigger */
|
|
81
|
+
sync?: DesiredSync;
|
|
64
82
|
/** Row-level security on `tenant_id` */
|
|
65
83
|
tenantIsolated: boolean;
|
|
66
84
|
}
|
|
@@ -119,7 +137,13 @@ export function desiredTable(model: ModelDescriptor, options: PostgresSchemaOpti
|
|
|
119
137
|
indexes.push(desiredIndex(model, name, { fields: { tenantId: 1 }, options: {} }) as DesiredIndex);
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
|
|
140
|
+
const sync = model.sync && model.fields[SYNC_SEQUENCE_FIELD] ? { sequence: syncSequenceName(name) } : undefined;
|
|
141
|
+
return { checks, columns, indexes, model, name, notices, ...(sync ? { sync } : {}), tenantIsolated };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The change-order sequence of a synchronized model's table */
|
|
145
|
+
export function syncSequenceName(table: string): string {
|
|
146
|
+
return shortName('lt_sync', table, ['seq']);
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
function desiredColumn(field: FieldDescriptor): DesiredColumn {
|
|
@@ -223,7 +247,9 @@ function desiredIndex(model: ModelDescriptor, table: string, index: IndexDescrip
|
|
|
223
247
|
const using = gin.length
|
|
224
248
|
? ` USING gin (${gin[0].expression} jsonb_path_ops)`
|
|
225
249
|
: ` (${parts.map((part) => part.sql).join(', ')})`;
|
|
226
|
-
|
|
250
|
+
// A partial index admits NULL repeatedly unless told otherwise. That is harmless where the condition excludes NULL
|
|
251
|
+
// (sparse), but a synchronized model's live-record condition keeps MongoDB's rule: a second live NULL collides
|
|
252
|
+
const nulls = unique && (!conditions.length || (!sparse && isLiveFilter(partial))) ? ' NULLS NOT DISTINCT' : '';
|
|
227
253
|
const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
|
|
228
254
|
return {
|
|
229
255
|
create: `CREATE ${unique ? 'UNIQUE ' : ''}INDEX ${quoteIdentifier(name)} ON ${quoteIdentifier(table)}${using}${nulls}${where}`,
|
|
@@ -280,6 +306,52 @@ export function grantStatement(table: string, appRole: string): string {
|
|
|
280
306
|
return `GRANT SELECT, INSERT, UPDATE, DELETE ON ${quoteIdentifier(table)} TO ${quoteIdentifier(appRole)}`;
|
|
281
307
|
}
|
|
282
308
|
|
|
309
|
+
/**
|
|
310
|
+
* The trigger function that numbers a synchronized model's writes (offline-sync plan §3.4, C1d; DEV-3900 measured
|
|
311
|
+
* the advisory lock against a counter row). A BEFORE ROW trigger runs after the row is locked, so a write takes its
|
|
312
|
+
* row first and the order last; the transaction-scoped advisory lock is released only after the commit is visible,
|
|
313
|
+
* so sequence order is commit order, and `nextval()` leaves no row version a long snapshot could pile up.
|
|
314
|
+
*
|
|
315
|
+
* A value the statement sets itself is kept: the store's multi-row writes lock their rows, take their numbers and set
|
|
316
|
+
* them explicitly in their final statement.
|
|
317
|
+
*/
|
|
318
|
+
export function syncFunctionStatements(): string[] {
|
|
319
|
+
const fn = quoteIdentifier(SYNC_STAMP_FUNCTION);
|
|
320
|
+
return [
|
|
321
|
+
`CREATE OR REPLACE FUNCTION ${fn}() RETURNS trigger
|
|
322
|
+
LANGUAGE plpgsql SET search_path = pg_catalog AS $lt$
|
|
323
|
+
BEGIN
|
|
324
|
+
IF TG_OP = 'INSERT' THEN
|
|
325
|
+
IF NEW.sync_seq IS NOT NULL THEN
|
|
326
|
+
RETURN NEW;
|
|
327
|
+
END IF;
|
|
328
|
+
ELSIF NEW.sync_seq IS DISTINCT FROM OLD.sync_seq THEN
|
|
329
|
+
RETURN NEW;
|
|
330
|
+
END IF;
|
|
331
|
+
PERFORM pg_advisory_xact_lock(hashtext('lenne.tech:sync:' || TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME));
|
|
332
|
+
NEW.sync_seq := nextval(format('%I.%I', TG_TABLE_SCHEMA, TG_ARGV[0])::regclass);
|
|
333
|
+
RETURN NEW;
|
|
334
|
+
END
|
|
335
|
+
$lt$`,
|
|
336
|
+
`COMMENT ON FUNCTION ${fn}() IS ${sqlLiteral(SYNC_TRIGGER_VERSION)}`,
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A synchronized table's sequence, the application role's use of it, the trigger, and the numbering of rows written
|
|
342
|
+
* before the table was synchronized (in the order of their ids, i.e. of their creation)
|
|
343
|
+
*/
|
|
344
|
+
export function syncTableStatements(table: string, sync: DesiredSync, appRole: string): string[] {
|
|
345
|
+
const quoted = quoteIdentifier(table);
|
|
346
|
+
const sequence = quoteIdentifier(sync.sequence);
|
|
347
|
+
return [
|
|
348
|
+
`CREATE SEQUENCE IF NOT EXISTS ${sequence}`,
|
|
349
|
+
`GRANT USAGE ON SEQUENCE ${sequence} TO ${quoteIdentifier(appRole)}`,
|
|
350
|
+
`CREATE TRIGGER ${quoteIdentifier(SYNC_TRIGGER)} BEFORE INSERT OR UPDATE ON ${quoted} FOR EACH ROW EXECUTE FUNCTION ${quoteIdentifier(SYNC_STAMP_FUNCTION)}(${sqlLiteral(sync.sequence)})`,
|
|
351
|
+
`UPDATE ${quoted} AS t SET "sync_seq" = n.seq FROM (SELECT "id", nextval(${sqlLiteral(sync.sequence)}::regclass) AS seq FROM (SELECT "id" FROM ${quoted} WHERE "sync_seq" IS NULL ORDER BY "id") AS o) AS n WHERE t."id" = n."id"`,
|
|
352
|
+
];
|
|
353
|
+
}
|
|
354
|
+
|
|
283
355
|
/**
|
|
284
356
|
* The guard every tenant policy calls. It reads the scope the store wrote into the transaction (`sql-session.ts`)
|
|
285
357
|
* and RAISES `42501` without one, so a query on a tenant table outside the framework's context — a forgotten scope,
|
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
grantStatement,
|
|
19
19
|
type PostgresSchemaOptions,
|
|
20
20
|
RLS_VERSION,
|
|
21
|
+
SYNC_STAMP_FUNCTION,
|
|
22
|
+
SYNC_TRIGGER,
|
|
23
|
+
SYNC_TRIGGER_VERSION,
|
|
24
|
+
syncFunctionStatements,
|
|
25
|
+
syncTableStatements,
|
|
21
26
|
TENANT_FILTER_FUNCTION,
|
|
22
27
|
TENANT_POLICY,
|
|
23
28
|
tenantFilterFunctionStatements,
|
|
@@ -63,10 +68,15 @@ interface CatalogTable {
|
|
|
63
68
|
granted: boolean;
|
|
64
69
|
policyVersion: null | string;
|
|
65
70
|
rls: boolean;
|
|
71
|
+
/** Change-order triggers (`lt_sync_*`) on the table */
|
|
72
|
+
syncTriggers: Set<string>;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
interface Catalog {
|
|
69
76
|
functionVersion: null | string;
|
|
77
|
+
/** Sequences of synchronized tables → whether the application role may use them */
|
|
78
|
+
sequences: Map<string, boolean>;
|
|
79
|
+
syncFunctionVersion: null | string;
|
|
70
80
|
schema: string;
|
|
71
81
|
/** Whether the application role may use the schema */
|
|
72
82
|
schemaUsage: boolean;
|
|
@@ -124,9 +134,18 @@ export async function planPostgresSchema(
|
|
|
124
134
|
run,
|
|
125
135
|
tables.map((table) => table.name),
|
|
126
136
|
options.appRole,
|
|
137
|
+
tables.flatMap((table) => (table.sync ? [table.sync.sequence] : [])),
|
|
127
138
|
);
|
|
128
139
|
const changes: SchemaChange[] = [];
|
|
129
140
|
|
|
141
|
+
if (tables.some((table) => table.sync) && catalog.syncFunctionVersion !== SYNC_TRIGGER_VERSION) {
|
|
142
|
+
changes.push({
|
|
143
|
+
description: `${catalog.syncFunctionVersion ? 'update' : 'create'} the change-order function ${SYNC_STAMP_FUNCTION}()`,
|
|
144
|
+
kind: 'automatic',
|
|
145
|
+
statements: syncFunctionStatements(),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
if (tables.some((table) => table.tenantIsolated) && catalog.functionVersion !== RLS_VERSION) {
|
|
131
150
|
changes.push({
|
|
132
151
|
description: `${catalog.functionVersion ? 'update' : 'create'} the tenant guard function ${TENANT_FILTER_FUNCTION}()`,
|
|
@@ -146,7 +165,9 @@ export async function planPostgresSchema(
|
|
|
146
165
|
changes.push({ description: notice, kind: 'notice', statements: [], table: table.name });
|
|
147
166
|
}
|
|
148
167
|
const existing = catalog.tables.get(table.name);
|
|
149
|
-
changes.push(
|
|
168
|
+
changes.push(
|
|
169
|
+
...(existing ? planExistingTable(table, existing, catalog.sequences, options) : planNewTable(table, options)),
|
|
170
|
+
);
|
|
150
171
|
}
|
|
151
172
|
return changes;
|
|
152
173
|
}
|
|
@@ -161,6 +182,7 @@ function planNewTable(table: DesiredTable, options: PostgresSchemaOptions): Sche
|
|
|
161
182
|
...table.indexes.map((index) => index.create),
|
|
162
183
|
...(table.tenantIsolated ? tenantPolicyStatements(table.name) : []),
|
|
163
184
|
grantStatement(table.name, options.appRole),
|
|
185
|
+
...(table.sync ? syncTableStatements(table.name, table.sync, options.appRole) : []),
|
|
164
186
|
],
|
|
165
187
|
table: table.name,
|
|
166
188
|
},
|
|
@@ -170,6 +192,7 @@ function planNewTable(table: DesiredTable, options: PostgresSchemaOptions): Sche
|
|
|
170
192
|
function planExistingTable(
|
|
171
193
|
table: DesiredTable,
|
|
172
194
|
existing: CatalogTable,
|
|
195
|
+
sequences: Map<string, boolean>,
|
|
173
196
|
options: PostgresSchemaOptions,
|
|
174
197
|
): SchemaChange[] {
|
|
175
198
|
const changes: SchemaChange[] = [];
|
|
@@ -249,7 +272,7 @@ function planExistingTable(
|
|
|
249
272
|
description: `create ${index.unique ? 'unique ' : ''}index ${index.name} on ${name}`,
|
|
250
273
|
// A unique index can fail on the rows already stored, and it changes what may be written: a reviewed decision
|
|
251
274
|
kind: index.unique ? 'migration' : 'automatic',
|
|
252
|
-
statements: [index.create],
|
|
275
|
+
statements: index.unique ? liveUniqueMigration(table, existing, index, wanted) : [index.create],
|
|
253
276
|
table: name,
|
|
254
277
|
});
|
|
255
278
|
}
|
|
@@ -294,9 +317,97 @@ function planExistingTable(
|
|
|
294
317
|
table: name,
|
|
295
318
|
});
|
|
296
319
|
}
|
|
320
|
+
changes.push(...planSync(table, existing, sequences, options));
|
|
297
321
|
return changes;
|
|
298
322
|
}
|
|
299
323
|
|
|
324
|
+
/**
|
|
325
|
+
* A synchronized table's change order: the trigger of the current version, its sequence and the application role's
|
|
326
|
+
* use of it. A trigger of an earlier version is replaced; a table that is no longer synchronized keeps its trigger
|
|
327
|
+
* (and its numbering) until a migration removes it
|
|
328
|
+
*/
|
|
329
|
+
function planSync(
|
|
330
|
+
table: DesiredTable,
|
|
331
|
+
existing: CatalogTable,
|
|
332
|
+
sequences: Map<string, boolean>,
|
|
333
|
+
options: PostgresSchemaOptions,
|
|
334
|
+
): SchemaChange[] {
|
|
335
|
+
const name = table.name;
|
|
336
|
+
const quoted = quoteIdentifier(name);
|
|
337
|
+
if (!table.sync) {
|
|
338
|
+
return existing.syncTriggers.size
|
|
339
|
+
? [
|
|
340
|
+
{
|
|
341
|
+
description: `${name} is no longer synchronized; its change-order trigger is kept until a migration drops it`,
|
|
342
|
+
kind: 'notice',
|
|
343
|
+
statements: [],
|
|
344
|
+
table: name,
|
|
345
|
+
},
|
|
346
|
+
]
|
|
347
|
+
: [];
|
|
348
|
+
}
|
|
349
|
+
if (!existing.syncTriggers.has(SYNC_TRIGGER)) {
|
|
350
|
+
const outdated = [...existing.syncTriggers].map(
|
|
351
|
+
(trigger) => `DROP TRIGGER IF EXISTS ${quoteIdentifier(trigger)} ON ${quoted}`,
|
|
352
|
+
);
|
|
353
|
+
return [
|
|
354
|
+
{
|
|
355
|
+
description: `${outdated.length ? 'replace' : 'add'} the change-order trigger of ${name}`,
|
|
356
|
+
kind: 'automatic',
|
|
357
|
+
statements: [...outdated, ...syncTableStatements(name, table.sync, options.appRole)],
|
|
358
|
+
table: name,
|
|
359
|
+
},
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
if (!sequences.get(table.sync.sequence)) {
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
description: `let ${options.appRole} use the change-order sequence of ${name}`,
|
|
366
|
+
kind: 'automatic',
|
|
367
|
+
statements: syncTableStatements(name, table.sync, options.appRole).slice(0, 2),
|
|
368
|
+
table: name,
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The statements of a new unique index's migration. A synchronized model's index reads `deleted_at`: when the table
|
|
377
|
+
* does not have it yet (an existing model that became synchronized), the column comes first — the boot that refuses the
|
|
378
|
+
* index never adds it. A framework unique index over the same fields that the model no longer declares is the one the
|
|
379
|
+
* live-only index replaces: it goes last, because a tombstone (which keeps no values) would collide on it
|
|
380
|
+
*/
|
|
381
|
+
function liveUniqueMigration(
|
|
382
|
+
table: DesiredTable,
|
|
383
|
+
existing: CatalogTable,
|
|
384
|
+
index: DesiredTable['indexes'][number],
|
|
385
|
+
wanted: Set<string>,
|
|
386
|
+
): string[] {
|
|
387
|
+
const statements: string[] = [];
|
|
388
|
+
const deletedAt = table.columns.find((column) => column.name === 'deleted_at');
|
|
389
|
+
if (table.sync && deletedAt && !existing.columns.has('deleted_at') && index.create.includes('"deleted_at"')) {
|
|
390
|
+
statements.push(
|
|
391
|
+
`ALTER TABLE ${quoteIdentifier(table.name)} ADD COLUMN ${columnDefinition({ ...deletedAt, primaryKey: false })}`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
statements.push(index.create);
|
|
395
|
+
if (table.sync) {
|
|
396
|
+
const stem = indexStem(index.name);
|
|
397
|
+
for (const [old, unique] of existing.indexes) {
|
|
398
|
+
if (unique && !wanted.has(old) && indexStem(old) === stem) {
|
|
399
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(old)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return statements;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** A framework index name without its definition hash (`ux_<table>__<fields>_<hash>`) */
|
|
407
|
+
function indexStem(name: string): string {
|
|
408
|
+
return name.replace(/_[0-9a-f]{8}$/, '');
|
|
409
|
+
}
|
|
410
|
+
|
|
300
411
|
function sameType(
|
|
301
412
|
column: DesiredTable['columns'][number],
|
|
302
413
|
current: { collation: null | string; type: string },
|
|
@@ -321,7 +432,7 @@ function typeChange(
|
|
|
321
432
|
};
|
|
322
433
|
}
|
|
323
434
|
|
|
324
|
-
async function readCatalog(run: Run, tables: string[], appRole: string): Promise<Catalog> {
|
|
435
|
+
async function readCatalog(run: Run, tables: string[], appRole: string, sequences: string[]): Promise<Catalog> {
|
|
325
436
|
// One query after the other: inside the migration transaction they share a single connection
|
|
326
437
|
const schemaResult = await run({
|
|
327
438
|
params: [appRole],
|
|
@@ -359,14 +470,31 @@ async function readCatalog(run: Run, tables: string[], appRole: string): Promise
|
|
|
359
470
|
WHERE n.nspname = current_schema() AND c.relname = ANY($1) AND k.contype = 'c'`,
|
|
360
471
|
});
|
|
361
472
|
const functionResult = await run({
|
|
362
|
-
params: [TENANT_FILTER_FUNCTION],
|
|
363
|
-
text: `SELECT obj_description(p.oid, 'pg_proc') AS version
|
|
473
|
+
params: [[TENANT_FILTER_FUNCTION, SYNC_STAMP_FUNCTION]],
|
|
474
|
+
text: `SELECT p.proname AS name, obj_description(p.oid, 'pg_proc') AS version
|
|
364
475
|
FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
365
|
-
WHERE n.nspname = current_schema() AND p.proname = $1 AND p.pronargs = 0`,
|
|
476
|
+
WHERE n.nspname = current_schema() AND p.proname = ANY($1) AND p.pronargs = 0`,
|
|
366
477
|
});
|
|
478
|
+
const triggerResult = await run({
|
|
479
|
+
params: [tables],
|
|
480
|
+
text: `SELECT c.relname AS table, t.tgname AS name
|
|
481
|
+
FROM pg_trigger t JOIN pg_class c ON c.oid = t.tgrelid JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
482
|
+
WHERE n.nspname = current_schema() AND c.relname = ANY($1) AND NOT t.tgisinternal AND t.tgname LIKE 'lt\\_sync\\_%'`,
|
|
483
|
+
});
|
|
484
|
+
const sequenceResult = sequences.length
|
|
485
|
+
? await run({
|
|
486
|
+
params: [sequences, appRole],
|
|
487
|
+
text: `SELECT c.relname AS name, has_sequence_privilege($2, c.oid, 'USAGE') AS usage
|
|
488
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
489
|
+
WHERE n.nspname = current_schema() AND c.relkind = 'S' AND c.relname = ANY($1)`,
|
|
490
|
+
})
|
|
491
|
+
: { rowCount: 0, rows: [] };
|
|
492
|
+
const versionOf = (fn: string) => functionResult.rows.find((row) => row.name === fn)?.version ?? null;
|
|
367
493
|
|
|
368
494
|
const catalog: Catalog = {
|
|
369
|
-
functionVersion:
|
|
495
|
+
functionVersion: versionOf(TENANT_FILTER_FUNCTION),
|
|
496
|
+
sequences: new Map(sequenceResult.rows.map((row: { name: string; usage: boolean }) => [row.name, row.usage])),
|
|
497
|
+
syncFunctionVersion: versionOf(SYNC_STAMP_FUNCTION),
|
|
370
498
|
schema: schemaResult.rows[0].schema,
|
|
371
499
|
schemaUsage: schemaResult.rows[0].usage,
|
|
372
500
|
tables: new Map(),
|
|
@@ -380,8 +508,12 @@ async function readCatalog(run: Run, tables: string[], appRole: string): Promise
|
|
|
380
508
|
indexes: new Map(),
|
|
381
509
|
policyVersion: row.policy_version ?? null,
|
|
382
510
|
rls: row.rls,
|
|
511
|
+
syncTriggers: new Set(),
|
|
383
512
|
});
|
|
384
513
|
}
|
|
514
|
+
for (const row of triggerResult.rows) {
|
|
515
|
+
catalog.tables.get(row.table)?.syncTriggers.add(row.name);
|
|
516
|
+
}
|
|
385
517
|
for (const row of columnResult.rows) {
|
|
386
518
|
catalog.tables.get(row.table)?.columns.set(row.name, { collation: row.collation ?? null, type: row.type });
|
|
387
519
|
}
|