@neondatabase/config-runtime 0.0.0 → 0.2.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.
Files changed (43) hide show
  1. package/LICENSE.md +178 -0
  2. package/dist/index.d.ts +6 -0
  3. package/dist/index.js +6 -0
  4. package/dist/lib/function-bundle.d.ts +34 -0
  5. package/dist/lib/function-bundle.d.ts.map +1 -0
  6. package/dist/lib/function-bundle.js +65 -0
  7. package/dist/lib/function-bundle.js.map +1 -0
  8. package/dist/lib/operations.d.ts +77 -0
  9. package/dist/lib/operations.d.ts.map +1 -0
  10. package/dist/lib/operations.js +61 -0
  11. package/dist/lib/operations.js.map +1 -0
  12. package/dist/lib/pull-config.d.ts +63 -0
  13. package/dist/lib/pull-config.d.ts.map +1 -0
  14. package/dist/lib/pull-config.js +113 -0
  15. package/dist/lib/pull-config.js.map +1 -0
  16. package/dist/lib/push-config.d.ts +111 -0
  17. package/dist/lib/push-config.d.ts.map +1 -0
  18. package/dist/lib/push-config.js +400 -0
  19. package/dist/lib/push-config.js.map +1 -0
  20. package/dist/v1.d.ts +6 -0
  21. package/dist/v1.js +6 -0
  22. package/package.json +69 -22
  23. package/e2e/conflict.e2e.test.ts +0 -34
  24. package/e2e/helpers.ts +0 -204
  25. package/e2e/lifecycle.e2e.test.ts +0 -50
  26. package/e2e/load-env.ts +0 -29
  27. package/e2e/setup.ts +0 -24
  28. package/src/index.ts +0 -5
  29. package/src/lib/fake-neon-api.ts +0 -782
  30. package/src/lib/function-bundle.test.ts +0 -60
  31. package/src/lib/function-bundle.ts +0 -104
  32. package/src/lib/operations.test.ts +0 -150
  33. package/src/lib/operations.ts +0 -103
  34. package/src/lib/pull-config.test.ts +0 -51
  35. package/src/lib/pull-config.ts +0 -215
  36. package/src/lib/push-config.test.ts +0 -421
  37. package/src/lib/push-config.ts +0 -619
  38. package/src/v1.test.ts +0 -30
  39. package/src/v1.ts +0 -74
  40. package/tsconfig.json +0 -4
  41. package/tsdown.config.ts +0 -22
  42. package/vitest.config.ts +0 -19
  43. package/vitest.e2e.config.ts +0 -29
@@ -1,619 +0,0 @@
1
- import {
2
- type AppliedChange,
3
- type Config,
4
- createNeonApiFromOptions,
5
- diffConfig,
6
- ErrorCode,
7
- type NeonApi,
8
- type NeonBranchSnapshot,
9
- type PlanStep,
10
- PlatformError,
11
- PushAbortedError,
12
- PushConflictError,
13
- type PushResult,
14
- type RemotePreviewState,
15
- type RemoteServiceState,
16
- type RemoteState,
17
- resolveConfig,
18
- } from "@neondatabase/config";
19
- import { buildFunctionBundle } from "./function-bundle.js";
20
-
21
- export interface PushConfigOptions {
22
- /**
23
- * Neon project id. **Required** — the management API addresses every branch through
24
- * its project, so there is no way to push without it. `pushConfig` never creates a
25
- * project; resolve the id yourself (e.g. via neonctl) and pass it in.
26
- */
27
- projectId: string;
28
- /**
29
- * Neon branch id (`br-…`). **Required.** `pushConfig` never creates a branch — it must
30
- * already exist on the project. Resolve names to ids before calling.
31
- */
32
- branchId: string;
33
- /** Neon API key. Falls back to `NEON_API_KEY` / neonctl credentials. Ignored when `api` is supplied. */
34
- apiKey?: string;
35
- /**
36
- * Inject a custom NeonApi adapter. Primarily used by tests; production callers can rely
37
- * on the default real adapter built from `apiKey`.
38
- */
39
- api?: NeonApi;
40
- /**
41
- * Auto-confirm overriding existing remote settings.
42
- *
43
- * When `true`, mutable drift on the selected branch (TTL, `protected` flag, compute
44
- * settings) is applied as actual mutations and the override-confirm prompt is
45
- * skipped. When `false` (default) the behaviour depends on whether `confirm` is
46
- * supplied:
47
- * - With `confirm`: the callback is asked whether to apply the override.
48
- * - Without `confirm`: drift is reported as a `PushConflictError` (legacy
49
- * non-interactive default — preserved so programmatic SDK callers don't
50
- * silently start mutating remote state).
51
- */
52
- updateExisting?: boolean;
53
- /**
54
- * Auto-confirm pushing to a protected branch.
55
- *
56
- * When `true`, no protected-branch confirmation is asked. When `false` (default):
57
- * - With `confirm`: the callback is asked.
58
- * - Without `confirm`: the push proceeds (legacy SDK default).
59
- */
60
- allowProtectedBranch?: boolean;
61
- /**
62
- * Optional confirmation callback. Invoked once with a single context object before
63
- * any mutations run when the push needs confirmation: pushing to a protected
64
- * branch (unless `allowProtectedBranch` is `true`) and/or applying mutable drift
65
- * (unless `updateExisting` is `true`).
66
- *
67
- * Both prompts collapse into a single callback invocation when both apply, so the
68
- * CLI can render one combined "are you sure?" prompt.
69
- *
70
- * Resolves to `true` to proceed, `false` to abort with {@link PushAbortedError}.
71
- *
72
- * Never invoked on `dryRun`.
73
- */
74
- confirm?: (context: PushConfirmContext) => boolean | Promise<boolean>;
75
- /**
76
- * When `true`, compute the full plan against the live remote state but **do not
77
- * execute any mutations**. The resulting `PushResult.applied` array records every
78
- * change that *would* run on a real push (with the same action / identifier / details
79
- * shape, so the existing CLI summary formatter just works), and conflicts are
80
- * reported instead of thrown.
81
- *
82
- * Used by `plan(config, branchId)` and any caller that wants a "would this push do
83
- * something dangerous?" check before invoking `pushConfig` for real.
84
- */
85
- dryRun?: boolean;
86
- }
87
-
88
- /**
89
- * Context handed to a {@link PushConfigOptions.confirm} callback. Both flags can be
90
- * `true` simultaneously when the push targets a protected branch *and* would override
91
- * existing settings — render a single combined prompt covering both reasons.
92
- */
93
- export interface PushConfirmContext {
94
- /** Name of the target branch on Neon. */
95
- branchName: string;
96
- /**
97
- * `true` when the target branch has the `protected` flag on Neon and the caller
98
- * did not pass `allowProtectedBranch: true`.
99
- */
100
- protectedBranch: boolean;
101
- /**
102
- * `true` when the plan would override existing remote settings (TTL, `protected`
103
- * flag, compute settings on an existing endpoint) and the caller did not pass
104
- * `updateExisting: true`. Additive operations (enabling Neon Auth / Data API for
105
- * the first time) are **not** counted here — those are unambiguous and never
106
- * prompt.
107
- */
108
- overrideUpdates: boolean;
109
- }
110
-
111
- /**
112
- * Push a Neon branch policy to a specific project + branch.
113
- *
114
- * Filesystem- and env-agnostic: the caller supplies an already-validated `Config` object
115
- * (from `defineConfig` / `loadConfigFromFile`) and explicit `projectId` + `branch` in
116
- * `options`. `pushConfig` performs no `.neon` lookups and reads no `NEON_*` env vars.
117
- *
118
- * It will **not** create a project or branch — both must already exist on Neon.
119
- */
120
- export async function pushConfig(
121
- config: Config,
122
- options: PushConfigOptions,
123
- ): Promise<PushResult> {
124
- const api = options.api ?? createApiFromOptions(options);
125
- const projectId = options.projectId;
126
-
127
- const dryRun = options.dryRun === true;
128
- const updateExisting = options.updateExisting === true;
129
- const allowProtectedBranch = options.allowProtectedBranch === true;
130
-
131
- const remoteProject = await api.getProject(projectId);
132
-
133
- const [branches, endpoints] = await Promise.all([
134
- api.listBranches(remoteProject.id),
135
- api.listEndpoints(remoteProject.id),
136
- ]);
137
- const branch = resolveRemoteBranch(options.branchId, branches);
138
- const resolved = resolveConfig(config, {
139
- name: branch.name,
140
- id: branch.id,
141
- exists: true,
142
- ...(branch.parentId ? { parentId: branch.parentId } : {}),
143
- isDefault: branch.isDefault,
144
- isProtected: branch.protected,
145
- ...(branch.expiresAt ? { expiresAt: branch.expiresAt } : {}),
146
- });
147
- const services = await resolveServiceState({
148
- api,
149
- projectId: remoteProject.id,
150
- branch,
151
- wantsAuth: resolved.authEnabled,
152
- wantsDataApi: resolved.dataApiEnabled,
153
- });
154
- const remote: RemoteState = {
155
- projectId: remoteProject.id,
156
- branch,
157
- endpoint: endpoints.find(
158
- (ep) => ep.type === "read_write" && ep.branchId === branch.id,
159
- ),
160
- services,
161
- };
162
- // Only fetch Preview state when the policy actually uses it — keeps pushes that don't
163
- // touch functions/buckets/aiGateway at the same number of API calls as before.
164
- if (resolved.preview) {
165
- remote.preview = await resolvePreviewState({
166
- api,
167
- projectId: remoteProject.id,
168
- branchId: branch.id,
169
- });
170
- }
171
-
172
- // Always compute the plan with `updateExisting: true` so we can see what *would* be
173
- // overridden. The decision of whether to apply / prompt / fail is gated below using
174
- // the recorded steps.
175
- const diff = diffConfig(resolved, remote, { updateExisting: true });
176
- const overrideSteps = diff.plan.filter(isOverrideStep);
177
- const needsOverrideConfirm = overrideSteps.length > 0 && !updateExisting;
178
- const needsProtectedConfirm = branch.protected && !allowProtectedBranch;
179
-
180
- if (!dryRun && diff.conflicts.length > 0) {
181
- throw new PushConflictError(diff.conflicts);
182
- }
183
-
184
- if (!dryRun && (needsOverrideConfirm || needsProtectedConfirm)) {
185
- if (options.confirm) {
186
- const ok = await options.confirm({
187
- branchName: branch.name,
188
- protectedBranch: needsProtectedConfirm,
189
- overrideUpdates: needsOverrideConfirm,
190
- });
191
- if (!ok) {
192
- const reasons: ("protected-branch" | "override-updates")[] = [];
193
- if (needsProtectedConfirm) reasons.push("protected-branch");
194
- if (needsOverrideConfirm) reasons.push("override-updates");
195
- throw new PushAbortedError(branch.name, reasons);
196
- }
197
- } else if (needsOverrideConfirm) {
198
- // Legacy non-interactive fallback: surface the would-be drift as a
199
- // `PushConflictError` so programmatic callers that skipped both
200
- // `updateExisting` and `confirm` see the previous fail-fast behavior.
201
- const legacy = diffConfig(resolved, remote, {
202
- updateExisting: false,
203
- });
204
- throw new PushConflictError(legacy.conflicts);
205
- }
206
- // Protected branch + no confirm callback: legacy default proceeds without
207
- // any extra check (no programmatic regression).
208
- }
209
-
210
- const applied: AppliedChange[] = [
211
- { kind: "branch", action: "noop", identifier: branch.name },
212
- ];
213
-
214
- const branchById = new Map(branches.map((b) => [b.id, b] as const));
215
- const branchByName = new Map(branches.map((b) => [b.name, b] as const));
216
-
217
- for (const step of diff.plan) {
218
- const change = dryRun
219
- ? synthesizeAppliedChange(step)
220
- : await applyStep(step, {
221
- api,
222
- remoteProjectId: remoteProject.id,
223
- branchById,
224
- branchByName,
225
- });
226
- applied.push(change);
227
- }
228
-
229
- const result: PushResult = {
230
- projectId: remoteProject.id,
231
- branchId: branch.id,
232
- branchName: branch.name,
233
- dryRun,
234
- applied,
235
- conflicts: diff.conflicts,
236
- };
237
- if (remoteProject.orgId) result.orgId = remoteProject.orgId;
238
- return result;
239
- }
240
-
241
- /**
242
- * `update-*` plan steps mutate existing remote state. `enable-*` steps are additive (no
243
- * existing resource to override) and never trigger the override-confirm prompt.
244
- */
245
- function isOverrideStep(step: PlanStep): boolean {
246
- return (
247
- step.kind === "update-branch-ttl" ||
248
- step.kind === "update-branch-protected" ||
249
- step.kind === "update-endpoint"
250
- );
251
- }
252
-
253
- /**
254
- * Build an {@link AppliedChange} from a {@link PlanStep} without calling the Neon API.
255
- * Used by dry-run mode so callers see the same record shape they would on a live push,
256
- * just with no side effects. Identifiers are the branch names from the plan; any
257
- * sub-resource ids (`branchId`, `endpointId`) flow through unchanged when known.
258
- */
259
- function synthesizeAppliedChange(step: PlanStep): AppliedChange {
260
- switch (step.kind) {
261
- case "update-branch-ttl":
262
- return {
263
- kind: "branch",
264
- action: "update",
265
- identifier: step.branchName,
266
- details: { field: "ttl", expiresAt: step.expiresAt },
267
- };
268
- case "update-branch-protected":
269
- return {
270
- kind: "branch",
271
- action: "update",
272
- identifier: step.branchName,
273
- details: { field: "protected", protected: step.protected },
274
- };
275
- case "update-endpoint":
276
- return {
277
- kind: "branch",
278
- action: "update",
279
- identifier: step.branchName,
280
- details: {
281
- field: "computeSettings",
282
- endpointId: step.endpointId,
283
- settings: step.settings,
284
- },
285
- };
286
- case "enable-auth":
287
- return {
288
- kind: "service",
289
- action: "create",
290
- identifier: "auth",
291
- details: {
292
- branchName: step.branchName,
293
- ...(step.databaseName
294
- ? { databaseName: step.databaseName }
295
- : {}),
296
- },
297
- };
298
- case "enable-data-api":
299
- return {
300
- kind: "service",
301
- action: "create",
302
- identifier: "dataApi",
303
- details: {
304
- branchName: step.branchName,
305
- databaseName: step.databaseName,
306
- },
307
- };
308
- case "create-bucket":
309
- return {
310
- kind: "service",
311
- action: "create",
312
- identifier: `bucket:${step.bucketName}`,
313
- details: {
314
- branchName: step.branchName,
315
- bucketName: step.bucketName,
316
- accessLevel: step.accessLevel,
317
- },
318
- };
319
- case "create-function":
320
- return {
321
- kind: "service",
322
- action: "create",
323
- identifier: `function:${step.fn.slug}`,
324
- details: {
325
- branchName: step.branchName,
326
- slug: step.fn.slug,
327
- name: step.fn.name,
328
- },
329
- };
330
- case "deploy-function":
331
- return {
332
- kind: "service",
333
- action: "update",
334
- identifier: `function:${step.fn.slug}`,
335
- details: {
336
- branchName: step.branchName,
337
- slug: step.fn.slug,
338
- source: step.fn.source,
339
- runtime: step.fn.runtime,
340
- memoryMib: step.fn.memoryMib,
341
- },
342
- };
343
- case "enable-ai-gateway":
344
- return {
345
- kind: "service",
346
- action: "create",
347
- identifier: "aiGateway",
348
- details: { branchName: step.branchName },
349
- };
350
- }
351
- }
352
-
353
- function createApiFromOptions(options: PushConfigOptions): NeonApi {
354
- return createNeonApiFromOptions("pushConfig", {
355
- ...(options.apiKey ? { apiKey: options.apiKey } : {}),
356
- });
357
- }
358
-
359
- function resolveRemoteBranch(
360
- branchId: string,
361
- branches: NeonBranchSnapshot[],
362
- ): NeonBranchSnapshot {
363
- const found = branches.find((b) => b.id === branchId);
364
- if (found) return found;
365
- throw new PlatformError(
366
- ErrorCode.BranchNotFound,
367
- [
368
- `pushConfig: branch id ${JSON.stringify(branchId)} does not exist on the project.`,
369
- `Available branches: ${branches.map((b) => `${b.name} (${b.id})`).join(", ") || "(none)"}.`,
370
- "Pass an existing branch id, or create the branch first with the neonctl CLI.",
371
- ].join(" "),
372
- { details: { branchId, available: branches.map((b) => b.id) } },
373
- );
374
- }
375
-
376
- /**
377
- * Pre-fetch the current state of branch-scoped integrations on the selected branch.
378
- */
379
- async function resolveServiceState(args: {
380
- api: NeonApi;
381
- projectId: string;
382
- branch: NeonBranchSnapshot;
383
- wantsAuth: boolean;
384
- wantsDataApi: boolean;
385
- }): Promise<RemoteServiceState> {
386
- const { api, projectId, branch, wantsAuth, wantsDataApi } = args;
387
- if (!wantsAuth && !wantsDataApi) {
388
- return {
389
- databaseName: "neondb",
390
- authEnabled: false,
391
- dataApiEnabled: false,
392
- };
393
- }
394
-
395
- const databaseName = await pickServiceDatabaseName(
396
- api,
397
- projectId,
398
- branch.id,
399
- );
400
-
401
- const [auth, dataApi] = await Promise.all([
402
- wantsAuth
403
- ? api.getNeonAuth(projectId, branch.id)
404
- : Promise.resolve(null),
405
- wantsDataApi
406
- ? api.getNeonDataApi(projectId, branch.id, databaseName)
407
- : Promise.resolve(null),
408
- ]);
409
- return {
410
- databaseName,
411
- authEnabled: auth !== null,
412
- dataApiEnabled: dataApi !== null,
413
- };
414
- }
415
-
416
- /**
417
- * Pre-fetch the current state of branch-scoped Preview features (buckets, functions, AI
418
- * Gateway) so the diff can be computed additively. Only called when the policy has a
419
- * `preview` block.
420
- */
421
- async function resolvePreviewState(args: {
422
- api: NeonApi;
423
- projectId: string;
424
- branchId: string;
425
- }): Promise<RemotePreviewState> {
426
- const { api, projectId, branchId } = args;
427
- const [buckets, functions, aiGatewayEnabled] = await Promise.all([
428
- api.listBranchBuckets(projectId, branchId),
429
- api.listBranchFunctions(projectId, branchId),
430
- api.getAiGatewayEnabled(projectId, branchId),
431
- ]);
432
- return { buckets, functions, aiGatewayEnabled };
433
- }
434
-
435
- /**
436
- * Resolve the database name for a Data API integration. Auto-pick when the branch has
437
- * exactly one database; otherwise fall back to Neon's default (`neondb`) so the call
438
- * stays useful even on branches with multiple databases — push doesn't have a way to
439
- * surface a "pick one" prompt the way `fetchEnv` does.
440
- */
441
- async function pickServiceDatabaseName(
442
- api: NeonApi,
443
- projectId: string,
444
- branchId: string,
445
- ): Promise<string> {
446
- const databases = await api.listBranchDatabases(projectId, branchId);
447
- if (databases.length === 1) return databases[0].name;
448
- const neondb = databases.find((d) => d.name === "neondb");
449
- if (neondb) return neondb.name;
450
- return databases[0]?.name ?? "neondb";
451
- }
452
-
453
- interface ApplyContext {
454
- api: NeonApi;
455
- remoteProjectId: string;
456
- branchById: Map<string, NeonBranchSnapshot>;
457
- branchByName: Map<string, NeonBranchSnapshot>;
458
- }
459
-
460
- async function applyStep(
461
- step: PlanStep,
462
- ctx: ApplyContext,
463
- ): Promise<AppliedChange> {
464
- switch (step.kind) {
465
- case "update-branch-ttl": {
466
- const updated = await ctx.api.updateBranch(
467
- ctx.remoteProjectId,
468
- step.branchId,
469
- {
470
- expiresAt: step.expiresAt ?? null,
471
- },
472
- );
473
- ctx.branchById.set(updated.id, updated);
474
- ctx.branchByName.set(updated.name, updated);
475
- return {
476
- kind: "branch",
477
- action: "update",
478
- identifier: updated.name,
479
- details: { field: "ttl", expiresAt: step.expiresAt },
480
- };
481
- }
482
- case "update-branch-protected": {
483
- const updated = await ctx.api.updateBranch(
484
- ctx.remoteProjectId,
485
- step.branchId,
486
- { protected: step.protected },
487
- );
488
- ctx.branchById.set(updated.id, updated);
489
- ctx.branchByName.set(updated.name, updated);
490
- return {
491
- kind: "branch",
492
- action: "update",
493
- identifier: updated.name,
494
- details: { field: "protected", protected: step.protected },
495
- };
496
- }
497
- case "update-endpoint": {
498
- const updated = await ctx.api.updateEndpoint(
499
- ctx.remoteProjectId,
500
- step.endpointId,
501
- step.settings,
502
- );
503
- return {
504
- kind: "branch",
505
- action: "update",
506
- identifier: step.branchName,
507
- details: {
508
- field: "computeSettings",
509
- endpointId: updated.id,
510
- settings: step.settings,
511
- },
512
- };
513
- }
514
- case "enable-auth": {
515
- await ctx.api.enableNeonAuth(ctx.remoteProjectId, step.branchId, {
516
- ...(step.databaseName
517
- ? { databaseName: step.databaseName }
518
- : {}),
519
- });
520
- return {
521
- kind: "service",
522
- action: "create",
523
- identifier: "auth",
524
- details: {
525
- branchName: step.branchName,
526
- ...(step.databaseName
527
- ? { databaseName: step.databaseName }
528
- : {}),
529
- },
530
- };
531
- }
532
- case "enable-data-api": {
533
- await ctx.api.enableProjectBranchDataApi(
534
- ctx.remoteProjectId,
535
- step.branchId,
536
- step.databaseName,
537
- );
538
- return {
539
- kind: "service",
540
- action: "create",
541
- identifier: "dataApi",
542
- details: {
543
- branchName: step.branchName,
544
- databaseName: step.databaseName,
545
- },
546
- };
547
- }
548
- case "create-bucket": {
549
- await ctx.api.createBranchBucket(
550
- ctx.remoteProjectId,
551
- step.branchId,
552
- { name: step.bucketName, accessLevel: step.accessLevel },
553
- );
554
- return {
555
- kind: "service",
556
- action: "create",
557
- identifier: `bucket:${step.bucketName}`,
558
- details: {
559
- branchName: step.branchName,
560
- bucketName: step.bucketName,
561
- accessLevel: step.accessLevel,
562
- },
563
- };
564
- }
565
- case "create-function": {
566
- await ctx.api.createBranchFunction(
567
- ctx.remoteProjectId,
568
- step.branchId,
569
- { slug: step.fn.slug, name: step.fn.name },
570
- );
571
- return {
572
- kind: "service",
573
- action: "create",
574
- identifier: `function:${step.fn.slug}`,
575
- details: {
576
- branchName: step.branchName,
577
- slug: step.fn.slug,
578
- name: step.fn.name,
579
- },
580
- };
581
- }
582
- case "deploy-function": {
583
- const bundle = await buildFunctionBundle(step.fn);
584
- const deployment = await ctx.api.deployBranchFunction(
585
- ctx.remoteProjectId,
586
- step.branchId,
587
- step.fn.slug,
588
- {
589
- bundle,
590
- runtime: step.fn.runtime,
591
- memoryMib: step.fn.memoryMib,
592
- environment: step.fn.env,
593
- },
594
- );
595
- return {
596
- kind: "service",
597
- action: "update",
598
- identifier: `function:${step.fn.slug}`,
599
- details: {
600
- branchName: step.branchName,
601
- slug: step.fn.slug,
602
- source: step.fn.source,
603
- runtime: step.fn.runtime,
604
- memoryMib: step.fn.memoryMib,
605
- deploymentId: deployment.id,
606
- },
607
- };
608
- }
609
- case "enable-ai-gateway": {
610
- await ctx.api.enableAiGateway(ctx.remoteProjectId, step.branchId);
611
- return {
612
- kind: "service",
613
- action: "create",
614
- identifier: "aiGateway",
615
- details: { branchName: step.branchName },
616
- };
617
- }
618
- }
619
- }
package/src/v1.test.ts DELETED
@@ -1,30 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import {
3
- apply,
4
- buildFunctionBundle,
5
- defineConfig,
6
- inspect,
7
- plan,
8
- pullConfig,
9
- pushConfig,
10
- } from "./v1.js";
11
-
12
- describe("config-runtime v1 surface", () => {
13
- test("exports the imperative operations, engine, and bundler", () => {
14
- expect(inspect).toBeTypeOf("function");
15
- expect(plan).toBeTypeOf("function");
16
- expect(apply).toBeTypeOf("function");
17
- expect(pushConfig).toBeTypeOf("function");
18
- expect(pullConfig).toBeTypeOf("function");
19
- expect(buildFunctionBundle).toBeTypeOf("function");
20
- });
21
-
22
- test("re-exports defineConfig from @neondatabase/config for one-stop deploy scripts", () => {
23
- const config = defineConfig((branch) => ({
24
- parent: branch.name === "main" ? undefined : "main",
25
- }));
26
- expect(config({ name: "dev", exists: false })).toEqual({
27
- parent: "main",
28
- });
29
- });
30
- });