@digimuza/saas-core 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1165 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/api-keys/index.d.ts +9 -0
- package/dist/api-keys/index.d.ts.map +1 -0
- package/dist/api-keys/index.js +37 -0
- package/dist/api-keys/index.js.map +1 -0
- package/dist/api-keys/keys.d.ts +55 -0
- package/dist/api-keys/keys.d.ts.map +1 -0
- package/dist/api-keys/keys.js +92 -0
- package/dist/api-keys/keys.js.map +1 -0
- package/dist/api-keys/service.d.ts +111 -0
- package/dist/api-keys/service.d.ts.map +1 -0
- package/dist/api-keys/service.js +210 -0
- package/dist/api-keys/service.js.map +1 -0
- package/dist/audit/index.d.ts +5 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +44 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/service.d.ts +88 -0
- package/dist/audit/service.d.ts.map +1 -0
- package/dist/audit/service.js +128 -0
- package/dist/audit/service.js.map +1 -0
- package/dist/auth/client.d.ts +5 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +26 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/cookies.d.ts +51 -0
- package/dist/auth/cookies.d.ts.map +1 -0
- package/dist/auth/cookies.js +58 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +46 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/service.d.ts +67 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +275 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/types.d.ts +56 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/authorization/index.d.ts +4 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/authorization/index.js +24 -0
- package/dist/authorization/index.js.map +1 -0
- package/dist/authorization/service.d.ts +92 -0
- package/dist/authorization/service.d.ts.map +1 -0
- package/dist/authorization/service.js +254 -0
- package/dist/authorization/service.js.map +1 -0
- package/dist/billing/index.d.ts +16 -0
- package/dist/billing/index.d.ts.map +1 -0
- package/dist/billing/index.js +47 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/provider.d.ts +114 -0
- package/dist/billing/provider.d.ts.map +1 -0
- package/dist/billing/provider.js +2 -0
- package/dist/billing/provider.js.map +1 -0
- package/dist/billing/revolut.d.ts +68 -0
- package/dist/billing/revolut.d.ts.map +1 -0
- package/dist/billing/revolut.js +181 -0
- package/dist/billing/revolut.js.map +1 -0
- package/dist/billing/service.d.ts +124 -0
- package/dist/billing/service.d.ts.map +1 -0
- package/dist/billing/service.js +470 -0
- package/dist/billing/service.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +43 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +212 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/project.d.ts +12 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +49 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/docker.d.ts +50 -0
- package/dist/cli/docker.d.ts.map +1 -0
- package/dist/cli/docker.js +84 -0
- package/dist/cli/docker.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +198 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +30 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +66 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/templates.d.ts +52 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +225 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/config/client.d.ts +60 -0
- package/dist/config/client.d.ts.map +1 -0
- package/dist/config/client.js +94 -0
- package/dist/config/client.js.map +1 -0
- package/dist/config/config-loader.d.ts +2 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/env.d.ts +24 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +40 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/errors.d.ts +17 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +26 -0
- package/dist/config/errors.js.map +1 -0
- package/dist/config/index.d.ts +72 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +106 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/runtime.d.ts +15 -0
- package/dist/config/runtime.d.ts.map +1 -0
- package/dist/config/runtime.js +22 -0
- package/dist/config/runtime.js.map +1 -0
- package/dist/config/sections.d.ts +226 -0
- package/dist/config/sections.d.ts.map +1 -0
- package/dist/config/sections.js +141 -0
- package/dist/config/sections.js.map +1 -0
- package/dist/db/client.d.ts +43 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +77 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/errors.d.ts +39 -0
- package/dist/db/errors.d.ts.map +1 -0
- package/dist/db/errors.js +67 -0
- package/dist/db/errors.js.map +1 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +10 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +21 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +28 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema/_shared.d.ts +30 -0
- package/dist/db/schema/_shared.d.ts.map +1 -0
- package/dist/db/schema/_shared.js +36 -0
- package/dist/db/schema/_shared.js.map +1 -0
- package/dist/db/schema/apikeys.d.ts +263 -0
- package/dist/db/schema/apikeys.d.ts.map +1 -0
- package/dist/db/schema/apikeys.js +70 -0
- package/dist/db/schema/apikeys.js.map +1 -0
- package/dist/db/schema/authorization.d.ts +465 -0
- package/dist/db/schema/authorization.d.ts.map +1 -0
- package/dist/db/schema/authorization.js +134 -0
- package/dist/db/schema/authorization.js.map +1 -0
- package/dist/db/schema/billing.d.ts +1100 -0
- package/dist/db/schema/billing.d.ts.map +1 -0
- package/dist/db/schema/billing.js +223 -0
- package/dist/db/schema/billing.js.map +1 -0
- package/dist/db/schema/events.d.ts +161 -0
- package/dist/db/schema/events.d.ts.map +1 -0
- package/dist/db/schema/events.js +66 -0
- package/dist/db/schema/events.js.map +1 -0
- package/dist/db/schema/features.d.ts +243 -0
- package/dist/db/schema/features.d.ts.map +1 -0
- package/dist/db/schema/features.js +87 -0
- package/dist/db/schema/features.js.map +1 -0
- package/dist/db/schema/files.d.ts +253 -0
- package/dist/db/schema/files.d.ts.map +1 -0
- package/dist/db/schema/files.js +74 -0
- package/dist/db/schema/files.js.map +1 -0
- package/dist/db/schema/index.d.ts +38 -0
- package/dist/db/schema/index.d.ts.map +1 -0
- package/dist/db/schema/index.js +38 -0
- package/dist/db/schema/index.js.map +1 -0
- package/dist/db/schema/invitations.d.ts +214 -0
- package/dist/db/schema/invitations.d.ts.map +1 -0
- package/dist/db/schema/invitations.js +80 -0
- package/dist/db/schema/invitations.js.map +1 -0
- package/dist/db/schema/messaging.d.ts +442 -0
- package/dist/db/schema/messaging.d.ts.map +1 -0
- package/dist/db/schema/messaging.js +120 -0
- package/dist/db/schema/messaging.js.map +1 -0
- package/dist/db/schema/users.d.ts +246 -0
- package/dist/db/schema/users.d.ts.map +1 -0
- package/dist/db/schema/users.js +60 -0
- package/dist/db/schema/users.js.map +1 -0
- package/dist/db/schema/webhooks.d.ts +693 -0
- package/dist/db/schema/webhooks.d.ts.map +1 -0
- package/dist/db/schema/webhooks.js +147 -0
- package/dist/db/schema/webhooks.js.map +1 -0
- package/dist/db/schema/workspaces.d.ts +196 -0
- package/dist/db/schema/workspaces.d.ts.map +1 -0
- package/dist/db/schema/workspaces.js +57 -0
- package/dist/db/schema/workspaces.js.map +1 -0
- package/dist/db/types.d.ts +18 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/email/adapters.d.ts +29 -0
- package/dist/email/adapters.d.ts.map +1 -0
- package/dist/email/adapters.js +60 -0
- package/dist/email/adapters.js.map +1 -0
- package/dist/email/index.d.ts +20 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +79 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/provider.d.ts +47 -0
- package/dist/email/provider.d.ts.map +1 -0
- package/dist/email/provider.js +7 -0
- package/dist/email/provider.js.map +1 -0
- package/dist/email/resend.d.ts +22 -0
- package/dist/email/resend.d.ts.map +1 -0
- package/dist/email/resend.js +51 -0
- package/dist/email/resend.js.map +1 -0
- package/dist/email/service.d.ts +55 -0
- package/dist/email/service.d.ts.map +1 -0
- package/dist/email/service.js +87 -0
- package/dist/email/service.js.map +1 -0
- package/dist/email/templates.d.ts +81 -0
- package/dist/email/templates.d.ts.map +1 -0
- package/dist/email/templates.js +84 -0
- package/dist/email/templates.js.map +1 -0
- package/dist/entitlements/index.d.ts +10 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +28 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/service.d.ts +83 -0
- package/dist/entitlements/service.d.ts.map +1 -0
- package/dist/entitlements/service.js +158 -0
- package/dist/entitlements/service.js.map +1 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +95 -0
- package/dist/errors.js.map +1 -0
- package/dist/events/core-events.d.ts +120 -0
- package/dist/events/core-events.d.ts.map +1 -0
- package/dist/events/core-events.js +109 -0
- package/dist/events/core-events.js.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +58 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/publisher.d.ts +46 -0
- package/dist/events/publisher.d.ts.map +1 -0
- package/dist/events/publisher.js +54 -0
- package/dist/events/publisher.js.map +1 -0
- package/dist/events/registry.d.ts +87 -0
- package/dist/events/registry.d.ts.map +1 -0
- package/dist/events/registry.js +34 -0
- package/dist/events/registry.js.map +1 -0
- package/dist/events/service.d.ts +78 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/service.js +182 -0
- package/dist/events/service.js.map +1 -0
- package/dist/features/bucket.d.ts +22 -0
- package/dist/features/bucket.d.ts.map +1 -0
- package/dist/features/bucket.js +42 -0
- package/dist/features/bucket.js.map +1 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +78 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +51 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +52 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/service.d.ts +78 -0
- package/dist/features/service.d.ts.map +1 -0
- package/dist/features/service.js +207 -0
- package/dist/features/service.js.map +1 -0
- package/dist/files/index.d.ts +6 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +64 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/service.d.ts +92 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +303 -0
- package/dist/files/service.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/backend.d.ts +23 -0
- package/dist/jobs/backend.d.ts.map +1 -0
- package/dist/jobs/backend.js +23 -0
- package/dist/jobs/backend.js.map +1 -0
- package/dist/jobs/index.d.ts +29 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +114 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/service.d.ts +67 -0
- package/dist/jobs/service.d.ts.map +1 -0
- package/dist/jobs/service.js +139 -0
- package/dist/jobs/service.js.map +1 -0
- package/dist/jobs/types.d.ts +78 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +4 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/logger/default.d.ts +7 -0
- package/dist/logger/default.d.ts.map +1 -0
- package/dist/logger/default.js +28 -0
- package/dist/logger/default.js.map +1 -0
- package/dist/logger/index.d.ts +24 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +90 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/redact.d.ts +40 -0
- package/dist/logger/redact.d.ts.map +1 -0
- package/dist/logger/redact.js +178 -0
- package/dist/logger/redact.js.map +1 -0
- package/dist/logger/service.d.ts +14 -0
- package/dist/logger/service.d.ts.map +1 -0
- package/dist/logger/service.js +56 -0
- package/dist/logger/service.js.map +1 -0
- package/dist/logger/sinks.d.ts +37 -0
- package/dist/logger/sinks.d.ts.map +1 -0
- package/dist/logger/sinks.js +135 -0
- package/dist/logger/sinks.js.map +1 -0
- package/dist/logger/types.d.ts +47 -0
- package/dist/logger/types.d.ts.map +1 -0
- package/dist/logger/types.js +8 -0
- package/dist/logger/types.js.map +1 -0
- package/dist/notifications/index.d.ts +8 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +72 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +79 -0
- package/dist/notifications/service.d.ts.map +1 -0
- package/dist/notifications/service.js +164 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/realtime/channels.d.ts +37 -0
- package/dist/realtime/channels.d.ts.map +1 -0
- package/dist/realtime/channels.js +58 -0
- package/dist/realtime/channels.js.map +1 -0
- package/dist/realtime/index.d.ts +19 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +118 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/postgres.d.ts +48 -0
- package/dist/realtime/postgres.d.ts.map +1 -0
- package/dist/realtime/postgres.js +151 -0
- package/dist/realtime/postgres.js.map +1 -0
- package/dist/realtime/service.d.ts +75 -0
- package/dist/realtime/service.d.ts.map +1 -0
- package/dist/realtime/service.js +103 -0
- package/dist/realtime/service.js.map +1 -0
- package/dist/realtime/sse.d.ts +55 -0
- package/dist/realtime/sse.d.ts.map +1 -0
- package/dist/realtime/sse.js +123 -0
- package/dist/realtime/sse.js.map +1 -0
- package/dist/realtime/transport.d.ts +52 -0
- package/dist/realtime/transport.d.ts.map +1 -0
- package/dist/realtime/transport.js +2 -0
- package/dist/realtime/transport.js.map +1 -0
- package/dist/storage/index.d.ts +18 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +89 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/keys.d.ts +50 -0
- package/dist/storage/keys.d.ts.map +1 -0
- package/dist/storage/keys.js +120 -0
- package/dist/storage/keys.js.map +1 -0
- package/dist/storage/memory.d.ts +32 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +84 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/provider.d.ts +93 -0
- package/dist/storage/provider.d.ts.map +1 -0
- package/dist/storage/provider.js +2 -0
- package/dist/storage/provider.js.map +1 -0
- package/dist/storage/s3.d.ts +34 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +175 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/service.d.ts +86 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +107 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/sigv4.d.ts +58 -0
- package/dist/storage/sigv4.d.ts.map +1 -0
- package/dist/storage/sigv4.js +114 -0
- package/dist/storage/sigv4.js.map +1 -0
- package/dist/streams/client.d.ts +91 -0
- package/dist/streams/client.d.ts.map +1 -0
- package/dist/streams/client.js +128 -0
- package/dist/streams/client.js.map +1 -0
- package/dist/streams/events.d.ts +68 -0
- package/dist/streams/events.d.ts.map +1 -0
- package/dist/streams/events.js +81 -0
- package/dist/streams/events.js.map +1 -0
- package/dist/streams/index.d.ts +47 -0
- package/dist/streams/index.d.ts.map +1 -0
- package/dist/streams/index.js +47 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/realtime.d.ts +27 -0
- package/dist/streams/realtime.d.ts.map +1 -0
- package/dist/streams/realtime.js +50 -0
- package/dist/streams/realtime.js.map +1 -0
- package/dist/streams/state.d.ts +26 -0
- package/dist/streams/state.d.ts.map +1 -0
- package/dist/streams/state.js +34 -0
- package/dist/streams/state.js.map +1 -0
- package/dist/users/index.d.ts +9 -0
- package/dist/users/index.d.ts.map +1 -0
- package/dist/users/index.js +23 -0
- package/dist/users/index.js.map +1 -0
- package/dist/users/service.d.ts +43 -0
- package/dist/users/service.d.ts.map +1 -0
- package/dist/users/service.js +113 -0
- package/dist/users/service.js.map +1 -0
- package/dist/webhooks/inbound.d.ts +70 -0
- package/dist/webhooks/inbound.d.ts.map +1 -0
- package/dist/webhooks/inbound.js +146 -0
- package/dist/webhooks/inbound.js.map +1 -0
- package/dist/webhooks/index.d.ts +12 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +65 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/outgoing.d.ts +89 -0
- package/dist/webhooks/outgoing.d.ts.map +1 -0
- package/dist/webhooks/outgoing.js +375 -0
- package/dist/webhooks/outgoing.js.map +1 -0
- package/dist/webhooks/signature.d.ts +40 -0
- package/dist/webhooks/signature.d.ts.map +1 -0
- package/dist/webhooks/signature.js +77 -0
- package/dist/webhooks/signature.js.map +1 -0
- package/dist/workspaces/context.d.ts +30 -0
- package/dist/workspaces/context.d.ts.map +1 -0
- package/dist/workspaces/context.js +12 -0
- package/dist/workspaces/context.js.map +1 -0
- package/dist/workspaces/index.d.ts +5 -0
- package/dist/workspaces/index.d.ts.map +1 -0
- package/dist/workspaces/index.js +21 -0
- package/dist/workspaces/index.js.map +1 -0
- package/dist/workspaces/service.d.ts +126 -0
- package/dist/workspaces/service.d.ts.map +1 -0
- package/dist/workspaces/service.js +495 -0
- package/dist/workspaces/service.js.map +1 -0
- package/docs/README.md +71 -0
- package/docs/api-keys.md +184 -0
- package/docs/audit.md +198 -0
- package/docs/auth.md +196 -0
- package/docs/authorization.md +225 -0
- package/docs/billing.md +293 -0
- package/docs/cli.md +238 -0
- package/docs/config.md +163 -0
- package/docs/db.md +219 -0
- package/docs/email.md +222 -0
- package/docs/entitlements.md +194 -0
- package/docs/errors.md +158 -0
- package/docs/events.md +279 -0
- package/docs/feature-flags.md +237 -0
- package/docs/files.md +229 -0
- package/docs/jobs.md +288 -0
- package/docs/logger.md +269 -0
- package/docs/notifications.md +179 -0
- package/docs/publishing.md +128 -0
- package/docs/realtime.md +360 -0
- package/docs/storage.md +279 -0
- package/docs/streams.md +296 -0
- package/docs/users.md +117 -0
- package/docs/webhooks.md +272 -0
- package/docs/workspaces.md +269 -0
- package/drizzle/0000_init_users.sql +21 -0
- package/drizzle/0001_workspaces_roles_permissions.sql +86 -0
- package/drizzle/0002_billing_entitlements.sql +97 -0
- package/drizzle/0003_events.sql +17 -0
- package/drizzle/0004_notifications_audit.sql +64 -0
- package/drizzle/0005_api_keys_webhooks.sql +77 -0
- package/drizzle/0006_feature_flags.sql +22 -0
- package/drizzle/0007_files.sql +23 -0
- package/drizzle/meta/0000_snapshot.json +160 -0
- package/drizzle/meta/0001_snapshot.json +837 -0
- package/drizzle/meta/0002_snapshot.json +1580 -0
- package/drizzle/meta/0003_snapshot.json +1751 -0
- package/drizzle/meta/0004_snapshot.json +2109 -0
- package/drizzle/meta/0005_snapshot.json +2719 -0
- package/drizzle/meta/0006_snapshot.json +2909 -0
- package/drizzle/meta/0007_snapshot.json +3113 -0
- package/drizzle/meta/_journal.json +62 -0
- package/package.json +180 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isUniqueViolation } from "../db/errors.ts";
|
|
2
|
+
import type { Notification } from "../db/schema/index.ts";
|
|
3
|
+
import type { CoreDatabase } from "../db/types.ts";
|
|
4
|
+
import type { WorkspacesService } from "../workspaces/service.ts";
|
|
5
|
+
export interface CreateNotificationInput {
|
|
6
|
+
/** The recipient. */
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
/**
|
|
9
|
+
* Tenant this concerns. Verified against membership when given — a
|
|
10
|
+
* notification about a workspace the recipient cannot see would leak its
|
|
11
|
+
* existence.
|
|
12
|
+
*/
|
|
13
|
+
readonly workspaceId?: string | null;
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly body?: string;
|
|
17
|
+
/** Relative, so it survives a domain change. */
|
|
18
|
+
readonly url?: string;
|
|
19
|
+
readonly data?: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Deduplication key, scoped to the recipient and type.
|
|
22
|
+
*
|
|
23
|
+
* Event-driven creation needs it: an event delivered twice, or a handler
|
|
24
|
+
* retried, must not produce two identical notifications.
|
|
25
|
+
*/
|
|
26
|
+
readonly dedupeKey?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ListNotificationsOptions {
|
|
29
|
+
/** Only unread. */
|
|
30
|
+
readonly unreadOnly?: boolean;
|
|
31
|
+
/** Restrict to one workspace, or to account-level notices with `null`. */
|
|
32
|
+
readonly workspaceId?: string | null;
|
|
33
|
+
readonly types?: readonly string[];
|
|
34
|
+
readonly limit?: number;
|
|
35
|
+
/** Keyset pagination: notifications strictly older than this. */
|
|
36
|
+
readonly before?: Date;
|
|
37
|
+
}
|
|
38
|
+
export interface NotificationsService {
|
|
39
|
+
/**
|
|
40
|
+
* Create an in-app notification.
|
|
41
|
+
*
|
|
42
|
+
* Returns the existing row when `dedupeKey` has already been used, so the call
|
|
43
|
+
* is safe to retry.
|
|
44
|
+
*/
|
|
45
|
+
create(input: CreateNotificationInput, tx?: CoreDatabase): Promise<Notification>;
|
|
46
|
+
/** Create several, skipping any that duplicate an existing dedupe key. */
|
|
47
|
+
createMany(inputs: readonly CreateNotificationInput[]): Promise<Notification[]>;
|
|
48
|
+
/**
|
|
49
|
+
* A user's own notifications, newest first.
|
|
50
|
+
*
|
|
51
|
+
* `userId` must come from the session. Every query filters on it, so one user's
|
|
52
|
+
* notifications can never appear in another's list.
|
|
53
|
+
*/
|
|
54
|
+
list(userId: string, options?: ListNotificationsOptions): Promise<Notification[]>;
|
|
55
|
+
countUnread(userId: string, options?: {
|
|
56
|
+
workspaceId?: string | null;
|
|
57
|
+
}): Promise<number>;
|
|
58
|
+
/** Mark specific notifications read. Silently ignores ids the user does not own. */
|
|
59
|
+
markRead(userId: string, notificationIds: readonly string[]): Promise<number>;
|
|
60
|
+
markAllRead(userId: string, options?: {
|
|
61
|
+
workspaceId?: string | null;
|
|
62
|
+
}): Promise<number>;
|
|
63
|
+
/** Remove one of the user's own notifications. */
|
|
64
|
+
remove(userId: string, notificationId: string): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
export interface NotificationsServiceDeps {
|
|
67
|
+
readonly db: CoreDatabase;
|
|
68
|
+
/**
|
|
69
|
+
* Used to verify the recipient belongs to the workspace a notification names.
|
|
70
|
+
*
|
|
71
|
+
* Optional: omit it and workspace membership is not checked, which is only safe
|
|
72
|
+
* when every caller already holds a verified context.
|
|
73
|
+
*/
|
|
74
|
+
readonly workspaces?: Pick<WorkspacesService, "resolveContext">;
|
|
75
|
+
readonly now?: () => Date;
|
|
76
|
+
}
|
|
77
|
+
export declare function createNotificationsService(deps: NotificationsServiceDeps): NotificationsService;
|
|
78
|
+
export { isUniqueViolation };
|
|
79
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/notifications/service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,WAAW,uBAAuB;IACtC,qBAAqB;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,wBAAwB;IACvC,mBAAmB;IACnB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,uBAAuB,EAAE,EAAE,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAEjF,0EAA0E;IAC1E,UAAU,CAAC,MAAM,EAAE,SAAS,uBAAuB,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAEhF;;;;;OAKG;IACH,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAElF,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAExF,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9E,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAExF,kDAAkD;IAClD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;IAChE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAMD,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,wBAAwB,GAC7B,oBAAoB,CAoMtB;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { and, desc, eq, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
2
|
+
import { isUniqueViolation } from "../db/errors.js";
|
|
3
|
+
import { notifications } from "../db/schema/index.js";
|
|
4
|
+
import { InvalidInputError, NotFoundError } from "../errors.js";
|
|
5
|
+
const DEFAULT_LIMIT = 50;
|
|
6
|
+
const MAX_LIMIT = 200;
|
|
7
|
+
const MAX_TITLE_LENGTH = 200;
|
|
8
|
+
export function createNotificationsService(deps) {
|
|
9
|
+
const now = deps.now ?? (() => new Date());
|
|
10
|
+
async function toRow(input) {
|
|
11
|
+
if (input.title.trim().length === 0) {
|
|
12
|
+
throw new InvalidInputError("A notification title is required.");
|
|
13
|
+
}
|
|
14
|
+
if (input.title.length > MAX_TITLE_LENGTH) {
|
|
15
|
+
throw new InvalidInputError(`A notification title may not exceed ${MAX_TITLE_LENGTH} characters.`);
|
|
16
|
+
}
|
|
17
|
+
const workspaceId = input.workspaceId ?? null;
|
|
18
|
+
/**
|
|
19
|
+
* Notifying someone about a workspace they are not in would tell them it
|
|
20
|
+
* exists — and, through the title, something about what happens in it.
|
|
21
|
+
*/
|
|
22
|
+
if (workspaceId !== null && deps.workspaces !== undefined) {
|
|
23
|
+
const context = await deps.workspaces.resolveContext({
|
|
24
|
+
userId: input.userId,
|
|
25
|
+
workspaceId,
|
|
26
|
+
});
|
|
27
|
+
if (context === undefined) {
|
|
28
|
+
throw new NotFoundError("Recipient is not a member of that workspace.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
userId: input.userId,
|
|
33
|
+
workspaceId,
|
|
34
|
+
type: input.type,
|
|
35
|
+
title: input.title,
|
|
36
|
+
body: input.body ?? null,
|
|
37
|
+
url: input.url ?? null,
|
|
38
|
+
data: (input.data ?? null),
|
|
39
|
+
dedupeKey: input.dedupeKey ?? null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Shared by list, count and mark-all: always narrowed to one recipient. */
|
|
43
|
+
function scope(userId, workspaceId) {
|
|
44
|
+
const clauses = [eq(notifications.userId, userId)];
|
|
45
|
+
if (workspaceId === null) {
|
|
46
|
+
clauses.push(isNull(notifications.workspaceId));
|
|
47
|
+
}
|
|
48
|
+
else if (workspaceId !== undefined) {
|
|
49
|
+
clauses.push(eq(notifications.workspaceId, workspaceId));
|
|
50
|
+
}
|
|
51
|
+
return clauses;
|
|
52
|
+
}
|
|
53
|
+
const service = {
|
|
54
|
+
async create(input, tx) {
|
|
55
|
+
const row = await toRow(input);
|
|
56
|
+
const db = tx ?? deps.db;
|
|
57
|
+
const inserted = await db
|
|
58
|
+
.insert(notifications)
|
|
59
|
+
.values(row)
|
|
60
|
+
/**
|
|
61
|
+
* The dedupe index is *partial* (`where dedupe_key is not null`), so the
|
|
62
|
+
* conflict target needs a matching predicate — PostgreSQL refuses to
|
|
63
|
+
* infer a partial index as the arbiter otherwise.
|
|
64
|
+
*/
|
|
65
|
+
.onConflictDoNothing({
|
|
66
|
+
target: [notifications.userId, notifications.type, notifications.dedupeKey],
|
|
67
|
+
where: sql `${notifications.dedupeKey} is not null`,
|
|
68
|
+
})
|
|
69
|
+
.returning();
|
|
70
|
+
const created = inserted[0];
|
|
71
|
+
if (created !== undefined) {
|
|
72
|
+
return created;
|
|
73
|
+
}
|
|
74
|
+
if (input.dedupeKey === undefined) {
|
|
75
|
+
throw new Error("Failed to create notification.");
|
|
76
|
+
}
|
|
77
|
+
// Already created by an earlier delivery; return the same row.
|
|
78
|
+
const existing = await db
|
|
79
|
+
.select()
|
|
80
|
+
.from(notifications)
|
|
81
|
+
.where(and(eq(notifications.userId, input.userId), eq(notifications.type, input.type), eq(notifications.dedupeKey, input.dedupeKey)))
|
|
82
|
+
.limit(1);
|
|
83
|
+
const found = existing[0];
|
|
84
|
+
if (found === undefined) {
|
|
85
|
+
throw new Error("Failed to create notification.");
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
},
|
|
89
|
+
async createMany(inputs) {
|
|
90
|
+
if (inputs.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const rows = await Promise.all(inputs.map((input) => toRow(input)));
|
|
94
|
+
return deps.db
|
|
95
|
+
.insert(notifications)
|
|
96
|
+
.values(rows)
|
|
97
|
+
.onConflictDoNothing({
|
|
98
|
+
target: [notifications.userId, notifications.type, notifications.dedupeKey],
|
|
99
|
+
where: sql `${notifications.dedupeKey} is not null`,
|
|
100
|
+
})
|
|
101
|
+
.returning();
|
|
102
|
+
},
|
|
103
|
+
async list(userId, options = {}) {
|
|
104
|
+
const clauses = scope(userId, options.workspaceId);
|
|
105
|
+
if (options.unreadOnly === true) {
|
|
106
|
+
clauses.push(isNull(notifications.readAt));
|
|
107
|
+
}
|
|
108
|
+
if (options.types !== undefined && options.types.length > 0) {
|
|
109
|
+
clauses.push(inArray(notifications.type, [...options.types]));
|
|
110
|
+
}
|
|
111
|
+
if (options.before !== undefined) {
|
|
112
|
+
clauses.push(lt(notifications.createdAt, options.before));
|
|
113
|
+
}
|
|
114
|
+
return deps.db
|
|
115
|
+
.select()
|
|
116
|
+
.from(notifications)
|
|
117
|
+
.where(and(...clauses))
|
|
118
|
+
// Tie-broken on id so keyset pagination cannot loop on equal timestamps.
|
|
119
|
+
.orderBy(desc(notifications.createdAt), desc(notifications.id))
|
|
120
|
+
.limit(Math.min(options.limit ?? DEFAULT_LIMIT, MAX_LIMIT));
|
|
121
|
+
},
|
|
122
|
+
async countUnread(userId, options = {}) {
|
|
123
|
+
const rows = await deps.db
|
|
124
|
+
.select({ count: sql `count(*)::int` })
|
|
125
|
+
.from(notifications)
|
|
126
|
+
.where(and(...scope(userId, options.workspaceId), isNull(notifications.readAt)));
|
|
127
|
+
return rows[0]?.count ?? 0;
|
|
128
|
+
},
|
|
129
|
+
async markRead(userId, notificationIds) {
|
|
130
|
+
if (notificationIds.length === 0) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const updated = await deps.db
|
|
134
|
+
.update(notifications)
|
|
135
|
+
.set({ readAt: now(), updatedAt: now() })
|
|
136
|
+
.where(and(
|
|
137
|
+
// Scoped to the caller, so an id belonging to someone else matches
|
|
138
|
+
// nothing rather than raising — which would confirm it exists.
|
|
139
|
+
eq(notifications.userId, userId), inArray(notifications.id, [...notificationIds]), isNull(notifications.readAt)))
|
|
140
|
+
.returning({ id: notifications.id });
|
|
141
|
+
return updated.length;
|
|
142
|
+
},
|
|
143
|
+
async markAllRead(userId, options = {}) {
|
|
144
|
+
const updated = await deps.db
|
|
145
|
+
.update(notifications)
|
|
146
|
+
.set({ readAt: now(), updatedAt: now() })
|
|
147
|
+
.where(and(...scope(userId, options.workspaceId), isNull(notifications.readAt)))
|
|
148
|
+
.returning({ id: notifications.id });
|
|
149
|
+
return updated.length;
|
|
150
|
+
},
|
|
151
|
+
async remove(userId, notificationId) {
|
|
152
|
+
const deleted = await deps.db
|
|
153
|
+
.delete(notifications)
|
|
154
|
+
.where(and(eq(notifications.userId, userId), eq(notifications.id, notificationId)))
|
|
155
|
+
.returning({ id: notifications.id });
|
|
156
|
+
if (deleted.length === 0) {
|
|
157
|
+
throw new NotFoundError("Notification not found.");
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
return service;
|
|
162
|
+
}
|
|
163
|
+
export { isUniqueViolation };
|
|
164
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/notifications/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A realtime channel.
|
|
3
|
+
*
|
|
4
|
+
* Only two kinds exist, and both are scoped to something the server can verify:
|
|
5
|
+
* a workspace the caller is a member of, or the caller themselves. There is no
|
|
6
|
+
* global channel and no wildcard — a client that could subscribe to `*` would
|
|
7
|
+
* receive every tenant's traffic, and no amount of downstream filtering makes
|
|
8
|
+
* that safe.
|
|
9
|
+
*/
|
|
10
|
+
export type Channel = {
|
|
11
|
+
readonly kind: "workspace";
|
|
12
|
+
readonly workspaceId: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "user";
|
|
15
|
+
readonly userId: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const WORKSPACE_CHANNEL_PREFIX = "workspace:";
|
|
18
|
+
export declare const USER_CHANNEL_PREFIX = "user:";
|
|
19
|
+
/** Longest channel string this module will ever produce. */
|
|
20
|
+
export declare const MAX_CHANNEL_LENGTH: number;
|
|
21
|
+
export declare function workspaceChannel(workspaceId: string): string;
|
|
22
|
+
export declare function userChannel(userId: string): string;
|
|
23
|
+
export declare function formatChannel(channel: Channel): string;
|
|
24
|
+
/**
|
|
25
|
+
* Parse a channel identifier, or return `undefined` if it is not one.
|
|
26
|
+
*
|
|
27
|
+
* Returning `undefined` rather than throwing is deliberate: a subscriber sending
|
|
28
|
+
* a batch of channels should have the bad ones denied, not the whole request
|
|
29
|
+
* rejected — a client on a stale deploy would otherwise be unable to connect at
|
|
30
|
+
* all.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseChannel(value: string): Channel | undefined;
|
|
33
|
+
/** As {@link parseChannel}, but throws. For server-side callers. */
|
|
34
|
+
export declare function requireChannel(value: string): Channel;
|
|
35
|
+
/** How many channels one subscriber may request. */
|
|
36
|
+
export declare const MAX_CHANNELS_PER_SUBSCRIPTION = 32;
|
|
37
|
+
//# sourceMappingURL=channels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["../../src/realtime/channels.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GACf;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD,eAAO,MAAM,wBAAwB,eAAe,CAAC;AACrD,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAW3C,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,QAAuC,CAAC;AAEvE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAItD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAgB/D;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOrD;AAED,oDAAoD;AACpD,eAAO,MAAM,6BAA6B,KAAK,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { InvalidInputError } from "../errors.js";
|
|
2
|
+
export const WORKSPACE_CHANNEL_PREFIX = "workspace:";
|
|
3
|
+
export const USER_CHANNEL_PREFIX = "user:";
|
|
4
|
+
/**
|
|
5
|
+
* Channel identifiers embed UUIDs, so the format is checked rather than trusted.
|
|
6
|
+
*
|
|
7
|
+
* A client supplies channel names directly, which makes them untrusted input:
|
|
8
|
+
* without this, `workspace:../../admin` or `workspace:%` would reach the
|
|
9
|
+
* authorizer and the transport as opaque strings.
|
|
10
|
+
*/
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
/** Longest channel string this module will ever produce. */
|
|
13
|
+
export const MAX_CHANNEL_LENGTH = WORKSPACE_CHANNEL_PREFIX.length + 36;
|
|
14
|
+
export function workspaceChannel(workspaceId) {
|
|
15
|
+
return `${WORKSPACE_CHANNEL_PREFIX}${workspaceId}`;
|
|
16
|
+
}
|
|
17
|
+
export function userChannel(userId) {
|
|
18
|
+
return `${USER_CHANNEL_PREFIX}${userId}`;
|
|
19
|
+
}
|
|
20
|
+
export function formatChannel(channel) {
|
|
21
|
+
return channel.kind === "workspace"
|
|
22
|
+
? workspaceChannel(channel.workspaceId)
|
|
23
|
+
: userChannel(channel.userId);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a channel identifier, or return `undefined` if it is not one.
|
|
27
|
+
*
|
|
28
|
+
* Returning `undefined` rather than throwing is deliberate: a subscriber sending
|
|
29
|
+
* a batch of channels should have the bad ones denied, not the whole request
|
|
30
|
+
* rejected — a client on a stale deploy would otherwise be unable to connect at
|
|
31
|
+
* all.
|
|
32
|
+
*/
|
|
33
|
+
export function parseChannel(value) {
|
|
34
|
+
if (value.length > MAX_CHANNEL_LENGTH) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (value.startsWith(WORKSPACE_CHANNEL_PREFIX)) {
|
|
38
|
+
const workspaceId = value.slice(WORKSPACE_CHANNEL_PREFIX.length);
|
|
39
|
+
return UUID.test(workspaceId) ? { kind: "workspace", workspaceId } : undefined;
|
|
40
|
+
}
|
|
41
|
+
if (value.startsWith(USER_CHANNEL_PREFIX)) {
|
|
42
|
+
const userId = value.slice(USER_CHANNEL_PREFIX.length);
|
|
43
|
+
return UUID.test(userId) ? { kind: "user", userId } : undefined;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** As {@link parseChannel}, but throws. For server-side callers. */
|
|
48
|
+
export function requireChannel(value) {
|
|
49
|
+
const channel = parseChannel(value);
|
|
50
|
+
if (channel === undefined) {
|
|
51
|
+
// The value is echoed because a server-side caller built it themselves.
|
|
52
|
+
throw new InvalidInputError(`"${value}" is not a valid realtime channel.`);
|
|
53
|
+
}
|
|
54
|
+
return channel;
|
|
55
|
+
}
|
|
56
|
+
/** How many channels one subscriber may request. */
|
|
57
|
+
export const MAX_CHANNELS_PER_SUBSCRIPTION = 32;
|
|
58
|
+
//# sourceMappingURL=channels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/realtime/channels.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAejD,MAAM,CAAC,MAAM,wBAAwB,GAAG,YAAY,CAAC;AACrD,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,IAAI,GAAG,4EAA4E,CAAC;AAE1F,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC,MAAM,GAAG,EAAE,CAAC;AAEvE,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,GAAG,wBAAwB,GAAG,WAAW,EAAE,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,GAAG,mBAAmB,GAAG,MAAM,EAAE,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO,OAAO,CAAC,IAAI,KAAK,WAAW;QACjC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC;QACvC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,KAAK,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;QACtC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,CAAC,wBAAwB,CAAC,EAAE,CAAC;QAC/C,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,wBAAwB,CAAC,MAAM,CAAC,CAAC;QACjE,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACjF,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAClE,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,wEAAwE;QACxE,MAAM,IAAI,iBAAiB,CAAC,IAAI,KAAK,oCAAoC,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,oDAAoD;AACpD,MAAM,CAAC,MAAM,6BAA6B,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RealtimeService } from "./service.ts";
|
|
2
|
+
import type { RealtimeTransport } from "./transport.ts";
|
|
3
|
+
export { formatChannel, MAX_CHANNELS_PER_SUBSCRIPTION, MAX_CHANNEL_LENGTH, parseChannel, requireChannel, USER_CHANNEL_PREFIX, userChannel, WORKSPACE_CHANNEL_PREFIX, workspaceChannel, type Channel, } from "./channels.ts";
|
|
4
|
+
export { createPostgresTransport, decodeMessage, encodeMessage, PG_NOTIFY_CHANNEL, postgresJsNotifyClient, type NotifyCapableClient, type PostgresTransportOptions, } from "./postgres.ts";
|
|
5
|
+
export { createRealtimeService, type AuthorizedChannels, type PublishOptions, type RealtimeService, type RealtimeServiceDeps, type SubscribeInput, } from "./service.ts";
|
|
6
|
+
export { createEventStream, type EventStream, type EventStreamOptions, } from "./sse.ts";
|
|
7
|
+
export type { RealtimeMessage, RealtimeSubscription, RealtimeTransport, } from "./transport.ts";
|
|
8
|
+
/** Replace the default transport. Call before first use. */
|
|
9
|
+
export declare function setDefaultRealtimeTransport(transport: RealtimeTransport | undefined): void;
|
|
10
|
+
/**
|
|
11
|
+
* Whether an external transport is configured.
|
|
12
|
+
*
|
|
13
|
+
* Exposed so an application can decide at boot whether to keep the PostgreSQL
|
|
14
|
+
* transport or construct its own from `config.realtime`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasExternalRealtimeConfig(): boolean;
|
|
17
|
+
/** Default service, bound to the PostgreSQL transport and verified membership. */
|
|
18
|
+
export declare const realtime: RealtimeService;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAwDA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EACL,aAAa,EACb,6BAA6B,EAC7B,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,wBAAwB,EACxB,gBAAgB,EAChB,KAAK,OAAO,GACb,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,GAC9B,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAkCxB,4DAA4D;AAC5D,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,CAE1F;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAOnD;AAED,kFAAkF;AAClF,eAAO,MAAM,QAAQ,EAAE,eAQrB,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public realtime surface: `@digimuza/saas-core/realtime`. Server-only.
|
|
3
|
+
*
|
|
4
|
+
* Realtime **delivers** events to connected clients. It is not the source of
|
|
5
|
+
* truth: the durable record is the `events` table, and a client that missed a
|
|
6
|
+
* message re-reads from it. That ordering is the whole design —
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* PostgreSQL → application events → realtime transport → browser
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* — and it is why a failed delivery never fails the operation that published it.
|
|
13
|
+
*
|
|
14
|
+
* ## Wiring
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { events } from "@digimuza/saas-core/events";
|
|
18
|
+
* import { realtime } from "@digimuza/saas-core/realtime";
|
|
19
|
+
*
|
|
20
|
+
* realtime.attach(events); // once at startup
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ## Serving a stream
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* // app/api/realtime/route.ts
|
|
27
|
+
* import { createEventStream } from "@digimuza/saas-core/realtime";
|
|
28
|
+
*
|
|
29
|
+
* export async function GET(request: Request) {
|
|
30
|
+
* const user = await auth.requireUser();
|
|
31
|
+
* const stream = await createEventStream(realtime, {
|
|
32
|
+
* userId: user.id,
|
|
33
|
+
* channels: new URL(request.url).searchParams.getAll("channel"),
|
|
34
|
+
* signal: request.signal,
|
|
35
|
+
* });
|
|
36
|
+
* return new Response(stream.body, { headers: stream.headers });
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ## Authorization
|
|
41
|
+
*
|
|
42
|
+
* Channels are named `workspace:<uuid>` and `user:<uuid>`, and there is no
|
|
43
|
+
* wildcard. A client names the channels it wants, so those names are untrusted:
|
|
44
|
+
* every one is parsed, then checked — workspace channels against
|
|
45
|
+
* `workspace_members`, user channels against the session's own id. Unauthorized
|
|
46
|
+
* channels are dropped, and the refusal never distinguishes "not a member" from
|
|
47
|
+
* "no such workspace", or channel probing would enumerate tenants.
|
|
48
|
+
*
|
|
49
|
+
* Browsers never see `LISTEN`/`NOTIFY`. The server holds the listening connection
|
|
50
|
+
* and re-emits only what a given subscriber was granted.
|
|
51
|
+
*/
|
|
52
|
+
import { config } from "../config/index.js";
|
|
53
|
+
import { getDatabaseHandle } from "../db/client.js";
|
|
54
|
+
import { getDefaultLogger } from "../logger/default.js";
|
|
55
|
+
import { workspaces } from "../workspaces/index.js";
|
|
56
|
+
import { createPostgresTransport } from "./postgres.js";
|
|
57
|
+
import { createRealtimeService } from "./service.js";
|
|
58
|
+
export { formatChannel, MAX_CHANNELS_PER_SUBSCRIPTION, MAX_CHANNEL_LENGTH, parseChannel, requireChannel, USER_CHANNEL_PREFIX, userChannel, WORKSPACE_CHANNEL_PREFIX, workspaceChannel, } from "./channels.js";
|
|
59
|
+
export { createPostgresTransport, decodeMessage, encodeMessage, PG_NOTIFY_CHANNEL, postgresJsNotifyClient, } from "./postgres.js";
|
|
60
|
+
export { createRealtimeService, } from "./service.js";
|
|
61
|
+
export { createEventStream, } from "./sse.js";
|
|
62
|
+
/**
|
|
63
|
+
* The default transport: PostgreSQL `LISTEN`/`NOTIFY` over the existing pool.
|
|
64
|
+
*
|
|
65
|
+
* Chosen because it adds no infrastructure — the database is already there, and
|
|
66
|
+
* the spec is explicit about preferring PostgreSQL where PostgreSQL is enough.
|
|
67
|
+
* `REALTIME_URL` and `REALTIME_API_KEY` exist for a deployment that outgrows it
|
|
68
|
+
* and swaps in a broker; this transport ignores them.
|
|
69
|
+
*/
|
|
70
|
+
let defaultTransport;
|
|
71
|
+
function getTransport() {
|
|
72
|
+
if (defaultTransport === undefined) {
|
|
73
|
+
const handle = getDatabaseHandle();
|
|
74
|
+
/**
|
|
75
|
+
* postgres.js exposes `listen` and `notify` on the client itself, so no
|
|
76
|
+
* separate connection is configured here — the driver already dedicates one
|
|
77
|
+
* to listening.
|
|
78
|
+
*/
|
|
79
|
+
const client = handle.client;
|
|
80
|
+
defaultTransport = createPostgresTransport({
|
|
81
|
+
client,
|
|
82
|
+
onError: (error) => {
|
|
83
|
+
getDefaultLogger().error("realtime transport error", { error });
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return defaultTransport;
|
|
88
|
+
}
|
|
89
|
+
/** Replace the default transport. Call before first use. */
|
|
90
|
+
export function setDefaultRealtimeTransport(transport) {
|
|
91
|
+
defaultTransport = transport;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether an external transport is configured.
|
|
95
|
+
*
|
|
96
|
+
* Exposed so an application can decide at boot whether to keep the PostgreSQL
|
|
97
|
+
* transport or construct its own from `config.realtime`.
|
|
98
|
+
*/
|
|
99
|
+
export function hasExternalRealtimeConfig() {
|
|
100
|
+
try {
|
|
101
|
+
return config.realtime.url.length > 0;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Not configured at all, which is the normal case for the default transport.
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Default service, bound to the PostgreSQL transport and verified membership. */
|
|
109
|
+
export const realtime = createRealtimeService({
|
|
110
|
+
get transport() {
|
|
111
|
+
return getTransport();
|
|
112
|
+
},
|
|
113
|
+
workspaces,
|
|
114
|
+
onError: (error) => {
|
|
115
|
+
getDefaultLogger().error("realtime delivery error", { error });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAA4B,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAAwB,MAAM,cAAc,CAAC;AAG3E,OAAO,EACL,aAAa,EACb,6BAA6B,EAC7B,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,WAAW,EACX,wBAAwB,EACxB,gBAAgB,GAEjB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,sBAAsB,GAGvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,qBAAqB,GAMtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,GAGlB,MAAM,UAAU,CAAC;AAQlB;;;;;;;GAOG;AACH,IAAI,gBAA+C,CAAC;AAEpD,SAAS,YAAY;IACnB,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;QAEnC;;;;WAIG;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,MAAwC,CAAC;QAE/D,gBAAgB,GAAG,uBAAuB,CAAC;YACzC,MAAM;YACN,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;gBACjB,gBAAgB,EAAE,CAAC,KAAK,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAClE,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,gBAAgB,CAAC;AAC1B,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,2BAA2B,CAAC,SAAwC;IAClF,gBAAgB,GAAG,SAAS,CAAC;AAC/B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB;IACvC,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;QAC7E,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,QAAQ,GAAoB,qBAAqB,CAAC;IAC7D,IAAI,SAAS;QACX,OAAO,YAAY,EAAE,CAAC;IACxB,CAAC;IACD,UAAU;IACV,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;QACjB,gBAAgB,EAAE,CAAC,KAAK,CAAC,yBAAyB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACjE,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RealtimeMessage, RealtimeTransport } from "./transport.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The single PostgreSQL notification channel every message travels on.
|
|
4
|
+
*
|
|
5
|
+
* One PG channel rather than one per application channel, because `LISTEN` is
|
|
6
|
+
* per-connection and per-name: a workspace-per-channel design would mean
|
|
7
|
+
* thousands of `LISTEN` statements and constant `LISTEN`/`UNLISTEN` churn as
|
|
8
|
+
* clients come and go. Instead every server sees every message and filters to the
|
|
9
|
+
* channels it has authorized subscribers for.
|
|
10
|
+
*
|
|
11
|
+
* The trade-off is real: fan-out is O(servers), so at large scale every node
|
|
12
|
+
* decodes traffic it discards. That is the point at which to swap in a broker —
|
|
13
|
+
* which is why {@link RealtimeTransport} exists.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PG_NOTIFY_CHANNEL = "saas_core_realtime";
|
|
16
|
+
/** Anything a postgres.js or PGlite handle can do that this transport needs. */
|
|
17
|
+
export interface NotifyCapableClient {
|
|
18
|
+
listen(channel: string, onNotify: (payload: string) => void): Promise<unknown>;
|
|
19
|
+
unlisten?(channel: string, onNotify?: (payload: string) => void): Promise<unknown>;
|
|
20
|
+
/** Parameterised `select pg_notify($1, $2)`. */
|
|
21
|
+
notify(channel: string, payload: string): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface PostgresTransportOptions {
|
|
24
|
+
readonly client: NotifyCapableClient;
|
|
25
|
+
/** Override the notification channel, e.g. to separate environments. */
|
|
26
|
+
readonly notifyChannel?: string;
|
|
27
|
+
/** Reported when a payload cannot be delivered at all. */
|
|
28
|
+
readonly onError?: (error: unknown) => void;
|
|
29
|
+
}
|
|
30
|
+
/** Shrink an over-large message rather than failing the notification. */
|
|
31
|
+
export declare function encodeMessage(message: RealtimeMessage, maxBytes?: number): string;
|
|
32
|
+
export declare function decodeMessage(payload: string): RealtimeMessage | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Realtime over PostgreSQL `LISTEN`/`NOTIFY`.
|
|
35
|
+
*
|
|
36
|
+
* The default transport because it adds no infrastructure: the database is
|
|
37
|
+
* already there. Browsers never touch `LISTEN`/`NOTIFY` — the server holds the
|
|
38
|
+
* listening connection and re-emits only what a given subscriber is authorized
|
|
39
|
+
* for, over SSE.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createPostgresTransport(options: PostgresTransportOptions): RealtimeTransport;
|
|
42
|
+
/** Adapt a postgres.js `Sql` instance to {@link NotifyCapableClient}. */
|
|
43
|
+
export declare function postgresJsNotifyClient(sql: {
|
|
44
|
+
listen: (channel: string, onNotify: (value: string) => void) => unknown;
|
|
45
|
+
unsafe?: unknown;
|
|
46
|
+
notify?: (channel: string, payload: string) => unknown;
|
|
47
|
+
}): NotifyCapableClient;
|
|
48
|
+
//# sourceMappingURL=postgres.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/realtime/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAwB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE/F;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAStD,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAClC,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,QAAQ,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,gDAAgD;IAChD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,wEAAwE;IACxE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,SAAoB,GAAG,MAAM,CAQ5F;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAuB1E;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CA2F5F;AAED,yEAAyE;AACzE,wBAAgB,sBAAsB,CAAC,GAAG,EAAE;IAC1C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;CACxD,GAAG,mBAAmB,CAUtB"}
|