@trokky/trokky 2.0.3
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/dist/adapters/filesystem/filesystem-adapter.d.ts +80 -0
- package/dist/adapters/filesystem/filesystem-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem/filesystem-adapter.js +1370 -0
- package/dist/adapters/filesystem/filesystem-adapter.js.map +1 -0
- package/dist/adapters/filesystem/index.d.ts +29 -0
- package/dist/adapters/filesystem/index.d.ts.map +1 -0
- package/dist/adapters/filesystem/index.js +31 -0
- package/dist/adapters/filesystem/index.js.map +1 -0
- package/dist/adapters/filesystem/types.d.ts +111 -0
- package/dist/adapters/filesystem/types.d.ts.map +1 -0
- package/dist/adapters/filesystem/types.js +2 -0
- package/dist/adapters/filesystem/types.js.map +1 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts +111 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.js +1363 -0
- package/dist/adapters/filesystem-data/filesystem-data-adapter.js.map +1 -0
- package/dist/adapters/filesystem-data/index.d.ts +3 -0
- package/dist/adapters/filesystem-data/index.d.ts.map +1 -0
- package/dist/adapters/filesystem-data/index.js +13 -0
- package/dist/adapters/filesystem-data/index.js.map +1 -0
- package/dist/adapters/filesystem-data/types.d.ts +147 -0
- package/dist/adapters/filesystem-data/types.d.ts.map +1 -0
- package/dist/adapters/filesystem-data/types.js +2 -0
- package/dist/adapters/filesystem-data/types.js.map +1 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts +32 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.d.ts.map +1 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.js +639 -0
- package/dist/adapters/filesystem-media/filesystem-media-adapter.js.map +1 -0
- package/dist/adapters/filesystem-media/index.d.ts +3 -0
- package/dist/adapters/filesystem-media/index.d.ts.map +1 -0
- package/dist/adapters/filesystem-media/index.js +13 -0
- package/dist/adapters/filesystem-media/index.js.map +1 -0
- package/dist/adapters/filesystem-media/types.d.ts +76 -0
- package/dist/adapters/filesystem-media/types.d.ts.map +1 -0
- package/dist/adapters/filesystem-media/types.js +2 -0
- package/dist/adapters/filesystem-media/types.js.map +1 -0
- package/dist/adapters/postgres-data/index.d.ts +3 -0
- package/dist/adapters/postgres-data/index.d.ts.map +1 -0
- package/dist/adapters/postgres-data/index.js +13 -0
- package/dist/adapters/postgres-data/index.js.map +1 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.d.ts +154 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.d.ts.map +1 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.js +1432 -0
- package/dist/adapters/postgres-data/postgres-data-adapter.js.map +1 -0
- package/dist/adapters/postgres-data/types.d.ts +139 -0
- package/dist/adapters/postgres-data/types.d.ts.map +1 -0
- package/dist/adapters/postgres-data/types.js +2 -0
- package/dist/adapters/postgres-data/types.js.map +1 -0
- package/dist/core/adapters/registry.d.ts +79 -0
- package/dist/core/adapters/registry.d.ts.map +1 -0
- package/dist/core/adapters/registry.js +149 -0
- package/dist/core/adapters/registry.js.map +1 -0
- package/dist/core/core/engine.d.ts +551 -0
- package/dist/core/core/engine.d.ts.map +1 -0
- package/dist/core/core/engine.js +1074 -0
- package/dist/core/core/engine.js.map +1 -0
- package/dist/core/crypto/adapter.d.ts +63 -0
- package/dist/core/crypto/adapter.d.ts.map +1 -0
- package/dist/core/crypto/adapter.js +63 -0
- package/dist/core/crypto/adapter.js.map +1 -0
- package/dist/core/crypto/fallback-adapter.d.ts +23 -0
- package/dist/core/crypto/fallback-adapter.d.ts.map +1 -0
- package/dist/core/crypto/fallback-adapter.js +195 -0
- package/dist/core/crypto/fallback-adapter.js.map +1 -0
- package/dist/core/crypto/node-adapter.d.ts +20 -0
- package/dist/core/crypto/node-adapter.d.ts.map +1 -0
- package/dist/core/crypto/node-adapter.js +85 -0
- package/dist/core/crypto/node-adapter.js.map +1 -0
- package/dist/core/crypto/password-hash.d.ts +73 -0
- package/dist/core/crypto/password-hash.d.ts.map +1 -0
- package/dist/core/crypto/password-hash.js +244 -0
- package/dist/core/crypto/password-hash.js.map +1 -0
- package/dist/core/crypto/webcrypto-adapter.d.ts +24 -0
- package/dist/core/crypto/webcrypto-adapter.d.ts.map +1 -0
- package/dist/core/crypto/webcrypto-adapter.js +158 -0
- package/dist/core/crypto/webcrypto-adapter.js.map +1 -0
- package/dist/core/errors/index.d.ts +30 -0
- package/dist/core/errors/index.d.ts.map +1 -0
- package/dist/core/errors/index.js +61 -0
- package/dist/core/errors/index.js.map +1 -0
- package/dist/core/events/event-bus.d.ts +121 -0
- package/dist/core/events/event-bus.d.ts.map +1 -0
- package/dist/core/events/event-bus.js +631 -0
- package/dist/core/events/event-bus.js.map +1 -0
- package/dist/core/events/index.d.ts +17 -0
- package/dist/core/events/index.d.ts.map +1 -0
- package/dist/core/events/index.js +22 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/events/memory-storage.d.ts +57 -0
- package/dist/core/events/memory-storage.d.ts.map +1 -0
- package/dist/core/events/memory-storage.js +175 -0
- package/dist/core/events/memory-storage.js.map +1 -0
- package/dist/core/events/types.d.ts +434 -0
- package/dist/core/events/types.d.ts.map +1 -0
- package/dist/core/events/types.js +9 -0
- package/dist/core/events/types.js.map +1 -0
- package/dist/core/events/utils.d.ts +134 -0
- package/dist/core/events/utils.d.ts.map +1 -0
- package/dist/core/events/utils.js +333 -0
- package/dist/core/events/utils.js.map +1 -0
- package/dist/core/fields/conditional.d.ts +115 -0
- package/dist/core/fields/conditional.d.ts.map +1 -0
- package/dist/core/fields/conditional.js +296 -0
- package/dist/core/fields/conditional.js.map +1 -0
- package/dist/core/fields/field-type.d.ts +218 -0
- package/dist/core/fields/field-type.d.ts.map +1 -0
- package/dist/core/fields/field-type.js +17 -0
- package/dist/core/fields/field-type.js.map +1 -0
- package/dist/core/fields/helpers.d.ts +86 -0
- package/dist/core/fields/helpers.d.ts.map +1 -0
- package/dist/core/fields/helpers.js +260 -0
- package/dist/core/fields/helpers.js.map +1 -0
- package/dist/core/fields/index.d.ts +5 -0
- package/dist/core/fields/index.d.ts.map +1 -0
- package/dist/core/fields/index.js +6 -0
- package/dist/core/fields/index.js.map +1 -0
- package/dist/core/fields/registry.d.ts +78 -0
- package/dist/core/fields/registry.d.ts.map +1 -0
- package/dist/core/fields/registry.js +185 -0
- package/dist/core/fields/registry.js.map +1 -0
- package/dist/core/index.d.ts +39 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +57 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/media/image-processor.d.ts +24 -0
- package/dist/core/media/image-processor.d.ts.map +1 -0
- package/dist/core/media/image-processor.js +69 -0
- package/dist/core/media/image-processor.js.map +1 -0
- package/dist/core/media/processors/cloudflare-images.d.ts +83 -0
- package/dist/core/media/processors/cloudflare-images.d.ts.map +1 -0
- package/dist/core/media/processors/cloudflare-images.js +241 -0
- package/dist/core/media/processors/cloudflare-images.js.map +1 -0
- package/dist/core/media/processors/cloudflare-transform-store.d.ts +127 -0
- package/dist/core/media/processors/cloudflare-transform-store.d.ts.map +1 -0
- package/dist/core/media/processors/cloudflare-transform-store.js +237 -0
- package/dist/core/media/processors/cloudflare-transform-store.js.map +1 -0
- package/dist/core/media/processors/cloudflare-transformations.d.ts +81 -0
- package/dist/core/media/processors/cloudflare-transformations.d.ts.map +1 -0
- package/dist/core/media/processors/cloudflare-transformations.js +144 -0
- package/dist/core/media/processors/cloudflare-transformations.js.map +1 -0
- package/dist/core/media/processors/index.d.ts +9 -0
- package/dist/core/media/processors/index.d.ts.map +1 -0
- package/dist/core/media/processors/index.js +13 -0
- package/dist/core/media/processors/index.js.map +1 -0
- package/dist/core/media/processors/noop.d.ts +22 -0
- package/dist/core/media/processors/noop.d.ts.map +1 -0
- package/dist/core/media/processors/noop.js +38 -0
- package/dist/core/media/processors/noop.js.map +1 -0
- package/dist/core/media/processors/sharp.d.ts +30 -0
- package/dist/core/media/processors/sharp.d.ts.map +1 -0
- package/dist/core/media/processors/sharp.js +126 -0
- package/dist/core/media/processors/sharp.js.map +1 -0
- package/dist/core/media/processors/types.d.ts +67 -0
- package/dist/core/media/processors/types.d.ts.map +1 -0
- package/dist/core/media/processors/types.js +42 -0
- package/dist/core/media/processors/types.js.map +1 -0
- package/dist/core/media/processors/workers-images.d.ts +112 -0
- package/dist/core/media/processors/workers-images.d.ts.map +1 -0
- package/dist/core/media/processors/workers-images.js +257 -0
- package/dist/core/media/processors/workers-images.js.map +1 -0
- package/dist/core/schema/registry.d.ts +41 -0
- package/dist/core/schema/registry.d.ts.map +1 -0
- package/dist/core/schema/registry.js +248 -0
- package/dist/core/schema/registry.js.map +1 -0
- package/dist/core/schema/singleton.d.ts +107 -0
- package/dist/core/schema/singleton.d.ts.map +1 -0
- package/dist/core/schema/singleton.js +180 -0
- package/dist/core/schema/singleton.js.map +1 -0
- package/dist/core/security/auth.d.ts +120 -0
- package/dist/core/security/auth.d.ts.map +1 -0
- package/dist/core/security/auth.js +274 -0
- package/dist/core/security/auth.js.map +1 -0
- package/dist/core/security/captcha/index.d.ts +21 -0
- package/dist/core/security/captcha/index.d.ts.map +1 -0
- package/dist/core/security/captcha/index.js +34 -0
- package/dist/core/security/captcha/index.js.map +1 -0
- package/dist/core/security/captcha/recaptcha.d.ts +41 -0
- package/dist/core/security/captcha/recaptcha.d.ts.map +1 -0
- package/dist/core/security/captcha/recaptcha.js +89 -0
- package/dist/core/security/captcha/recaptcha.js.map +1 -0
- package/dist/core/security/captcha/turnstile.d.ts +41 -0
- package/dist/core/security/captcha/turnstile.d.ts.map +1 -0
- package/dist/core/security/captcha/turnstile.js +90 -0
- package/dist/core/security/captcha/turnstile.js.map +1 -0
- package/dist/core/security/captcha/types.d.ts +66 -0
- package/dist/core/security/captcha/types.d.ts.map +1 -0
- package/dist/core/security/captcha/types.js +7 -0
- package/dist/core/security/captcha/types.js.map +1 -0
- package/dist/core/security/mfa/email-otp.d.ts +91 -0
- package/dist/core/security/mfa/email-otp.d.ts.map +1 -0
- package/dist/core/security/mfa/email-otp.js +264 -0
- package/dist/core/security/mfa/email-otp.js.map +1 -0
- package/dist/core/security/mfa/index.d.ts +8 -0
- package/dist/core/security/mfa/index.d.ts.map +1 -0
- package/dist/core/security/mfa/index.js +8 -0
- package/dist/core/security/mfa/index.js.map +1 -0
- package/dist/core/security/mfa/totp.d.ts +87 -0
- package/dist/core/security/mfa/totp.d.ts.map +1 -0
- package/dist/core/security/mfa/totp.js +293 -0
- package/dist/core/security/mfa/totp.js.map +1 -0
- package/dist/core/security/middleware.d.ts +102 -0
- package/dist/core/security/middleware.d.ts.map +1 -0
- package/dist/core/security/middleware.js +261 -0
- package/dist/core/security/middleware.js.map +1 -0
- package/dist/core/security/oauth/google.d.ts +76 -0
- package/dist/core/security/oauth/google.d.ts.map +1 -0
- package/dist/core/security/oauth/google.js +154 -0
- package/dist/core/security/oauth/google.js.map +1 -0
- package/dist/core/security/oauth/index.d.ts +8 -0
- package/dist/core/security/oauth/index.d.ts.map +1 -0
- package/dist/core/security/oauth/index.js +7 -0
- package/dist/core/security/oauth/index.js.map +1 -0
- package/dist/core/security/oauth2/authorization-server.d.ts +195 -0
- package/dist/core/security/oauth2/authorization-server.d.ts.map +1 -0
- package/dist/core/security/oauth2/authorization-server.js +569 -0
- package/dist/core/security/oauth2/authorization-server.js.map +1 -0
- package/dist/core/security/oauth2/index.d.ts +9 -0
- package/dist/core/security/oauth2/index.d.ts.map +1 -0
- package/dist/core/security/oauth2/index.js +9 -0
- package/dist/core/security/oauth2/index.js.map +1 -0
- package/dist/core/security/rate-limiter.d.ts +15 -0
- package/dist/core/security/rate-limiter.d.ts.map +1 -0
- package/dist/core/security/rate-limiter.js +48 -0
- package/dist/core/security/rate-limiter.js.map +1 -0
- package/dist/core/security/validation.d.ts +20 -0
- package/dist/core/security/validation.d.ts.map +1 -0
- package/dist/core/security/validation.js +273 -0
- package/dist/core/security/validation.js.map +1 -0
- package/dist/core/services/auth-service.d.ts +128 -0
- package/dist/core/services/auth-service.d.ts.map +1 -0
- package/dist/core/services/auth-service.js +530 -0
- package/dist/core/services/auth-service.js.map +1 -0
- package/dist/core/services/captcha-service.d.ts +44 -0
- package/dist/core/services/captcha-service.d.ts.map +1 -0
- package/dist/core/services/captcha-service.js +82 -0
- package/dist/core/services/captcha-service.js.map +1 -0
- package/dist/core/services/document-service.d.ts +59 -0
- package/dist/core/services/document-service.d.ts.map +1 -0
- package/dist/core/services/document-service.js +269 -0
- package/dist/core/services/document-service.js.map +1 -0
- package/dist/core/services/media-service.d.ts +40 -0
- package/dist/core/services/media-service.d.ts.map +1 -0
- package/dist/core/services/media-service.js +396 -0
- package/dist/core/services/media-service.js.map +1 -0
- package/dist/core/services/mfa-service.d.ts +123 -0
- package/dist/core/services/mfa-service.d.ts.map +1 -0
- package/dist/core/services/mfa-service.js +606 -0
- package/dist/core/services/mfa-service.js.map +1 -0
- package/dist/core/services/oauth-service.d.ts +58 -0
- package/dist/core/services/oauth-service.d.ts.map +1 -0
- package/dist/core/services/oauth-service.js +253 -0
- package/dist/core/services/oauth-service.js.map +1 -0
- package/dist/core/services/oauth2-server-service.d.ts +61 -0
- package/dist/core/services/oauth2-server-service.d.ts.map +1 -0
- package/dist/core/services/oauth2-server-service.js +259 -0
- package/dist/core/services/oauth2-server-service.js.map +1 -0
- package/dist/core/services/passkey-service.d.ts +59 -0
- package/dist/core/services/passkey-service.d.ts.map +1 -0
- package/dist/core/services/passkey-service.js +238 -0
- package/dist/core/services/passkey-service.js.map +1 -0
- package/dist/core/services/token-service.d.ts +31 -0
- package/dist/core/services/token-service.d.ts.map +1 -0
- package/dist/core/services/token-service.js +132 -0
- package/dist/core/services/token-service.js.map +1 -0
- package/dist/core/services/trusted-device-service.d.ts +39 -0
- package/dist/core/services/trusted-device-service.d.ts.map +1 -0
- package/dist/core/services/trusted-device-service.js +123 -0
- package/dist/core/services/trusted-device-service.js.map +1 -0
- package/dist/core/services/user-service.d.ts +38 -0
- package/dist/core/services/user-service.d.ts.map +1 -0
- package/dist/core/services/user-service.js +265 -0
- package/dist/core/services/user-service.js.map +1 -0
- package/dist/core/studio/integration.d.ts +54 -0
- package/dist/core/studio/integration.d.ts.map +1 -0
- package/dist/core/studio/integration.js +326 -0
- package/dist/core/studio/integration.js.map +1 -0
- package/dist/core/types/index.d.ts +282 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +83 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/core/types/mail.d.ts +90 -0
- package/dist/core/types/mail.d.ts.map +1 -0
- package/dist/core/types/mail.js +7 -0
- package/dist/core/types/mail.js.map +1 -0
- package/dist/core/types/oauth2.d.ts +9 -0
- package/dist/core/types/oauth2.d.ts.map +1 -0
- package/dist/core/types/oauth2.js +7 -0
- package/dist/core/types/oauth2.js.map +1 -0
- package/dist/core/types/storage-adapters.d.ts +611 -0
- package/dist/core/types/storage-adapters.d.ts.map +1 -0
- package/dist/core/types/storage-adapters.js +14 -0
- package/dist/core/types/storage-adapters.js.map +1 -0
- package/dist/core/types/user.d.ts +10 -0
- package/dist/core/types/user.d.ts.map +1 -0
- package/dist/core/types/user.js +45 -0
- package/dist/core/types/user.js.map +1 -0
- package/dist/core/utils/id-generator.d.ts +16 -0
- package/dist/core/utils/id-generator.d.ts.map +1 -0
- package/dist/core/utils/id-generator.js +61 -0
- package/dist/core/utils/id-generator.js.map +1 -0
- package/dist/core/utils/logger.d.ts +100 -0
- package/dist/core/utils/logger.d.ts.map +1 -0
- package/dist/core/utils/logger.js +212 -0
- package/dist/core/utils/logger.js.map +1 -0
- package/dist/core/utils/reference-expander.d.ts +57 -0
- package/dist/core/utils/reference-expander.d.ts.map +1 -0
- package/dist/core/utils/reference-expander.js +192 -0
- package/dist/core/utils/reference-expander.js.map +1 -0
- package/dist/core/utils/universal-crypto.d.ts +46 -0
- package/dist/core/utils/universal-crypto.d.ts.map +1 -0
- package/dist/core/utils/universal-crypto.js +171 -0
- package/dist/core/utils/universal-crypto.js.map +1 -0
- package/dist/core/validation/validator.d.ts +11 -0
- package/dist/core/validation/validator.d.ts.map +1 -0
- package/dist/core/validation/validator.js +164 -0
- package/dist/core/validation/validator.js.map +1 -0
- package/dist/i18n/config.d.ts +4077 -0
- package/dist/i18n/config.d.ts.map +1 -0
- package/dist/i18n/config.js +116 -0
- package/dist/i18n/config.js.map +1 -0
- package/dist/i18n/hooks.d.ts +86 -0
- package/dist/i18n/hooks.d.ts.map +1 -0
- package/dist/i18n/hooks.js +122 -0
- package/dist/i18n/hooks.js.map +1 -0
- package/dist/i18n/index.d.ts +39 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +40 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/locales/en/auth.json +108 -0
- package/dist/i18n/locales/en/common.json +100 -0
- package/dist/i18n/locales/en/errors.json +55 -0
- package/dist/i18n/locales/en/fields.json +456 -0
- package/dist/i18n/locales/en/studio.json +1329 -0
- package/dist/i18n/locales/fr/auth.json +108 -0
- package/dist/i18n/locales/fr/common.json +100 -0
- package/dist/i18n/locales/fr/errors.json +55 -0
- package/dist/i18n/locales/fr/fields.json +456 -0
- package/dist/i18n/locales/fr/studio.json +1274 -0
- package/dist/i18n/provider.d.ts +45 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +49 -0
- package/dist/i18n/provider.js.map +1 -0
- package/dist/i18n/types.d.ts +8 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +7 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express/adapter.d.ts +33 -0
- package/dist/integrations/express/adapter.d.ts.map +1 -0
- package/dist/integrations/express/adapter.js +203 -0
- package/dist/integrations/express/adapter.js.map +1 -0
- package/dist/integrations/express/config.d.ts +599 -0
- package/dist/integrations/express/config.d.ts.map +1 -0
- package/dist/integrations/express/config.js +166 -0
- package/dist/integrations/express/config.js.map +1 -0
- package/dist/integrations/express/index.d.ts +10 -0
- package/dist/integrations/express/index.d.ts.map +1 -0
- package/dist/integrations/express/index.js +9 -0
- package/dist/integrations/express/index.js.map +1 -0
- package/dist/integrations/express/integration.d.ts +111 -0
- package/dist/integrations/express/integration.d.ts.map +1 -0
- package/dist/integrations/express/integration.js +730 -0
- package/dist/integrations/express/integration.js.map +1 -0
- package/dist/integrations/express/media-rewrite-middleware.d.ts +78 -0
- package/dist/integrations/express/media-rewrite-middleware.d.ts.map +1 -0
- package/dist/integrations/express/media-rewrite-middleware.js +211 -0
- package/dist/integrations/express/media-rewrite-middleware.js.map +1 -0
- package/dist/integrations/express/media-url-generator.d.ts +52 -0
- package/dist/integrations/express/media-url-generator.d.ts.map +1 -0
- package/dist/integrations/express/media-url-generator.js +107 -0
- package/dist/integrations/express/media-url-generator.js.map +1 -0
- package/dist/integrations/express/middleware.d.ts +25 -0
- package/dist/integrations/express/middleware.d.ts.map +1 -0
- package/dist/integrations/express/middleware.js +130 -0
- package/dist/integrations/express/middleware.js.map +1 -0
- package/dist/integrations/express/server.d.ts +99 -0
- package/dist/integrations/express/server.d.ts.map +1 -0
- package/dist/integrations/express/server.js +597 -0
- package/dist/integrations/express/server.js.map +1 -0
- package/dist/integrations/express/types.d.ts +106 -0
- package/dist/integrations/express/types.d.ts.map +1 -0
- package/dist/integrations/express/types.js +2 -0
- package/dist/integrations/express/types.js.map +1 -0
- package/dist/mail/adapters/console.d.ts +30 -0
- package/dist/mail/adapters/console.d.ts.map +1 -0
- package/dist/mail/adapters/console.js +169 -0
- package/dist/mail/adapters/console.js.map +1 -0
- package/dist/mail/adapters/resend.d.ts +18 -0
- package/dist/mail/adapters/resend.d.ts.map +1 -0
- package/dist/mail/adapters/resend.js +148 -0
- package/dist/mail/adapters/resend.js.map +1 -0
- package/dist/mail/adapters/smtp.d.ts +22 -0
- package/dist/mail/adapters/smtp.d.ts.map +1 -0
- package/dist/mail/adapters/smtp.js +180 -0
- package/dist/mail/adapters/smtp.js.map +1 -0
- package/dist/mail/index.d.ts +15 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +13 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/mail-service.d.ts +180 -0
- package/dist/mail/mail-service.d.ts.map +1 -0
- package/dist/mail/mail-service.js +370 -0
- package/dist/mail/mail-service.js.map +1 -0
- package/dist/mail/notification-service.d.ts +82 -0
- package/dist/mail/notification-service.d.ts.map +1 -0
- package/dist/mail/notification-service.js +262 -0
- package/dist/mail/notification-service.js.map +1 -0
- package/dist/mail/templates/built-in-templates.d.ts +47 -0
- package/dist/mail/templates/built-in-templates.d.ts.map +1 -0
- package/dist/mail/templates/built-in-templates.js +416 -0
- package/dist/mail/templates/built-in-templates.js.map +1 -0
- package/dist/mail/templates/custom-template-renderer.d.ts +115 -0
- package/dist/mail/templates/custom-template-renderer.d.ts.map +1 -0
- package/dist/mail/templates/custom-template-renderer.js +225 -0
- package/dist/mail/templates/custom-template-renderer.js.map +1 -0
- package/dist/mail/types.d.ts +7 -0
- package/dist/mail/types.d.ts.map +1 -0
- package/dist/mail/types.js +7 -0
- package/dist/mail/types.js.map +1 -0
- package/dist/routes/auth/captcha.d.ts +33 -0
- package/dist/routes/auth/captcha.d.ts.map +1 -0
- package/dist/routes/auth/captcha.js +102 -0
- package/dist/routes/auth/captcha.js.map +1 -0
- package/dist/routes/auth/change-password.d.ts +17 -0
- package/dist/routes/auth/change-password.d.ts.map +1 -0
- package/dist/routes/auth/change-password.js +121 -0
- package/dist/routes/auth/change-password.js.map +1 -0
- package/dist/routes/auth/index.d.ts +13 -0
- package/dist/routes/auth/index.d.ts.map +1 -0
- package/dist/routes/auth/index.js +35 -0
- package/dist/routes/auth/index.js.map +1 -0
- package/dist/routes/auth/mfa.d.ts +121 -0
- package/dist/routes/auth/mfa.d.ts.map +1 -0
- package/dist/routes/auth/mfa.js +1143 -0
- package/dist/routes/auth/mfa.js.map +1 -0
- package/dist/routes/auth/oauth.d.ts +37 -0
- package/dist/routes/auth/oauth.d.ts.map +1 -0
- package/dist/routes/auth/oauth.js +420 -0
- package/dist/routes/auth/oauth.js.map +1 -0
- package/dist/routes/auth/oauth2-server.d.ts +70 -0
- package/dist/routes/auth/oauth2-server.d.ts.map +1 -0
- package/dist/routes/auth/oauth2-server.js +775 -0
- package/dist/routes/auth/oauth2-server.js.map +1 -0
- package/dist/routes/auth/passkey.d.ts +65 -0
- package/dist/routes/auth/passkey.d.ts.map +1 -0
- package/dist/routes/auth/passkey.js +910 -0
- package/dist/routes/auth/passkey.js.map +1 -0
- package/dist/routes/auth/password-reset.d.ts +23 -0
- package/dist/routes/auth/password-reset.d.ts.map +1 -0
- package/dist/routes/auth/password-reset.js +336 -0
- package/dist/routes/auth/password-reset.js.map +1 -0
- package/dist/routes/handlers/audit.d.ts +21 -0
- package/dist/routes/handlers/audit.d.ts.map +1 -0
- package/dist/routes/handlers/audit.js +124 -0
- package/dist/routes/handlers/audit.js.map +1 -0
- package/dist/routes/handlers/auth.d.ts +150 -0
- package/dist/routes/handlers/auth.d.ts.map +1 -0
- package/dist/routes/handlers/auth.js +512 -0
- package/dist/routes/handlers/auth.js.map +1 -0
- package/dist/routes/handlers/base.d.ts +34 -0
- package/dist/routes/handlers/base.d.ts.map +1 -0
- package/dist/routes/handlers/base.js +366 -0
- package/dist/routes/handlers/base.js.map +1 -0
- package/dist/routes/handlers/config.d.ts +31 -0
- package/dist/routes/handlers/config.d.ts.map +1 -0
- package/dist/routes/handlers/config.js +483 -0
- package/dist/routes/handlers/config.js.map +1 -0
- package/dist/routes/handlers/documents.d.ts +50 -0
- package/dist/routes/handlers/documents.d.ts.map +1 -0
- package/dist/routes/handlers/documents.js +708 -0
- package/dist/routes/handlers/documents.js.map +1 -0
- package/dist/routes/handlers/index.d.ts +12 -0
- package/dist/routes/handlers/index.d.ts.map +1 -0
- package/dist/routes/handlers/index.js +11 -0
- package/dist/routes/handlers/index.js.map +1 -0
- package/dist/routes/handlers/media.d.ts +24 -0
- package/dist/routes/handlers/media.d.ts.map +1 -0
- package/dist/routes/handlers/media.js +432 -0
- package/dist/routes/handlers/media.js.map +1 -0
- package/dist/routes/handlers/search.d.ts +10 -0
- package/dist/routes/handlers/search.d.ts.map +1 -0
- package/dist/routes/handlers/search.js +153 -0
- package/dist/routes/handlers/search.js.map +1 -0
- package/dist/routes/handlers/tokens.d.ts +14 -0
- package/dist/routes/handlers/tokens.d.ts.map +1 -0
- package/dist/routes/handlers/tokens.js +109 -0
- package/dist/routes/handlers/tokens.js.map +1 -0
- package/dist/routes/handlers/types.d.ts +13 -0
- package/dist/routes/handlers/types.d.ts.map +1 -0
- package/dist/routes/handlers/types.js +5 -0
- package/dist/routes/handlers/types.js.map +1 -0
- package/dist/routes/handlers/users.d.ts +16 -0
- package/dist/routes/handlers/users.d.ts.map +1 -0
- package/dist/routes/handlers/users.js +198 -0
- package/dist/routes/handlers/users.js.map +1 -0
- package/dist/routes/handlers/webhooks.d.ts +37 -0
- package/dist/routes/handlers/webhooks.d.ts.map +1 -0
- package/dist/routes/handlers/webhooks.js +343 -0
- package/dist/routes/handlers/webhooks.js.map +1 -0
- package/dist/routes/index.d.ts +5 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +5 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/routes.d.ts +29 -0
- package/dist/routes/routes.d.ts.map +1 -0
- package/dist/routes/routes.js +531 -0
- package/dist/routes/routes.js.map +1 -0
- package/dist/routes/slug-processor.d.ts +32 -0
- package/dist/routes/slug-processor.d.ts.map +1 -0
- package/dist/routes/slug-processor.js +328 -0
- package/dist/routes/slug-processor.js.map +1 -0
- package/dist/routes/types.d.ts +31 -0
- package/dist/routes/types.d.ts.map +1 -0
- package/dist/routes/types.js +7 -0
- package/dist/routes/types.js.map +1 -0
- package/dist/routes/users/index.d.ts +7 -0
- package/dist/routes/users/index.d.ts.map +1 -0
- package/dist/routes/users/index.js +7 -0
- package/dist/routes/users/index.js.map +1 -0
- package/dist/structure/builder/StructureBuilder.d.ts +209 -0
- package/dist/structure/builder/StructureBuilder.d.ts.map +1 -0
- package/dist/structure/builder/StructureBuilder.js +425 -0
- package/dist/structure/builder/StructureBuilder.js.map +1 -0
- package/dist/structure/builder/index.d.ts +8 -0
- package/dist/structure/builder/index.d.ts.map +1 -0
- package/dist/structure/builder/index.js +6 -0
- package/dist/structure/builder/index.js.map +1 -0
- package/dist/structure/builder/navigation-tree-builder.d.ts +33 -0
- package/dist/structure/builder/navigation-tree-builder.d.ts.map +1 -0
- package/dist/structure/builder/navigation-tree-builder.js +284 -0
- package/dist/structure/builder/navigation-tree-builder.js.map +1 -0
- package/dist/structure/errors/index.d.ts +72 -0
- package/dist/structure/errors/index.d.ts.map +1 -0
- package/dist/structure/errors/index.js +136 -0
- package/dist/structure/errors/index.js.map +1 -0
- package/dist/structure/index.d.ts +14 -0
- package/dist/structure/index.d.ts.map +1 -0
- package/dist/structure/index.js +16 -0
- package/dist/structure/index.js.map +1 -0
- package/dist/structure/types/filters.d.ts +167 -0
- package/dist/structure/types/filters.d.ts.map +1 -0
- package/dist/structure/types/filters.js +38 -0
- package/dist/structure/types/filters.js.map +1 -0
- package/dist/structure/types/index.d.ts +11 -0
- package/dist/structure/types/index.d.ts.map +1 -0
- package/dist/structure/types/index.js +7 -0
- package/dist/structure/types/index.js.map +1 -0
- package/dist/structure/types/permissions.d.ts +159 -0
- package/dist/structure/types/permissions.d.ts.map +1 -0
- package/dist/structure/types/permissions.js +49 -0
- package/dist/structure/types/permissions.js.map +1 -0
- package/dist/structure/types/structure.d.ts +342 -0
- package/dist/structure/types/structure.d.ts.map +1 -0
- package/dist/structure/types/structure.js +6 -0
- package/dist/structure/types/structure.js.map +1 -0
- package/dist/structure/types/views.d.ts +278 -0
- package/dist/structure/types/views.d.ts.map +1 -0
- package/dist/structure/types/views.js +6 -0
- package/dist/structure/types/views.js.map +1 -0
- package/dist/structure/utils/hash.d.ts +61 -0
- package/dist/structure/utils/hash.d.ts.map +1 -0
- package/dist/structure/utils/hash.js +118 -0
- package/dist/structure/utils/hash.js.map +1 -0
- package/dist/structure/utils/index.d.ts +10 -0
- package/dist/structure/utils/index.d.ts.map +1 -0
- package/dist/structure/utils/index.js +7 -0
- package/dist/structure/utils/index.js.map +1 -0
- package/dist/structure/utils/permission-checker.d.ts +57 -0
- package/dist/structure/utils/permission-checker.d.ts.map +1 -0
- package/dist/structure/utils/permission-checker.js +286 -0
- package/dist/structure/utils/permission-checker.js.map +1 -0
- package/dist/structure/utils/query-builder.d.ts +82 -0
- package/dist/structure/utils/query-builder.d.ts.map +1 -0
- package/dist/structure/utils/query-builder.js +489 -0
- package/dist/structure/utils/query-builder.js.map +1 -0
- package/dist/structure/utils/structure-merger.d.ts +91 -0
- package/dist/structure/utils/structure-merger.d.ts.map +1 -0
- package/dist/structure/utils/structure-merger.js +445 -0
- package/dist/structure/utils/structure-merger.js.map +1 -0
- package/dist/structure/validation/schemas.d.ts +3107 -0
- package/dist/structure/validation/schemas.d.ts.map +1 -0
- package/dist/structure/validation/schemas.js +436 -0
- package/dist/structure/validation/schemas.js.map +1 -0
- package/dist/types/api-requests.d.ts +334 -0
- package/dist/types/api-requests.d.ts.map +1 -0
- package/dist/types/api-requests.js +8 -0
- package/dist/types/api-requests.js.map +1 -0
- package/dist/types/auth.d.ts +394 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +8 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/types/document.d.ts +49 -0
- package/dist/types/document.d.ts.map +1 -0
- package/dist/types/document.js +6 -0
- package/dist/types/document.js.map +1 -0
- package/dist/types/field-system.d.ts +197 -0
- package/dist/types/field-system.d.ts.map +1 -0
- package/dist/types/field-system.js +8 -0
- package/dist/types/field-system.js.map +1 -0
- package/dist/types/fields.d.ts +17 -0
- package/dist/types/fields.d.ts.map +1 -0
- package/dist/types/fields.js +21 -0
- package/dist/types/fields.js.map +1 -0
- package/dist/types/http.d.ts +162 -0
- package/dist/types/http.d.ts.map +1 -0
- package/dist/types/http.js +8 -0
- package/dist/types/http.js.map +1 -0
- package/dist/types/i18n.d.ts +56 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +25 -0
- package/dist/types/i18n.js.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mail.d.ts +224 -0
- package/dist/types/mail.d.ts.map +1 -0
- package/dist/types/mail.js +8 -0
- package/dist/types/mail.js.map +1 -0
- package/dist/types/media.d.ts +56 -0
- package/dist/types/media.d.ts.map +1 -0
- package/dist/types/media.js +6 -0
- package/dist/types/media.js.map +1 -0
- package/dist/types/oauth2.d.ts +287 -0
- package/dist/types/oauth2.d.ts.map +1 -0
- package/dist/types/oauth2.js +43 -0
- package/dist/types/oauth2.js.map +1 -0
- package/dist/types/passkey.d.ts +217 -0
- package/dist/types/passkey.d.ts.map +1 -0
- package/dist/types/passkey.js +8 -0
- package/dist/types/passkey.js.map +1 -0
- package/dist/types/slug.d.ts +99 -0
- package/dist/types/slug.d.ts.map +1 -0
- package/dist/types/slug.js +8 -0
- package/dist/types/slug.js.map +1 -0
- package/package.json +183 -0
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { SecurityValidator, InvalidInputError, createLogger, AUDIT_OPERATIONS } from '../../core/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Serialise a value for a JSONB column. `undefined` and `null` both become SQL NULL, which
|
|
5
|
+
* reads back as `undefined` — an unset field is never stored as `'{}'` or `'[]'`.
|
|
6
|
+
*/
|
|
7
|
+
function jsonColumnValue(value) {
|
|
8
|
+
return value === undefined || value === null ? null : JSON.stringify(value);
|
|
9
|
+
}
|
|
10
|
+
export class PostgresDataAdapter {
|
|
11
|
+
config;
|
|
12
|
+
pool;
|
|
13
|
+
logger = createLogger('adapter', 'PostgresDataAdapter');
|
|
14
|
+
initialized = false;
|
|
15
|
+
initializationPromise = null;
|
|
16
|
+
closePromise = null;
|
|
17
|
+
// Security limits
|
|
18
|
+
MAX_DOCUMENT_SIZE = 10 * 1024 * 1024; // 10MB
|
|
19
|
+
MAX_LIST_LIMIT = 1000;
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
// Security: Validate schema and table prefix to prevent SQL injection
|
|
22
|
+
const schemaPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
23
|
+
const prefixPattern = /^[a-zA-Z0-9_]*$/;
|
|
24
|
+
const schema = config.schema || 'public';
|
|
25
|
+
const tablePrefix = config.tablePrefix || 'trokky_';
|
|
26
|
+
if (!schemaPattern.test(schema)) {
|
|
27
|
+
throw new Error(`Invalid PostgreSQL schema name: "${schema}". Schema names must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
|
|
28
|
+
}
|
|
29
|
+
if (!prefixPattern.test(tablePrefix)) {
|
|
30
|
+
throw new Error(`Invalid table prefix: "${tablePrefix}". Table prefixes must contain only alphanumeric characters and underscores.`);
|
|
31
|
+
}
|
|
32
|
+
this.config = {
|
|
33
|
+
connection: config.connection || process.env.DATABASE_URL || 'postgresql://localhost:5432/trokky',
|
|
34
|
+
pool: {
|
|
35
|
+
max: 20,
|
|
36
|
+
idleTimeoutMillis: 30000,
|
|
37
|
+
connectionTimeoutMillis: 2000,
|
|
38
|
+
...config.pool
|
|
39
|
+
},
|
|
40
|
+
schema,
|
|
41
|
+
tablePrefix,
|
|
42
|
+
autoMigrate: config.autoMigrate ?? true,
|
|
43
|
+
enableQueryLogging: config.enableQueryLogging ?? false,
|
|
44
|
+
ssl: config.ssl ?? false,
|
|
45
|
+
connectionTimeout: config.connectionTimeout || 5000
|
|
46
|
+
};
|
|
47
|
+
// Initialize connection pool
|
|
48
|
+
const poolConfig = typeof this.config.connection === 'string'
|
|
49
|
+
? {
|
|
50
|
+
connectionString: this.config.connection,
|
|
51
|
+
...this.config.pool,
|
|
52
|
+
...(this.config.ssl !== undefined && { ssl: this.config.ssl })
|
|
53
|
+
}
|
|
54
|
+
: {
|
|
55
|
+
...this.config.connection,
|
|
56
|
+
...this.config.pool,
|
|
57
|
+
...(this.config.ssl !== undefined && { ssl: this.config.ssl })
|
|
58
|
+
};
|
|
59
|
+
this.pool = new Pool(poolConfig);
|
|
60
|
+
// Setup error handling
|
|
61
|
+
this.pool.on('error', (err) => {
|
|
62
|
+
this.logger.error('PostgreSQL pool error', err);
|
|
63
|
+
});
|
|
64
|
+
this.pool.on('connect', (client) => {
|
|
65
|
+
this.logger.debug('New PostgreSQL client connected');
|
|
66
|
+
});
|
|
67
|
+
this.logger.info('PostgresDataAdapter constructed', {
|
|
68
|
+
schema: this.config.schema,
|
|
69
|
+
tablePrefix: this.config.tablePrefix,
|
|
70
|
+
autoMigrate: this.config.autoMigrate,
|
|
71
|
+
enableQueryLogging: this.config.enableQueryLogging
|
|
72
|
+
});
|
|
73
|
+
// Initialize immediately like filesystem adapter - but async
|
|
74
|
+
// Store initialization promise for later awaiting
|
|
75
|
+
if (this.config.autoMigrate) {
|
|
76
|
+
this.logger.info('Starting PostgreSQL auto-migration during construction');
|
|
77
|
+
this.initializationPromise = this.initialize();
|
|
78
|
+
// Start initialization but don't block construction
|
|
79
|
+
this.initializationPromise
|
|
80
|
+
.then(() => {
|
|
81
|
+
this.logger.info('PostgreSQL adapter initialization completed successfully during construction');
|
|
82
|
+
})
|
|
83
|
+
.catch(error => {
|
|
84
|
+
this.logger.error('Failed to initialize PostgreSQL adapter during construction', error);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.logger.info('PostgreSQL auto-migration disabled in config');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async initialize() {
|
|
92
|
+
if (this.initialized) {
|
|
93
|
+
this.logger.debug('PostgreSQL adapter already initialized, skipping');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.logger.info('Starting PostgreSQL adapter initialization');
|
|
97
|
+
try {
|
|
98
|
+
// Test connection
|
|
99
|
+
this.logger.info('Testing PostgreSQL connection...');
|
|
100
|
+
const isHealthy = await this.healthCheck();
|
|
101
|
+
this.logger.info('PostgreSQL connection test result', { healthy: isHealthy });
|
|
102
|
+
// Run migrations if enabled
|
|
103
|
+
if (this.config.autoMigrate) {
|
|
104
|
+
this.logger.info('Running PostgreSQL migrations...');
|
|
105
|
+
await this.runMigrations();
|
|
106
|
+
this.logger.info('PostgreSQL migrations completed');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.logger.info('PostgreSQL auto-migration disabled, skipping migrations');
|
|
110
|
+
}
|
|
111
|
+
this.initialized = true;
|
|
112
|
+
this.logger.info('✅ PostgreSQL adapter initialized successfully', {
|
|
113
|
+
schema: this.config.schema,
|
|
114
|
+
tablePrefix: this.config.tablePrefix,
|
|
115
|
+
autoMigrate: this.config.autoMigrate
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.logger.error('❌ Failed to initialize PostgreSQL adapter', error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Close the connection pool. Until this runs the pool's idle clients keep the event loop
|
|
125
|
+
* alive, so a process that stops its HTTP server never exits and a test file leaks its
|
|
126
|
+
* sockets into the next one.
|
|
127
|
+
*
|
|
128
|
+
* `pool.end()` throws if it is called a second time, and shutdown paths do get entered
|
|
129
|
+
* twice (a SIGTERM arriving while a manual stop is already in flight), so the first call's
|
|
130
|
+
* promise is remembered and replayed rather than re-ending the pool.
|
|
131
|
+
*/
|
|
132
|
+
async close() {
|
|
133
|
+
if (!this.closePromise) {
|
|
134
|
+
this.closePromise = this.pool.end();
|
|
135
|
+
this.closePromise.then(() => this.logger.info('PostgreSQL connection pool closed'), error => this.logger.error('Error closing PostgreSQL connection pool', error));
|
|
136
|
+
}
|
|
137
|
+
return this.closePromise;
|
|
138
|
+
}
|
|
139
|
+
async ensureInitialized() {
|
|
140
|
+
if (!this.initialized) {
|
|
141
|
+
// If we have an initialization promise from constructor, await it
|
|
142
|
+
if (this.initializationPromise) {
|
|
143
|
+
await this.initializationPromise;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Fallback: initialize now if not done during construction
|
|
147
|
+
await this.initialize();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
tableName(entity) {
|
|
152
|
+
return `${this.config.schema}.${this.config.tablePrefix}${entity}`;
|
|
153
|
+
}
|
|
154
|
+
async query(text, params) {
|
|
155
|
+
await this.ensureInitialized();
|
|
156
|
+
if (this.config.enableQueryLogging) {
|
|
157
|
+
this.logger.debug('Executing query', { text, params });
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const result = await this.pool.query(text, params);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.logger.error('Query failed', { text, params, error });
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async directQuery(text, params) {
|
|
169
|
+
// Direct query without ensureInitialized() - used during initialization
|
|
170
|
+
if (this.config.enableQueryLogging) {
|
|
171
|
+
this.logger.debug('Executing direct query', { text, params });
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const result = await this.pool.query(text, params);
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
this.logger.error('Direct query failed', { text, params, error });
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
// DOCUMENT OPERATIONS
|
|
184
|
+
// ==========================================================================
|
|
185
|
+
/**
|
|
186
|
+
* Shared shape for every document read path, so get/save/list cannot drift apart.
|
|
187
|
+
*
|
|
188
|
+
* `_status` lives inside the JSONB payload, so it is pulled out of the data and re-emitted as
|
|
189
|
+
* a metadata field with the same `'draft'` fallback the filesystem adapter applies; the rest of
|
|
190
|
+
* the user data is spread last, exactly as the filesystem adapter does. SQL NULLs become
|
|
191
|
+
* `undefined` — the contract never returns `null` for an unknown actor.
|
|
192
|
+
*/
|
|
193
|
+
mapRowToDocument(row) {
|
|
194
|
+
const { _status, ...data } = (row.data ?? {});
|
|
195
|
+
return {
|
|
196
|
+
id: row.id,
|
|
197
|
+
_id: row.id,
|
|
198
|
+
_collection: row.collection,
|
|
199
|
+
_createdAt: row.created_at,
|
|
200
|
+
_updatedAt: row.updated_at,
|
|
201
|
+
_revision: row.revision ?? 1,
|
|
202
|
+
_status: _status ?? 'draft',
|
|
203
|
+
_createdBy: row.created_by ?? undefined,
|
|
204
|
+
_updatedBy: row.updated_by ?? undefined,
|
|
205
|
+
_createdByType: row.created_by_type ?? undefined,
|
|
206
|
+
_updatedByType: row.updated_by_type ?? undefined,
|
|
207
|
+
...data
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async getDocument(collection, id) {
|
|
211
|
+
SecurityValidator.validateCollectionName(collection);
|
|
212
|
+
SecurityValidator.validateDocumentId(id);
|
|
213
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('documents')} WHERE collection = $1 AND id = $2`, [collection, id]);
|
|
214
|
+
if (result.rows.length === 0) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return this.mapRowToDocument(result.rows[0]);
|
|
218
|
+
}
|
|
219
|
+
async saveDocument(collection, id, data, auditContext) {
|
|
220
|
+
SecurityValidator.validateCollectionName(collection);
|
|
221
|
+
SecurityValidator.validateDocumentId(id);
|
|
222
|
+
SecurityValidator.validateDocumentData(data);
|
|
223
|
+
const dataJson = JSON.stringify(data);
|
|
224
|
+
if (dataJson.length > this.MAX_DOCUMENT_SIZE) {
|
|
225
|
+
throw new InvalidInputError(`Document exceeds maximum size of ${this.MAX_DOCUMENT_SIZE} bytes`);
|
|
226
|
+
}
|
|
227
|
+
const now = new Date();
|
|
228
|
+
const userId = auditContext?.userId;
|
|
229
|
+
const userType = auditContext?.userType;
|
|
230
|
+
try {
|
|
231
|
+
// UPSERT (INSERT ... ON CONFLICT ... DO UPDATE). `_status` is defaulted in SQL so the
|
|
232
|
+
// create/update distinction is decided by the same statement that performs the write:
|
|
233
|
+
// an explicit incoming status wins, otherwise the stored one is preserved on update and
|
|
234
|
+
// 'draft' is used on create. `created_by`/`created_by_type` are left out of the SET list,
|
|
235
|
+
// so the original creator survives an update (filesystem behaviour).
|
|
236
|
+
const result = await this.query(`
|
|
237
|
+
INSERT INTO ${this.tableName('documents')}
|
|
238
|
+
(collection, id, data, created_at, updated_at, created_by, updated_by, created_by_type, updated_by_type, revision)
|
|
239
|
+
VALUES (
|
|
240
|
+
$1, $2,
|
|
241
|
+
$3::jsonb || jsonb_build_object('_status', COALESCE($3::jsonb->>'_status', 'draft')),
|
|
242
|
+
$4, $4, $5, $5, $6, $6, 1
|
|
243
|
+
)
|
|
244
|
+
ON CONFLICT (collection, id)
|
|
245
|
+
DO UPDATE SET
|
|
246
|
+
data = $3::jsonb || jsonb_build_object('_status', COALESCE($3::jsonb->>'_status', ${this.config.tablePrefix}documents.data->>'_status', 'draft')),
|
|
247
|
+
updated_at = $4,
|
|
248
|
+
updated_by = $5,
|
|
249
|
+
updated_by_type = $6,
|
|
250
|
+
revision = COALESCE(${this.config.tablePrefix}documents.revision, 1) + 1
|
|
251
|
+
RETURNING *
|
|
252
|
+
`, [collection, id, JSON.stringify(data), now, userId, userType]);
|
|
253
|
+
const row = result.rows[0];
|
|
254
|
+
// Create audit log if context provided
|
|
255
|
+
if (auditContext) {
|
|
256
|
+
await this.createAuditLog({
|
|
257
|
+
documentId: id,
|
|
258
|
+
collection,
|
|
259
|
+
operation: AUDIT_OPERATIONS.UPDATE,
|
|
260
|
+
actorType: auditContext.userType,
|
|
261
|
+
actorId: auditContext.userId,
|
|
262
|
+
actorUsername: auditContext.username,
|
|
263
|
+
changes: { after: data },
|
|
264
|
+
timestamp: now,
|
|
265
|
+
revision: row.revision ?? 1,
|
|
266
|
+
ipAddress: auditContext.ipAddress,
|
|
267
|
+
userAgent: auditContext.userAgent
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return this.mapRowToDocument(row);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.logger.error('Failed to save document', { collection, id, error });
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Document fields that live in their own column rather than inside the JSONB payload.
|
|
279
|
+
* Anything not listed here — including `_status` and any other leading-underscore field the
|
|
280
|
+
* schema happens to define — is addressed through `data`.
|
|
281
|
+
*/
|
|
282
|
+
static DOCUMENT_SYSTEM_COLUMNS = {
|
|
283
|
+
id: 'id',
|
|
284
|
+
_id: 'id',
|
|
285
|
+
_revision: 'revision',
|
|
286
|
+
_createdAt: 'created_at',
|
|
287
|
+
_updatedAt: 'updated_at',
|
|
288
|
+
_createdBy: 'created_by',
|
|
289
|
+
_updatedBy: 'updated_by',
|
|
290
|
+
_createdByType: 'created_by_type',
|
|
291
|
+
_updatedByType: 'updated_by_type'
|
|
292
|
+
};
|
|
293
|
+
/** SECURITY: field names reach the SQL text directly, so they are whitelisted first. */
|
|
294
|
+
static DOCUMENT_FIELD_PATTERN = /^[a-zA-Z0-9_.\-]+$/;
|
|
295
|
+
/** Updatable user columns, in the order they are written. `json` columns are JSONB. */
|
|
296
|
+
static USER_UPDATE_COLUMNS = [
|
|
297
|
+
{ key: 'username', column: 'username' },
|
|
298
|
+
{ key: 'email', column: 'email' },
|
|
299
|
+
{ key: 'passwordHash', column: 'password_hash' },
|
|
300
|
+
{ key: 'firstName', column: 'first_name' },
|
|
301
|
+
{ key: 'lastName', column: 'last_name' },
|
|
302
|
+
{ key: 'role', column: 'role' },
|
|
303
|
+
{ key: 'permissions', column: 'permissions', json: true },
|
|
304
|
+
{ key: 'isActive', column: 'is_active' },
|
|
305
|
+
{ key: 'profileImage', column: 'profile_image' },
|
|
306
|
+
{ key: 'preferences', column: 'preferences', json: true },
|
|
307
|
+
{ key: 'oauthProviders', column: 'oauth_providers', json: true },
|
|
308
|
+
{ key: 'mfa', column: 'mfa', json: true },
|
|
309
|
+
{ key: 'passkeys', column: 'passkeys', json: true },
|
|
310
|
+
{ key: 'lastLoginAt', column: 'last_login_at' }
|
|
311
|
+
];
|
|
312
|
+
/**
|
|
313
|
+
* Filter clauses shared by listDocuments and countDocuments, so the two cannot disagree
|
|
314
|
+
* about what a filter means. Data fields are compared as `jsonb`, not as text: `String(value)`
|
|
315
|
+
* turned `false`, `0` and `null` into strings that never matched anything.
|
|
316
|
+
*/
|
|
317
|
+
buildDocumentFilterClauses(filter, params) {
|
|
318
|
+
return Object.entries(filter).map(([key, value]) => {
|
|
319
|
+
// SECURITY: Validate field name to prevent SQL injection
|
|
320
|
+
// Allow only alphanumeric, underscore, dash, and dot (for nested fields)
|
|
321
|
+
if (!PostgresDataAdapter.DOCUMENT_FIELD_PATTERN.test(key)) {
|
|
322
|
+
throw new Error(`Invalid filter field name: ${key}`);
|
|
323
|
+
}
|
|
324
|
+
const column = PostgresDataAdapter.DOCUMENT_SYSTEM_COLUMNS[key];
|
|
325
|
+
if (column) {
|
|
326
|
+
if (value === null || value === undefined) {
|
|
327
|
+
return `${column} IS NULL`;
|
|
328
|
+
}
|
|
329
|
+
params.push(value);
|
|
330
|
+
return `${column} = $${params.length}`;
|
|
331
|
+
}
|
|
332
|
+
// Data field (including _status, which is stored inside the payload)
|
|
333
|
+
params.push(key);
|
|
334
|
+
const keyParam = `$${params.length}::text`;
|
|
335
|
+
params.push(JSON.stringify(value ?? null));
|
|
336
|
+
return `data->${keyParam} = $${params.length}::jsonb`;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Accepts both sort grammars the API surfaces: `-field` (leading minus = descending) and
|
|
341
|
+
* `field.asc` / `field.desc`. The direction is stripped before the field name is validated,
|
|
342
|
+
* otherwise `-title` reads as a field literally named `-title`.
|
|
343
|
+
*/
|
|
344
|
+
buildDocumentSortClause(rawSortField) {
|
|
345
|
+
let field = rawSortField.trim();
|
|
346
|
+
let direction = 'ASC';
|
|
347
|
+
if (field.startsWith('-')) {
|
|
348
|
+
direction = 'DESC';
|
|
349
|
+
field = field.slice(1);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
const dotted = /^(.+)\.(asc|desc)$/i.exec(field);
|
|
353
|
+
if (dotted) {
|
|
354
|
+
field = dotted[1];
|
|
355
|
+
direction = dotted[2].toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// SECURITY: Validate field name to prevent SQL injection
|
|
359
|
+
if (!PostgresDataAdapter.DOCUMENT_FIELD_PATTERN.test(field)) {
|
|
360
|
+
throw new Error(`Invalid sort field name: ${field}`);
|
|
361
|
+
}
|
|
362
|
+
// Data fields are ordered by the stored jsonb value (`->`), not by its text rendering
|
|
363
|
+
// (`->>`): as text, 13 sorts before 2. jsonb ordering compares numbers numerically and
|
|
364
|
+
// strings by collation, which is what the filesystem adapter's `<` / `>` on the parsed
|
|
365
|
+
// JS values does for a homogeneously-typed field.
|
|
366
|
+
const column = PostgresDataAdapter.DOCUMENT_SYSTEM_COLUMNS[field] ?? `data->'${field}'`;
|
|
367
|
+
return `${column} ${direction}`;
|
|
368
|
+
}
|
|
369
|
+
async listDocuments(collection, options = {}) {
|
|
370
|
+
SecurityValidator.validateCollectionName(collection);
|
|
371
|
+
// No implicit default: an unbounded list returns everything up to the hard cap
|
|
372
|
+
const limit = Math.min(options.limit ?? this.MAX_LIST_LIMIT, this.MAX_LIST_LIMIT);
|
|
373
|
+
const offset = options.offset || 0;
|
|
374
|
+
let query = `SELECT * FROM ${this.tableName('documents')} WHERE collection = $1`;
|
|
375
|
+
const params = [collection];
|
|
376
|
+
if (options.filter) {
|
|
377
|
+
const clauses = this.buildDocumentFilterClauses(options.filter, params);
|
|
378
|
+
if (clauses.length > 0) {
|
|
379
|
+
query += ` AND ${clauses.join(' AND ')}`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (options.sort) {
|
|
383
|
+
const sortFields = Array.isArray(options.sort) ? options.sort : [options.sort];
|
|
384
|
+
query += ` ORDER BY ${sortFields.map(field => this.buildDocumentSortClause(field)).join(', ')}`;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
query += ` ORDER BY created_at DESC`;
|
|
388
|
+
}
|
|
389
|
+
query += ` LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
390
|
+
params.push(limit, offset);
|
|
391
|
+
this.logger.debug('Listing documents', { collection, query, params });
|
|
392
|
+
const result = await this.query(query, params);
|
|
393
|
+
return result.rows.map((row) => this.mapRowToDocument(row));
|
|
394
|
+
}
|
|
395
|
+
async deleteDocument(collection, id) {
|
|
396
|
+
SecurityValidator.validateCollectionName(collection);
|
|
397
|
+
SecurityValidator.validateDocumentId(id);
|
|
398
|
+
const result = await this.query(`DELETE FROM ${this.tableName('documents')} WHERE collection = $1 AND id = $2`, [collection, id]);
|
|
399
|
+
if (result.rowCount === 0) {
|
|
400
|
+
throw new InvalidInputError(`Document ${collection}:${id} not found`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async documentExists(collection, id) {
|
|
404
|
+
SecurityValidator.validateCollectionName(collection);
|
|
405
|
+
SecurityValidator.validateDocumentId(id);
|
|
406
|
+
const result = await this.query(`SELECT 1 FROM ${this.tableName('documents')} WHERE collection = $1 AND id = $2 LIMIT 1`, [collection, id]);
|
|
407
|
+
return result.rows.length > 0;
|
|
408
|
+
}
|
|
409
|
+
async countDocuments(collection, filter) {
|
|
410
|
+
SecurityValidator.validateCollectionName(collection);
|
|
411
|
+
let query = `SELECT COUNT(*) as count FROM ${this.tableName('documents')} WHERE collection = $1`;
|
|
412
|
+
const params = [collection];
|
|
413
|
+
if (filter) {
|
|
414
|
+
// Same clause builder as listDocuments: same field validation, same jsonb equality,
|
|
415
|
+
// same system-field column mapping
|
|
416
|
+
const clauses = this.buildDocumentFilterClauses(filter, params);
|
|
417
|
+
if (clauses.length > 0) {
|
|
418
|
+
query += ` AND ${clauses.join(' AND ')}`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const result = await this.query(query, params);
|
|
422
|
+
return parseInt(result.rows[0].count, 10);
|
|
423
|
+
}
|
|
424
|
+
// ==========================================================================
|
|
425
|
+
// AUDIT LOG OPERATIONS
|
|
426
|
+
// ==========================================================================
|
|
427
|
+
async createAuditLog(auditLog) {
|
|
428
|
+
const id = `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
429
|
+
const result = await this.query(`
|
|
430
|
+
INSERT INTO ${this.tableName('audit_logs')}
|
|
431
|
+
(id, operation, resource_type, resource_id, actor_type, actor_id, changes, metadata, timestamp)
|
|
432
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
433
|
+
RETURNING *
|
|
434
|
+
`, [
|
|
435
|
+
id,
|
|
436
|
+
auditLog.operation,
|
|
437
|
+
auditLog.collection,
|
|
438
|
+
auditLog.documentId,
|
|
439
|
+
auditLog.actorType,
|
|
440
|
+
auditLog.actorId,
|
|
441
|
+
auditLog.changes,
|
|
442
|
+
auditLog.metadata || {},
|
|
443
|
+
auditLog.timestamp
|
|
444
|
+
]);
|
|
445
|
+
const row = result.rows[0];
|
|
446
|
+
return {
|
|
447
|
+
id: row.id,
|
|
448
|
+
documentId: row.resource_id,
|
|
449
|
+
collection: row.resource_type,
|
|
450
|
+
operation: row.operation,
|
|
451
|
+
actorType: row.actor_type,
|
|
452
|
+
actorId: row.actor_id,
|
|
453
|
+
actorUsername: auditLog.actorUsername,
|
|
454
|
+
changes: row.changes,
|
|
455
|
+
timestamp: row.timestamp,
|
|
456
|
+
revision: auditLog.revision,
|
|
457
|
+
ipAddress: auditLog.ipAddress,
|
|
458
|
+
userAgent: auditLog.userAgent,
|
|
459
|
+
sessionId: auditLog.sessionId,
|
|
460
|
+
metadata: row.metadata
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get audit logs for a specific document
|
|
465
|
+
*/
|
|
466
|
+
async getDocumentAuditLogs(documentId, options = {}) {
|
|
467
|
+
const limit = options.limit || 100;
|
|
468
|
+
const offset = options.offset || 0;
|
|
469
|
+
const result = await this.query(`
|
|
470
|
+
SELECT * FROM ${this.tableName('audit_logs')}
|
|
471
|
+
WHERE resource_id = $1
|
|
472
|
+
ORDER BY timestamp DESC
|
|
473
|
+
LIMIT $2 OFFSET $3
|
|
474
|
+
`, [documentId, limit, offset]);
|
|
475
|
+
return result.rows.map((row) => ({
|
|
476
|
+
id: row.id,
|
|
477
|
+
documentId: row.resource_id,
|
|
478
|
+
collection: row.resource_type,
|
|
479
|
+
operation: row.operation,
|
|
480
|
+
actorType: row.actor_type,
|
|
481
|
+
actorId: row.actor_id,
|
|
482
|
+
changes: row.changes,
|
|
483
|
+
timestamp: row.timestamp,
|
|
484
|
+
metadata: row.metadata
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Get audit logs for a collection
|
|
489
|
+
*/
|
|
490
|
+
async getCollectionAuditLogs(collection, options = {}) {
|
|
491
|
+
const limit = options.limit || 100;
|
|
492
|
+
const offset = options.offset || 0;
|
|
493
|
+
const result = await this.query(`
|
|
494
|
+
SELECT * FROM ${this.tableName('audit_logs')}
|
|
495
|
+
WHERE resource_type = $1
|
|
496
|
+
ORDER BY timestamp DESC
|
|
497
|
+
LIMIT $2 OFFSET $3
|
|
498
|
+
`, [collection, limit, offset]);
|
|
499
|
+
return result.rows.map((row) => ({
|
|
500
|
+
id: row.id,
|
|
501
|
+
documentId: row.resource_id,
|
|
502
|
+
collection: row.resource_type,
|
|
503
|
+
operation: row.operation,
|
|
504
|
+
actorType: row.actor_type,
|
|
505
|
+
actorId: row.actor_id,
|
|
506
|
+
changes: row.changes,
|
|
507
|
+
timestamp: row.timestamp,
|
|
508
|
+
metadata: row.metadata
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Get audit logs by actor
|
|
513
|
+
*/
|
|
514
|
+
async getActorAuditLogs(actorId, options = {}) {
|
|
515
|
+
const limit = options.limit || 100;
|
|
516
|
+
const offset = options.offset || 0;
|
|
517
|
+
const result = await this.query(`
|
|
518
|
+
SELECT * FROM ${this.tableName('audit_logs')}
|
|
519
|
+
WHERE actor_id = $1
|
|
520
|
+
ORDER BY timestamp DESC
|
|
521
|
+
LIMIT $2 OFFSET $3
|
|
522
|
+
`, [actorId, limit, offset]);
|
|
523
|
+
return result.rows.map((row) => ({
|
|
524
|
+
id: row.id,
|
|
525
|
+
documentId: row.resource_id,
|
|
526
|
+
collection: row.resource_type,
|
|
527
|
+
operation: row.operation,
|
|
528
|
+
actorType: row.actor_type,
|
|
529
|
+
actorId: row.actor_id,
|
|
530
|
+
changes: row.changes,
|
|
531
|
+
timestamp: row.timestamp,
|
|
532
|
+
metadata: row.metadata
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
// ==========================================================================
|
|
536
|
+
// UTILITY OPERATIONS
|
|
537
|
+
// ==========================================================================
|
|
538
|
+
async healthCheck() {
|
|
539
|
+
try {
|
|
540
|
+
const result = await this.pool.query('SELECT 1 as health');
|
|
541
|
+
return result.rows[0].health === 1;
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
this.logger.error('Health check failed', error);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async getStorageInfo() {
|
|
549
|
+
const [collectionsResult, docsResult, usersResult, tokensResult] = await Promise.all([
|
|
550
|
+
this.query(`SELECT DISTINCT collection FROM ${this.tableName('documents')}`),
|
|
551
|
+
this.query(`SELECT COUNT(*) as count FROM ${this.tableName('documents')}`),
|
|
552
|
+
this.query(`SELECT COUNT(*) as count FROM ${this.tableName('users')}`),
|
|
553
|
+
this.query(`SELECT COUNT(*) as count FROM ${this.tableName('app_tokens')}`)
|
|
554
|
+
]);
|
|
555
|
+
return {
|
|
556
|
+
collections: collectionsResult.rows.map((row) => row.collection),
|
|
557
|
+
documentCount: parseInt(docsResult.rows[0].count, 10),
|
|
558
|
+
userCount: parseInt(usersResult.rows[0].count, 10),
|
|
559
|
+
tokenCount: parseInt(tokensResult.rows[0].count, 10),
|
|
560
|
+
database: 'postgresql'
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
async beginTransaction() {
|
|
564
|
+
const client = await this.pool.connect();
|
|
565
|
+
await client.query('BEGIN');
|
|
566
|
+
return new PostgresTransaction(client);
|
|
567
|
+
}
|
|
568
|
+
// ==========================================================================
|
|
569
|
+
// USER OPERATIONS
|
|
570
|
+
// ==========================================================================
|
|
571
|
+
async getUser(id) {
|
|
572
|
+
SecurityValidator.validateDocumentId(id);
|
|
573
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('users')} WHERE id = $1`, [id]);
|
|
574
|
+
if (result.rows.length === 0) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
const row = result.rows[0];
|
|
578
|
+
return this.mapRowToUser(row);
|
|
579
|
+
}
|
|
580
|
+
async saveUser(id, userData) {
|
|
581
|
+
SecurityValidator.validateDocumentId(id);
|
|
582
|
+
const now = new Date().toISOString();
|
|
583
|
+
try {
|
|
584
|
+
// Check if this is an update (user exists) or create (new user)
|
|
585
|
+
const existingUser = await this.getUser(id);
|
|
586
|
+
if (existingUser) {
|
|
587
|
+
// Update existing user
|
|
588
|
+
const result = await this.updateUserRow(id, userData, now);
|
|
589
|
+
const row = result.rows[0];
|
|
590
|
+
return this.mapRowToUser(row);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// Create new user
|
|
594
|
+
const createData = userData;
|
|
595
|
+
// Use passwordHash from caller (like filesystem adapter)
|
|
596
|
+
const passwordHash = createData.passwordHash || '';
|
|
597
|
+
// Every supplied field is persisted: a restore writes a user in one create, and
|
|
598
|
+
// hardcoding oauth_providers/mfa/passkeys or dropping lastLoginAt silently lost them.
|
|
599
|
+
// Fields that were not supplied are stored as SQL NULL (not '{}' / '[]') so they read
|
|
600
|
+
// back as undefined rather than as an empty value the caller never asked for.
|
|
601
|
+
const result = await this.query(`
|
|
602
|
+
INSERT INTO ${this.tableName('users')}
|
|
603
|
+
(id, username, email, password_hash, first_name, last_name, role, permissions, is_active, profile_image, preferences, oauth_providers, mfa, passkeys, last_login_at, created_at, updated_at)
|
|
604
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $16)
|
|
605
|
+
RETURNING *
|
|
606
|
+
`, [
|
|
607
|
+
id,
|
|
608
|
+
createData.username,
|
|
609
|
+
createData.email,
|
|
610
|
+
passwordHash,
|
|
611
|
+
createData.firstName,
|
|
612
|
+
createData.lastName,
|
|
613
|
+
createData.role,
|
|
614
|
+
JSON.stringify(createData.permissions ?? []),
|
|
615
|
+
createData.isActive ?? true,
|
|
616
|
+
createData.profileImage ?? null,
|
|
617
|
+
jsonColumnValue(createData.preferences),
|
|
618
|
+
jsonColumnValue(createData.oauthProviders),
|
|
619
|
+
jsonColumnValue(createData.mfa),
|
|
620
|
+
jsonColumnValue(createData.passkeys),
|
|
621
|
+
createData.lastLoginAt ?? null,
|
|
622
|
+
now
|
|
623
|
+
]);
|
|
624
|
+
const row = result.rows[0];
|
|
625
|
+
return this.mapRowToUser(row);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
this.logger.error('Failed to save user', { id, userData, error });
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async saveUserIf(id, userData, condition) {
|
|
634
|
+
SecurityValidator.validateDocumentId(id);
|
|
635
|
+
try {
|
|
636
|
+
const result = await this.updateUserRow(id, userData, new Date().toISOString(), condition.passwordHash);
|
|
637
|
+
if (!result.rowCount) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const row = result.rows[0];
|
|
641
|
+
return this.mapRowToUser(row);
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
this.logger.error('Failed to conditionally save user', { id, error });
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Shared UPDATE for user rows. When expectedPasswordHash is provided the
|
|
650
|
+
* write only applies while the stored hash still matches, in one statement.
|
|
651
|
+
*
|
|
652
|
+
* The SET list is built from the keys actually present in `updateData`. The previous
|
|
653
|
+
* `COALESCE($n, col)` form could not tell "omitted" from "explicitly null", so clearing a
|
|
654
|
+
* field was impossible: an explicit `null` silently kept the old value.
|
|
655
|
+
*/
|
|
656
|
+
async updateUserRow(id, updateData, now, expectedPasswordHash) {
|
|
657
|
+
const params = [id];
|
|
658
|
+
const assignments = [];
|
|
659
|
+
for (const { key, column, json } of PostgresDataAdapter.USER_UPDATE_COLUMNS) {
|
|
660
|
+
// An absent key and an explicit `undefined` both mean "unchanged"; an explicit `null`
|
|
661
|
+
// writes NULL and clears the field.
|
|
662
|
+
if (!Object.prototype.hasOwnProperty.call(updateData, key)) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const value = updateData[key];
|
|
666
|
+
if (value === undefined) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
params.push(json ? jsonColumnValue(value) : value);
|
|
670
|
+
assignments.push(`${column} = $${params.length}`);
|
|
671
|
+
}
|
|
672
|
+
params.push(now);
|
|
673
|
+
assignments.push(`updated_at = $${params.length}`);
|
|
674
|
+
let condition = '';
|
|
675
|
+
if (expectedPasswordHash !== undefined) {
|
|
676
|
+
params.push(expectedPasswordHash);
|
|
677
|
+
condition = ` AND password_hash = $${params.length}`;
|
|
678
|
+
}
|
|
679
|
+
return await this.query(`
|
|
680
|
+
UPDATE ${this.tableName('users')}
|
|
681
|
+
SET
|
|
682
|
+
${assignments.join(',\n ')}
|
|
683
|
+
WHERE id = $1${condition}
|
|
684
|
+
RETURNING *
|
|
685
|
+
`, params);
|
|
686
|
+
}
|
|
687
|
+
async listUsers(options = {}) {
|
|
688
|
+
// No implicit default: an unbounded list returns everything up to the hard cap
|
|
689
|
+
const limit = Math.min(options.limit ?? this.MAX_LIST_LIMIT, this.MAX_LIST_LIMIT);
|
|
690
|
+
const offset = options.offset || 0;
|
|
691
|
+
let query = `SELECT * FROM ${this.tableName('users')}`;
|
|
692
|
+
const params = [];
|
|
693
|
+
const conditions = [];
|
|
694
|
+
// Add filtering
|
|
695
|
+
if (options.role) {
|
|
696
|
+
conditions.push(`role = $${params.length + 1}`);
|
|
697
|
+
params.push(options.role);
|
|
698
|
+
}
|
|
699
|
+
if (options.isActive !== undefined) {
|
|
700
|
+
conditions.push(`is_active = $${params.length + 1}`);
|
|
701
|
+
params.push(options.isActive);
|
|
702
|
+
}
|
|
703
|
+
if (conditions.length > 0) {
|
|
704
|
+
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
705
|
+
}
|
|
706
|
+
query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
707
|
+
params.push(limit, offset);
|
|
708
|
+
const result = await this.query(query, params);
|
|
709
|
+
return result.rows.map((row) => this.mapRowToUser(row));
|
|
710
|
+
}
|
|
711
|
+
async deleteUser(id) {
|
|
712
|
+
SecurityValidator.validateDocumentId(id);
|
|
713
|
+
const result = await this.query(`DELETE FROM ${this.tableName('users')} WHERE id = $1`, [id]);
|
|
714
|
+
if (result.rowCount === 0) {
|
|
715
|
+
throw new InvalidInputError(`User ${id} not found`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async getUserByUsername(username) {
|
|
719
|
+
if (!username || typeof username !== 'string') {
|
|
720
|
+
throw new InvalidInputError('Username is required');
|
|
721
|
+
}
|
|
722
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('users')} WHERE username = $1`, [username]);
|
|
723
|
+
if (result.rows.length === 0) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const row = result.rows[0];
|
|
727
|
+
return this.mapRowToUser(row);
|
|
728
|
+
}
|
|
729
|
+
async getUserByEmail(email) {
|
|
730
|
+
if (!email || typeof email !== 'string') {
|
|
731
|
+
throw new InvalidInputError('Email is required');
|
|
732
|
+
}
|
|
733
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('users')} WHERE email = $1`, [email]);
|
|
734
|
+
if (result.rows.length === 0) {
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
const row = result.rows[0];
|
|
738
|
+
return this.mapRowToUser(row);
|
|
739
|
+
}
|
|
740
|
+
async isUsernameAvailable(username) {
|
|
741
|
+
const user = await this.getUserByUsername(username);
|
|
742
|
+
return user === null;
|
|
743
|
+
}
|
|
744
|
+
async isEmailAvailable(email) {
|
|
745
|
+
const user = await this.getUserByEmail(email);
|
|
746
|
+
return user === null;
|
|
747
|
+
}
|
|
748
|
+
async getUserByOAuthProvider(provider, providerId) {
|
|
749
|
+
if (!provider || !providerId) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
// Query using JSONB containment operator to find user with matching OAuth provider
|
|
753
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('users')}
|
|
754
|
+
WHERE oauth_providers @> $1::jsonb`, [JSON.stringify([{ provider, providerId }])]);
|
|
755
|
+
if (result.rows.length === 0) {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
const row = result.rows[0];
|
|
759
|
+
return this.mapRowToUser(row);
|
|
760
|
+
}
|
|
761
|
+
async getUserByPasskeyCredentialId(credentialId) {
|
|
762
|
+
if (!credentialId || typeof credentialId !== 'string') {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
// Security: Validate credential ID format (base64url)
|
|
766
|
+
// Base64url uses A-Z, a-z, 0-9, -, and _ characters
|
|
767
|
+
if (!/^[A-Za-z0-9_-]+$/.test(credentialId)) {
|
|
768
|
+
this.logger.warn('Invalid passkey credential ID format', {
|
|
769
|
+
credentialIdPrefix: credentialId.substring(0, 8)
|
|
770
|
+
});
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
// Query using JSONB containment operator to find user with matching passkey credential ID
|
|
774
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('users')}
|
|
775
|
+
WHERE passkeys @> $1::jsonb`, [JSON.stringify([{ id: credentialId }])]);
|
|
776
|
+
if (result.rows.length === 0) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
const row = result.rows[0];
|
|
780
|
+
return this.mapRowToUser(row);
|
|
781
|
+
}
|
|
782
|
+
/** Every nullable column is mapped to `undefined`: the contract never surfaces `null`. */
|
|
783
|
+
mapRowToUser(row) {
|
|
784
|
+
return {
|
|
785
|
+
id: row.id,
|
|
786
|
+
username: row.username,
|
|
787
|
+
email: row.email,
|
|
788
|
+
passwordHash: row.password_hash,
|
|
789
|
+
firstName: row.first_name,
|
|
790
|
+
lastName: row.last_name,
|
|
791
|
+
role: row.role,
|
|
792
|
+
// Created users always get `[]`; only an update that explicitly clears permissions
|
|
793
|
+
// leaves NULL behind, and a cleared field reads back as undefined
|
|
794
|
+
permissions: (row.permissions ?? undefined),
|
|
795
|
+
isActive: row.is_active,
|
|
796
|
+
profileImage: row.profile_image ?? undefined,
|
|
797
|
+
preferences: row.preferences ?? undefined,
|
|
798
|
+
oauthProviders: row.oauth_providers ?? undefined,
|
|
799
|
+
mfa: row.mfa ?? undefined,
|
|
800
|
+
passkeys: row.passkeys ?? undefined,
|
|
801
|
+
lastLoginAt: row.last_login_at ?? undefined,
|
|
802
|
+
createdAt: row.created_at,
|
|
803
|
+
updatedAt: row.updated_at
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
// ==========================================================================
|
|
807
|
+
// APP TOKEN OPERATIONS
|
|
808
|
+
// ==========================================================================
|
|
809
|
+
async getAppToken(id) {
|
|
810
|
+
SecurityValidator.validateDocumentId(id);
|
|
811
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('app_tokens')} WHERE id = $1`, [id]);
|
|
812
|
+
if (result.rows.length === 0) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
const row = result.rows[0];
|
|
816
|
+
return this.mapRowToAppToken(row);
|
|
817
|
+
}
|
|
818
|
+
async saveAppToken(id, tokenData) {
|
|
819
|
+
SecurityValidator.validateDocumentId(id);
|
|
820
|
+
const now = new Date();
|
|
821
|
+
try {
|
|
822
|
+
// Check if this is an update (token exists) or create (new token)
|
|
823
|
+
const existingToken = await this.getAppToken(id);
|
|
824
|
+
if (existingToken) {
|
|
825
|
+
// Update existing token
|
|
826
|
+
const updateData = tokenData;
|
|
827
|
+
const result = await this.query(`
|
|
828
|
+
UPDATE ${this.tableName('app_tokens')}
|
|
829
|
+
SET
|
|
830
|
+
name = COALESCE($2, name),
|
|
831
|
+
permissions = COALESCE($3, permissions),
|
|
832
|
+
is_active = COALESCE($4, is_active),
|
|
833
|
+
expires_at = COALESCE($5, expires_at),
|
|
834
|
+
updated_at = $6
|
|
835
|
+
WHERE id = $1
|
|
836
|
+
RETURNING *
|
|
837
|
+
`, [
|
|
838
|
+
id,
|
|
839
|
+
updateData.name,
|
|
840
|
+
JSON.stringify(updateData.permissions || []),
|
|
841
|
+
updateData.isActive,
|
|
842
|
+
updateData.expiresAt ? new Date(updateData.expiresAt) : null,
|
|
843
|
+
now
|
|
844
|
+
]);
|
|
845
|
+
const row = result.rows[0];
|
|
846
|
+
return this.mapRowToAppToken(row);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
// Create new token
|
|
850
|
+
const createData = tokenData;
|
|
851
|
+
// Use tokenHash from caller (this should be set by the calling code)
|
|
852
|
+
const tokenHash = createData.tokenHash || '';
|
|
853
|
+
const createdBy = createData.createdBy || 'system';
|
|
854
|
+
const result = await this.query(`
|
|
855
|
+
INSERT INTO ${this.tableName('app_tokens')}
|
|
856
|
+
(id, name, hash, permissions, created_by, is_active, expires_at, created_at, updated_at)
|
|
857
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8)
|
|
858
|
+
RETURNING *
|
|
859
|
+
`, [
|
|
860
|
+
id,
|
|
861
|
+
createData.name,
|
|
862
|
+
tokenHash,
|
|
863
|
+
JSON.stringify(createData.permissions || []),
|
|
864
|
+
createdBy,
|
|
865
|
+
true,
|
|
866
|
+
createData.expiresAt ? new Date(createData.expiresAt) : null,
|
|
867
|
+
now
|
|
868
|
+
]);
|
|
869
|
+
const row = result.rows[0];
|
|
870
|
+
return this.mapRowToAppToken(row);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
this.logger.error('Failed to save app token', { id, tokenData, error });
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async listAppTokens(options = {}) {
|
|
879
|
+
const limit = Math.min(options.limit || 50, this.MAX_LIST_LIMIT);
|
|
880
|
+
const offset = options.offset || 0;
|
|
881
|
+
let query = `SELECT * FROM ${this.tableName('app_tokens')}`;
|
|
882
|
+
const params = [];
|
|
883
|
+
const conditions = [];
|
|
884
|
+
// Add filtering
|
|
885
|
+
if (options.createdBy) {
|
|
886
|
+
conditions.push(`created_by = $${params.length + 1}`);
|
|
887
|
+
params.push(options.createdBy);
|
|
888
|
+
}
|
|
889
|
+
if (options.isActive !== undefined) {
|
|
890
|
+
conditions.push(`is_active = $${params.length + 1}`);
|
|
891
|
+
params.push(options.isActive);
|
|
892
|
+
}
|
|
893
|
+
if (conditions.length > 0) {
|
|
894
|
+
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
895
|
+
}
|
|
896
|
+
query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
897
|
+
params.push(limit, offset);
|
|
898
|
+
const result = await this.query(query, params);
|
|
899
|
+
return result.rows.map((row) => this.mapRowToAppToken(row));
|
|
900
|
+
}
|
|
901
|
+
async deleteAppToken(id) {
|
|
902
|
+
SecurityValidator.validateDocumentId(id);
|
|
903
|
+
const result = await this.query(`DELETE FROM ${this.tableName('app_tokens')} WHERE id = $1`, [id]);
|
|
904
|
+
if (result.rowCount === 0) {
|
|
905
|
+
throw new InvalidInputError(`App token ${id} not found`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
async getAppTokenByHash(hash) {
|
|
909
|
+
if (!hash || typeof hash !== 'string') {
|
|
910
|
+
throw new InvalidInputError('Token hash is required');
|
|
911
|
+
}
|
|
912
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('app_tokens')} WHERE hash = $1 AND is_active = true`, [hash]);
|
|
913
|
+
if (result.rows.length === 0) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
const row = result.rows[0];
|
|
917
|
+
// Update last_used_at
|
|
918
|
+
await this.query(`UPDATE ${this.tableName('app_tokens')} SET last_used_at = $1 WHERE id = $2`, [new Date(), row.id]);
|
|
919
|
+
return this.mapRowToAppToken(row);
|
|
920
|
+
}
|
|
921
|
+
mapRowToAppToken(row) {
|
|
922
|
+
return {
|
|
923
|
+
id: row.id,
|
|
924
|
+
name: row.name,
|
|
925
|
+
description: '', // Not stored in current schema, could be added
|
|
926
|
+
tokenHash: row.hash,
|
|
927
|
+
permissions: row.permissions,
|
|
928
|
+
createdBy: row.created_by,
|
|
929
|
+
isActive: row.is_active,
|
|
930
|
+
lastUsedAt: row.last_used_at ? row.last_used_at.toISOString() : undefined,
|
|
931
|
+
usageCount: 0, // Not tracked in current schema, could be added
|
|
932
|
+
expiresAt: row.expires_at ? row.expires_at.toISOString() : undefined,
|
|
933
|
+
createdAt: row.created_at.toISOString(),
|
|
934
|
+
updatedAt: row.updated_at.toISOString()
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
// ==========================================================================
|
|
938
|
+
// MIGRATION SUPPORT
|
|
939
|
+
// ==========================================================================
|
|
940
|
+
async runMigrations() {
|
|
941
|
+
this.logger.info('Creating migrations tracking table...');
|
|
942
|
+
// Create migrations table if it doesn't exist
|
|
943
|
+
await this.directQuery(`
|
|
944
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('migrations')} (
|
|
945
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
946
|
+
name VARCHAR(255) NOT NULL,
|
|
947
|
+
applied_at TIMESTAMP DEFAULT NOW(),
|
|
948
|
+
checksum VARCHAR(255) NOT NULL
|
|
949
|
+
)
|
|
950
|
+
`);
|
|
951
|
+
this.logger.info('Running initial schema migrations...');
|
|
952
|
+
// Run initial schema migrations
|
|
953
|
+
await this.createTables();
|
|
954
|
+
this.logger.info('Schema migrations completed');
|
|
955
|
+
}
|
|
956
|
+
async createTables() {
|
|
957
|
+
this.logger.info('Creating documents table...');
|
|
958
|
+
// Documents table
|
|
959
|
+
await this.directQuery(`
|
|
960
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('documents')} (
|
|
961
|
+
collection VARCHAR(255) NOT NULL,
|
|
962
|
+
id VARCHAR(255) NOT NULL,
|
|
963
|
+
data JSONB NOT NULL,
|
|
964
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
965
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
966
|
+
created_by VARCHAR(255),
|
|
967
|
+
updated_by VARCHAR(255),
|
|
968
|
+
created_by_type VARCHAR(50),
|
|
969
|
+
updated_by_type VARCHAR(50),
|
|
970
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
971
|
+
PRIMARY KEY (collection, id)
|
|
972
|
+
)
|
|
973
|
+
`);
|
|
974
|
+
// Migrations for databases created before revision / actor-type tracking existed.
|
|
975
|
+
// ADD COLUMN IF NOT EXISTS keeps them safe to re-run on every boot.
|
|
976
|
+
for (const [column, definition] of [
|
|
977
|
+
['revision', 'INTEGER NOT NULL DEFAULT 1'],
|
|
978
|
+
['created_by_type', 'VARCHAR(50)'],
|
|
979
|
+
['updated_by_type', 'VARCHAR(50)']
|
|
980
|
+
]) {
|
|
981
|
+
await this.directQuery(`
|
|
982
|
+
DO $$
|
|
983
|
+
BEGIN
|
|
984
|
+
IF NOT EXISTS (
|
|
985
|
+
SELECT 1 FROM information_schema.columns
|
|
986
|
+
WHERE table_schema = '${this.config.schema}'
|
|
987
|
+
AND table_name = '${this.config.tablePrefix}documents'
|
|
988
|
+
AND column_name = '${column}'
|
|
989
|
+
) THEN
|
|
990
|
+
ALTER TABLE ${this.tableName('documents')}
|
|
991
|
+
ADD COLUMN IF NOT EXISTS ${column} ${definition};
|
|
992
|
+
END IF;
|
|
993
|
+
END $$
|
|
994
|
+
`);
|
|
995
|
+
}
|
|
996
|
+
this.logger.info('Creating performance indexes for documents...');
|
|
997
|
+
// Create indexes for better performance
|
|
998
|
+
await this.directQuery(`
|
|
999
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_collection_idx
|
|
1000
|
+
ON ${this.tableName('documents')} (collection)
|
|
1001
|
+
`);
|
|
1002
|
+
await this.directQuery(`
|
|
1003
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_updated_at_idx
|
|
1004
|
+
ON ${this.tableName('documents')} (updated_at DESC)
|
|
1005
|
+
`);
|
|
1006
|
+
// The default list order is created_at DESC
|
|
1007
|
+
await this.directQuery(`
|
|
1008
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_created_at_idx
|
|
1009
|
+
ON ${this.tableName('documents')} (created_at DESC)
|
|
1010
|
+
`);
|
|
1011
|
+
await this.directQuery(`
|
|
1012
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}documents_data_gin_idx
|
|
1013
|
+
ON ${this.tableName('documents')} USING GIN (data)
|
|
1014
|
+
`);
|
|
1015
|
+
this.logger.info('Creating audit logs table...');
|
|
1016
|
+
// Audit logs table
|
|
1017
|
+
await this.directQuery(`
|
|
1018
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('audit_logs')} (
|
|
1019
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1020
|
+
operation VARCHAR(100) NOT NULL,
|
|
1021
|
+
resource_type VARCHAR(100) NOT NULL,
|
|
1022
|
+
resource_id VARCHAR(255) NOT NULL,
|
|
1023
|
+
actor_type VARCHAR(50) NOT NULL,
|
|
1024
|
+
actor_id VARCHAR(255) NOT NULL,
|
|
1025
|
+
changes JSONB,
|
|
1026
|
+
metadata JSONB,
|
|
1027
|
+
timestamp TIMESTAMP DEFAULT NOW()
|
|
1028
|
+
)
|
|
1029
|
+
`);
|
|
1030
|
+
this.logger.info('Creating performance indexes for audit logs...');
|
|
1031
|
+
await this.directQuery(`
|
|
1032
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}audit_logs_resource_idx
|
|
1033
|
+
ON ${this.tableName('audit_logs')} (resource_type, resource_id)
|
|
1034
|
+
`);
|
|
1035
|
+
await this.directQuery(`
|
|
1036
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}audit_logs_timestamp_idx
|
|
1037
|
+
ON ${this.tableName('audit_logs')} (timestamp DESC)
|
|
1038
|
+
`);
|
|
1039
|
+
this.logger.info('Creating users table...');
|
|
1040
|
+
// Users table
|
|
1041
|
+
await this.directQuery(`
|
|
1042
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('users')} (
|
|
1043
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1044
|
+
username VARCHAR(255) UNIQUE NOT NULL,
|
|
1045
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
1046
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
1047
|
+
first_name VARCHAR(255) NOT NULL,
|
|
1048
|
+
last_name VARCHAR(255) NOT NULL,
|
|
1049
|
+
role VARCHAR(50) NOT NULL DEFAULT 'viewer',
|
|
1050
|
+
permissions JSONB DEFAULT '[]',
|
|
1051
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
1052
|
+
profile_image VARCHAR(500),
|
|
1053
|
+
last_login_at VARCHAR(50),
|
|
1054
|
+
preferences JSONB DEFAULT '{}',
|
|
1055
|
+
oauth_providers JSONB DEFAULT '[]',
|
|
1056
|
+
mfa JSONB DEFAULT '{}',
|
|
1057
|
+
passkeys JSONB DEFAULT '[]',
|
|
1058
|
+
created_at VARCHAR(50) NOT NULL,
|
|
1059
|
+
updated_at VARCHAR(50) NOT NULL
|
|
1060
|
+
)
|
|
1061
|
+
`);
|
|
1062
|
+
// Migration: Add oauth_providers column if it doesn't exist (for existing databases)
|
|
1063
|
+
await this.directQuery(`
|
|
1064
|
+
DO $$
|
|
1065
|
+
BEGIN
|
|
1066
|
+
IF NOT EXISTS (
|
|
1067
|
+
SELECT 1 FROM information_schema.columns
|
|
1068
|
+
WHERE table_schema = '${this.config.schema}'
|
|
1069
|
+
AND table_name = '${this.config.tablePrefix}users'
|
|
1070
|
+
AND column_name = 'oauth_providers'
|
|
1071
|
+
) THEN
|
|
1072
|
+
ALTER TABLE ${this.tableName('users')}
|
|
1073
|
+
ADD COLUMN oauth_providers JSONB DEFAULT '[]';
|
|
1074
|
+
END IF;
|
|
1075
|
+
END $$
|
|
1076
|
+
`);
|
|
1077
|
+
// Migration: Add mfa column if it doesn't exist (for existing databases)
|
|
1078
|
+
await this.directQuery(`
|
|
1079
|
+
DO $$
|
|
1080
|
+
BEGIN
|
|
1081
|
+
IF NOT EXISTS (
|
|
1082
|
+
SELECT 1 FROM information_schema.columns
|
|
1083
|
+
WHERE table_schema = '${this.config.schema}'
|
|
1084
|
+
AND table_name = '${this.config.tablePrefix}users'
|
|
1085
|
+
AND column_name = 'mfa'
|
|
1086
|
+
) THEN
|
|
1087
|
+
ALTER TABLE ${this.tableName('users')}
|
|
1088
|
+
ADD COLUMN mfa JSONB DEFAULT '{}';
|
|
1089
|
+
END IF;
|
|
1090
|
+
END $$
|
|
1091
|
+
`);
|
|
1092
|
+
// Migration: Add passkeys column if it doesn't exist (for existing databases)
|
|
1093
|
+
await this.directQuery(`
|
|
1094
|
+
DO $$
|
|
1095
|
+
BEGIN
|
|
1096
|
+
IF NOT EXISTS (
|
|
1097
|
+
SELECT 1 FROM information_schema.columns
|
|
1098
|
+
WHERE table_schema = '${this.config.schema}'
|
|
1099
|
+
AND table_name = '${this.config.tablePrefix}users'
|
|
1100
|
+
AND column_name = 'passkeys'
|
|
1101
|
+
) THEN
|
|
1102
|
+
ALTER TABLE ${this.tableName('users')}
|
|
1103
|
+
ADD COLUMN passkeys JSONB DEFAULT '[]';
|
|
1104
|
+
END IF;
|
|
1105
|
+
END $$
|
|
1106
|
+
`);
|
|
1107
|
+
await this.directQuery(`
|
|
1108
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_username_idx
|
|
1109
|
+
ON ${this.tableName('users')} (username)
|
|
1110
|
+
`);
|
|
1111
|
+
await this.directQuery(`
|
|
1112
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_email_idx
|
|
1113
|
+
ON ${this.tableName('users')} (email)
|
|
1114
|
+
`);
|
|
1115
|
+
// GIN index for efficient OAuth provider lookups
|
|
1116
|
+
await this.directQuery(`
|
|
1117
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_oauth_providers_idx
|
|
1118
|
+
ON ${this.tableName('users')} USING GIN (oauth_providers)
|
|
1119
|
+
`);
|
|
1120
|
+
// GIN index for efficient passkey credential lookups
|
|
1121
|
+
await this.directQuery(`
|
|
1122
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}users_passkeys_idx
|
|
1123
|
+
ON ${this.tableName('users')} USING GIN (passkeys)
|
|
1124
|
+
`);
|
|
1125
|
+
this.logger.info('Creating app tokens table...');
|
|
1126
|
+
// App tokens table
|
|
1127
|
+
await this.directQuery(`
|
|
1128
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('app_tokens')} (
|
|
1129
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1130
|
+
name VARCHAR(255) NOT NULL,
|
|
1131
|
+
hash VARCHAR(255) UNIQUE NOT NULL,
|
|
1132
|
+
permissions JSONB DEFAULT '[]',
|
|
1133
|
+
created_by VARCHAR(255) NOT NULL,
|
|
1134
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
1135
|
+
last_used_at TIMESTAMP,
|
|
1136
|
+
expires_at TIMESTAMP,
|
|
1137
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
1138
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
1139
|
+
)
|
|
1140
|
+
`);
|
|
1141
|
+
await this.directQuery(`
|
|
1142
|
+
CREATE INDEX IF NOT EXISTS ${this.config.tablePrefix}app_tokens_hash_idx
|
|
1143
|
+
ON ${this.tableName('app_tokens')} (hash)
|
|
1144
|
+
`);
|
|
1145
|
+
this.logger.info('Creating webhooks table...');
|
|
1146
|
+
// Webhooks table
|
|
1147
|
+
await this.directQuery(`
|
|
1148
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('webhooks')} (
|
|
1149
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1150
|
+
name VARCHAR(255) NOT NULL,
|
|
1151
|
+
url VARCHAR(500) NOT NULL,
|
|
1152
|
+
events JSONB DEFAULT '[]',
|
|
1153
|
+
headers JSONB DEFAULT '{}',
|
|
1154
|
+
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
1155
|
+
secret VARCHAR(255),
|
|
1156
|
+
timeout_ms INTEGER DEFAULT 5000,
|
|
1157
|
+
retry_attempts INTEGER DEFAULT 3,
|
|
1158
|
+
created_by VARCHAR(255) NOT NULL,
|
|
1159
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
1160
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
1161
|
+
)
|
|
1162
|
+
`);
|
|
1163
|
+
this.logger.info('Creating settings table...');
|
|
1164
|
+
// Settings table
|
|
1165
|
+
await this.directQuery(`
|
|
1166
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName('settings')} (
|
|
1167
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1168
|
+
public_url VARCHAR(500),
|
|
1169
|
+
studio_title VARCHAR(255) DEFAULT 'Trokky Studio',
|
|
1170
|
+
default_theme VARCHAR(50) DEFAULT 'system',
|
|
1171
|
+
config JSONB DEFAULT '{}',
|
|
1172
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
1173
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
1174
|
+
updated_by VARCHAR(255)
|
|
1175
|
+
)
|
|
1176
|
+
`);
|
|
1177
|
+
this.logger.info('✅ All database tables and indexes created successfully');
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Get settings from database
|
|
1181
|
+
*/
|
|
1182
|
+
async getSettings() {
|
|
1183
|
+
try {
|
|
1184
|
+
const result = await this.pool.query(`SELECT * FROM ${this.config.tablePrefix}settings WHERE id = $1`, ['studio-settings']);
|
|
1185
|
+
if (result.rows.length === 0) {
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
const row = result.rows[0];
|
|
1189
|
+
return {
|
|
1190
|
+
id: row.id,
|
|
1191
|
+
publicUrl: row.public_url,
|
|
1192
|
+
studioTitle: row.studio_title,
|
|
1193
|
+
organizationName: row.config?.organizationName,
|
|
1194
|
+
primaryColor: row.config?.primaryColor,
|
|
1195
|
+
secondaryColor: row.config?.secondaryColor,
|
|
1196
|
+
logo: row.config?.logo,
|
|
1197
|
+
defaultTheme: row.default_theme,
|
|
1198
|
+
// MFA settings
|
|
1199
|
+
mfaRequired: row.config?.mfaRequired,
|
|
1200
|
+
mfaEnforcedRoles: row.config?.mfaEnforcedRoles,
|
|
1201
|
+
mfaAllowedMethods: row.config?.mfaAllowedMethods,
|
|
1202
|
+
mfaTrustDeviceDays: row.config?.mfaTrustDeviceDays,
|
|
1203
|
+
mfaGracePeriodDays: row.config?.mfaGracePeriodDays,
|
|
1204
|
+
_createdAt: row.created_at.toISOString(),
|
|
1205
|
+
_updatedAt: row.updated_at.toISOString(),
|
|
1206
|
+
_updatedBy: row.updated_by
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
this.logger.error('Failed to get settings', { error });
|
|
1211
|
+
throw error;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Save settings to database
|
|
1216
|
+
*/
|
|
1217
|
+
async saveSettings(settings) {
|
|
1218
|
+
try {
|
|
1219
|
+
// Extract branding and MFA fields for config JSONB
|
|
1220
|
+
const config = {
|
|
1221
|
+
organizationName: settings.organizationName,
|
|
1222
|
+
primaryColor: settings.primaryColor,
|
|
1223
|
+
secondaryColor: settings.secondaryColor,
|
|
1224
|
+
logo: settings.logo,
|
|
1225
|
+
// MFA settings
|
|
1226
|
+
mfaRequired: settings.mfaRequired,
|
|
1227
|
+
mfaEnforcedRoles: settings.mfaEnforcedRoles,
|
|
1228
|
+
mfaAllowedMethods: settings.mfaAllowedMethods,
|
|
1229
|
+
mfaTrustDeviceDays: settings.mfaTrustDeviceDays,
|
|
1230
|
+
mfaGracePeriodDays: settings.mfaGracePeriodDays
|
|
1231
|
+
};
|
|
1232
|
+
await this.query(`INSERT INTO ${this.tableName('settings')}
|
|
1233
|
+
(id, public_url, studio_title, default_theme, config, created_at, updated_at, updated_by)
|
|
1234
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
1235
|
+
ON CONFLICT (id)
|
|
1236
|
+
DO UPDATE SET
|
|
1237
|
+
public_url = EXCLUDED.public_url,
|
|
1238
|
+
studio_title = EXCLUDED.studio_title,
|
|
1239
|
+
default_theme = EXCLUDED.default_theme,
|
|
1240
|
+
config = EXCLUDED.config,
|
|
1241
|
+
updated_at = EXCLUDED.updated_at,
|
|
1242
|
+
updated_by = EXCLUDED.updated_by`, [
|
|
1243
|
+
settings.id || 'studio-settings',
|
|
1244
|
+
settings.publicUrl,
|
|
1245
|
+
settings.studioTitle,
|
|
1246
|
+
settings.defaultTheme,
|
|
1247
|
+
JSON.stringify(config),
|
|
1248
|
+
settings._createdAt || new Date().toISOString(),
|
|
1249
|
+
settings._updatedAt || new Date().toISOString(),
|
|
1250
|
+
settings._updatedBy || 'system'
|
|
1251
|
+
]);
|
|
1252
|
+
this.logger.info('Settings saved successfully', { id: settings.id, config });
|
|
1253
|
+
}
|
|
1254
|
+
catch (error) {
|
|
1255
|
+
this.logger.error('Failed to save settings', { error });
|
|
1256
|
+
throw error;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// ==========================================================================
|
|
1260
|
+
// WEBHOOK OPERATIONS
|
|
1261
|
+
// ==========================================================================
|
|
1262
|
+
async getWebhook(id) {
|
|
1263
|
+
SecurityValidator.validateDocumentId(id);
|
|
1264
|
+
const result = await this.query(`SELECT * FROM ${this.tableName('webhooks')} WHERE id = $1`, [id]);
|
|
1265
|
+
if (result.rows.length === 0) {
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
const row = result.rows[0];
|
|
1269
|
+
return this.mapRowToWebhook(row);
|
|
1270
|
+
}
|
|
1271
|
+
async saveWebhook(id, webhookData) {
|
|
1272
|
+
SecurityValidator.validateDocumentId(id);
|
|
1273
|
+
const now = new Date();
|
|
1274
|
+
try {
|
|
1275
|
+
// Check if this is an update (webhook exists) or create (new webhook)
|
|
1276
|
+
const existingWebhook = await this.getWebhook(id);
|
|
1277
|
+
if (existingWebhook) {
|
|
1278
|
+
// Update existing webhook
|
|
1279
|
+
const result = await this.query(`
|
|
1280
|
+
UPDATE ${this.tableName('webhooks')}
|
|
1281
|
+
SET
|
|
1282
|
+
name = COALESCE($2, name),
|
|
1283
|
+
url = COALESCE($3, url),
|
|
1284
|
+
events = COALESCE($4, events),
|
|
1285
|
+
headers = COALESCE($5, headers),
|
|
1286
|
+
is_active = COALESCE($6, is_active),
|
|
1287
|
+
secret = COALESCE($7, secret),
|
|
1288
|
+
timeout_ms = COALESCE($8, timeout_ms),
|
|
1289
|
+
retry_attempts = COALESCE($9, retry_attempts),
|
|
1290
|
+
updated_at = $10
|
|
1291
|
+
WHERE id = $1
|
|
1292
|
+
RETURNING *
|
|
1293
|
+
`, [
|
|
1294
|
+
id,
|
|
1295
|
+
webhookData.name,
|
|
1296
|
+
webhookData.url,
|
|
1297
|
+
webhookData.events ? JSON.stringify(webhookData.events) : null,
|
|
1298
|
+
webhookData.headers ? JSON.stringify(webhookData.headers) : null,
|
|
1299
|
+
webhookData.active,
|
|
1300
|
+
webhookData.secret,
|
|
1301
|
+
webhookData.retryPolicy?.baseDelay || null,
|
|
1302
|
+
webhookData.retryPolicy?.maxRetries || null,
|
|
1303
|
+
now
|
|
1304
|
+
]);
|
|
1305
|
+
const row = result.rows[0];
|
|
1306
|
+
return this.mapRowToWebhook(row);
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
// Create new webhook
|
|
1310
|
+
const result = await this.query(`
|
|
1311
|
+
INSERT INTO ${this.tableName('webhooks')}
|
|
1312
|
+
(id, name, url, events, headers, is_active, secret, timeout_ms, retry_attempts, created_by, created_at, updated_at)
|
|
1313
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)
|
|
1314
|
+
RETURNING *
|
|
1315
|
+
`, [
|
|
1316
|
+
id,
|
|
1317
|
+
webhookData.name || 'Untitled Webhook',
|
|
1318
|
+
webhookData.url || '',
|
|
1319
|
+
JSON.stringify(webhookData.events || []),
|
|
1320
|
+
JSON.stringify(webhookData.headers || {}),
|
|
1321
|
+
webhookData.active ?? true,
|
|
1322
|
+
webhookData.secret || '',
|
|
1323
|
+
webhookData.retryPolicy?.baseDelay || 5000,
|
|
1324
|
+
webhookData.retryPolicy?.maxRetries || 3,
|
|
1325
|
+
webhookData.createdBy || 'system',
|
|
1326
|
+
now
|
|
1327
|
+
]);
|
|
1328
|
+
const row = result.rows[0];
|
|
1329
|
+
return this.mapRowToWebhook(row);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
this.logger.error('Failed to save webhook', { id, webhookData, error });
|
|
1334
|
+
throw error;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async listWebhooks(options = {}) {
|
|
1338
|
+
const limit = Math.min(options.limit || 50, this.MAX_LIST_LIMIT);
|
|
1339
|
+
const offset = options.offset || 0;
|
|
1340
|
+
let query = `SELECT * FROM ${this.tableName('webhooks')}`;
|
|
1341
|
+
const params = [];
|
|
1342
|
+
const conditions = [];
|
|
1343
|
+
// Add filtering
|
|
1344
|
+
if (options.active !== undefined) {
|
|
1345
|
+
conditions.push(`is_active = $${params.length + 1}`);
|
|
1346
|
+
params.push(options.active);
|
|
1347
|
+
}
|
|
1348
|
+
if (options.createdBy) {
|
|
1349
|
+
conditions.push(`created_by = $${params.length + 1}`);
|
|
1350
|
+
params.push(options.createdBy);
|
|
1351
|
+
}
|
|
1352
|
+
if (options.events && options.events.length > 0) {
|
|
1353
|
+
// Match webhooks that have any of the specified events
|
|
1354
|
+
// Uses JSONB array contains operator
|
|
1355
|
+
conditions.push(`events ?| $${params.length + 1}`);
|
|
1356
|
+
params.push(options.events);
|
|
1357
|
+
}
|
|
1358
|
+
if (conditions.length > 0) {
|
|
1359
|
+
query += ` WHERE ${conditions.join(' AND ')}`;
|
|
1360
|
+
}
|
|
1361
|
+
query += ` ORDER BY created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`;
|
|
1362
|
+
params.push(limit, offset);
|
|
1363
|
+
const result = await this.query(query, params);
|
|
1364
|
+
return result.rows.map((row) => this.mapRowToWebhook(row));
|
|
1365
|
+
}
|
|
1366
|
+
async deleteWebhook(id) {
|
|
1367
|
+
SecurityValidator.validateDocumentId(id);
|
|
1368
|
+
const result = await this.query(`DELETE FROM ${this.tableName('webhooks')} WHERE id = $1`, [id]);
|
|
1369
|
+
if (result.rowCount === 0) {
|
|
1370
|
+
this.logger.debug(`Webhook ${id} does not exist, nothing to delete`);
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
this.logger.info(`Webhook deleted: ${id}`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
mapRowToWebhook(row) {
|
|
1377
|
+
return {
|
|
1378
|
+
id: row.id,
|
|
1379
|
+
name: row.name,
|
|
1380
|
+
url: row.url,
|
|
1381
|
+
events: row.events || [],
|
|
1382
|
+
secret: row.secret || '',
|
|
1383
|
+
active: row.is_active,
|
|
1384
|
+
headers: row.headers || {},
|
|
1385
|
+
retryPolicy: {
|
|
1386
|
+
maxRetries: row.retry_attempts || 3,
|
|
1387
|
+
backoffType: 'exponential',
|
|
1388
|
+
baseDelay: row.timeout_ms || 5000,
|
|
1389
|
+
maxDelay: 30000,
|
|
1390
|
+
retryOnStatus: [500, 502, 503, 504, 408, 429]
|
|
1391
|
+
},
|
|
1392
|
+
createdBy: row.created_by,
|
|
1393
|
+
createdAt: row.created_at,
|
|
1394
|
+
updatedAt: row.updated_at
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* PostgreSQL transaction implementation
|
|
1400
|
+
*/
|
|
1401
|
+
export class PostgresTransaction {
|
|
1402
|
+
client;
|
|
1403
|
+
constructor(client) {
|
|
1404
|
+
this.client = client;
|
|
1405
|
+
}
|
|
1406
|
+
async commit() {
|
|
1407
|
+
try {
|
|
1408
|
+
await this.client.query('COMMIT');
|
|
1409
|
+
}
|
|
1410
|
+
finally {
|
|
1411
|
+
this.client.release();
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async rollback() {
|
|
1415
|
+
try {
|
|
1416
|
+
await this.client.query('ROLLBACK');
|
|
1417
|
+
}
|
|
1418
|
+
finally {
|
|
1419
|
+
this.client.release();
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
async execute(operations) {
|
|
1423
|
+
try {
|
|
1424
|
+
return await operations();
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
await this.rollback();
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
//# sourceMappingURL=postgres-data-adapter.js.map
|