@nextsparkjs/core 0.1.0-beta.189 → 0.1.0-beta.190
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/components/api/keys/CreateApiKeyDialog.d.ts.map +1 -1
- package/dist/components/api/keys/CreateApiKeyDialog.js +2 -1
- package/dist/components/app/layouts/PublicFooter.d.ts.map +1 -1
- package/dist/components/app/layouts/PublicFooter.js +2 -23
- package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
- package/dist/components/app/misc/ThemeToggle.js +2 -1
- package/dist/components/auth/forms/LoginForm.js +1 -1
- package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
- package/dist/components/auth/forms/SignupForm.js +2 -1
- package/dist/components/auth/pages/AuthErrorPage.d.ts.map +1 -1
- package/dist/components/auth/pages/AuthErrorPage.js +2 -1
- package/dist/components/billing/ManageBillingButton.d.ts.map +1 -1
- package/dist/components/billing/ManageBillingButton.js +2 -1
- package/dist/components/dashboard/block-editor/array-field.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/array-field.js +2 -1
- package/dist/components/dashboard/block-editor/block-picker.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/block-picker.js +2 -1
- package/dist/components/dashboard/block-editor/builder-editor-view.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/builder-editor-view.js +3 -2
- package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/config-panel.js +3 -2
- package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/dynamic-form.js +2 -1
- package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
- package/dist/components/dashboard/block-editor/pattern-reference-preview.d.ts.map +1 -1
- package/dist/components/dashboard/block-editor/pattern-reference-preview.js +2 -1
- package/dist/components/dashboard/layouts/TopNavbar.d.ts.map +1 -1
- package/dist/components/dashboard/layouts/TopNavbar.js +4 -3
- package/dist/components/dashboard/misc/SearchDropdown.d.ts.map +1 -1
- package/dist/components/dashboard/misc/SearchDropdown.js +2 -1
- package/dist/components/dashboard/mobile/MobileTopBar.d.ts.map +1 -1
- package/dist/components/dashboard/mobile/MobileTopBar.js +2 -1
- package/dist/components/devtools/BlockDetailViewer.d.ts.map +1 -1
- package/dist/components/devtools/BlockDetailViewer.js +2 -1
- package/dist/components/devtools/BlocksViewer.d.ts.map +1 -1
- package/dist/components/devtools/BlocksViewer.js +2 -1
- package/dist/components/devtools/ConfigViewer.d.ts.map +1 -1
- package/dist/components/devtools/ConfigViewer.js +3 -2
- package/dist/components/devtools/TestCasesViewer.d.ts.map +1 -1
- package/dist/components/devtools/TestCasesViewer.js +3 -2
- package/dist/components/devtools/TestCoverageDashboard.d.ts.map +1 -1
- package/dist/components/devtools/TestCoverageDashboard.js +2 -1
- package/dist/components/devtools/api-explorer/ApiDocsModal.d.ts.map +1 -1
- package/dist/components/devtools/api-explorer/ApiDocsModal.js +0 -0
- package/dist/components/devtools/api-explorer/ApiExplorer.d.ts.map +1 -1
- package/dist/components/devtools/api-explorer/ApiExplorer.js +2 -1
- package/dist/components/devtools/scheduled-actions/actions-table.d.ts.map +1 -1
- package/dist/components/devtools/scheduled-actions/actions-table.js +3 -2
- package/dist/components/docs/docs-sidebar.d.ts.map +1 -1
- package/dist/components/docs/docs-sidebar.js +3 -2
- package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
- package/dist/components/entities/EntityFieldRenderer.js +2 -1
- package/dist/components/entities/EntityList.d.ts.map +1 -1
- package/dist/components/entities/EntityList.js +2 -1
- package/dist/components/entities/EntityTable.d.ts.map +1 -1
- package/dist/components/entities/EntityTable.js +3 -2
- package/dist/components/entities/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/entities/ErrorBoundary.js +2 -1
- package/dist/components/entities/wrappers/EntityDetailWrapper.d.ts.map +1 -1
- package/dist/components/entities/wrappers/EntityDetailWrapper.js +3 -2
- package/dist/components/media/MediaCard.d.ts.map +1 -1
- package/dist/components/media/MediaCard.js +2 -1
- package/dist/components/media/MediaDetailPanel.d.ts.map +1 -1
- package/dist/components/media/MediaDetailPanel.js +2 -1
- package/dist/components/media/MediaList.d.ts.map +1 -1
- package/dist/components/media/MediaList.js +2 -1
- package/dist/components/media/MediaSelector.d.ts.map +1 -1
- package/dist/components/media/MediaSelector.js +2 -1
- package/dist/components/media/MediaUploadZone.d.ts.map +1 -1
- package/dist/components/media/MediaUploadZone.js +2 -1
- package/dist/components/public/entities/PublicEntityDetail.d.ts.map +1 -1
- package/dist/components/public/entities/PublicEntityDetail.js +2 -1
- package/dist/components/teams/TeamPendingInvitations.d.ts.map +1 -1
- package/dist/components/teams/TeamPendingInvitations.js +3 -2
- package/dist/components/ui/accordion.js +1 -1
- package/dist/components/ui/audio-upload.d.ts.map +1 -1
- package/dist/components/ui/audio-upload.js +2 -1
- package/dist/components/ui/avatar.d.ts +17 -1
- package/dist/components/ui/avatar.d.ts.map +1 -1
- package/dist/components/ui/avatar.js +29 -2
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.d.ts +13 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -1
- package/dist/components/ui/breadcrumb.js +23 -10
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/file-upload.d.ts.map +1 -1
- package/dist/components/ui/file-upload.js +2 -1
- package/dist/components/ui/image-upload.d.ts.map +1 -1
- package/dist/components/ui/image-upload.js +4 -3
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/label.js +1 -1
- package/dist/components/ui/optimized-image.d.ts.map +1 -1
- package/dist/components/ui/optimized-image.js +5 -0
- package/dist/components/ui/pagination.d.ts +1 -1
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/pagination.js +3 -0
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/relation-display.d.ts.map +1 -1
- package/dist/components/ui/relation-display.js +3 -2
- package/dist/components/ui/separator.js +1 -1
- package/dist/components/ui/simple-relation-select.d.ts.map +1 -1
- package/dist/components/ui/simple-relation-select.js +7 -6
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/slider.js +1 -1
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/user-display.d.ts.map +1 -1
- package/dist/components/ui/user-display.js +2 -1
- package/dist/components/ui/user-select.d.ts.map +1 -1
- package/dist/components/ui/user-select.js +2 -1
- package/dist/components/ui/video-upload.d.ts.map +1 -1
- package/dist/components/ui/video-upload.js +3 -2
- package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
- package/dist/components/users/tables/UsersTable.js +3 -2
- package/dist/contexts/SubscriptionContext.d.ts.map +1 -1
- package/dist/contexts/SubscriptionContext.js +4 -3
- package/dist/contexts/TeamContext.d.ts.map +1 -1
- package/dist/contexts/TeamContext.js +3 -2
- package/dist/hooks/useAccountPreferencesSync.d.ts.map +1 -1
- package/dist/hooks/useAccountPreferencesSync.js +2 -1
- package/dist/hooks/useApiExplorerNavigation.d.ts.map +1 -1
- package/dist/hooks/useApiExplorerNavigation.js +2 -1
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +3 -2
- package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
- package/dist/hooks/useEnsureUserMetadata.js +3 -2
- package/dist/hooks/useEntityWithMeta.d.ts.map +1 -1
- package/dist/hooks/useEntityWithMeta.js +3 -2
- package/dist/hooks/useInvoice.d.ts.map +1 -1
- package/dist/hooks/useInvoice.js +2 -1
- package/dist/hooks/useInvoices.d.ts.map +1 -1
- package/dist/hooks/useInvoices.js +2 -1
- package/dist/hooks/useMedia.d.ts.map +1 -1
- package/dist/hooks/useMedia.js +10 -9
- package/dist/hooks/useMediaUpload.d.ts.map +1 -1
- package/dist/hooks/useMediaUpload.js +2 -1
- package/dist/hooks/useMembership.d.ts.map +1 -1
- package/dist/hooks/useMembership.js +3 -2
- package/dist/hooks/usePatternUsages.d.ts.map +1 -1
- package/dist/hooks/usePatternUsages.js +3 -2
- package/dist/hooks/usePrefetch.d.ts.map +1 -1
- package/dist/hooks/usePrefetch.js +3 -2
- package/dist/hooks/useQuota.d.ts.map +1 -1
- package/dist/hooks/useQuota.js +2 -1
- package/dist/hooks/useTeamInvitations.d.ts.map +1 -1
- package/dist/hooks/useTeamInvitations.js +4 -3
- package/dist/hooks/useTeamMembers.d.ts.map +1 -1
- package/dist/hooks/useTeamMembers.js +5 -4
- package/dist/hooks/useTeamUpdate.d.ts.map +1 -1
- package/dist/hooks/useTeamUpdate.js +2 -1
- package/dist/hooks/useUserProfile.d.ts.map +1 -1
- package/dist/hooks/useUserProfile.js +3 -2
- package/dist/hooks/useUserSettings.d.ts.map +1 -1
- package/dist/hooks/useUserSettings.js +3 -2
- package/dist/lib/actions/entity.actions.d.ts.map +1 -1
- package/dist/lib/actions/entity.actions.js +2 -1
- package/dist/lib/api/entities.d.ts.map +1 -1
- package/dist/lib/api/entities.js +4 -3
- package/dist/lib/auth/verify-email-link.d.ts +10 -0
- package/dist/lib/auth/verify-email-link.d.ts.map +1 -0
- package/dist/lib/auth/verify-email-link.js +14 -0
- package/dist/lib/auth-client.d.ts +2 -0
- package/dist/lib/auth-client.d.ts.map +1 -1
- package/dist/lib/auth-client.js +4 -0
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +10 -2
- package/dist/lib/base-path.d.ts +85 -0
- package/dist/lib/base-path.d.ts.map +1 -0
- package/dist/lib/base-path.js +79 -0
- package/dist/lib/billing/schema.d.ts +1 -1
- package/dist/lib/billing/schema.d.ts.map +1 -1
- package/dist/lib/billing/schema.js +1 -1
- package/dist/lib/blocks/sanitize-html.d.ts.map +1 -1
- package/dist/lib/blocks/sanitize-html.js +18 -2
- package/dist/lib/cache/revalidate-tag.d.ts +14 -0
- package/dist/lib/cache/revalidate-tag.d.ts.map +1 -0
- package/dist/lib/cache/revalidate-tag.js +8 -0
- package/dist/lib/config/dashboard-schema.d.ts +1 -1
- package/dist/lib/config/dashboard-schema.d.ts.map +1 -1
- package/dist/lib/config/dashboard-schema.js +1 -1
- package/dist/lib/config/types.d.ts +36 -16
- package/dist/lib/config/types.d.ts.map +1 -1
- package/dist/lib/docs/access.d.ts +28 -0
- package/dist/lib/docs/access.d.ts.map +1 -0
- package/dist/lib/docs/access.js +16 -0
- package/dist/lib/docs/parser.d.ts.map +1 -1
- package/dist/lib/docs/parser.js +2 -1
- package/dist/lib/docs/remark-doc-links.d.ts +14 -0
- package/dist/lib/docs/remark-doc-links.d.ts.map +1 -0
- package/dist/lib/docs/remark-doc-links.js +38 -0
- package/dist/lib/entities/api-generator.d.ts +1 -1
- package/dist/lib/entities/api-generator.d.ts.map +1 -1
- package/dist/lib/entities/api-generator.js +1 -1
- package/dist/lib/entities/schema-generator.d.ts +1 -1
- package/dist/lib/entities/schema-generator.d.ts.map +1 -1
- package/dist/lib/entities/schema-generator.js +1 -1
- package/dist/lib/mcp/engine.js +1 -1
- package/dist/lib/mcp/schema-builder.d.ts +1 -1
- package/dist/lib/mcp/schema-builder.d.ts.map +1 -1
- package/dist/lib/mcp/schema-builder.js +1 -1
- package/dist/lib/mcp/tool-generator.js +1 -1
- package/dist/lib/mcp/types.d.ts +1 -1
- package/dist/lib/mcp/types.d.ts.map +1 -1
- package/dist/lib/media/schemas.d.ts +1 -1
- package/dist/lib/media/schemas.d.ts.map +1 -1
- package/dist/lib/media/schemas.js +1 -1
- package/dist/lib/middleware/index.d.ts +5 -1
- package/dist/lib/middleware/index.d.ts.map +1 -1
- package/dist/lib/middleware/index.js +3 -2
- package/dist/lib/services/meta.service.d.ts +60 -0
- package/dist/lib/services/meta.service.d.ts.map +1 -1
- package/dist/lib/services/meta.service.js +132 -12
- package/dist/lib/services/middleware.service.d.ts.map +1 -1
- package/dist/lib/services/middleware.service.js +3 -2
- package/dist/lib/teams/schema.d.ts +1 -1
- package/dist/lib/teams/schema.d.ts.map +1 -1
- package/dist/lib/teams/schema.js +1 -1
- package/dist/lib/teams/teams.api.d.ts.map +1 -1
- package/dist/lib/teams/teams.api.js +4 -3
- package/dist/lib/user-data-client.d.ts.map +1 -1
- package/dist/lib/user-data-client.js +3 -2
- package/dist/lib/validation/invoices.d.ts +1 -1
- package/dist/lib/validation/invoices.d.ts.map +1 -1
- package/dist/lib/validation/invoices.js +1 -1
- package/dist/lib/validation-schemas.d.ts +1 -1
- package/dist/lib/validation-schemas.d.ts.map +1 -1
- package/dist/lib/validation-schemas.js +1 -1
- package/dist/lib/validation.d.ts +1 -1
- package/dist/lib/validation.d.ts.map +1 -1
- package/dist/lib/validation.js +1 -1
- package/dist/messages/de/footer.json +0 -1
- package/dist/messages/de/index.d.ts +0 -1
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/en/footer.json +0 -1
- package/dist/messages/en/index.d.ts +0 -1
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/es/footer.json +0 -1
- package/dist/messages/es/index.d.ts +0 -1
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/fr/footer.json +0 -1
- package/dist/messages/fr/index.d.ts +0 -1
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/it/footer.json +0 -1
- package/dist/messages/it/index.d.ts +0 -1
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/pt/footer.json +0 -1
- package/dist/messages/pt/index.d.ts +0 -1
- package/dist/messages/pt/index.d.ts.map +1 -1
- package/dist/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
- package/dist/templates/app/(auth)/verify-email/page.tsx +4 -3
- package/dist/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
- package/dist/templates/app/api/auth/[...all]/route.ts +15 -14
- package/dist/templates/app/api/internal/user-metadata/route.ts +6 -8
- package/dist/templates/app/api/user/plan-flags/route.ts +1 -1
- package/dist/templates/app/api/user/profile/route.ts +12 -17
- package/dist/templates/app/api/v1/api-keys/route.ts +1 -1
- package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
- package/dist/templates/app/api/v1/billing/cancel/route.ts +1 -1
- package/dist/templates/app/api/v1/billing/change-plan/route.ts +1 -1
- package/dist/templates/app/api/v1/billing/check-action/route.ts +1 -1
- package/dist/templates/app/api/v1/billing/checkout/route.ts +1 -1
- package/dist/templates/app/api/v1/blocks/validate/route.ts +1 -1
- package/dist/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
- package/dist/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
- package/dist/templates/app/api/v1/post-categories/route.ts +1 -1
- package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
- package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
- package/dist/templates/app/api/v1/teams/docs.md +4 -9
- package/dist/templates/app/api/v1/teams/switch/route.ts +1 -1
- package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
- package/dist/templates/app/api/v1/users/[id]/route.ts +1 -1
- package/dist/templates/app/api/v1/users/route.ts +1 -1
- package/dist/templates/app/dashboard/(main)/media/page.tsx +3 -1
- package/dist/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
- package/dist/templates/app/dashboard/settings/profile/page.tsx +3 -2
- package/dist/templates/app/devtools/scheduled-actions/page.tsx +2 -1
- package/dist/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
- package/dist/templates/app/superadmin/subscriptions/page.tsx +3 -2
- package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
- package/dist/templates/app/superadmin/users/[userId]/page.tsx +2 -1
- package/dist/templates/app/superadmin/users/page.tsx +2 -1
- package/dist/templates/blocks/cta-section/component.tsx +3 -2
- package/dist/templates/blocks/cta-section/schema.ts +1 -1
- package/dist/templates/blocks/features-grid/component.tsx +2 -1
- package/dist/templates/blocks/features-grid/schema.ts +1 -1
- package/dist/templates/blocks/hero/component.tsx +3 -2
- package/dist/templates/blocks/hero/schema.ts +1 -1
- package/dist/templates/blocks/testimonials/component.tsx +2 -1
- package/dist/templates/blocks/testimonials/schema.ts +1 -1
- package/dist/templates/blocks/text-content/component.tsx +2 -1
- package/dist/templates/blocks/text-content/schema.ts +1 -1
- package/dist/templates/ci-workflows/README.md +1 -1
- package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
- package/dist/templates/ci-workflows/github/cypress-smoke.yml +1 -1
- package/dist/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
- package/dist/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
- package/dist/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
- package/dist/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
- package/dist/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
- package/dist/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
- package/dist/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
- package/dist/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
- package/dist/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
- package/dist/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
- package/dist/templates/eslint.config.mjs +133 -9
- package/dist/templates/features/blog/blocks/post-content/component.tsx +2 -1
- package/dist/templates/features/blog/blocks/post-content/schema.ts +1 -1
- package/dist/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
- package/dist/templates/features/pages/blocks/hero/component.tsx +3 -2
- package/dist/templates/features/pages/blocks/hero/schema.ts +1 -1
- package/dist/templates/next.config.mjs +50 -29
- package/dist/templates/proxy.ts +100 -21
- package/dist/templates/public/sw.js +2 -2
- package/dist/templates/scripts/cy-run-prod.cjs +5 -5
- package/dist/types/blocks.d.ts +1 -1
- package/dist/types/blocks.d.ts.map +1 -1
- package/dist/types/blocks.js +1 -1
- package/package.json +9 -5
- package/scripts/build/docs-registry.mjs +7 -7
- package/scripts/build/registry/__tests__/billing-registry.test.mjs +43 -0
- package/scripts/build/registry/__tests__/build-output.test.mjs +209 -0
- package/scripts/build/registry/__tests__/copy-race-child.mjs +14 -0
- package/scripts/build/registry/__tests__/direct-fs-writes.mjs +203 -0
- package/scripts/build/registry/__tests__/globals-css-tag.test.mjs +66 -0
- package/scripts/build/registry/__tests__/icons-discovery.test.mjs +530 -29
- package/scripts/build/registry/__tests__/page-generator.test.mjs +211 -37
- package/scripts/build/registry/__tests__/published-signatures.test.mjs +239 -0
- package/scripts/build/registry/__tests__/route-file-rule.test.mjs +170 -0
- package/scripts/build/registry/__tests__/safe-fs.test.mjs +333 -0
- package/scripts/build/registry/__tests__/template-analysis.test.mjs +142 -0
- package/scripts/build/registry/__tests__/template-registry.test.mjs +279 -9
- package/scripts/build/registry/__tests__/templates-plan.test.mjs +128 -0
- package/scripts/build/registry/__tests__/templates-tree.test.mjs +367 -0
- package/scripts/build/registry/__tests__/write-places.test.mjs +425 -0
- package/scripts/build/registry/config.mjs +6 -65
- package/scripts/build/registry/discovery/icons.mjs +498 -43
- package/scripts/build/registry/discovery/templates.mjs +1 -1
- package/scripts/build/registry/generators/billing-registry.mjs +1 -1
- package/scripts/build/registry/generators/docs-registry.mjs +3 -4
- package/scripts/build/registry/generators/feature-registry.mjs +1 -1
- package/scripts/build/registry/generators/permissions-registry.mjs +2 -2
- package/scripts/build/registry/generators/template-registry.mjs +65 -49
- package/scripts/build/registry/index.mjs +1 -2
- package/scripts/build/registry/post-build/index.mjs +1 -1
- package/scripts/build/registry/post-build/own-gitignores.mjs +220 -0
- package/scripts/build/registry/post-build/page-generator.mjs +512 -268
- package/scripts/build/registry/post-build/route-cleanup.mjs +16 -13
- package/scripts/build/registry/post-build/test-fixtures.mjs +26 -8
- package/scripts/build/registry/project-mode.mjs +76 -0
- package/scripts/build/registry/shared/typescript-compiler.mjs +77 -0
- package/scripts/build/registry/validate-env.test.mjs +12 -8
- package/scripts/build/registry/watch.mjs +3 -53
- package/scripts/build/registry/write-places.mjs +228 -0
- package/scripts/build/registry.mjs +53 -30
- package/scripts/build/safe-fs.mjs +279 -0
- package/scripts/build/templates-plan.mjs +44 -0
- package/scripts/build/theme.mjs +46 -42
- package/scripts/build/update-tsconfig.mjs +16 -15
- package/scripts/db/cluster-changes.mjs +205 -0
- package/scripts/db/connection-time-limits.mjs +53 -0
- package/scripts/db/inspect-server.mjs +108 -0
- package/scripts/db/migration-time-limit.mjs +695 -0
- package/scripts/db/run-migrations.mjs +46 -41
- package/scripts/db/ssl-config.mjs +194 -0
- package/scripts/db/theme-location.mjs +69 -0
- package/scripts/db/verify-tables.mjs +3 -8
- package/scripts/db/verify-teams.mjs +2 -6
- package/scripts/db/verify-theme-migrations.mjs +169 -0
- package/scripts/maintenance/core-updater.mjs +1003 -0
- package/scripts/maintenance/step-guard.mjs +74 -0
- package/scripts/maintenance/update-core.mjs +14 -831
- package/scripts/utils/console-guard.mjs +13 -0
- package/scripts/utils/generated-tag.mjs +66 -0
- package/scripts/utils/index.mjs +9 -1
- package/scripts/utils/logging.mjs +140 -0
- package/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
- package/templates/app/(auth)/verify-email/page.tsx +4 -3
- package/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
- package/templates/app/api/auth/[...all]/route.ts +15 -14
- package/templates/app/api/internal/user-metadata/route.ts +6 -8
- package/templates/app/api/user/plan-flags/route.ts +1 -1
- package/templates/app/api/user/profile/route.ts +12 -17
- package/templates/app/api/v1/api-keys/route.ts +1 -1
- package/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
- package/templates/app/api/v1/billing/cancel/route.ts +1 -1
- package/templates/app/api/v1/billing/change-plan/route.ts +1 -1
- package/templates/app/api/v1/billing/check-action/route.ts +1 -1
- package/templates/app/api/v1/billing/checkout/route.ts +1 -1
- package/templates/app/api/v1/blocks/validate/route.ts +1 -1
- package/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
- package/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
- package/templates/app/api/v1/post-categories/route.ts +1 -1
- package/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
- package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
- package/templates/app/api/v1/teams/docs.md +4 -9
- package/templates/app/api/v1/teams/switch/route.ts +1 -1
- package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
- package/templates/app/api/v1/users/[id]/route.ts +1 -1
- package/templates/app/api/v1/users/route.ts +1 -1
- package/templates/app/dashboard/(main)/media/page.tsx +3 -1
- package/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
- package/templates/app/dashboard/settings/profile/page.tsx +3 -2
- package/templates/app/devtools/scheduled-actions/page.tsx +2 -1
- package/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
- package/templates/app/superadmin/subscriptions/page.tsx +3 -2
- package/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
- package/templates/app/superadmin/users/[userId]/page.tsx +2 -1
- package/templates/app/superadmin/users/page.tsx +2 -1
- package/templates/blocks/cta-section/component.tsx +3 -2
- package/templates/blocks/cta-section/schema.ts +1 -1
- package/templates/blocks/features-grid/component.tsx +2 -1
- package/templates/blocks/features-grid/schema.ts +1 -1
- package/templates/blocks/hero/component.tsx +3 -2
- package/templates/blocks/hero/schema.ts +1 -1
- package/templates/blocks/testimonials/component.tsx +2 -1
- package/templates/blocks/testimonials/schema.ts +1 -1
- package/templates/blocks/text-content/component.tsx +2 -1
- package/templates/blocks/text-content/schema.ts +1 -1
- package/templates/ci-workflows/README.md +1 -1
- package/templates/ci-workflows/github/cypress-regression.yml +1 -1
- package/templates/ci-workflows/github/cypress-smoke.yml +1 -1
- package/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
- package/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
- package/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
- package/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
- package/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
- package/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
- package/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
- package/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
- package/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
- package/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
- package/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
- package/templates/eslint.config.mjs +133 -9
- package/templates/features/blog/blocks/post-content/component.tsx +2 -1
- package/templates/features/blog/blocks/post-content/schema.ts +1 -1
- package/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
- package/templates/features/pages/blocks/hero/component.tsx +3 -2
- package/templates/features/pages/blocks/hero/schema.ts +1 -1
- package/templates/next.config.mjs +50 -29
- package/templates/proxy.ts +100 -21
- package/templates/public/sw.js +2 -2
- package/templates/scripts/cy-run-prod.cjs +5 -5
- package/tests/jest/__mocks__/@nextsparkjs/registries/template-registry.client.ts +11 -0
- package/tests/jest/__mocks__/next-server.js +10 -0
- package/scripts/build/docs.mjs +0 -392
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
// The time a migration may run for, when whoever runs the migrations asks for
|
|
2
|
+
// a limit, and how a migration that runs past it is stopped.
|
|
3
|
+
//
|
|
4
|
+
// MIGRATION_TIMEOUT_SECONDS sets it. db:verify-theme always does: a migration
|
|
5
|
+
// that waits on a lock, loops, or talks to a server that stops answering would
|
|
6
|
+
// otherwise leave the command waiting until something outside kills it, and
|
|
7
|
+
// killing the command does not stop the statement on the server. db:migrate
|
|
8
|
+
// leaves it unset unless asked, since a real database can hold data a
|
|
9
|
+
// migration legitimately spends longer on.
|
|
10
|
+
//
|
|
11
|
+
// Each migration file gets the limit as a whole, and a file still running when
|
|
12
|
+
// the limit has passed fails:
|
|
13
|
+
// - the client gives up on the file when the limit has passed since it sent
|
|
14
|
+
// it. That holds for a server that does not answer at all, for a file of
|
|
15
|
+
// several statements each shorter than the limit, and for a migration that
|
|
16
|
+
// switches statement_timeout off, with SET, SET LOCAL or set_config(), for
|
|
17
|
+
// itself or for the migrations after it. It then asks the server, over a
|
|
18
|
+
// connection of its own that waits up to 5 s (less under a shorter limit),
|
|
19
|
+
// to end the session the migration was running in, and the error says
|
|
20
|
+
// whether it was ended. When it was not, the statement goes on running on
|
|
21
|
+
// the server, stopped only by a statement_timeout the migration left on;
|
|
22
|
+
// - the server's statement_timeout, set to the same limit, cancels a statement
|
|
23
|
+
// that runs past it, including one waiting on a lock, even when the process
|
|
24
|
+
// that sent it is no longer there to give up on it. Postgres 13 and later
|
|
25
|
+
// apply it to each statement in a file, and a migration can switch it off.
|
|
26
|
+
// Whichever comes first ends the file. The client starts counting before the
|
|
27
|
+
// server does, so it is usually the client.
|
|
28
|
+
//
|
|
29
|
+
// When the server cancels a statement, the error says after how long, and the
|
|
30
|
+
// server's reason. The time alone does not say what cancelled it: a
|
|
31
|
+
// statement_timeout the migration sets for itself, or a cancellation from
|
|
32
|
+
// another session, can come at any moment. It does rule out the limit when the
|
|
33
|
+
// cancellation comes sooner than the limit after the file was sent, since the
|
|
34
|
+
// server starts counting only once a statement has reached it, and the error
|
|
35
|
+
// then says the limit had not run out.
|
|
36
|
+
//
|
|
37
|
+
// A migration stopped either way is not recorded as run, and the next run starts
|
|
38
|
+
// the file over. Postgres keeps nothing the migration had not committed, but
|
|
39
|
+
// what it committed before it was stopped, with a COMMIT in the file or in a
|
|
40
|
+
// procedure it calls, stays; the error says so, and what to do about it.
|
|
41
|
+
//
|
|
42
|
+
// A file has to leave its session outside a transaction, with or without a
|
|
43
|
+
// limit. Recording it goes on the same session, so a record sent while the file
|
|
44
|
+
// still has a transaction open would go inside that transaction, and be undone
|
|
45
|
+
// with it when the connection closes. A file that ends inside a transaction it
|
|
46
|
+
// opened and did not close, or fails inside one, has that transaction rolled
|
|
47
|
+
// back before anything is recorded, and fails with an error that says its
|
|
48
|
+
// transactional changes are undone. Effects that are not transactional, such as
|
|
49
|
+
// a sequence a statement inside it advanced with nextval or set with setval,
|
|
50
|
+
// are not: a ROLLBACK leaves them as the file left them. Where the file left
|
|
51
|
+
// the session is the transaction status the server sends with the answer to
|
|
52
|
+
// the file: BEGIN sets it whether or not the transaction has written anything.
|
|
53
|
+
//
|
|
54
|
+
// A file that leaves a transaction prepared with PREPARE TRANSACTION, rather
|
|
55
|
+
// than committed or rolled back, ends its session outside a transaction too, so
|
|
56
|
+
// none of the above notices it: the prepared transaction stays on the server,
|
|
57
|
+
// neither applied nor undone, until something issues COMMIT PREPARED or
|
|
58
|
+
// ROLLBACK PREPARED for it.
|
|
59
|
+
//
|
|
60
|
+
// What tells the file's prepared transactions apart is the id the file writes
|
|
61
|
+
// for each. Postgres takes that id only as a string constant, and runs PREPARE
|
|
62
|
+
// TRANSACTION only as a statement of the query itself: inside a DO block, a
|
|
63
|
+
// function, a procedure or an EXECUTE it refuses it. Before the file is sent,
|
|
64
|
+
// the constant of each PREPARE TRANSACTION statement at the top level of its
|
|
65
|
+
// text is picked out, skipping comments, quoted strings, quoted identifiers and
|
|
66
|
+
// dollar quotes, and Postgres decodes it on the file's session, under the
|
|
67
|
+
// session's standard_conforming_strings, which is what the file is read under.
|
|
68
|
+
// For each decoded id this database already lists, its identity -- transaction
|
|
69
|
+
// id and prepared timestamp -- is kept. Once the file has run, however it ended,
|
|
70
|
+
// this database's pg_prepared_xacts is read for every id the file writes,
|
|
71
|
+
// whoever owns the transaction: on the file's session when the server answered
|
|
72
|
+
// the file, and otherwise, or when that read fails, on a connection of its own.
|
|
73
|
+
// A transaction newly listed under one of those ids, or listed with an identity
|
|
74
|
+
// different from the one kept, is taken for the file's; one still listed with
|
|
75
|
+
// the same identity is not. A file that leaves one prepared is not recorded,
|
|
76
|
+
// and fails with an error naming each one and the command that resolves it;
|
|
77
|
+
// neither is run for it. When the ids cannot be decoded the file is not run,
|
|
78
|
+
// and when they cannot be read afterwards the error says so and names every id,
|
|
79
|
+
// since none can be ruled out.
|
|
80
|
+
//
|
|
81
|
+
// A transaction prepared under an id the file does not write, by another
|
|
82
|
+
// session or by one the file opens itself (through dblink, for instance), is
|
|
83
|
+
// not reported.
|
|
84
|
+
//
|
|
85
|
+
// The limit is for the file, not for recording it as run. A migration that ran
|
|
86
|
+
// to the end is recorded in its tracking table on the same session, and under a
|
|
87
|
+
// limit the record gets RECORD_WAIT_MS instead, whatever the limit: on the
|
|
88
|
+
// server as a SET LOCAL statement_timeout sent with the INSERT, together with
|
|
89
|
+
// lock_timeout off (on Postgres 13 and later both hold for the INSERT whatever
|
|
90
|
+
// the limit or the migration left on the session), and on the client as the
|
|
91
|
+
// wait for the answer. A tracking table another session holds a lock on is
|
|
92
|
+
// waited for up to then, and a server that stops answering is given up on then.
|
|
93
|
+
// When the record fails, however it fails, the error says the file ran to the
|
|
94
|
+
// end and is not recorded, and gives the INSERT that records it; the runner
|
|
95
|
+
// prints that instead of calling the file failed.
|
|
96
|
+
//
|
|
97
|
+
// The limit holds whatever the database URL says. A URL that sets
|
|
98
|
+
// statement_timeout or query_timeout itself, `?statement_timeout=0` included,
|
|
99
|
+
// has the limit take the place of those parameters on every connection that runs
|
|
100
|
+
// under it (see connection-time-limits.mjs), and the runner says so before it
|
|
101
|
+
// starts. Without a limit the URL's parameters apply as pg reads them.
|
|
102
|
+
|
|
103
|
+
import pg from 'pg';
|
|
104
|
+
import { timeLimitedClient, timeLimitParametersIn } from './connection-time-limits.mjs';
|
|
105
|
+
import { scriptClient } from './ssl-config.mjs';
|
|
106
|
+
|
|
107
|
+
const { Client, DatabaseError, escapeIdentifier, escapeLiteral } = pg;
|
|
108
|
+
|
|
109
|
+
const CONNECT_MS = 10000;
|
|
110
|
+
|
|
111
|
+
export const TIME_LIMIT_VARIABLE = 'MIGRATION_TIMEOUT_SECONDS';
|
|
112
|
+
|
|
113
|
+
/** How long recording a migration that ran to the end may take, under a limit. */
|
|
114
|
+
export const RECORD_WAIT_MS = 60000;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The limit MIGRATION_TIMEOUT_SECONDS asks for, or null when it is unset.
|
|
118
|
+
* `statementMs` is the server's limit and `queryMs` the client's, and both are
|
|
119
|
+
* the limit itself.
|
|
120
|
+
*/
|
|
121
|
+
export function migrationTimeLimit(env = process.env) {
|
|
122
|
+
const value = env[TIME_LIMIT_VARIABLE];
|
|
123
|
+
if (value === undefined || value === '') return null;
|
|
124
|
+
const seconds = Number(value);
|
|
125
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
126
|
+
throw new Error(`${TIME_LIMIT_VARIABLE} must be a number of seconds greater than 0, not "${value}".`);
|
|
127
|
+
}
|
|
128
|
+
const limitMs = Math.ceil(seconds * 1000);
|
|
129
|
+
return { seconds, statementMs: limitMs, queryMs: limitMs };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The client migrations run on, under the limit when there is one. */
|
|
133
|
+
export function migrationClient(connectionString, limit) {
|
|
134
|
+
return limit
|
|
135
|
+
? timeLimitedClient(connectionString, { connectMs: CONNECT_MS, statementMs: limit.statementMs, queryMs: limit.queryMs })
|
|
136
|
+
: scriptClient(connectionString, { connectionTimeoutMillis: CONNECT_MS });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* What the runner says when the database URL carries time limits the migration
|
|
141
|
+
* limit takes the place of, or null when it carries none or there is no limit.
|
|
142
|
+
* The URL itself is never shown: it holds the password.
|
|
143
|
+
*/
|
|
144
|
+
export function ignoredParametersNotice(connectionString, limit) {
|
|
145
|
+
if (!limit) return null;
|
|
146
|
+
const ignored = timeLimitParametersIn(connectionString);
|
|
147
|
+
if (ignored.length === 0) return null;
|
|
148
|
+
return (
|
|
149
|
+
`The database URL sets ${ignored.join(' and ')}, which migrations do not use: ` +
|
|
150
|
+
`each one runs under ${TIME_LIMIT_VARIABLE} (${limit.seconds} s).`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** What a migration stopped partway leaves behind, and what to do before running the migrations again. */
|
|
155
|
+
function leftBehind({ stillRunning, prepared = NONE_PREPARED }) {
|
|
156
|
+
const committed = stillRunning
|
|
157
|
+
? 'It may still be running there, and what it commits, with a COMMIT in the file or in a procedure it calls, stays in the database.'
|
|
158
|
+
: `What it had ${mayLeavePrepared(prepared) ? 'neither committed nor left prepared' : 'not committed'} is gone, but what it committed ` +
|
|
159
|
+
'before it was stopped, with a COMMIT in the file or in a procedure it calls, stays in the database.';
|
|
160
|
+
return withSentences(
|
|
161
|
+
`${committed} It is not recorded as run, so the next run starts the file over: ` +
|
|
162
|
+
'undo those changes, or make the file safe to run again, before running the migrations again.',
|
|
163
|
+
preparedSentences(prepared, { stillRunning })
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The transaction status the server sends with the next ReadyForQuery on the
|
|
169
|
+
* client's connection: 'I' idle, 'T' in a transaction, 'E' in a failed one. It
|
|
170
|
+
* is null when the connection closes first. pg settles a query that fails as
|
|
171
|
+
* soon as the error arrives, before that ReadyForQuery, so the status has to be
|
|
172
|
+
* waited for; listening starts before the query is sent.
|
|
173
|
+
*/
|
|
174
|
+
function nextTransactionStatus(client) {
|
|
175
|
+
const { connection } = client;
|
|
176
|
+
if (!connection) return Promise.resolve(null);
|
|
177
|
+
return new Promise(resolve => {
|
|
178
|
+
const settle = status => {
|
|
179
|
+
connection.off('readyForQuery', onReady);
|
|
180
|
+
connection.off('end', onEnd);
|
|
181
|
+
resolve(status);
|
|
182
|
+
};
|
|
183
|
+
const onReady = message => settle(message.status);
|
|
184
|
+
const onEnd = () => settle(null);
|
|
185
|
+
connection.on('readyForQuery', onReady);
|
|
186
|
+
connection.on('end', onEnd);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Rolls back the transaction a migration file left its session in, open or
|
|
192
|
+
* failed, as `status` says. Returns null when the file left none, and otherwise
|
|
193
|
+
* how that transaction ends: rolled back here, or, when the ROLLBACK fails, when
|
|
194
|
+
* the run stops and closes the connection.
|
|
195
|
+
*/
|
|
196
|
+
async function rollBackLeftTransaction(client, status) {
|
|
197
|
+
if (status !== 'T' && status !== 'E') return null;
|
|
198
|
+
try {
|
|
199
|
+
await client.query('ROLLBACK');
|
|
200
|
+
return 'that transaction was rolled back';
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return `rolling that transaction back failed (${error.message}), and Postgres rolls it back when the run stops and closes the connection`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Runs one migration file's SQL. When the limit or a statement cancellation
|
|
208
|
+
* stops it, the error says which, whether its session on the server was ended,
|
|
209
|
+
* and what the migration leaves behind; the runner prints it next to the file's
|
|
210
|
+
* name. A file the server answers inside a transaction fails too, once that
|
|
211
|
+
* transaction is rolled back, so it is never recorded as run. A file that
|
|
212
|
+
* leaves prepared a transaction it names in PREPARE TRANSACTION fails too,
|
|
213
|
+
* however it ended, naming it and how to resolve it; nothing resolves it here.
|
|
214
|
+
*/
|
|
215
|
+
export async function runMigrationSql(client, { sql, limit, connectionString }) {
|
|
216
|
+
// Read before listening for the file's own ReadyForQuery: these queries' are answered, and consumed, first.
|
|
217
|
+
const named = await transactionsTheFileNames(client, sql);
|
|
218
|
+
const preparedNow = async ({ onSession }) =>
|
|
219
|
+
named.length === 0 ? NONE_PREPARED : preparedLeft(named, { client, onSession, connectionString, limit });
|
|
220
|
+
const sentAt = performance.now();
|
|
221
|
+
const answered = nextTransactionStatus(client);
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = await client.query(sql);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const elapsedMs = performance.now() - sentAt;
|
|
227
|
+
// Only an error the server sent comes with the status it answered the file with; a client that gave up has none coming
|
|
228
|
+
const answeredByServer = error instanceof DatabaseError;
|
|
229
|
+
const rolledBack = answeredByServer ? await rollBackLeftTransaction(client, await answered) : null;
|
|
230
|
+
|
|
231
|
+
// The server cancelled a statement, which ends the file there
|
|
232
|
+
if (limit && error.code === '57014') {
|
|
233
|
+
const prepared = await preparedNow({ onSession: answeredByServer });
|
|
234
|
+
const cancelled = `the server cancelled it after ${Math.floor(elapsedMs)} ms`;
|
|
235
|
+
const stopped =
|
|
236
|
+
elapsedMs < limit.statementMs
|
|
237
|
+
? `${cancelled}, before ${TIME_LIMIT_VARIABLE} (${limit.seconds} s) ran out`
|
|
238
|
+
: `did not finish within ${limit.seconds} s (${TIME_LIMIT_VARIABLE}); ${cancelled}`;
|
|
239
|
+
throw new Error(`${stopped}: ${error.message}. ${leftBehind({ stillRunning: false, prepared })}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!limit || error.message !== 'Query read timeout') {
|
|
243
|
+
// Nothing but an answer from the server says the file is no longer running there
|
|
244
|
+
const prepared = await preparedNow({ onSession: answeredByServer });
|
|
245
|
+
throw failedInsideTransaction(error, rolledBack, prepared, { stillRunning: !answeredByServer });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The limit has passed with no answer, and the statement may still be running
|
|
249
|
+
// there. The client lets go of its connection first: a session the server
|
|
250
|
+
// ends under a client still holding it surfaces as an unhandled 'error' event.
|
|
251
|
+
const processID = client.processID;
|
|
252
|
+
await client.end();
|
|
253
|
+
const ended = await endSession(connectionString, processID, limit);
|
|
254
|
+
const prepared = await preparedNow({ onSession: false });
|
|
255
|
+
throw new Error(
|
|
256
|
+
`did not finish within ${limit.seconds} s (${TIME_LIMIT_VARIABLE}); ` +
|
|
257
|
+
(ended === true
|
|
258
|
+
? `its session on the server was ended. ${leftBehind({ stillRunning: false, prepared })}`
|
|
259
|
+
: `its session on the server could not be ended: ${ended}. ${leftBehind({ stillRunning: true, prepared })}`)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const rolledBack = await rollBackLeftTransaction(client, await answered);
|
|
264
|
+
const prepared = await preparedNow({ onSession: true });
|
|
265
|
+
if (rolledBack) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
withSentences(
|
|
268
|
+
`the file ends inside a transaction it opened and did not close; ${rolledBack}: ${TRANSACTIONAL_ONLY}, ` +
|
|
269
|
+
'and it is not recorded as run, so the next run starts the file over. Anything it committed before opening that transaction ' +
|
|
270
|
+
'stays in the database. Add the COMMIT the file is missing, and undo anything it committed or make the file safe to run again, ' +
|
|
271
|
+
'before running the migrations again.',
|
|
272
|
+
preparedSentences(prepared, { stillRunning: false })
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (prepared.unchecked !== undefined) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`the file ran to the end, and it is not recorded as run, so the next run starts the file over. ${preparedSentences(prepared, { stillRunning: false })}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (prepared.left.length > 0) throw new Error(leftoverPreparedMessage(prepared.left));
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** What a ROLLBACK does and does not undo, said as a clause: transactional changes, not effects such as a sequence. */
|
|
287
|
+
const TRANSACTIONAL_ONLY = 'its transactional changes inside it are undone, but effects that are not transactional, ' +
|
|
288
|
+
'such as a sequence advanced with nextval or set with setval, are not';
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The error a file that failed fails with: the server's own, unless it failed
|
|
292
|
+
* inside a transaction it had not closed, or there is something to say about the
|
|
293
|
+
* transactions it names in PREPARE TRANSACTION. `stillRunning` when nothing says
|
|
294
|
+
* the file is no longer running on the server.
|
|
295
|
+
*/
|
|
296
|
+
function failedInsideTransaction(error, rolledBack, prepared = NONE_PREPARED, { stillRunning = false } = {}) {
|
|
297
|
+
const sentences = preparedSentences(prepared, { stillRunning });
|
|
298
|
+
if (!rolledBack && !sentences) return error;
|
|
299
|
+
const outcome = rolledBack
|
|
300
|
+
? `It failed inside a transaction it had not closed, and ${rolledBack}: ${TRANSACTIONAL_ONLY}. ` +
|
|
301
|
+
'Anything it committed before opening that transaction stays in the database, and it is not recorded as run, so the next run ' +
|
|
302
|
+
'starts the file over.'
|
|
303
|
+
: 'It is not recorded as run, so the next run starts the file over.';
|
|
304
|
+
return new Error(withSentences(`${error.message}. ${outcome}`, sentences), { cause: error });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** How long a connection of the run's own, beside the migration's, waits to connect and for each answer. */
|
|
308
|
+
function sideConnectionWaitMs(limit) {
|
|
309
|
+
return limit ? Math.min(5000, limit.statementMs) : 5000;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** The outcome for a file that names no transaction in PREPARE TRANSACTION. */
|
|
313
|
+
const NONE_PREPARED = Object.freeze({ named: [], left: [], unlisted: [] });
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The distinct ids a file's top-level PREPARE TRANSACTION statements name, as
|
|
317
|
+
* the server decodes them on the file's session, and each one's identity before
|
|
318
|
+
* the file runs, or null when none is listed. The file is not run when they
|
|
319
|
+
* cannot be read.
|
|
320
|
+
*/
|
|
321
|
+
async function transactionsTheFileNames(client, sql) {
|
|
322
|
+
// A PREPARE keyword is these letters in a row: nothing in SQL stands for a keyword's letters
|
|
323
|
+
if (!/prepare/i.test(sql)) return [];
|
|
324
|
+
let constants = [];
|
|
325
|
+
try {
|
|
326
|
+
const setting = await client.query({ text: 'SHOW standard_conforming_strings', rowMode: 'array' });
|
|
327
|
+
constants = preparedTransactionConstants(sql, { standardConformingStrings: setting.rows[0]?.[0] !== 'off' });
|
|
328
|
+
if (constants.length === 0) return [];
|
|
329
|
+
const rows = await preparedAmong(client, constants);
|
|
330
|
+
const named = new Map();
|
|
331
|
+
for (const [gid, listed, transaction, prepared] of rows) {
|
|
332
|
+
if (!named.has(gid)) named.set(gid, [gid, listed === null ? null : [transaction, prepared]]);
|
|
333
|
+
}
|
|
334
|
+
return [...named.values()];
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`the file was not run: reading the transaction ids its PREPARE TRANSACTION statements name` +
|
|
338
|
+
`${constants.length > 0 ? ` (${constants.join(', ')})` : ''} failed: ${error.message}. It is not recorded as run.`,
|
|
339
|
+
{ cause: error }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* For each id constant, [the id it stands for, that id again when this database
|
|
346
|
+
* lists a prepared transaction under it or null, its transaction id, and its
|
|
347
|
+
* prepared timestamp]. The constants go into the statement as the file writes
|
|
348
|
+
* them; the extended protocol holds it to a single statement.
|
|
349
|
+
*/
|
|
350
|
+
async function preparedAmong(client, constants) {
|
|
351
|
+
const result = await client.query({
|
|
352
|
+
text:
|
|
353
|
+
`SELECT named.gid, listed.gid, listed.transaction, listed.prepared ` +
|
|
354
|
+
`FROM (VALUES ${constants.map(constant => `(${constant})`).join(', ')}) AS named (gid) ` +
|
|
355
|
+
'LEFT JOIN pg_catalog.pg_prepared_xacts AS listed ON listed.gid = named.gid AND listed.database = pg_catalog.current_database()',
|
|
356
|
+
rowMode: 'array',
|
|
357
|
+
queryMode: 'extended',
|
|
358
|
+
});
|
|
359
|
+
return result.rows;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Whether two snapshots identify the same row of pg_prepared_xacts. */
|
|
363
|
+
function samePreparedIdentity(before, after) {
|
|
364
|
+
const sameValue = (left, right) =>
|
|
365
|
+
left instanceof Date && right instanceof Date ? Object.is(left.getTime(), right.getTime()) : Object.is(left, right);
|
|
366
|
+
return sameValue(before[0], after[0]) && sameValue(before[1], after[1]);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Which of the ids `named` this database now lists under a new identity:
|
|
371
|
+
* `left`; which it does not list, `unlisted`; or `unchecked`, why that could not
|
|
372
|
+
* be read. It is read on the file's session when `onSession`, and otherwise, or
|
|
373
|
+
* when that read fails, on a connection of its own.
|
|
374
|
+
*/
|
|
375
|
+
async function preparedLeft(named, { client, onSession, connectionString, limit }) {
|
|
376
|
+
const gids = named.map(([gid]) => gid);
|
|
377
|
+
const constants = gids.map(escapeLiteral);
|
|
378
|
+
const outcome = rows => {
|
|
379
|
+
const current = new Map(
|
|
380
|
+
rows.map(([gid, listed, transaction, prepared]) => [gid, listed === null ? null : [transaction, prepared]])
|
|
381
|
+
);
|
|
382
|
+
const left = [];
|
|
383
|
+
const unlisted = [];
|
|
384
|
+
for (const [gid, before] of named) {
|
|
385
|
+
const after = current.get(gid) ?? null;
|
|
386
|
+
if (after === null) unlisted.push(gid);
|
|
387
|
+
else if (before === null || !samePreparedIdentity(before, after)) left.push(gid);
|
|
388
|
+
}
|
|
389
|
+
return { named: gids, left, unlisted };
|
|
390
|
+
};
|
|
391
|
+
const unchecked = error => ({ named: gids, left: [], unlisted: [], unchecked: error.message });
|
|
392
|
+
if (onSession) {
|
|
393
|
+
try {
|
|
394
|
+
return outcome(await preparedAmong(client, constants));
|
|
395
|
+
} catch {
|
|
396
|
+
// read on a connection of its own instead, which says why when it cannot either
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const waitMs = sideConnectionWaitMs(limit);
|
|
400
|
+
const own = timeLimitedClient(connectionString, { connectMs: waitMs, statementMs: waitMs, queryMs: waitMs });
|
|
401
|
+
try {
|
|
402
|
+
await own.connect();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return unchecked(error);
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
return outcome(await preparedAmong(own, constants));
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return unchecked(error);
|
|
410
|
+
} finally {
|
|
411
|
+
await own.end();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Whether the file may have left a transaction prepared, as far as the outcome goes. */
|
|
416
|
+
function mayLeavePrepared({ left, unchecked }) {
|
|
417
|
+
return left.length > 0 || unchecked !== undefined;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The text, followed by the sentences when there are any. */
|
|
421
|
+
function withSentences(text, sentences) {
|
|
422
|
+
return sentences ? `${text} ${sentences}` : text;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Each id with the commands that resolve the transaction prepared under it. */
|
|
426
|
+
function resolvingEach(gids) {
|
|
427
|
+
return gids
|
|
428
|
+
.map(gid => {
|
|
429
|
+
const literal = escapeLiteral(gid);
|
|
430
|
+
return `${literal}: ROLLBACK PREPARED ${literal} undoes its transactional changes (not effects such as a sequence), ` +
|
|
431
|
+
`COMMIT PREPARED ${literal} applies them`;
|
|
432
|
+
})
|
|
433
|
+
.join('; ');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* What an error says about the transactions a file names in PREPARE
|
|
438
|
+
* TRANSACTION, or '' when there is nothing to say: the ones it leaves prepared,
|
|
439
|
+
* the ones that could not be checked, and, while it may still be running, the
|
|
440
|
+
* ones it has not prepared yet.
|
|
441
|
+
*/
|
|
442
|
+
function preparedSentences({ named, left, unlisted, unchecked }, { stillRunning }) {
|
|
443
|
+
const theOnes = gids => (gids.length > 1 ? 'the transactions it names in PREPARE TRANSACTION' : 'the transaction it names in PREPARE TRANSACTION');
|
|
444
|
+
const sentences = [];
|
|
445
|
+
if (unchecked !== undefined) {
|
|
446
|
+
sentences.push(
|
|
447
|
+
`Whether it leaves prepared ${theOnes(named)} could not be checked (${unchecked}). ` +
|
|
448
|
+
`${named.length > 1 ? 'For each one pg_prepared_xacts lists' : 'If pg_prepared_xacts lists it'}: ${resolvingEach(named)}.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (left.length > 0) {
|
|
452
|
+
const plural = left.length > 1;
|
|
453
|
+
sentences.push(
|
|
454
|
+
`It leaves ${plural ? 'transactions' : 'a transaction'} prepared, neither committed nor rolled back: ${resolvingEach(left)}. ` +
|
|
455
|
+
`Resolve ${plural ? 'each one' : 'it'} before running the migrations again; nothing resolves ${plural ? 'them' : 'it'} on its own.`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (stillRunning && unlisted.length > 0) {
|
|
459
|
+
sentences.push(
|
|
460
|
+
`It may yet leave prepared ${theOnes(unlisted)}, which pg_prepared_xacts does not list yet. ` +
|
|
461
|
+
`${unlisted.length > 1 ? 'For each one it comes to list' : 'Once it does'}: ${resolvingEach(unlisted)}.`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return sentences.join(' ');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** What the runner says about a file that ran to the end and leaves one or more transactions prepared, and how to resolve each. */
|
|
468
|
+
function leftoverPreparedMessage(gids) {
|
|
469
|
+
const plural = gids.length > 1;
|
|
470
|
+
return (
|
|
471
|
+
`the file leaves ${plural ? 'transactions' : 'a transaction'} prepared, neither committed nor rolled back: ` +
|
|
472
|
+
`${resolvingEach(gids)}. It is not recorded as run, so the next run starts the file over: resolve ` +
|
|
473
|
+
`${plural ? 'each one' : 'it'} before running the migrations again. Nothing resolves it on its own.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const SPACE = /[ \t\n\r\f\v]/;
|
|
478
|
+
const HORIZONTAL_SPACE = /[ \t\f\v]/;
|
|
479
|
+
const NEWLINE = /[\n\r]/;
|
|
480
|
+
const IDENTIFIER_START = /[A-Za-z_-]/;
|
|
481
|
+
const IDENTIFIER_PART = /[A-Za-z0-9_$-]/;
|
|
482
|
+
const DOLLAR_QUOTE = /\$(?:[A-Za-z_-][A-Za-z0-9_-]*)?\$/y;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The string constant of each PREPARE TRANSACTION statement at the top level
|
|
486
|
+
* of a query string, as the string writes it: '…', E'…', U&'…' with the
|
|
487
|
+
* UESCAPE clause after it, or a dollar-quoted string, together with the strings
|
|
488
|
+
* a quote continues it into across a newline. The string is read as Postgres's
|
|
489
|
+
* scanner reads it under the standard_conforming_strings given: -- and nested
|
|
490
|
+
* block comments, quoted strings, quoted identifiers and dollar quotes are not
|
|
491
|
+
* searched. A string or dollar quote that does not end is not a constant, and
|
|
492
|
+
* the server refuses the query it is in.
|
|
493
|
+
*/
|
|
494
|
+
export function preparedTransactionConstants(sql, { standardConformingStrings = true } = {}) {
|
|
495
|
+
const tokens = [];
|
|
496
|
+
for (let start = afterSpace(sql, 0); start < sql.length; ) {
|
|
497
|
+
const [kind, end] = tokenAt(sql, start, standardConformingStrings);
|
|
498
|
+
tokens.push({ kind, start, end, word: kind === 'word' ? sql.slice(start, end).replace(/[A-Z]/g, letter => letter.toLowerCase()) : null });
|
|
499
|
+
start = afterSpace(sql, end);
|
|
500
|
+
}
|
|
501
|
+
return tokens.flatMap((token, index) => {
|
|
502
|
+
const startsStatement = index === 0 || tokens[index - 1].kind === ';';
|
|
503
|
+
const constant = tokens[index + 2];
|
|
504
|
+
return startsStatement && token.word === 'prepare' && tokens[index + 1]?.word === 'transaction' && constant?.kind === 'constant'
|
|
505
|
+
? [sql.slice(constant.start, constant.end)]
|
|
506
|
+
: [];
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Where the whitespace and comments from `i` on end. */
|
|
511
|
+
function afterSpace(sql, i) {
|
|
512
|
+
for (;;) {
|
|
513
|
+
if (SPACE.test(sql.charAt(i))) i++;
|
|
514
|
+
else if (sql.startsWith('--', i)) {
|
|
515
|
+
while (i < sql.length && !NEWLINE.test(sql.charAt(i))) i++;
|
|
516
|
+
} else if (sql.startsWith('/*', i)) {
|
|
517
|
+
let depth = 0;
|
|
518
|
+
do {
|
|
519
|
+
if (sql.startsWith('/*', i)) {
|
|
520
|
+
depth++;
|
|
521
|
+
i += 2;
|
|
522
|
+
} else if (sql.startsWith('*/', i)) {
|
|
523
|
+
depth--;
|
|
524
|
+
i += 2;
|
|
525
|
+
} else i++;
|
|
526
|
+
} while (depth > 0 && i < sql.length);
|
|
527
|
+
} else return i;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The kind of the token that starts at `i`, and where it ends: 'word' for a
|
|
533
|
+
* keyword or an unquoted identifier, 'constant' for a string constant PREPARE
|
|
534
|
+
* TRANSACTION takes, ';', or 'other'. One that does not end runs to the end of
|
|
535
|
+
* the string, as 'other'.
|
|
536
|
+
*/
|
|
537
|
+
function tokenAt(sql, i, standardConformingStrings) {
|
|
538
|
+
const unended = ['other', sql.length];
|
|
539
|
+
const quoted = (end, kind = 'constant') => (end === -1 ? unended : [kind, end]);
|
|
540
|
+
const char = sql.charAt(i);
|
|
541
|
+
if (char === "'") return quoted(quotedStringEnd(sql, i, { backslashes: !standardConformingStrings, doubled: true }));
|
|
542
|
+
if (/^[eE]'/.test(sql.slice(i, i + 2))) return quoted(quotedStringEnd(sql, i + 1, { backslashes: true, doubled: true }));
|
|
543
|
+
if (/^[uU]&'/.test(sql.slice(i, i + 3))) {
|
|
544
|
+
const end = quotedStringEnd(sql, i + 2, { backslashes: false, doubled: true });
|
|
545
|
+
return end === -1 ? unended : ['constant', withUnicodeEscapeClause(sql, end, standardConformingStrings)];
|
|
546
|
+
}
|
|
547
|
+
// bit strings, which PREPARE TRANSACTION does not take
|
|
548
|
+
if (/^[bBxX]'/.test(sql.slice(i, i + 2))) return quoted(quotedStringEnd(sql, i + 1, { backslashes: false, doubled: false }), 'other');
|
|
549
|
+
if (char === '"') return quoted(quotedIdentifierEnd(sql, i), 'other');
|
|
550
|
+
if (/^[uU]&"/.test(sql.slice(i, i + 3))) return quoted(quotedIdentifierEnd(sql, i + 2), 'other');
|
|
551
|
+
if (char === '$') {
|
|
552
|
+
DOLLAR_QUOTE.lastIndex = i;
|
|
553
|
+
const delimiter = DOLLAR_QUOTE.exec(sql)?.[0];
|
|
554
|
+
if (delimiter) {
|
|
555
|
+
const close = sql.indexOf(delimiter, i + delimiter.length);
|
|
556
|
+
return close === -1 ? unended : ['constant', close + delimiter.length];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (IDENTIFIER_START.test(char)) {
|
|
560
|
+
let end = i + 1;
|
|
561
|
+
while (IDENTIFIER_PART.test(sql.charAt(end))) end++;
|
|
562
|
+
return ['word', end];
|
|
563
|
+
}
|
|
564
|
+
if (/[0-9]/.test(char)) {
|
|
565
|
+
let end = i + 1;
|
|
566
|
+
while (/[0-9A-Za-z_.]/.test(sql.charAt(end))) end++;
|
|
567
|
+
return ['other', end];
|
|
568
|
+
}
|
|
569
|
+
return [char === ';' ? ';' : 'other', i + 1];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Where the quoted string whose opening quote is at `i` ends, including the
|
|
574
|
+
* strings a quote continues it into, or -1 when it does not end. A backslash
|
|
575
|
+
* escapes the character after it when `backslashes`, and two quotes stand for
|
|
576
|
+
* one when `doubled`.
|
|
577
|
+
*/
|
|
578
|
+
function quotedStringEnd(sql, i, { backslashes, doubled }) {
|
|
579
|
+
for (i++; i < sql.length; ) {
|
|
580
|
+
const char = sql.charAt(i);
|
|
581
|
+
if (backslashes && char === '\\') i += 2;
|
|
582
|
+
else if (char !== "'") i++;
|
|
583
|
+
else if (doubled && sql.charAt(i + 1) === "'") i += 2;
|
|
584
|
+
else {
|
|
585
|
+
const next = continuingQuote(sql, i + 1);
|
|
586
|
+
if (next === -1) return i + 1;
|
|
587
|
+
i = next + 1;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return -1;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Where the quote that continues a string whose closing quote is just before
|
|
595
|
+
* `i` is, or -1 when none does: Postgres joins two strings when what separates
|
|
596
|
+
* them is whitespace with at least one newline in it, and -- comments.
|
|
597
|
+
*/
|
|
598
|
+
function continuingQuote(sql, i) {
|
|
599
|
+
let newline = false;
|
|
600
|
+
for (;;) {
|
|
601
|
+
const char = sql.charAt(i);
|
|
602
|
+
if (NEWLINE.test(char)) {
|
|
603
|
+
newline = true;
|
|
604
|
+
i++;
|
|
605
|
+
} else if (HORIZONTAL_SPACE.test(char)) i++;
|
|
606
|
+
else if (sql.startsWith('--', i)) {
|
|
607
|
+
while (i < sql.length && !NEWLINE.test(sql.charAt(i))) i++;
|
|
608
|
+
} else return char === "'" && newline ? i : -1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Where the quoted identifier whose opening quote is at `i` ends, or -1 when it does not end. */
|
|
613
|
+
function quotedIdentifierEnd(sql, i) {
|
|
614
|
+
for (i++; i < sql.length; i++) {
|
|
615
|
+
if (sql.charAt(i) !== '"') continue;
|
|
616
|
+
if (sql.charAt(i + 1) !== '"') return i + 1;
|
|
617
|
+
i++;
|
|
618
|
+
}
|
|
619
|
+
return -1;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Where a U&'…' string that ends at `end` ends together with the UESCAPE clause that follows it, if one does. */
|
|
623
|
+
function withUnicodeEscapeClause(sql, end, standardConformingStrings) {
|
|
624
|
+
const keyword = afterSpace(sql, end);
|
|
625
|
+
if (sql.slice(keyword, keyword + 7).toLowerCase() !== 'uescape' || IDENTIFIER_PART.test(sql.charAt(keyword + 7))) return end;
|
|
626
|
+
const [kind, clauseEnd] = tokenAt(sql, afterSpace(sql, keyword + 7), standardConformingStrings);
|
|
627
|
+
return kind === 'constant' ? clauseEnd : end;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** A migration that ran to the end and could not be recorded as run. */
|
|
631
|
+
export class MigrationNotRecordedError extends Error {}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Records a migration that ran to the end as the row given (column → value) of
|
|
635
|
+
* its tracking table. Under a limit, the record waits up to `waitMs` on the
|
|
636
|
+
* server and on the client, not the limit. Throws MigrationNotRecordedError when
|
|
637
|
+
* it fails, with the INSERT that records it; that INSERT does nothing when the
|
|
638
|
+
* row is already there, since a record whose answer never came may have been
|
|
639
|
+
* written.
|
|
640
|
+
*/
|
|
641
|
+
export async function recordMigration(client, { file, table, row, limit, waitMs = RECORD_WAIT_MS }) {
|
|
642
|
+
const insert =
|
|
643
|
+
`INSERT INTO ${escapeIdentifier(table)} (${Object.keys(row).map(escapeIdentifier).join(', ')}) ` +
|
|
644
|
+
`VALUES (${Object.values(row).map(escapeLiteral).join(', ')})`;
|
|
645
|
+
try {
|
|
646
|
+
if (limit) {
|
|
647
|
+
await client.query({
|
|
648
|
+
text: `SET LOCAL statement_timeout = ${waitMs}; SET LOCAL lock_timeout = 0; ${insert}`,
|
|
649
|
+
query_timeout: waitMs,
|
|
650
|
+
});
|
|
651
|
+
} else {
|
|
652
|
+
await client.query(insert);
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
const recording = `recording it as run in ${escapeIdentifier(table)}`;
|
|
656
|
+
const outcome =
|
|
657
|
+
error.message === 'Query read timeout'
|
|
658
|
+
? `${recording} got no answer within ${waitMs / 1000} s, so it may or may not be recorded`
|
|
659
|
+
: `${recording} failed: ${error.message}`;
|
|
660
|
+
throw new MigrationNotRecordedError(
|
|
661
|
+
`${file} ran to the end, but ${outcome}. ` +
|
|
662
|
+
'What it committed stays in the database, and until it is recorded the next run starts the file over. ' +
|
|
663
|
+
`Record it before running the migrations again: ${insert} ON CONFLICT DO NOTHING;`
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** The line the runner prints for a migration that failed. */
|
|
669
|
+
export function migrationFailure(file, error) {
|
|
670
|
+
return error instanceof MigrationNotRecordedError ? error.message : `Failed to execute ${file}: ${error.message}`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Ends a backend from a separate connection. Returns true, or why it could not.
|
|
675
|
+
* The connection is limited too: a server that did not answer the migration may
|
|
676
|
+
* not answer this either.
|
|
677
|
+
*/
|
|
678
|
+
async function endSession(connectionString, processID, limit) {
|
|
679
|
+
if (!Number.isInteger(processID)) return 'the server never said which session it was';
|
|
680
|
+
const waitMs = sideConnectionWaitMs(limit);
|
|
681
|
+
const admin = timeLimitedClient(connectionString, { connectMs: waitMs, statementMs: waitMs, queryMs: waitMs });
|
|
682
|
+
try {
|
|
683
|
+
await admin.connect();
|
|
684
|
+
} catch (error) {
|
|
685
|
+
return error.message;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
await admin.query(`SELECT pg_terminate_backend(${processID})`);
|
|
689
|
+
return true;
|
|
690
|
+
} catch (error) {
|
|
691
|
+
return error.message;
|
|
692
|
+
} finally {
|
|
693
|
+
await admin.end();
|
|
694
|
+
}
|
|
695
|
+
}
|