@neondatabase/config 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 (90) hide show
  1. package/LICENSE.md +178 -0
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.js +8 -0
  4. package/dist/lib/auth.d.ts +63 -0
  5. package/dist/lib/auth.d.ts.map +1 -0
  6. package/dist/lib/auth.js +93 -0
  7. package/dist/lib/auth.js.map +1 -0
  8. package/dist/lib/define-config.d.ts +43 -0
  9. package/dist/lib/define-config.d.ts.map +1 -0
  10. package/dist/lib/define-config.js +112 -0
  11. package/dist/lib/define-config.js.map +1 -0
  12. package/dist/lib/diff.d.ts +109 -0
  13. package/dist/lib/diff.d.ts.map +1 -0
  14. package/dist/lib/diff.js +205 -0
  15. package/dist/lib/diff.js.map +1 -0
  16. package/dist/lib/duration.d.ts +46 -0
  17. package/dist/lib/duration.d.ts.map +1 -0
  18. package/dist/lib/duration.js +96 -0
  19. package/dist/lib/duration.js.map +1 -0
  20. package/dist/lib/errors.d.ts +129 -0
  21. package/dist/lib/errors.d.ts.map +1 -0
  22. package/dist/lib/errors.js +168 -0
  23. package/dist/lib/errors.js.map +1 -0
  24. package/dist/lib/loader.d.ts +44 -0
  25. package/dist/lib/loader.d.ts.map +1 -0
  26. package/dist/lib/loader.js +119 -0
  27. package/dist/lib/loader.js.map +1 -0
  28. package/dist/lib/neon-api-real.d.ts +45 -0
  29. package/dist/lib/neon-api-real.d.ts.map +1 -0
  30. package/dist/lib/neon-api-real.js +582 -0
  31. package/dist/lib/neon-api-real.js.map +1 -0
  32. package/dist/lib/neon-api.d.ts +262 -0
  33. package/dist/lib/neon-api.d.ts.map +1 -0
  34. package/dist/lib/neon-api.js +1 -0
  35. package/dist/lib/patterns.d.ts +43 -0
  36. package/dist/lib/patterns.d.ts.map +1 -0
  37. package/dist/lib/patterns.js +76 -0
  38. package/dist/lib/patterns.js.map +1 -0
  39. package/dist/lib/schema.d.ts +130 -0
  40. package/dist/lib/schema.d.ts.map +1 -0
  41. package/dist/lib/schema.js +218 -0
  42. package/dist/lib/schema.js.map +1 -0
  43. package/dist/lib/types.d.ts +289 -0
  44. package/dist/lib/types.d.ts.map +1 -0
  45. package/dist/lib/types.js +1 -0
  46. package/dist/lib/wrap-neon-error.d.ts +30 -0
  47. package/dist/lib/wrap-neon-error.d.ts.map +1 -0
  48. package/dist/lib/wrap-neon-error.js +139 -0
  49. package/dist/lib/wrap-neon-error.js.map +1 -0
  50. package/dist/v1.d.ts +153 -0
  51. package/dist/v1.d.ts.map +1 -0
  52. package/dist/v1.js +69 -0
  53. package/dist/v1.js.map +1 -0
  54. package/package.json +67 -17
  55. package/.env.example +0 -5
  56. package/e2e/errors.e2e.test.ts +0 -52
  57. package/e2e/helpers.ts +0 -205
  58. package/e2e/load-env.ts +0 -29
  59. package/e2e/setup.ts +0 -24
  60. package/src/index.ts +0 -5
  61. package/src/lib/auth.test.ts +0 -166
  62. package/src/lib/auth.ts +0 -124
  63. package/src/lib/define-config.test.ts +0 -161
  64. package/src/lib/define-config.ts +0 -152
  65. package/src/lib/diff.test.ts +0 -142
  66. package/src/lib/diff.ts +0 -391
  67. package/src/lib/duration.test.ts +0 -105
  68. package/src/lib/duration.ts +0 -147
  69. package/src/lib/errors.test.ts +0 -26
  70. package/src/lib/errors.ts +0 -220
  71. package/src/lib/fake-neon-api.ts +0 -782
  72. package/src/lib/loader.test.ts +0 -35
  73. package/src/lib/loader.ts +0 -215
  74. package/src/lib/neon-api-real.test.ts +0 -72
  75. package/src/lib/neon-api-real.ts +0 -1123
  76. package/src/lib/neon-api.ts +0 -356
  77. package/src/lib/patterns.test.ts +0 -80
  78. package/src/lib/patterns.ts +0 -98
  79. package/src/lib/schema.test.ts +0 -88
  80. package/src/lib/schema.ts +0 -252
  81. package/src/lib/test-utils.ts +0 -83
  82. package/src/lib/types.ts +0 -268
  83. package/src/lib/wrap-neon-error.test.ts +0 -145
  84. package/src/lib/wrap-neon-error.ts +0 -204
  85. package/src/v1.test.ts +0 -33
  86. package/src/v1.ts +0 -148
  87. package/tsconfig.json +0 -4
  88. package/tsdown.config.ts +0 -19
  89. package/vitest.config.ts +0 -19
  90. package/vitest.e2e.config.ts +0 -29
@@ -1,161 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { defineConfig, resolveConfig } from "./define-config.js";
3
- import { ConfigValidationError } from "./errors.js";
4
-
5
- describe("defineConfig", () => {
6
- test("accepts a branch policy function and preserves literal behavior", () => {
7
- const config = defineConfig((branch) => {
8
- if (branch.name === "main")
9
- return { protected: true, auth: { enabled: true } };
10
- return { parent: "main", ttl: "7d" };
11
- });
12
- expect(typeof config).toBe("function");
13
- expect(config({ name: "main", exists: true })).toEqual({
14
- protected: true,
15
- auth: { enabled: true },
16
- });
17
- });
18
-
19
- test("rejects object configs so project-level config is not accepted", () => {
20
- expect(() => defineConfig({ project: { name: "x" } } as never)).toThrow(
21
- ConfigValidationError,
22
- );
23
- });
24
- });
25
-
26
- describe("resolveConfig", () => {
27
- test("normalizes branch policy output", () => {
28
- const config = defineConfig(() => ({
29
- parent: "main",
30
- ttl: "1h",
31
- protected: true,
32
- postgres: { computeSettings: { autoscalingLimitMaxCu: 2 } },
33
- auth: {},
34
- dataApi: {},
35
- }));
36
- const resolved = resolveConfig(config, {
37
- name: "dev-a",
38
- exists: false,
39
- });
40
- expect(resolved).toMatchObject({
41
- parent: "main",
42
- ttlSeconds: 3600,
43
- protected: true,
44
- authEnabled: true,
45
- dataApiEnabled: true,
46
- postgres: { computeSettings: { autoscalingLimitMaxCu: 2 } },
47
- });
48
- });
49
-
50
- test("treats explicit service false as disabled", () => {
51
- const config = defineConfig(() => ({
52
- auth: { enabled: false },
53
- dataApi: { enabled: false },
54
- }));
55
- const resolved = resolveConfig(config, {
56
- name: "dev-a",
57
- exists: false,
58
- });
59
- expect(resolved.authEnabled).toBe(false);
60
- expect(resolved.dataApiEnabled).toBe(false);
61
- });
62
-
63
- test("reports invalid returned branch config", () => {
64
- const config = defineConfig(() => ({ parent: "preview-*" }));
65
- expect(() =>
66
- resolveConfig(config, { name: "dev", exists: false }),
67
- ).toThrow(ConfigValidationError);
68
- });
69
-
70
- test("resolves preview functions with deploy defaults and buckets with private access", () => {
71
- const config = defineConfig(() => ({
72
- preview: {
73
- functions: [
74
- {
75
- name: "Hello World",
76
- slug: "hello-world",
77
- source: "./functions/hello-world.ts",
78
- env: { RESEND_API_KEY: "re_abc" },
79
- },
80
- ],
81
- buckets: [{ name: "uploads" }],
82
- aiGateway: { enabled: true },
83
- },
84
- }));
85
- const resolved = resolveConfig(config, {
86
- name: "preview-1",
87
- exists: false,
88
- });
89
- expect(resolved.preview).toEqual({
90
- functions: [
91
- {
92
- slug: "hello-world",
93
- name: "Hello World",
94
- source: "./functions/hello-world.ts",
95
- env: { RESEND_API_KEY: "re_abc" },
96
- runtime: "nodejs24",
97
- memoryMib: 512,
98
- },
99
- ],
100
- buckets: [{ name: "uploads", access: "private" }],
101
- aiGatewayEnabled: true,
102
- });
103
- });
104
-
105
- test("treats aiGateway enabled:false as disabled", () => {
106
- const config = defineConfig(() => ({
107
- preview: { aiGateway: { enabled: false } },
108
- }));
109
- const resolved = resolveConfig(config, {
110
- name: "preview-1",
111
- exists: false,
112
- });
113
- expect(resolved.preview?.aiGatewayEnabled).toBe(false);
114
- });
115
-
116
- test("rejects a function env value that is undefined (e.g. unset process.env)", () => {
117
- const config = defineConfig(() => ({
118
- preview: {
119
- functions: [
120
- {
121
- name: "Hello",
122
- slug: "hello",
123
- source: "./hello.ts",
124
- // Simulates `RESEND_API_KEY: process.env.RESEND_API_KEY` when unset.
125
- env: { RESEND_API_KEY: undefined as unknown as string },
126
- },
127
- ],
128
- },
129
- }));
130
- expect(() =>
131
- resolveConfig(config, { name: "preview-1", exists: false }),
132
- ).toThrow(ConfigValidationError);
133
- });
134
-
135
- test("rejects an invalid function slug", () => {
136
- const config = defineConfig(() => ({
137
- preview: {
138
- functions: [
139
- { name: "Bad", slug: "Hello World", source: "./x.ts" },
140
- ],
141
- },
142
- }));
143
- expect(() =>
144
- resolveConfig(config, { name: "preview-1", exists: false }),
145
- ).toThrow(ConfigValidationError);
146
- });
147
-
148
- test("rejects duplicate function slugs", () => {
149
- const config = defineConfig(() => ({
150
- preview: {
151
- functions: [
152
- { name: "A", slug: "dup", source: "./a.ts" },
153
- { name: "B", slug: "dup", source: "./b.ts" },
154
- ],
155
- },
156
- }));
157
- expect(() =>
158
- resolveConfig(config, { name: "preview-1", exists: false }),
159
- ).toThrow(ConfigValidationError);
160
- });
161
- });
@@ -1,152 +0,0 @@
1
- import { parseDuration } from "./duration.js";
2
- import { ConfigValidationError } from "./errors.js";
3
- import { branchConfigSchema, formatZodIssues } from "./schema.js";
4
- import type {
5
- BranchTarget,
6
- Config,
7
- FunctionConfig,
8
- PreviewConfig,
9
- ResolvedBranchConfig,
10
- ResolvedPreviewConfig,
11
- } from "./types.js";
12
-
13
- /** Default deploy parameters applied to functions that omit them in `neon.ts`. */
14
- const DEFAULT_FUNCTION_RUNTIME = "nodejs24" as const;
15
- const DEFAULT_FUNCTION_MEMORY_MIB = 512 as const;
16
-
17
- const REGION_PREFIX = /^(aws|azure|gcp)-/;
18
-
19
- /**
20
- * Validate and freeze a Neon Platform branch policy.
21
- *
22
- * Used at the top of `neon.ts`:
23
- * ```ts
24
- * import { defineConfig } from "@neondatabase/config/v1";
25
- *
26
- * export default defineConfig((branch) => {
27
- * if (branch.name === "main") {
28
- * return { protected: true, auth: {} };
29
- * }
30
- * return { parent: "main", ttl: "7d" };
31
- * });
32
- * ```
33
- *
34
- * The `branch` parameter is a **read-only {@link BranchTarget} descriptor** of the branch
35
- * this policy invocation is deciding for — not a live branch handle. You don't mutate it
36
- * (`branch.protected = true` does nothing); you switch on its facts (`branch.name`,
37
- * `branch.isDefault`, `branch.exists`, …) and **return** the desired {@link BranchConfig}.
38
- * The same callback runs in two modes: against an existing branch (fields populated from
39
- * Neon) and during pre-create evaluation (`exists: false`, `id` undefined).
40
- *
41
- * Pure function — no I/O, no side effects. The returned policy validates its output every
42
- * time it is evaluated so errors point at the concrete branch target that triggered them.
43
- */
44
- export function defineConfig<const C extends Config>(input: C): C {
45
- if (typeof input !== "function") {
46
- throw new ConfigValidationError([
47
- "defineConfig expects a function: `export default defineConfig((branch) => ({ ... }))`.",
48
- "Project-level config has moved to `neonctl link`; neon.ts now describes branch-level policy only.",
49
- ]);
50
- }
51
-
52
- return Object.freeze(input) as C;
53
- }
54
-
55
- /**
56
- * Evaluate a branch policy for a specific branch target and return a normalized config.
57
- */
58
- export function resolveConfig(
59
- config: Config,
60
- branch: BranchTarget,
61
- ): ResolvedBranchConfig {
62
- let raw: unknown;
63
- try {
64
- raw = config(Object.freeze({ ...branch }));
65
- } catch (cause) {
66
- throw new ConfigValidationError([
67
- `Config function threw while evaluating branch "${branch.name}".`,
68
- (cause as Error)?.message ?? String(cause),
69
- ]);
70
- }
71
-
72
- const parsed = branchConfigSchema.safeParse(raw);
73
- if (!parsed.success) {
74
- throw new ConfigValidationError(formatZodIssues(parsed.error));
75
- }
76
-
77
- const cfg = parsed.data;
78
- const issues: string[] = [];
79
- let ttlSeconds: number | undefined;
80
- if (cfg.ttl !== undefined) {
81
- const parsedTtl = parseDuration(cfg.ttl);
82
- if ("error" in parsedTtl) {
83
- issues.push(`ttl: ${parsedTtl.error}`);
84
- } else {
85
- ttlSeconds = parsedTtl.seconds;
86
- }
87
- }
88
- if (issues.length > 0) {
89
- throw new ConfigValidationError(issues);
90
- }
91
-
92
- const resolved: ResolvedBranchConfig = {
93
- authEnabled: isServiceEnabled(cfg.auth),
94
- dataApiEnabled: isServiceEnabled(cfg.dataApi),
95
- };
96
- if (cfg.parent !== undefined) resolved.parent = cfg.parent;
97
- if (ttlSeconds !== undefined) resolved.ttlSeconds = ttlSeconds;
98
- if (cfg.protected !== undefined) resolved.protected = cfg.protected;
99
- if (cfg.postgres) {
100
- resolved.postgres = {
101
- ...(cfg.postgres.computeSettings
102
- ? { computeSettings: { ...cfg.postgres.computeSettings } }
103
- : {}),
104
- };
105
- }
106
- if (cfg.preview) {
107
- resolved.preview = resolvePreviewConfig(cfg.preview);
108
- }
109
- return resolved;
110
- }
111
-
112
- function isServiceEnabled(service: { enabled?: boolean } | undefined): boolean {
113
- return service !== undefined && service.enabled !== false;
114
- }
115
-
116
- /**
117
- * Normalize a {@link PreviewConfig} into a {@link ResolvedPreviewConfig}: apply per-function
118
- * deploy defaults, default each bucket's access level to `private`, and collapse the
119
- * `aiGateway` toggle to a boolean using the same present-and-not-`false` rule as
120
- * `auth` / `dataApi`.
121
- */
122
- function resolvePreviewConfig(preview: PreviewConfig): ResolvedPreviewConfig {
123
- return {
124
- functions: (preview.functions ?? []).map(resolveFunctionConfig),
125
- buckets: (preview.buckets ?? []).map((bucket) => ({
126
- name: bucket.name,
127
- access: bucket.access ?? "private",
128
- })),
129
- aiGatewayEnabled: isServiceEnabled(preview.aiGateway),
130
- };
131
- }
132
-
133
- function resolveFunctionConfig(fn: FunctionConfig) {
134
- return {
135
- slug: fn.slug,
136
- name: fn.name,
137
- source: fn.source,
138
- env: { ...(fn.env ?? {}) },
139
- runtime: fn.runtime ?? DEFAULT_FUNCTION_RUNTIME,
140
- memoryMib: fn.memoryMib ?? DEFAULT_FUNCTION_MEMORY_MIB,
141
- };
142
- }
143
-
144
- /**
145
- * Normalize a region identifier to Neon's `<cloud>-<region>` format. When the user writes
146
- * `us-east-1` we assume `aws-us-east-1`. Pure helper used by both the validator and the
147
- * NeonApi adapter.
148
- */
149
- export function normalizeRegion(region: string): string {
150
- if (REGION_PREFIX.test(region)) return region;
151
- return `aws-${region}`;
152
- }
@@ -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
- });