@neondatabase/config 0.0.0 → 0.1.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/LICENSE.md +178 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/lib/auth.d.ts +63 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +93 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/define-config.d.ts +43 -0
- package/dist/lib/define-config.d.ts.map +1 -0
- package/dist/lib/define-config.js +111 -0
- package/dist/lib/define-config.js.map +1 -0
- package/dist/lib/diff.d.ts +109 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +205 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/duration.d.ts +46 -0
- package/dist/lib/duration.d.ts.map +1 -0
- package/dist/lib/duration.js +96 -0
- package/dist/lib/duration.js.map +1 -0
- package/dist/lib/errors.d.ts +129 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +168 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/loader.d.ts +44 -0
- package/dist/lib/loader.d.ts.map +1 -0
- package/dist/lib/loader.js +119 -0
- package/dist/lib/loader.js.map +1 -0
- package/dist/lib/neon-api-real.d.ts +45 -0
- package/dist/lib/neon-api-real.d.ts.map +1 -0
- package/dist/lib/neon-api-real.js +582 -0
- package/dist/lib/neon-api-real.js.map +1 -0
- package/dist/lib/neon-api.d.ts +262 -0
- package/dist/lib/neon-api.d.ts.map +1 -0
- package/dist/lib/neon-api.js +1 -0
- package/dist/lib/patterns.d.ts +43 -0
- package/dist/lib/patterns.d.ts.map +1 -0
- package/dist/lib/patterns.js +76 -0
- package/dist/lib/patterns.js.map +1 -0
- package/dist/lib/schema.d.ts +109 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +199 -0
- package/dist/lib/schema.js.map +1 -0
- package/dist/lib/types.d.ts +259 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/wrap-neon-error.d.ts +30 -0
- package/dist/lib/wrap-neon-error.d.ts.map +1 -0
- package/dist/lib/wrap-neon-error.js +139 -0
- package/dist/lib/wrap-neon-error.js.map +1 -0
- package/dist/v1.d.ts +132 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +69 -0
- package/dist/v1.js.map +1 -0
- package/package.json +67 -17
- package/.env.example +0 -5
- package/e2e/errors.e2e.test.ts +0 -52
- package/e2e/helpers.ts +0 -205
- package/e2e/load-env.ts +0 -29
- package/e2e/setup.ts +0 -24
- package/src/index.ts +0 -5
- package/src/lib/auth.test.ts +0 -166
- package/src/lib/auth.ts +0 -124
- package/src/lib/define-config.test.ts +0 -161
- package/src/lib/define-config.ts +0 -152
- package/src/lib/diff.test.ts +0 -142
- package/src/lib/diff.ts +0 -391
- package/src/lib/duration.test.ts +0 -105
- package/src/lib/duration.ts +0 -147
- package/src/lib/errors.test.ts +0 -26
- package/src/lib/errors.ts +0 -220
- package/src/lib/fake-neon-api.ts +0 -782
- package/src/lib/loader.test.ts +0 -35
- package/src/lib/loader.ts +0 -215
- package/src/lib/neon-api-real.test.ts +0 -72
- package/src/lib/neon-api-real.ts +0 -1123
- package/src/lib/neon-api.ts +0 -356
- package/src/lib/patterns.test.ts +0 -80
- package/src/lib/patterns.ts +0 -98
- package/src/lib/schema.test.ts +0 -88
- package/src/lib/schema.ts +0 -252
- package/src/lib/test-utils.ts +0 -83
- package/src/lib/types.ts +0 -268
- package/src/lib/wrap-neon-error.test.ts +0 -145
- package/src/lib/wrap-neon-error.ts +0 -204
- package/src/v1.test.ts +0 -33
- package/src/v1.ts +0 -148
- package/tsconfig.json +0 -4
- package/tsdown.config.ts +0 -19
- package/vitest.config.ts +0 -19
- package/vitest.e2e.config.ts +0 -29
package/src/lib/diff.test.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { diffConfig, type RemoteState } from "./diff.js";
|
|
3
|
-
|
|
4
|
-
describe("diffConfig", () => {
|
|
5
|
-
const remote: RemoteState = {
|
|
6
|
-
projectId: "proj",
|
|
7
|
-
branch: {
|
|
8
|
-
id: "br-main",
|
|
9
|
-
name: "main",
|
|
10
|
-
isDefault: true,
|
|
11
|
-
protected: false,
|
|
12
|
-
},
|
|
13
|
-
endpoint: {
|
|
14
|
-
id: "ep",
|
|
15
|
-
branchId: "br-main",
|
|
16
|
-
type: "read_write" as const,
|
|
17
|
-
autoscalingLimitMinCu: 0.25,
|
|
18
|
-
autoscalingLimitMaxCu: 1,
|
|
19
|
-
suspendTimeout: "5m",
|
|
20
|
-
},
|
|
21
|
-
services: {
|
|
22
|
-
databaseName: "neondb",
|
|
23
|
-
authEnabled: false,
|
|
24
|
-
dataApiEnabled: false,
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
test("plans service enables", () => {
|
|
29
|
-
const diff = diffConfig(
|
|
30
|
-
{ authEnabled: true, dataApiEnabled: true },
|
|
31
|
-
remote,
|
|
32
|
-
{ updateExisting: false },
|
|
33
|
-
);
|
|
34
|
-
expect(diff.plan.map((p) => p.kind)).toEqual([
|
|
35
|
-
"enable-auth",
|
|
36
|
-
"enable-data-api",
|
|
37
|
-
]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("reports compute drift unless updateExisting is set", () => {
|
|
41
|
-
const diff = diffConfig(
|
|
42
|
-
{
|
|
43
|
-
authEnabled: false,
|
|
44
|
-
dataApiEnabled: false,
|
|
45
|
-
postgres: { computeSettings: { autoscalingLimitMaxCu: 2 } },
|
|
46
|
-
},
|
|
47
|
-
remote,
|
|
48
|
-
{ updateExisting: false },
|
|
49
|
-
);
|
|
50
|
-
expect(diff.conflicts[0]).toMatchObject({ field: "computeSettings" });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("plans mutable branch updates with updateExisting", () => {
|
|
54
|
-
const diff = diffConfig(
|
|
55
|
-
{ authEnabled: false, dataApiEnabled: false, protected: true },
|
|
56
|
-
remote,
|
|
57
|
-
{ updateExisting: true },
|
|
58
|
-
);
|
|
59
|
-
expect(diff.plan[0]).toMatchObject({
|
|
60
|
-
kind: "update-branch-protected",
|
|
61
|
-
branchId: "br-main",
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("plans preview create + deploy + bucket + ai-gateway when nothing exists", () => {
|
|
66
|
-
const diff = diffConfig(
|
|
67
|
-
{
|
|
68
|
-
authEnabled: false,
|
|
69
|
-
dataApiEnabled: false,
|
|
70
|
-
preview: {
|
|
71
|
-
functions: [
|
|
72
|
-
{
|
|
73
|
-
slug: "hello-world",
|
|
74
|
-
name: "Hello World",
|
|
75
|
-
source: "./hello.ts",
|
|
76
|
-
env: {},
|
|
77
|
-
runtime: "nodejs24",
|
|
78
|
-
memoryMib: 512,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
buckets: [{ name: "uploads", access: "private" }],
|
|
82
|
-
aiGatewayEnabled: true,
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
...remote,
|
|
87
|
-
preview: {
|
|
88
|
-
buckets: [],
|
|
89
|
-
functions: [],
|
|
90
|
-
aiGatewayEnabled: false,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
{ updateExisting: false },
|
|
94
|
-
);
|
|
95
|
-
expect(diff.plan.map((p) => p.kind)).toEqual([
|
|
96
|
-
"create-bucket",
|
|
97
|
-
"create-function",
|
|
98
|
-
"deploy-function",
|
|
99
|
-
"enable-ai-gateway",
|
|
100
|
-
]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("skips create-function and skips enable-ai-gateway when already present, but still re-deploys", () => {
|
|
104
|
-
const diff = diffConfig(
|
|
105
|
-
{
|
|
106
|
-
authEnabled: false,
|
|
107
|
-
dataApiEnabled: false,
|
|
108
|
-
preview: {
|
|
109
|
-
functions: [
|
|
110
|
-
{
|
|
111
|
-
slug: "hello-world",
|
|
112
|
-
name: "Hello World",
|
|
113
|
-
source: "./hello.ts",
|
|
114
|
-
env: {},
|
|
115
|
-
runtime: "nodejs24",
|
|
116
|
-
memoryMib: 512,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
buckets: [{ name: "uploads", access: "private" }],
|
|
120
|
-
aiGatewayEnabled: true,
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
...remote,
|
|
125
|
-
preview: {
|
|
126
|
-
buckets: [{ name: "uploads", accessLevel: "private" }],
|
|
127
|
-
functions: [
|
|
128
|
-
{
|
|
129
|
-
id: "fn-1",
|
|
130
|
-
slug: "hello-world",
|
|
131
|
-
name: "Hello World",
|
|
132
|
-
invocationUrl: "https://x/functions/hello-world",
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
aiGatewayEnabled: true,
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{ updateExisting: false },
|
|
139
|
-
);
|
|
140
|
-
expect(diff.plan.map((p) => p.kind)).toEqual(["deploy-function"]);
|
|
141
|
-
});
|
|
142
|
-
});
|
package/src/lib/diff.ts
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
NeonBranchSnapshot,
|
|
3
|
-
NeonBucketSnapshot,
|
|
4
|
-
NeonEndpointSnapshot,
|
|
5
|
-
NeonFunctionSnapshot,
|
|
6
|
-
} from "./neon-api.js";
|
|
7
|
-
import type {
|
|
8
|
-
BucketAccessLevel,
|
|
9
|
-
ComputeSettings,
|
|
10
|
-
ConflictReport,
|
|
11
|
-
ResolvedBranchConfig,
|
|
12
|
-
ResolvedFunctionConfig,
|
|
13
|
-
} from "./types.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A planned action to perform a single mutation against the Neon API. The diff engine
|
|
17
|
-
* produces a list of these for `pushConfig` to execute (or report).
|
|
18
|
-
*/
|
|
19
|
-
export type PlanStep =
|
|
20
|
-
| {
|
|
21
|
-
kind: "update-branch-ttl";
|
|
22
|
-
projectId: string;
|
|
23
|
-
branchId: string;
|
|
24
|
-
branchName: string;
|
|
25
|
-
expiresAt: string | null;
|
|
26
|
-
}
|
|
27
|
-
| {
|
|
28
|
-
kind: "update-branch-protected";
|
|
29
|
-
projectId: string;
|
|
30
|
-
branchId: string;
|
|
31
|
-
branchName: string;
|
|
32
|
-
protected: boolean;
|
|
33
|
-
}
|
|
34
|
-
| {
|
|
35
|
-
kind: "update-endpoint";
|
|
36
|
-
projectId: string;
|
|
37
|
-
branchName: string;
|
|
38
|
-
endpointId: string;
|
|
39
|
-
settings: ComputeSettings;
|
|
40
|
-
}
|
|
41
|
-
| {
|
|
42
|
-
kind: "enable-auth";
|
|
43
|
-
projectId: string;
|
|
44
|
-
branchId: string;
|
|
45
|
-
branchName: string;
|
|
46
|
-
databaseName?: string;
|
|
47
|
-
}
|
|
48
|
-
| {
|
|
49
|
-
kind: "enable-data-api";
|
|
50
|
-
projectId: string;
|
|
51
|
-
branchId: string;
|
|
52
|
-
branchName: string;
|
|
53
|
-
databaseName: string;
|
|
54
|
-
}
|
|
55
|
-
| {
|
|
56
|
-
kind: "create-bucket";
|
|
57
|
-
projectId: string;
|
|
58
|
-
branchId: string;
|
|
59
|
-
branchName: string;
|
|
60
|
-
bucketName: string;
|
|
61
|
-
accessLevel: BucketAccessLevel;
|
|
62
|
-
}
|
|
63
|
-
| {
|
|
64
|
-
kind: "create-function";
|
|
65
|
-
projectId: string;
|
|
66
|
-
branchId: string;
|
|
67
|
-
branchName: string;
|
|
68
|
-
fn: ResolvedFunctionConfig;
|
|
69
|
-
}
|
|
70
|
-
| {
|
|
71
|
-
/**
|
|
72
|
-
* Deploy (or re-deploy) code to a function. Always planned for every desired
|
|
73
|
-
* function — deployments are versioned and the newest becomes active, so a push
|
|
74
|
-
* ships the current source each time. `functionExists` tells `pushConfig` whether
|
|
75
|
-
* it must create the function first (covered by a preceding `create-function` step).
|
|
76
|
-
*/
|
|
77
|
-
kind: "deploy-function";
|
|
78
|
-
projectId: string;
|
|
79
|
-
branchId: string;
|
|
80
|
-
branchName: string;
|
|
81
|
-
fn: ResolvedFunctionConfig;
|
|
82
|
-
}
|
|
83
|
-
| {
|
|
84
|
-
kind: "enable-ai-gateway";
|
|
85
|
-
projectId: string;
|
|
86
|
-
branchId: string;
|
|
87
|
-
branchName: string;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export interface RemoteServiceState {
|
|
91
|
-
databaseName: string;
|
|
92
|
-
authEnabled: boolean;
|
|
93
|
-
dataApiEnabled: boolean;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Snapshot of the branch's current Preview-feature state. Absent (`undefined`) when the
|
|
98
|
-
* policy has no `preview` block — `pushConfig` only fetches this when needed.
|
|
99
|
-
*/
|
|
100
|
-
export interface RemotePreviewState {
|
|
101
|
-
buckets: NeonBucketSnapshot[];
|
|
102
|
-
functions: NeonFunctionSnapshot[];
|
|
103
|
-
aiGatewayEnabled: boolean;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface RemoteState {
|
|
107
|
-
projectId: string;
|
|
108
|
-
branch: NeonBranchSnapshot;
|
|
109
|
-
endpoint?: NeonEndpointSnapshot;
|
|
110
|
-
services: RemoteServiceState;
|
|
111
|
-
preview?: RemotePreviewState;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface DiffOptions {
|
|
115
|
-
/**
|
|
116
|
-
* Apply mutable drift on the selected branch as plan steps instead of conflicts.
|
|
117
|
-
* Default: `false`.
|
|
118
|
-
*/
|
|
119
|
-
updateExisting: boolean;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface DiffResult {
|
|
123
|
-
plan: PlanStep[];
|
|
124
|
-
conflicts: ConflictReport[];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Diff desired branch policy against the selected remote branch. Pure function.
|
|
129
|
-
*/
|
|
130
|
-
export function diffConfig(
|
|
131
|
-
config: ResolvedBranchConfig,
|
|
132
|
-
remote: RemoteState,
|
|
133
|
-
options: DiffOptions,
|
|
134
|
-
): DiffResult {
|
|
135
|
-
const conflicts: ConflictReport[] = [];
|
|
136
|
-
const plan: PlanStep[] = [];
|
|
137
|
-
diffBranchConfig({ config, remote, options, plan, conflicts });
|
|
138
|
-
diffServices({ config, remote, plan });
|
|
139
|
-
diffPreview({ config, remote, plan });
|
|
140
|
-
return { plan, conflicts };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Plan Preview features (functions, buckets, AI Gateway). Like {@link diffServices}, this
|
|
145
|
-
* is **additive**: it creates desired buckets/functions and enables the AI Gateway, but
|
|
146
|
-
* never deletes buckets/functions or disables the gateway. Teardown is destructive, so it
|
|
147
|
-
* stays explicit/manual — matching the existing auth / dataApi behaviour.
|
|
148
|
-
*
|
|
149
|
-
* Functions are always (re-)deployed: deployments are versioned and the newest becomes
|
|
150
|
-
* active, so each push ships the current source. A `create-function` step precedes the
|
|
151
|
-
* `deploy-function` step when the function does not yet exist remotely.
|
|
152
|
-
*/
|
|
153
|
-
function diffPreview(args: {
|
|
154
|
-
config: ResolvedBranchConfig;
|
|
155
|
-
remote: RemoteState;
|
|
156
|
-
plan: PlanStep[];
|
|
157
|
-
}): void {
|
|
158
|
-
const { config, remote, plan } = args;
|
|
159
|
-
const preview = config.preview;
|
|
160
|
-
if (!preview) return;
|
|
161
|
-
// `remote.preview` is only fetched when the policy has a preview block; treat a missing
|
|
162
|
-
// snapshot as "nothing exists yet" so the diff is still well-defined.
|
|
163
|
-
const state: RemotePreviewState = remote.preview ?? {
|
|
164
|
-
buckets: [],
|
|
165
|
-
functions: [],
|
|
166
|
-
aiGatewayEnabled: false,
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
for (const bucket of preview.buckets) {
|
|
170
|
-
if (state.buckets.some((b) => b.name === bucket.name)) continue;
|
|
171
|
-
plan.push({
|
|
172
|
-
kind: "create-bucket",
|
|
173
|
-
projectId: remote.projectId,
|
|
174
|
-
branchId: remote.branch.id,
|
|
175
|
-
branchName: remote.branch.name,
|
|
176
|
-
bucketName: bucket.name,
|
|
177
|
-
accessLevel: bucket.access,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
for (const fn of preview.functions) {
|
|
182
|
-
const exists = state.functions.some((f) => f.slug === fn.slug);
|
|
183
|
-
if (!exists) {
|
|
184
|
-
plan.push({
|
|
185
|
-
kind: "create-function",
|
|
186
|
-
projectId: remote.projectId,
|
|
187
|
-
branchId: remote.branch.id,
|
|
188
|
-
branchName: remote.branch.name,
|
|
189
|
-
fn,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
plan.push({
|
|
193
|
-
kind: "deploy-function",
|
|
194
|
-
projectId: remote.projectId,
|
|
195
|
-
branchId: remote.branch.id,
|
|
196
|
-
branchName: remote.branch.name,
|
|
197
|
-
fn,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (preview.aiGatewayEnabled && !state.aiGatewayEnabled) {
|
|
202
|
-
plan.push({
|
|
203
|
-
kind: "enable-ai-gateway",
|
|
204
|
-
projectId: remote.projectId,
|
|
205
|
-
branchId: remote.branch.id,
|
|
206
|
-
branchName: remote.branch.name,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Plan additive branch-scoped integrations. Disabling remains explicit/manual because
|
|
213
|
-
* teardown is destructive.
|
|
214
|
-
*/
|
|
215
|
-
function diffServices(args: {
|
|
216
|
-
config: ResolvedBranchConfig;
|
|
217
|
-
remote: RemoteState;
|
|
218
|
-
plan: PlanStep[];
|
|
219
|
-
}): void {
|
|
220
|
-
const { config, remote, plan } = args;
|
|
221
|
-
const state = remote.services;
|
|
222
|
-
if (config.authEnabled && !state.authEnabled) {
|
|
223
|
-
const step: PlanStep = {
|
|
224
|
-
kind: "enable-auth",
|
|
225
|
-
projectId: remote.projectId,
|
|
226
|
-
branchId: remote.branch.id,
|
|
227
|
-
branchName: remote.branch.name,
|
|
228
|
-
};
|
|
229
|
-
if (state.databaseName) step.databaseName = state.databaseName;
|
|
230
|
-
plan.push(step);
|
|
231
|
-
}
|
|
232
|
-
if (config.dataApiEnabled && !state.dataApiEnabled) {
|
|
233
|
-
plan.push({
|
|
234
|
-
kind: "enable-data-api",
|
|
235
|
-
projectId: remote.projectId,
|
|
236
|
-
branchId: remote.branch.id,
|
|
237
|
-
branchName: remote.branch.name,
|
|
238
|
-
databaseName: state.databaseName,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
interface BranchConfigArgs {
|
|
244
|
-
config: ResolvedBranchConfig;
|
|
245
|
-
remote: RemoteState;
|
|
246
|
-
options: DiffOptions;
|
|
247
|
-
plan: PlanStep[];
|
|
248
|
-
conflicts: ConflictReport[];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function diffBranchConfig(args: BranchConfigArgs): void {
|
|
252
|
-
const { config, remote, options, plan, conflicts } = args;
|
|
253
|
-
const branchName = remote.branch.name;
|
|
254
|
-
const computeSettings = config.postgres?.computeSettings;
|
|
255
|
-
|
|
256
|
-
if (computeSettings) {
|
|
257
|
-
const endpoint = remote.endpoint;
|
|
258
|
-
if (!endpoint) {
|
|
259
|
-
conflicts.push({
|
|
260
|
-
kind: "branch",
|
|
261
|
-
identifier: branchName,
|
|
262
|
-
field: "endpoint",
|
|
263
|
-
current: undefined,
|
|
264
|
-
desired: computeSettings,
|
|
265
|
-
reason: "Branch has no read-write endpoint; cannot apply compute settings.",
|
|
266
|
-
});
|
|
267
|
-
} else {
|
|
268
|
-
const drift = computeDriftBetween(computeSettings, endpoint);
|
|
269
|
-
if (drift) {
|
|
270
|
-
if (options.updateExisting) {
|
|
271
|
-
plan.push({
|
|
272
|
-
kind: "update-endpoint",
|
|
273
|
-
projectId: remote.projectId,
|
|
274
|
-
branchName,
|
|
275
|
-
endpointId: endpoint.id,
|
|
276
|
-
settings: computeSettings,
|
|
277
|
-
});
|
|
278
|
-
} else {
|
|
279
|
-
conflicts.push({
|
|
280
|
-
kind: "branch",
|
|
281
|
-
identifier: branchName,
|
|
282
|
-
field: "computeSettings",
|
|
283
|
-
current: drift.current,
|
|
284
|
-
desired: drift.desired,
|
|
285
|
-
reason: "Existing branch has different compute settings. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.",
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
config.protected !== undefined &&
|
|
294
|
-
config.protected !== remote.branch.protected
|
|
295
|
-
) {
|
|
296
|
-
if (options.updateExisting) {
|
|
297
|
-
plan.push({
|
|
298
|
-
kind: "update-branch-protected",
|
|
299
|
-
projectId: remote.projectId,
|
|
300
|
-
branchId: remote.branch.id,
|
|
301
|
-
branchName,
|
|
302
|
-
protected: config.protected,
|
|
303
|
-
});
|
|
304
|
-
} else {
|
|
305
|
-
conflicts.push({
|
|
306
|
-
kind: "branch",
|
|
307
|
-
identifier: branchName,
|
|
308
|
-
field: "protected",
|
|
309
|
-
current: remote.branch.protected,
|
|
310
|
-
desired: config.protected,
|
|
311
|
-
reason: "Existing branch has a different `protected` flag. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.",
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (config.ttlSeconds !== undefined) {
|
|
317
|
-
const current = remote.branch.expiresAt
|
|
318
|
-
? Math.max(
|
|
319
|
-
0,
|
|
320
|
-
Math.round(
|
|
321
|
-
(Date.parse(remote.branch.expiresAt) - Date.now()) /
|
|
322
|
-
1000,
|
|
323
|
-
),
|
|
324
|
-
)
|
|
325
|
-
: undefined;
|
|
326
|
-
if (
|
|
327
|
-
current === undefined ||
|
|
328
|
-
Math.abs(current - config.ttlSeconds) > 30
|
|
329
|
-
) {
|
|
330
|
-
const expiresAt = new Date(
|
|
331
|
-
Date.now() + config.ttlSeconds * 1000,
|
|
332
|
-
).toISOString();
|
|
333
|
-
if (options.updateExisting) {
|
|
334
|
-
plan.push({
|
|
335
|
-
kind: "update-branch-ttl",
|
|
336
|
-
projectId: remote.projectId,
|
|
337
|
-
branchId: remote.branch.id,
|
|
338
|
-
branchName,
|
|
339
|
-
expiresAt,
|
|
340
|
-
});
|
|
341
|
-
} else {
|
|
342
|
-
conflicts.push({
|
|
343
|
-
kind: "branch",
|
|
344
|
-
identifier: branchName,
|
|
345
|
-
field: "ttl",
|
|
346
|
-
current: remote.branch.expiresAt,
|
|
347
|
-
desired: expiresAt,
|
|
348
|
-
reason: "Existing branch has a different TTL. Pass `updateExisting: true` (SDK) or `--update-existing` (CLI) to apply.",
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function computeDriftBetween(
|
|
356
|
-
desired: ComputeSettings,
|
|
357
|
-
endpoint: NeonEndpointSnapshot,
|
|
358
|
-
): {
|
|
359
|
-
current: Partial<ComputeSettings>;
|
|
360
|
-
desired: Partial<ComputeSettings>;
|
|
361
|
-
} | null {
|
|
362
|
-
const currentDrift: Partial<ComputeSettings> = {};
|
|
363
|
-
const desiredDrift: Partial<ComputeSettings> = {};
|
|
364
|
-
let drift = false;
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
desired.autoscalingLimitMinCu !== undefined &&
|
|
368
|
-
desired.autoscalingLimitMinCu !== endpoint.autoscalingLimitMinCu
|
|
369
|
-
) {
|
|
370
|
-
currentDrift.autoscalingLimitMinCu = endpoint.autoscalingLimitMinCu;
|
|
371
|
-
desiredDrift.autoscalingLimitMinCu = desired.autoscalingLimitMinCu;
|
|
372
|
-
drift = true;
|
|
373
|
-
}
|
|
374
|
-
if (
|
|
375
|
-
desired.autoscalingLimitMaxCu !== undefined &&
|
|
376
|
-
desired.autoscalingLimitMaxCu !== endpoint.autoscalingLimitMaxCu
|
|
377
|
-
) {
|
|
378
|
-
currentDrift.autoscalingLimitMaxCu = endpoint.autoscalingLimitMaxCu;
|
|
379
|
-
desiredDrift.autoscalingLimitMaxCu = desired.autoscalingLimitMaxCu;
|
|
380
|
-
drift = true;
|
|
381
|
-
}
|
|
382
|
-
if (
|
|
383
|
-
desired.suspendTimeout !== undefined &&
|
|
384
|
-
desired.suspendTimeout !== endpoint.suspendTimeout
|
|
385
|
-
) {
|
|
386
|
-
currentDrift.suspendTimeout = endpoint.suspendTimeout;
|
|
387
|
-
desiredDrift.suspendTimeout = desired.suspendTimeout;
|
|
388
|
-
drift = true;
|
|
389
|
-
}
|
|
390
|
-
return drift ? { current: currentDrift, desired: desiredDrift } : null;
|
|
391
|
-
}
|
package/src/lib/duration.test.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { formatDurationSeconds, parseDuration } from "./duration.js";
|
|
3
|
-
|
|
4
|
-
describe("parseDuration", () => {
|
|
5
|
-
test.each([
|
|
6
|
-
["30s", 30],
|
|
7
|
-
["5m", 300],
|
|
8
|
-
["1h", 3600],
|
|
9
|
-
["2h", 7200],
|
|
10
|
-
["1d", 86_400],
|
|
11
|
-
["7d", 604_800],
|
|
12
|
-
["2w", 1_209_600],
|
|
13
|
-
["1W", 604_800],
|
|
14
|
-
["3600", 3600],
|
|
15
|
-
])("parses %s as %d seconds", (input, expected) => {
|
|
16
|
-
const result = parseDuration(input);
|
|
17
|
-
expect(result).toEqual({ seconds: expected });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("accepts integer numbers", () => {
|
|
21
|
-
expect(parseDuration(60)).toEqual({ seconds: 60 });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test.each([
|
|
25
|
-
["", "duration string is empty"],
|
|
26
|
-
[" ", "duration string is empty"],
|
|
27
|
-
["abc", 'invalid duration "abc"'],
|
|
28
|
-
["1h30m", 'invalid duration "1h30m"'],
|
|
29
|
-
["0", 'must be > 0, got "0"'],
|
|
30
|
-
["0s", 'must be > 0, got "0s"'],
|
|
31
|
-
["-5", 'invalid duration "-5"'],
|
|
32
|
-
])("rejects %s", (input, expectedErrorFragment) => {
|
|
33
|
-
const result = parseDuration(input);
|
|
34
|
-
expect(result).toHaveProperty("error");
|
|
35
|
-
expect((result as { error: string }).error).toContain(
|
|
36
|
-
expectedErrorFragment,
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("rejects non-integer numbers", () => {
|
|
41
|
-
expect(parseDuration(1.5)).toEqual({
|
|
42
|
-
error: expect.stringContaining("must be an integer"),
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("rejects zero and negative numbers", () => {
|
|
47
|
-
expect(parseDuration(0)).toEqual({
|
|
48
|
-
error: expect.stringContaining("must be > 0"),
|
|
49
|
-
});
|
|
50
|
-
expect(parseDuration(-1)).toEqual({
|
|
51
|
-
error: expect.stringContaining("must be > 0"),
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("rejects non-finite numbers", () => {
|
|
56
|
-
expect(parseDuration(Number.POSITIVE_INFINITY)).toEqual({
|
|
57
|
-
error: expect.stringContaining("not a finite number"),
|
|
58
|
-
});
|
|
59
|
-
expect(parseDuration(Number.NaN)).toEqual({
|
|
60
|
-
error: expect.stringContaining("not a finite number"),
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("formatDurationSeconds", () => {
|
|
66
|
-
test.each([
|
|
67
|
-
[30, "30s"],
|
|
68
|
-
[60, "1m"],
|
|
69
|
-
[3600, "1h"],
|
|
70
|
-
[86_400, "1d"],
|
|
71
|
-
[604_800, "1w"],
|
|
72
|
-
[1_209_600, "2w"],
|
|
73
|
-
[7200, "2h"],
|
|
74
|
-
[120, "2m"],
|
|
75
|
-
[90, "90s"], // doesn't fit a clean minute boundary above 60 → falls back to seconds
|
|
76
|
-
])("formats %d seconds as %s", (input, expected) => {
|
|
77
|
-
expect(formatDurationSeconds(input)).toBe(expected);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("throws on non-positive input", () => {
|
|
81
|
-
expect(() => formatDurationSeconds(0)).toThrow(RangeError);
|
|
82
|
-
expect(() => formatDurationSeconds(-1)).toThrow(RangeError);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("round-trips parseDuration → formatDurationSeconds in canonical form", () => {
|
|
86
|
-
// `7d` and `1w` both represent 604_800 seconds; the formatter chooses the largest
|
|
87
|
-
// clean unit, so the canonical form is `1w`. This documents the chosen tie-break.
|
|
88
|
-
const cases: Array<[string, string]> = [
|
|
89
|
-
["30s", "30s"],
|
|
90
|
-
["5m", "5m"],
|
|
91
|
-
["1h", "1h"],
|
|
92
|
-
["2h", "2h"],
|
|
93
|
-
["1d", "1d"],
|
|
94
|
-
["7d", "1w"],
|
|
95
|
-
["1w", "1w"],
|
|
96
|
-
["2w", "2w"],
|
|
97
|
-
];
|
|
98
|
-
for (const [input, canonical] of cases) {
|
|
99
|
-
const parsed = parseDuration(input);
|
|
100
|
-
if (!("seconds" in parsed))
|
|
101
|
-
throw new Error(`failed to parse ${input}`);
|
|
102
|
-
expect(formatDurationSeconds(parsed.seconds)).toBe(canonical);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
});
|