@agent-native/core 0.145.7 → 0.146.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/analytics/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +20 -4
- package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +59 -24
- package/corpus/templates/analytics/app/pages/settings/AlertRulesSettingsCard.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-08-07-analytics-migrations-now-use-bounded-pressure-aware-backgrou.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-07-settings-skeleton-placeholders.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-07-template-activity-charts-now-show-only-core-agent-native-app.md +6 -0
- package/corpus/templates/analytics/netlify.toml +1 -1
- package/corpus/templates/analytics/package.json +1 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +110 -0
- package/corpus/templates/analytics/scripts/migrate-production.ts +24 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +13 -13
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +448 -0
- package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +11 -2
- package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +4 -5
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +35 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +3 -5
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +10 -4
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +4 -11
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +74 -28
- package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +2 -5
- package/corpus/templates/analytics/server/plugins/db.ts +56 -3
- package/corpus/templates/analytics/server/routes/api/analytics-bigquery/backfill.post.ts +55 -0
- package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +22 -4
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/assets/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/brain/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/brain/server/lib/connectors.ts +38 -14
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/calendar/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/chat/AGENTS.md +3 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/clips/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/content/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/crm/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +21 -3
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +23 -5
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +24 -5
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +23 -3
- package/corpus/templates/crm/changelog/2026-08-07-settings-loading-skeletons.md +6 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/design/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/factory/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/forms/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/macros/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/mail/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/plan/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/slides/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +13 -0
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +5 -0
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +5 -0
- package/corpus/templates/slides/app/components/presentation/PresenterView.tsx +5 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
- package/corpus/templates/slides/app/pages/Presentation.tsx +3 -0
- package/corpus/templates/slides/app/pages/SharedPresentation.tsx +5 -0
- package/corpus/templates/slides/app/routes/slide.tsx +10 -5
- package/corpus/templates/slides/changelog/2026-08-07-slide-previews-and-present-now-keep-deck-colors-consistent.md +6 -0
- package/corpus/templates/slides/server/db/schema.ts +1 -0
- package/corpus/templates/slides/server/handlers/share.ts +51 -3
- package/corpus/templates/slides/server/plugins/db.ts +5 -0
- package/corpus/templates/slides/shared/api.ts +2 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -1
- package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/corpus/templates/tasks/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/dist/cli/code-agent-executor.js +8 -3
- package/dist/client/ConnectBuilderCard.d.ts +7 -7
- package/dist/client/ConnectBuilderCard.js +45 -20
- package/dist/client/components/ApiKeySettings.js +2 -5
- package/dist/client/org/TeamPage.js +4 -2
- package/dist/client/settings/AccountSettingsCard.js +2 -1
- package/dist/client/settings/AgentsSection.js +2 -1
- package/dist/client/settings/SecretsSection.js +3 -2
- package/dist/client/settings/SettingsPanel.js +6 -12
- package/dist/client/settings/SettingsSkeleton.d.ts +12 -0
- package/dist/client/settings/SettingsSkeleton.js +12 -0
- package/dist/client/settings/VoiceTranscriptionSection.js +5 -4
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.js +1 -0
- package/dist/config.d.ts +9 -2
- package/dist/config.js +38 -2
- package/dist/db/client.d.ts +26 -8
- package/dist/db/client.js +200 -9
- package/dist/db/create-get-db.js +2 -2
- package/dist/db/ddl-guard.js +3 -1
- package/dist/db/ensure-additive-columns.d.ts +2 -0
- package/dist/db/ensure-additive-columns.js +5 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +2 -2
- package/dist/db/migrations.d.ts +11 -54
- package/dist/db/migrations.js +70 -0
- package/dist/deploy/build.js +5 -1
- package/dist/jobs/index.d.ts +1 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/run-history.d.ts +4 -0
- package/dist/jobs/run-history.js +44 -0
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/scripts/docs/search.js +3 -2
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/prompt-resources.js +4 -3
- package/dist/server/agent-chat-plugin.js +6 -4
- package/dist/server/agents-bundle.d.ts +20 -3
- package/dist/server/agents-bundle.js +57 -30
- package/dist/server/auth-plugin.js +5 -0
- package/dist/server/better-auth-instance.js +2 -63
- package/dist/server/better-auth-migrations.d.ts +8 -0
- package/dist/server/better-auth-migrations.js +179 -0
- package/dist/server/edge.d.ts +2 -0
- package/dist/server/edge.js +2 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/release-migrations.d.ts +8 -0
- package/dist/server/release-migrations.js +24 -0
- package/dist/server/sweep-runtime.d.ts +1 -1
- package/dist/server/sweep-runtime.js +15 -0
- package/dist/shared/runtime-config.js +1 -1
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -1
- package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/dist/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/dist/templates/chat/AGENTS.md +3 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -1
- package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/dist/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/dist/templates/default/AGENTS.md +3 -0
- package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/dist/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -1
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/dist/templates/workspace-core/AGENTS.md +3 -0
- package/dist/templates/workspace-root/AGENTS.md +3 -0
- package/dist/vite/agent-native-config-loader.d.ts +10 -0
- package/dist/vite/agent-native-config-loader.js +56 -0
- package/dist/vite/agents-bundle-plugin.d.ts +5 -1
- package/dist/vite/agents-bundle-plugin.js +29 -6
- package/dist/vite/client.d.ts +4 -2
- package/dist/vite/client.js +4 -49
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/docs/content/agent-native-config.mdx +43 -17
- package/docs/content/environment-variables.mdx +1 -1
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -1
- package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/src/templates/chat/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/src/templates/chat/AGENTS.md +3 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -1
- package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/src/templates/default/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/src/templates/default/AGENTS.md +3 -0
- package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/src/templates/headless/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -1
- package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +37 -11
- package/src/templates/workspace-core/.agents/skills/turn-into-app/references/fresh-project.md +13 -2
- package/src/templates/workspace-core/AGENTS.md +3 -0
- package/src/templates/workspace-root/AGENTS.md +3 -0
package/dist/config.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Public, non-secret configuration for an Agent-Native app.
|
|
3
3
|
*
|
|
4
4
|
* This module is intentionally free of Node and framework imports so it can be
|
|
5
|
-
* used from a typed `agent-native.ts` file and from browser code after Vite
|
|
5
|
+
* used from a typed `agent-native.config.ts` file and from browser code after Vite
|
|
6
6
|
* serializes the resolved config into the client bundle.
|
|
7
7
|
*/
|
|
8
8
|
export const AGENT_NATIVE_CONFIG_VERSION = 1;
|
|
9
9
|
/**
|
|
10
|
-
* Type-safe authoring helper for `agent-native.ts`.
|
|
10
|
+
* Type-safe authoring helper for `agent-native.config.ts`.
|
|
11
11
|
*
|
|
12
12
|
* Like Next's typed config file, this is deliberately identity-like: the
|
|
13
13
|
* framework evaluates the exported object or factory in the Vite config
|
|
@@ -27,6 +27,7 @@ export function normalizeAgentNativeConfig(input, source = "agent-native config"
|
|
|
27
27
|
const onboardingValue = input.onboarding;
|
|
28
28
|
const runtimeValue = input.runtime;
|
|
29
29
|
const diagnosticsValue = input.diagnostics;
|
|
30
|
+
const instructionsValue = input.instructions;
|
|
30
31
|
const normalized = {
|
|
31
32
|
...(input.version === undefined
|
|
32
33
|
? {}
|
|
@@ -45,6 +46,9 @@ export function normalizeAgentNativeConfig(input, source = "agent-native config"
|
|
|
45
46
|
if (diagnosticsValue !== undefined) {
|
|
46
47
|
normalized.diagnostics = normalizeDiagnosticsConfig(diagnosticsValue, `${source}.diagnostics`);
|
|
47
48
|
}
|
|
49
|
+
if (instructionsValue !== undefined) {
|
|
50
|
+
normalized.instructions = normalizeInstructionsConfig(instructionsValue, `${source}.instructions`);
|
|
51
|
+
}
|
|
48
52
|
return normalized;
|
|
49
53
|
}
|
|
50
54
|
export function mergeAgentNativeConfigs(base, override) {
|
|
@@ -91,6 +95,12 @@ export function mergeAgentNativeConfigs(base, override) {
|
|
|
91
95
|
...override.diagnostics,
|
|
92
96
|
}
|
|
93
97
|
: undefined,
|
|
98
|
+
instructions: base.instructions || override.instructions
|
|
99
|
+
? {
|
|
100
|
+
...base.instructions,
|
|
101
|
+
...override.instructions,
|
|
102
|
+
}
|
|
103
|
+
: undefined,
|
|
94
104
|
};
|
|
95
105
|
}
|
|
96
106
|
export function resolveAgentNativeConfig(input, context) {
|
|
@@ -175,6 +185,32 @@ function normalizeDiagnosticsConfig(value, source) {
|
|
|
175
185
|
? {}
|
|
176
186
|
: { failOnBuild: value.failOnBuild };
|
|
177
187
|
}
|
|
188
|
+
function normalizeInstructionsConfig(value, source) {
|
|
189
|
+
if (!isRecord(value)) {
|
|
190
|
+
throw new Error(`${source} must be an object`);
|
|
191
|
+
}
|
|
192
|
+
const result = {};
|
|
193
|
+
for (const audience of ["runtime", "development"]) {
|
|
194
|
+
const pathValue = value[audience];
|
|
195
|
+
if (pathValue === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
if (typeof pathValue !== "string") {
|
|
198
|
+
throw new Error(`${source}.${audience} must be a relative file path`);
|
|
199
|
+
}
|
|
200
|
+
result[audience] = normalizeRelativeFilePath(pathValue, `${source}.${audience}`);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
function normalizeRelativeFilePath(value, source) {
|
|
205
|
+
const normalized = value.trim().replaceAll("\\", "/");
|
|
206
|
+
if (!normalized ||
|
|
207
|
+
normalized.startsWith("/") ||
|
|
208
|
+
/^[A-Za-z]:\//.test(normalized) ||
|
|
209
|
+
normalized.split("/").some((segment) => segment === "..")) {
|
|
210
|
+
throw new Error(`${source} must be a non-empty relative file path inside the app root`);
|
|
211
|
+
}
|
|
212
|
+
return normalized;
|
|
213
|
+
}
|
|
178
214
|
function normalizeRequiredEnvKeys(value, source) {
|
|
179
215
|
if (value === undefined)
|
|
180
216
|
return undefined;
|
package/dist/db/client.d.ts
CHANGED
|
@@ -174,14 +174,20 @@ export declare function withDbTimeout<T>(op: string, run: () => Promise<T>, ms?:
|
|
|
174
174
|
*/
|
|
175
175
|
export declare function isServerlessRuntime(): boolean;
|
|
176
176
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
* True for production serverless execution contexts. Netlify also exposes
|
|
178
|
+
* `NETLIFY=true` in its build environment, so build-time code that owns
|
|
179
|
+
* migrations must use `withMigrationRuntime()` explicitly.
|
|
180
|
+
*/
|
|
181
|
+
export declare function isProductionServerlessFunctionRuntime(env?: NodeJS.ProcessEnv): boolean;
|
|
182
|
+
/** True for statements that change database schema or database privileges. */
|
|
183
|
+
export declare function isSchemaMutationStatement(statement: DbExecStatement): boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Request functions must never be able to prepare or mutate schema. Throwing
|
|
186
|
+
* here makes an unconverted ensureTable path fail loudly instead of silently
|
|
187
|
+
* reporting success against a missing table. The release migration wrapper
|
|
188
|
+
* is the only supported production opt-in.
|
|
184
189
|
*/
|
|
190
|
+
export declare function assertSchemaMutationAllowed(statement: DbExecStatement): void;
|
|
185
191
|
export declare function pgPoolOptions(url: string): Record<string, unknown>;
|
|
186
192
|
/**
|
|
187
193
|
* Shared options for every Neon serverless pool. The startup parameter is
|
|
@@ -221,7 +227,19 @@ export declare function isBackgroundFunctionPoolContext(): boolean;
|
|
|
221
227
|
* actual context.
|
|
222
228
|
*/
|
|
223
229
|
export declare function describeDbError(err: unknown): string;
|
|
224
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Thrown INSTEAD of attempting, while an endpoint is in cooldown.
|
|
232
|
+
*
|
|
233
|
+
* The refused attempt's error is described in the message and deliberately NOT
|
|
234
|
+
* attached as `cause`: isConnectionError() walks `cause.code`/`cause.message`,
|
|
235
|
+
* so a cause would reclassify this as retryable and retryOnConnectionError
|
|
236
|
+
* would drive the storm straight back.
|
|
237
|
+
*/
|
|
238
|
+
export declare class DbConnectCooldownError extends Error {
|
|
239
|
+
code: string;
|
|
240
|
+
constructor(remainingMs: number, refusedBy: string);
|
|
241
|
+
}
|
|
242
|
+
export declare function guardNeonPool(pool: unknown, url: string, label?: string): void;
|
|
225
243
|
interface ClosablePool {
|
|
226
244
|
end(): Promise<unknown>;
|
|
227
245
|
}
|
package/dist/db/client.js
CHANGED
|
@@ -613,6 +613,9 @@ export function isTransientDatabaseError(err) {
|
|
|
613
613
|
const error = err;
|
|
614
614
|
const code = String(error?.code ?? error?.cause?.code ?? "");
|
|
615
615
|
if (code === "ECHECKOUTTIMEOUT" ||
|
|
616
|
+
// Shed load during a connect cooldown is transient by construction — the
|
|
617
|
+
// endpoint refused, we chose not to re-ask yet. Surfaces as 503, not 500.
|
|
618
|
+
code === "DB_CONNECT_COOLDOWN" ||
|
|
616
619
|
code === "EMAXCONN" ||
|
|
617
620
|
code === "53300" ||
|
|
618
621
|
code === "57014" ||
|
|
@@ -765,6 +768,47 @@ export function isServerlessRuntime() {
|
|
|
765
768
|
!!process.env.LAMBDA_TASK_ROOT ||
|
|
766
769
|
!!process.env.CF_PAGES);
|
|
767
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* True for production serverless execution contexts. Netlify also exposes
|
|
773
|
+
* `NETLIFY=true` in its build environment, so build-time code that owns
|
|
774
|
+
* migrations must use `withMigrationRuntime()` explicitly.
|
|
775
|
+
*/
|
|
776
|
+
export function isProductionServerlessFunctionRuntime(env = process.env) {
|
|
777
|
+
if (env.NODE_ENV !== "production" || env.NETLIFY_LOCAL === "true") {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
return Boolean(env.NETLIFY === "true" ||
|
|
781
|
+
env.NETLIFY_FUNCTION_NAME ||
|
|
782
|
+
env.AWS_LAMBDA_FUNCTION_NAME ||
|
|
783
|
+
env.AWS_LAMBDA_FUNCTION_VERSION ||
|
|
784
|
+
env.LAMBDA_TASK_ROOT ||
|
|
785
|
+
env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
|
|
786
|
+
env.VERCEL_FUNCTION_ID ||
|
|
787
|
+
env.VERCEL_REGION ||
|
|
788
|
+
env.VERCEL === "1");
|
|
789
|
+
}
|
|
790
|
+
const SCHEMA_MUTATION_STATEMENT = /^\s*(?:CREATE|ALTER|DROP|TRUNCATE|COMMENT|REINDEX|GRANT|REVOKE)\b/i;
|
|
791
|
+
function rawSql(statement) {
|
|
792
|
+
return typeof statement === "string" ? statement : statement.sql;
|
|
793
|
+
}
|
|
794
|
+
/** True for statements that change database schema or database privileges. */
|
|
795
|
+
export function isSchemaMutationStatement(statement) {
|
|
796
|
+
return SCHEMA_MUTATION_STATEMENT.test(rawSql(statement));
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Request functions must never be able to prepare or mutate schema. Throwing
|
|
800
|
+
* here makes an unconverted ensureTable path fail loudly instead of silently
|
|
801
|
+
* reporting success against a missing table. The release migration wrapper
|
|
802
|
+
* is the only supported production opt-in.
|
|
803
|
+
*/
|
|
804
|
+
export function assertSchemaMutationAllowed(statement) {
|
|
805
|
+
const migrationRuntime = globalThis.__AGENT_NATIVE_MIGRATION_RUNTIME__ === true;
|
|
806
|
+
if (isProductionServerlessFunctionRuntime() &&
|
|
807
|
+
!migrationRuntime &&
|
|
808
|
+
isSchemaMutationStatement(statement)) {
|
|
809
|
+
throw new Error("Schema mutation attempted in a production serverless request. Run migrations in the release job instead.");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
768
812
|
/**
|
|
769
813
|
* postgres.js pool options tuned per runtime. idle_timeout is shortened on
|
|
770
814
|
* serverless so a thawed-but-idle instance releases its connections quickly.
|
|
@@ -774,10 +818,27 @@ export function isServerlessRuntime() {
|
|
|
774
818
|
* process, not one per consumer. See {@link neonPoolMax} for why the cap must
|
|
775
819
|
* leave room for concurrency.
|
|
776
820
|
*/
|
|
821
|
+
/**
|
|
822
|
+
* Identifies the connection in `pg_stat_activity.application_name`.
|
|
823
|
+
*
|
|
824
|
+
* Without it every backend reports `pgbouncer` and a runaway query is
|
|
825
|
+
* anonymous. A 58 MB `SELECT id, config FROM dashboards` ran 20-wide against
|
|
826
|
+
* production and could not be traced to a caller: it appears nowhere in the
|
|
827
|
+
* repo or any built bundle, and `pg_stat_statements` is not installed. This is
|
|
828
|
+
* the cheapest thing that would have named it.
|
|
829
|
+
*/
|
|
830
|
+
function poolApplicationName() {
|
|
831
|
+
const site = process.env.SITE_NAME ??
|
|
832
|
+
process.env.NETLIFY_SITE_NAME ??
|
|
833
|
+
process.env.AGENT_NATIVE_APP_NAME ??
|
|
834
|
+
"app";
|
|
835
|
+
return `agent-native:${site}`.slice(0, 63);
|
|
836
|
+
}
|
|
777
837
|
export function pgPoolOptions(url) {
|
|
778
838
|
const serverless = isServerlessRuntime();
|
|
779
839
|
return {
|
|
780
840
|
onnotice: () => { },
|
|
841
|
+
connection: { application_name: poolApplicationName() },
|
|
781
842
|
max: serverless ? serverlessPoolMax() : 20,
|
|
782
843
|
idle_timeout: serverless ? 20 : 240,
|
|
783
844
|
max_lifetime: 60 * 30,
|
|
@@ -785,6 +846,7 @@ export function pgPoolOptions(url) {
|
|
|
785
846
|
...(serverless
|
|
786
847
|
? {
|
|
787
848
|
connection: {
|
|
849
|
+
application_name: poolApplicationName(),
|
|
788
850
|
idle_in_transaction_session_timeout: 30_000,
|
|
789
851
|
},
|
|
790
852
|
}
|
|
@@ -901,7 +963,77 @@ export function describeDbError(err) {
|
|
|
901
963
|
}
|
|
902
964
|
return String(err);
|
|
903
965
|
}
|
|
904
|
-
|
|
966
|
+
/**
|
|
967
|
+
* How long one endpoint stops ATTEMPTING to connect after an attempt failed.
|
|
968
|
+
* Jittered so concurrent function instances do not re-probe in lockstep.
|
|
969
|
+
*/
|
|
970
|
+
function connectCooldownMs() {
|
|
971
|
+
const raw = Number(process.env.DB_CONNECT_COOLDOWN_MS);
|
|
972
|
+
const base = Number.isFinite(raw) && raw > 0 ? raw : 2_000;
|
|
973
|
+
return Math.round(base * (0.5 + Math.random()));
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Thrown INSTEAD of attempting, while an endpoint is in cooldown.
|
|
977
|
+
*
|
|
978
|
+
* The refused attempt's error is described in the message and deliberately NOT
|
|
979
|
+
* attached as `cause`: isConnectionError() walks `cause.code`/`cause.message`,
|
|
980
|
+
* so a cause would reclassify this as retryable and retryOnConnectionError
|
|
981
|
+
* would drive the storm straight back.
|
|
982
|
+
*/
|
|
983
|
+
export class DbConnectCooldownError extends Error {
|
|
984
|
+
code = "DB_CONNECT_COOLDOWN";
|
|
985
|
+
constructor(remainingMs, refusedBy) {
|
|
986
|
+
super(`Database is refusing connection attempts; not attempting again for ${remainingMs}ms. Last failure: ${refusedBy}`);
|
|
987
|
+
this.name = "DbConnectCooldownError";
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const connectCooldowns = new Map();
|
|
991
|
+
/**
|
|
992
|
+
* Neon rejects a connection ATTEMPT, not a connection: "Failed to acquire
|
|
993
|
+
* permit to connect to the database. Too many database connection attempts are
|
|
994
|
+
* currently ongoing." A failed acquire leaves the pool with zero idle clients,
|
|
995
|
+
* so the next execute() calls connect() again — and retryOnConnectionError
|
|
996
|
+
* backs off only 100ms. The process answers a refusal by manufacturing the next
|
|
997
|
+
* attempt, which is what keeps the refusal true. Production sat in this loop
|
|
998
|
+
* until the compute was restarted by hand.
|
|
999
|
+
*
|
|
1000
|
+
* Hold a short cooldown per endpoint so a refused attempt cannot produce the
|
|
1001
|
+
* next one. Checking out an ALREADY-IDLE client is not an attempt and passes
|
|
1002
|
+
* through, so a cooldown degrades throughput instead of taking the process
|
|
1003
|
+
* offline.
|
|
1004
|
+
*/
|
|
1005
|
+
function gateNeonConnect(pool, url, label) {
|
|
1006
|
+
const connect = pool.connect;
|
|
1007
|
+
if (typeof connect !== "function")
|
|
1008
|
+
return;
|
|
1009
|
+
const gated = function gatedConnect(...args) {
|
|
1010
|
+
const idle = typeof pool.idleCount === "number" ? pool.idleCount : 0;
|
|
1011
|
+
const gate = connectCooldowns.get(url);
|
|
1012
|
+
if (idle === 0 && gate && Date.now() < gate.until) {
|
|
1013
|
+
return Promise.reject(new DbConnectCooldownError(gate.until - Date.now(), gate.refusedBy));
|
|
1014
|
+
}
|
|
1015
|
+
return Promise.resolve(connect.apply(this, args)).then((client) => {
|
|
1016
|
+
connectCooldowns.delete(url);
|
|
1017
|
+
return client;
|
|
1018
|
+
}, (err) => {
|
|
1019
|
+
const refusedBy = describeDbError(err);
|
|
1020
|
+
const ms = connectCooldownMs();
|
|
1021
|
+
// One line per cooldown window, not per shed request.
|
|
1022
|
+
if (!gate || Date.now() >= gate.until) {
|
|
1023
|
+
console.warn(`[${label}] connection attempt refused; pausing attempts ${ms}ms:`, refusedBy);
|
|
1024
|
+
}
|
|
1025
|
+
connectCooldowns.set(url, { until: Date.now() + ms, refusedBy });
|
|
1026
|
+
throw err;
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
// Carry the wrapped function's own properties onto the wrapper. A pool's
|
|
1030
|
+
// `connect` may be instrumented — metrics, tracing, a test spy — and
|
|
1031
|
+
// replacing it with a bare closure would silently drop that instrumentation
|
|
1032
|
+
// along with any assertions built on it.
|
|
1033
|
+
Object.assign(gated, connect);
|
|
1034
|
+
pool.connect = gated;
|
|
1035
|
+
}
|
|
1036
|
+
export function guardNeonPool(pool, url, label = "db/neon") {
|
|
905
1037
|
if (!pool || typeof pool !== "object")
|
|
906
1038
|
return;
|
|
907
1039
|
if (loggedNeonPools.has(pool))
|
|
@@ -910,6 +1042,7 @@ export function attachNeonPoolErrorLogger(pool, label = "db/neon") {
|
|
|
910
1042
|
if (typeof withEvents.on !== "function")
|
|
911
1043
|
return;
|
|
912
1044
|
loggedNeonPools.add(pool);
|
|
1045
|
+
gateNeonConnect(pool, url, label);
|
|
913
1046
|
withEvents.on("error", (err) => {
|
|
914
1047
|
console.warn(`[${label}] pool error (will reconnect on next query):`, describeDbError(err));
|
|
915
1048
|
});
|
|
@@ -1118,7 +1251,7 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1118
1251
|
const pool = trackSingletonResources
|
|
1119
1252
|
? sharedDbPool("neon", url, makePool)
|
|
1120
1253
|
: makePool();
|
|
1121
|
-
|
|
1254
|
+
guardNeonPool(pool, url);
|
|
1122
1255
|
const httpSql = bgHttp ? neon(url, { fullResults: true }) : null;
|
|
1123
1256
|
async function queryNeonClient(client, sql, timeoutOverrideMs) {
|
|
1124
1257
|
const { rawSql, args } = sqlAndArgs(sql);
|
|
@@ -1504,7 +1637,36 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1504
1637
|
};
|
|
1505
1638
|
}
|
|
1506
1639
|
export async function createDbExec(config = {}) {
|
|
1507
|
-
|
|
1640
|
+
const exec = await createDbExecInternal(config, false);
|
|
1641
|
+
return guardSchemaMutations(exec);
|
|
1642
|
+
}
|
|
1643
|
+
function guardSchemaMutations(exec) {
|
|
1644
|
+
const guarded = {
|
|
1645
|
+
async execute(statement) {
|
|
1646
|
+
assertSchemaMutationAllowed(statement);
|
|
1647
|
+
return exec.execute(statement);
|
|
1648
|
+
},
|
|
1649
|
+
};
|
|
1650
|
+
if (exec.atomicBatch) {
|
|
1651
|
+
guarded.atomicBatch = async (statements) => {
|
|
1652
|
+
for (const statement of statements) {
|
|
1653
|
+
assertSchemaMutationAllowed(statement);
|
|
1654
|
+
}
|
|
1655
|
+
return exec.atomicBatch(statements);
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (exec.transaction) {
|
|
1659
|
+
guarded.transaction = (fn) => exec.transaction((tx) => fn({
|
|
1660
|
+
...tx,
|
|
1661
|
+
execute: async (statement) => {
|
|
1662
|
+
assertSchemaMutationAllowed(statement);
|
|
1663
|
+
return tx.execute(statement);
|
|
1664
|
+
},
|
|
1665
|
+
}));
|
|
1666
|
+
}
|
|
1667
|
+
if (exec.close)
|
|
1668
|
+
guarded.close = () => exec.close();
|
|
1669
|
+
return guarded;
|
|
1508
1670
|
}
|
|
1509
1671
|
async function initClient() {
|
|
1510
1672
|
if (_exec)
|
|
@@ -1561,6 +1723,7 @@ export function getDbExec() {
|
|
|
1561
1723
|
return sql;
|
|
1562
1724
|
}
|
|
1563
1725
|
async function execAnnotated(s) {
|
|
1726
|
+
assertSchemaMutationAllowed(s);
|
|
1564
1727
|
try {
|
|
1565
1728
|
return await _exec.execute(sanitize(s));
|
|
1566
1729
|
}
|
|
@@ -1571,6 +1734,7 @@ export function getDbExec() {
|
|
|
1571
1734
|
// Return a proxy that lazy-inits on first call
|
|
1572
1735
|
const proxy = {
|
|
1573
1736
|
async execute(sql) {
|
|
1737
|
+
assertSchemaMutationAllowed(sql);
|
|
1574
1738
|
if (!_initPromise)
|
|
1575
1739
|
_initPromise = initClient();
|
|
1576
1740
|
try {
|
|
@@ -1588,11 +1752,19 @@ export function getDbExec() {
|
|
|
1588
1752
|
const wrapper = {
|
|
1589
1753
|
execute: (s) => execAnnotated(s),
|
|
1590
1754
|
atomicBatch: _exec.atomicBatch
|
|
1591
|
-
?
|
|
1755
|
+
? async (statements) => {
|
|
1756
|
+
for (const statement of statements) {
|
|
1757
|
+
assertSchemaMutationAllowed(statement);
|
|
1758
|
+
}
|
|
1759
|
+
return _exec.atomicBatch(statements.map((s) => sanitize(s)));
|
|
1760
|
+
}
|
|
1592
1761
|
: undefined,
|
|
1593
1762
|
transaction: _exec.transaction
|
|
1594
1763
|
? (fn) => _exec.transaction((tx) => fn({
|
|
1595
|
-
execute: (s) =>
|
|
1764
|
+
execute: (s) => {
|
|
1765
|
+
assertSchemaMutationAllowed(s);
|
|
1766
|
+
return tx.execute(sanitize(s));
|
|
1767
|
+
},
|
|
1596
1768
|
transaction: tx.transaction,
|
|
1597
1769
|
}))
|
|
1598
1770
|
: undefined,
|
|
@@ -1614,11 +1786,19 @@ export function getDbExec() {
|
|
|
1614
1786
|
const wrapper = {
|
|
1615
1787
|
execute: (s) => execAnnotated(s),
|
|
1616
1788
|
atomicBatch: _exec.atomicBatch
|
|
1617
|
-
?
|
|
1789
|
+
? async (statements) => {
|
|
1790
|
+
for (const statement of statements) {
|
|
1791
|
+
assertSchemaMutationAllowed(statement);
|
|
1792
|
+
}
|
|
1793
|
+
return _exec.atomicBatch(statements.map((s) => sanitize(s)));
|
|
1794
|
+
}
|
|
1618
1795
|
: undefined,
|
|
1619
1796
|
transaction: _exec.transaction
|
|
1620
1797
|
? (innerFn) => _exec.transaction((tx) => innerFn({
|
|
1621
|
-
execute: (s) =>
|
|
1798
|
+
execute: (s) => {
|
|
1799
|
+
assertSchemaMutationAllowed(s);
|
|
1800
|
+
return tx.execute(sanitize(s));
|
|
1801
|
+
},
|
|
1622
1802
|
transaction: tx.transaction,
|
|
1623
1803
|
}))
|
|
1624
1804
|
: undefined,
|
|
@@ -1626,7 +1806,10 @@ export function getDbExec() {
|
|
|
1626
1806
|
Object.assign(proxy, wrapper);
|
|
1627
1807
|
if (_exec.transaction) {
|
|
1628
1808
|
return _exec.transaction((tx) => fn({
|
|
1629
|
-
execute: (s) =>
|
|
1809
|
+
execute: (s) => {
|
|
1810
|
+
assertSchemaMutationAllowed(s);
|
|
1811
|
+
return tx.execute(sanitize(s));
|
|
1812
|
+
},
|
|
1630
1813
|
transaction: tx.transaction,
|
|
1631
1814
|
}));
|
|
1632
1815
|
}
|
|
@@ -1636,6 +1819,9 @@ export function getDbExec() {
|
|
|
1636
1819
|
return explicitTransaction(wrapper.execute)(fn);
|
|
1637
1820
|
},
|
|
1638
1821
|
async atomicBatch(statements) {
|
|
1822
|
+
for (const statement of statements) {
|
|
1823
|
+
assertSchemaMutationAllowed(statement);
|
|
1824
|
+
}
|
|
1639
1825
|
if (!_initPromise)
|
|
1640
1826
|
_initPromise = initClient();
|
|
1641
1827
|
try {
|
|
@@ -1649,7 +1835,12 @@ export function getDbExec() {
|
|
|
1649
1835
|
if (!_exec.atomicBatch) {
|
|
1650
1836
|
throw new Error("This database does not support atomic batches.");
|
|
1651
1837
|
}
|
|
1652
|
-
const batch =
|
|
1838
|
+
const batch = async (items) => {
|
|
1839
|
+
for (const item of items) {
|
|
1840
|
+
assertSchemaMutationAllowed(item);
|
|
1841
|
+
}
|
|
1842
|
+
return _exec.atomicBatch(items.map((item) => sanitize(item)));
|
|
1843
|
+
};
|
|
1653
1844
|
Object.assign(proxy, { atomicBatch: batch });
|
|
1654
1845
|
return batch(statements);
|
|
1655
1846
|
},
|
package/dist/db/create-get-db.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { drizzle as drizzleD1 } from "drizzle-orm/d1";
|
|
3
|
-
import { getDialect, getCloudflareD1Binding, getDatabaseUrl, getDatabaseAuthToken, isLocalSqliteUrl, isPgliteUrl, isConnectionError, getPgliteClient, loadPgliteDrizzle, prepareLocalSqliteUrl, sqliteFilenameFromUrl, pgPoolOptions, neonPoolOptions,
|
|
3
|
+
import { getDialect, getCloudflareD1Binding, getDatabaseUrl, getDatabaseAuthToken, isLocalSqliteUrl, isPgliteUrl, isConnectionError, getPgliteClient, loadPgliteDrizzle, prepareLocalSqliteUrl, sqliteFilenameFromUrl, pgPoolOptions, neonPoolOptions, guardNeonPool, withDbTimeout, retryOnConnectionError, dbOpTimeoutMs, sharedDbPool, onSharedDbPoolsClosed, onSharedDbPoolReplaced, } from "./client.js";
|
|
4
4
|
// Lazy driver loaders — cached promises so dynamic import only runs once.
|
|
5
5
|
let _pgDrizzle;
|
|
6
6
|
function getPgDrizzle() {
|
|
@@ -408,7 +408,7 @@ export function createGetDb(schema) {
|
|
|
408
408
|
// schema module. See `sharedDbPool` in client.ts.
|
|
409
409
|
resetOnPoolClose("neon", url);
|
|
410
410
|
const rawPool = sharedDbPool("neon", url, () => new Pool({ connectionString: url, ...neonPoolOptions() }));
|
|
411
|
-
|
|
411
|
+
guardNeonPool(rawPool, url);
|
|
412
412
|
// Wrap the pool with the resilience layer so Drizzle queries get the
|
|
413
413
|
// same withDbTimeout + retryOnConnectionError protection as the raw
|
|
414
414
|
// DbExec path in client.ts. Reads retry freely; writes only retry on
|
package/dist/db/ddl-guard.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* (local dev) there is no such lock problem, so callers keep their existing
|
|
29
29
|
* behaviour there.
|
|
30
30
|
*/
|
|
31
|
-
import { isPostgres,
|
|
31
|
+
import { getDbExec, isPostgres, isProductionServerlessFunctionRuntime, } from "./client.js";
|
|
32
32
|
const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
33
33
|
/**
|
|
34
34
|
* Skip on-demand schema DDL entirely. For deployments that run real migrations,
|
|
@@ -45,6 +45,8 @@ const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
45
45
|
* down the DDL path this flag exists to avoid.
|
|
46
46
|
*/
|
|
47
47
|
function schemaEnsureDisabled() {
|
|
48
|
+
if (isProductionServerlessFunctionRuntime())
|
|
49
|
+
return true;
|
|
48
50
|
const raw = process.env.AGENT_NATIVE_SKIP_ENSURE_TABLES?.trim();
|
|
49
51
|
return !!raw && ["1", "true", "yes", "on"].includes(raw.toLowerCase());
|
|
50
52
|
}
|
|
@@ -67,6 +67,8 @@ export interface EnsureAdditiveColumnsOptions {
|
|
|
67
67
|
logger?: EnsureAdditiveColumnsLogger;
|
|
68
68
|
}
|
|
69
69
|
export interface EnsureAdditiveColumnsResult {
|
|
70
|
+
/** Whether this invocation actually inspected the live schema. */
|
|
71
|
+
mode: "checked" | "skipped-serverless";
|
|
70
72
|
/** `"table.column"` entries that were successfully added. */
|
|
71
73
|
applied: string[];
|
|
72
74
|
/** Declared columns that were intentionally left unpatched, with why. */
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* unreadable) must never crash boot — the caller decides whether/how to
|
|
55
55
|
* surface `errors` in the returned summary.
|
|
56
56
|
*/
|
|
57
|
-
import { isPostgres } from "./client.js";
|
|
57
|
+
import { isPostgres, isProductionServerlessFunctionRuntime, } from "./client.js";
|
|
58
58
|
const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
59
59
|
const defaultLogger = {
|
|
60
60
|
info: (message) => console.log(message),
|
|
@@ -62,7 +62,7 @@ const defaultLogger = {
|
|
|
62
62
|
error: (message) => console.error(message),
|
|
63
63
|
};
|
|
64
64
|
function emptyResult() {
|
|
65
|
-
return { applied: [], skipped: [], errors: [] };
|
|
65
|
+
return { mode: "checked", applied: [], skipped: [], errors: [] };
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
68
|
* Resolve `getTableConfig` for the dialect currently in effect. Both
|
|
@@ -246,6 +246,9 @@ function describeSchemaDriftError(err) {
|
|
|
246
246
|
*/
|
|
247
247
|
export async function ensureAdditiveColumns(options) {
|
|
248
248
|
const { db, tables, logger = defaultLogger } = options;
|
|
249
|
+
if (isProductionServerlessFunctionRuntime()) {
|
|
250
|
+
return { ...emptyResult(), mode: "skipped-serverless" };
|
|
251
|
+
}
|
|
249
252
|
const result = emptyResult();
|
|
250
253
|
let getTableConfig;
|
|
251
254
|
try {
|
package/dist/db/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare function createDb(config: DbConfig): Promise<(import("drizzle-orm
|
|
|
21
21
|
})>;
|
|
22
22
|
export type DrizzleDb = Awaited<ReturnType<typeof createDb>>;
|
|
23
23
|
export { createGetDb } from "./create-get-db.js";
|
|
24
|
-
export { deferMigration, MIGRATION_DEFERRED, runMigrations, type MigrationEntry, type MigrationRunResult, type MigrationSql, } from "./migrations.js";
|
|
25
|
-
export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, intType, closeDbExec, type DbExec, type DbExecConfig, type DbExecQuery, type DbExecStatement, type Dialect, } from "./client.js";
|
|
24
|
+
export { deferMigration, MIGRATION_DEFERRED, runMigrations, withMigrationRuntime, type MigrationEntry, type MigrationRunResult, type MigrationSql, } from "./migrations.js";
|
|
25
|
+
export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, assertSchemaMutationAllowed, isSchemaMutationStatement, isProductionServerlessFunctionRuntime, intType, closeDbExec, type DbExec, type DbExecConfig, type DbExecQuery, type DbExecStatement, type Dialect, } from "./client.js";
|
|
26
26
|
export { table, text, integer, now } from "./schema.js";
|
|
27
27
|
export { ensureAdditiveColumns, type EnsureAdditiveColumnsOptions, type EnsureAdditiveColumnsResult, type EnsureAdditiveColumnsLogger, } from "./ensure-additive-columns.js";
|
package/dist/db/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function createDb(config) {
|
|
|
20
20
|
throw new Error(`Unsupported driver: ${config.driver}`);
|
|
21
21
|
}
|
|
22
22
|
export { createGetDb } from "./create-get-db.js";
|
|
23
|
-
export { deferMigration, MIGRATION_DEFERRED, runMigrations, } from "./migrations.js";
|
|
24
|
-
export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, intType, closeDbExec, } from "./client.js";
|
|
23
|
+
export { deferMigration, MIGRATION_DEFERRED, runMigrations, withMigrationRuntime, } from "./migrations.js";
|
|
24
|
+
export { getDbExec, createDbExec, getDatabaseUrl, getDialect, isLocalDatabase, isPostgres, assertSchemaMutationAllowed, isSchemaMutationStatement, isProductionServerlessFunctionRuntime, intType, closeDbExec, } from "./client.js";
|
|
25
25
|
export { table, text, integer, now } from "./schema.js";
|
|
26
26
|
export { ensureAdditiveColumns, } from "./ensure-additive-columns.js";
|
package/dist/db/migrations.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export declare function isDuplicateColumnError(err: unknown): boolean;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function isPermissionError(err: unknown): boolean;
|
|
21
21
|
export interface RunMigrationsOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Run migrations even inside a serverless request runtime. Off by default:
|
|
24
|
+
* see the guard in {@link runMigrations} for why request-path DDL is what
|
|
25
|
+
* took analytics down. Only set this when the caller has no scheduled or
|
|
26
|
+
* background runtime that could migrate instead.
|
|
27
|
+
*/
|
|
28
|
+
runInServerlessRequest?: boolean;
|
|
22
29
|
/**
|
|
23
30
|
* Name of the migrations bookkeeping table. REQUIRED — there is intentionally
|
|
24
31
|
* no default. Two templates that share a database (e.g. via the same Neon URL)
|
|
@@ -85,61 +92,11 @@ export interface MigrationEntry {
|
|
|
85
92
|
run?: () => Promise<MigrationRunResult>;
|
|
86
93
|
}
|
|
87
94
|
/**
|
|
88
|
-
*
|
|
89
|
-
* per-table bookkeeping row (`options.table`) so repeated boots only apply
|
|
90
|
-
* new migrations.
|
|
91
|
-
*
|
|
92
|
-
* ## Name-based tracking (why it exists)
|
|
93
|
-
*
|
|
94
|
-
* The legacy gate is purely numeric: `SELECT MAX(version)` from the
|
|
95
|
-
* bookkeeping table, then apply every entry whose `version` is greater. That
|
|
96
|
-
* scheme silently breaks down when **two independent branches each extend the
|
|
97
|
-
* same migration list with different DDL under the same version numbers** —
|
|
98
|
-
* exactly what happened to the analytics template's v75-v83 range. Branch A
|
|
99
|
-
* shipped alert-rule tables as v75-v78; branch B shipped unrelated DDL as its
|
|
100
|
-
* own v75-v83. Whichever branch deployed first recorded "v75..v83 applied"
|
|
101
|
-
* in the bookkeeping table. When the other branch merged and deployed, its
|
|
102
|
-
* migrations at those same version numbers were never applied — `MAX(version)`
|
|
103
|
-
* was already ≥ their version, so the gate treated them as done even though
|
|
104
|
-
* their actual DDL had never run. The result: `analytics_migrations` showed
|
|
105
|
-
* rows for 1..83 with no gaps, yet `analytics_alert_rules`,
|
|
106
|
-
* `analytics_alert_incidents`, and `session_recordings.network_error_count`
|
|
107
|
-
* did not exist in production. Version numbers are not a stable identity —
|
|
108
|
-
* they're just sequence position, and sequence position collides across
|
|
109
|
-
* branches.
|
|
110
|
-
*
|
|
111
|
-
* Name-based tracking fixes this by keying application on a stable, unique
|
|
112
|
-
* **string slug** instead of a position in a shared integer sequence:
|
|
113
|
-
*
|
|
114
|
-
* - Entries with a `name` are recorded in a companion table,
|
|
115
|
-
* `${table}_named` (`name TEXT PRIMARY KEY`), and APPLY IFF their name is
|
|
116
|
-
* absent from that table — completely independent of version numbers or
|
|
117
|
-
* the legacy `MAX(version)` gate. Two branches can both ship a migration
|
|
118
|
-
* named `"analytics-alert-rules-table"` at version 75, or one at version 75
|
|
119
|
-
* and the other at version 90 after a rebase — either way it applies
|
|
120
|
-
* exactly once per database, keyed on the name.
|
|
121
|
-
* - Entries WITHOUT a `name` keep the exact legacy behavior
|
|
122
|
-
* (`version > MAX(recorded version)`), so nothing changes for existing
|
|
123
|
-
* unnamed migrations.
|
|
124
|
-
* - Named migrations still execute in list order, interleaved with unnamed
|
|
125
|
-
* ones exactly as written.
|
|
126
|
-
* - When a named migration's DDL runs, we record BOTH the named row (always)
|
|
127
|
-
* and — if its `version` is greater than the current legacy max — the
|
|
128
|
-
* legacy version row too, in the SAME atomic batch/transaction as the DDL.
|
|
129
|
-
* This keeps the legacy `MAX(version)` gate monotonically advancing for
|
|
130
|
-
* any unnamed migrations that come after it in the list, while ensuring a
|
|
131
|
-
* named migration is never "double recorded" in a way that would let it
|
|
132
|
-
* re-apply.
|
|
133
|
-
* - A duplicate `name` across the migration list throws at startup — that's
|
|
134
|
-
* a programmer error (copy-paste or merge mistake), not a runtime data
|
|
135
|
-
* problem, and failing loud beats silently tracking the wrong row.
|
|
95
|
+
* Run an explicit release-time migration job with migration duty enabled.
|
|
136
96
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* same list — but if a table is EVER at risk of being shared across parallel
|
|
140
|
-
* branches (any template's own migrations qualify, since branches routinely
|
|
141
|
-
* extend the same list concurrently), giving new entries a name is what makes
|
|
142
|
-
* them immune to the collision class described above.
|
|
97
|
+
* The flag is process-local and restored even when the job fails, so a build
|
|
98
|
+
* step can opt in without creating a permanent escape hatch for request code.
|
|
143
99
|
*/
|
|
100
|
+
export declare function withMigrationRuntime<T>(run: () => Promise<T>): Promise<T>;
|
|
144
101
|
export declare function runMigrations(migrations: Array<MigrationEntry>, options: RunMigrationsOptions): NitroPluginDef;
|
|
145
102
|
export {};
|