neon 8.3.5 → 8.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{env-L2DPzDAK.js → env-4WGRW2nw.js} +25 -8
- package/dist/_chunks/{env-XsUZj_Ay.js → env-BpP3Dqpe.js} +71 -1
- package/dist/api.js +15 -0
- package/dist/commands/claim.js +5 -4
- package/dist/commands/config.js +49 -10
- package/dist/commands/dev.js +2 -2
- package/dist/commands/env.js +8 -6
- package/dist/commands/env_output.js +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/init.js +2 -1
- package/dist/commands/manifest.js +25 -2
- package/dist/commands/neon_auth.js +7 -3
- package/dist/commands/realtime.js +50 -0
- package/dist/commands/subscribe.js +247 -0
- package/dist/config_edit.js +49 -0
- package/dist/config_services.js +4 -1
- package/dist/config_template.js +2 -1
- package/dist/dev/env.js +1 -1
- package/dist/env_services.js +5 -1
- package/dist/index.js +10 -2
- package/dist/init/run.js +2 -1
- package/dist/neon_services.js +5 -3
- package/dist/parameters.gen.js +10 -0
- package/dist/test_utils/fixtures.js +22 -3
- package/dist/utils/config_diff.js +1 -0
- package/dist/utils/hooks.js +3 -1
- package/dist/utils/repeated_flags.js +43 -0
- package/dist/utils/staging.js +49 -0
- package/package.json +8 -6
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
|
+
import { log } from "../log.js";
|
|
3
|
+
import { retryOnLock } from "../api.js";
|
|
4
|
+
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
5
|
+
import { defineConfig } from "@neon/config";
|
|
6
|
+
//#region src/commands/realtime.ts
|
|
7
|
+
var realtime_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
builder: () => builder,
|
|
9
|
+
command: () => command,
|
|
10
|
+
describe: () => describe,
|
|
11
|
+
disable: () => disable,
|
|
12
|
+
enable: () => enable,
|
|
13
|
+
handler: () => handler
|
|
14
|
+
});
|
|
15
|
+
/** Request Realtime provisioning or an allowed-origin update for a branch. */
|
|
16
|
+
const enable = async (props) => {
|
|
17
|
+
const realtime = props.allowedOrigins === void 0 ? true : { allowedOrigins: props.allowedOrigins };
|
|
18
|
+
defineConfig({ preview: { realtime } });
|
|
19
|
+
const branchId = await branchIdFromProps(props);
|
|
20
|
+
await retryOnLock(() => props.apiClient.enableProjectBranchRealtime(props.projectId, branchId, props.allowedOrigins === void 0 ? {} : { allowed_origins: [...props.allowedOrigins] }));
|
|
21
|
+
log.info("Realtime enable requested for branch %s.", branchId);
|
|
22
|
+
};
|
|
23
|
+
/** Request Realtime deprovisioning for a branch. */
|
|
24
|
+
const disable = async (props) => {
|
|
25
|
+
const branchId = await branchIdFromProps(props);
|
|
26
|
+
await retryOnLock(() => props.apiClient.disableProjectBranchRealtime(props.projectId, branchId));
|
|
27
|
+
log.info("Realtime disable requested for branch %s.", branchId);
|
|
28
|
+
};
|
|
29
|
+
const command = "realtime";
|
|
30
|
+
const describe = "Manage Neon Realtime for a branch";
|
|
31
|
+
const builder = (argv) => argv.usage("$0 realtime <sub-command> [options]").options({
|
|
32
|
+
"project-id": {
|
|
33
|
+
describe: "Project ID",
|
|
34
|
+
type: "string"
|
|
35
|
+
},
|
|
36
|
+
branch: {
|
|
37
|
+
describe: "Branch ID or name",
|
|
38
|
+
type: "string"
|
|
39
|
+
}
|
|
40
|
+
}).middleware(fillSingleProject).command("enable", "Enable Realtime on a branch", (yargs) => yargs.strict().option("allowed-origin", {
|
|
41
|
+
describe: "Allowed browser origin (repeatable). \"*\" allows every origin",
|
|
42
|
+
type: "array",
|
|
43
|
+
string: true
|
|
44
|
+
}), (args) => enable({
|
|
45
|
+
...args,
|
|
46
|
+
...args.allowedOrigin !== void 0 ? { allowedOrigins: args.allowedOrigin } : {}
|
|
47
|
+
})).command("disable", "Disable Realtime on a branch", (yargs) => yargs.strict(), (args) => disable(args));
|
|
48
|
+
const handler = (_args) => {};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { builder, command, describe, disable, enable, handler, realtime_exports as t };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
|
+
import { writer } from "../writer.js";
|
|
3
|
+
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
4
|
+
import { o as resolveNeonEnvVars } from "../_chunks/env-4WGRW2nw.js";
|
|
5
|
+
import YAML from "yaml";
|
|
6
|
+
//#region src/commands/subscribe.ts
|
|
7
|
+
var subscribe_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
builder: () => builder,
|
|
9
|
+
command: () => command,
|
|
10
|
+
describe: () => describe,
|
|
11
|
+
handler: () => handler,
|
|
12
|
+
subscribe: () => subscribe
|
|
13
|
+
});
|
|
14
|
+
const command = "subscribe <query>";
|
|
15
|
+
const describe = "Subscribe to a live SQL query with Neon Realtime";
|
|
16
|
+
const builder = (argv) => argv.usage("$0 subscribe \"<query>\" [options]").positional("query", {
|
|
17
|
+
describe: "SELECT query to subscribe to",
|
|
18
|
+
type: "string"
|
|
19
|
+
}).options({
|
|
20
|
+
"project-id": {
|
|
21
|
+
describe: "Project ID",
|
|
22
|
+
type: "string"
|
|
23
|
+
},
|
|
24
|
+
branch: {
|
|
25
|
+
describe: "Branch ID or name",
|
|
26
|
+
type: "string"
|
|
27
|
+
},
|
|
28
|
+
changes: {
|
|
29
|
+
describe: "Show raw resets and transaction batches instead of full query results",
|
|
30
|
+
type: "boolean",
|
|
31
|
+
default: false
|
|
32
|
+
}
|
|
33
|
+
}).example("$0 subscribe \"SELECT * FROM items\"", "Print the full result whenever it changes").example("$0 subscribe --changes \"SELECT * FROM items\"", "Print raw reset and delta events").epilogue("The subscription runs until interrupted. JSON output is newline-delimited; YAML output uses one document per event.").middleware(fillSingleProject).strict();
|
|
34
|
+
const handler = async (props) => {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const stop = (exitCode) => {
|
|
37
|
+
controller.abort();
|
|
38
|
+
setImmediate(() => process.exit(exitCode));
|
|
39
|
+
};
|
|
40
|
+
const onSigint = () => stop(130);
|
|
41
|
+
const onSigterm = () => stop(143);
|
|
42
|
+
process.once("SIGINT", onSigint);
|
|
43
|
+
process.once("SIGTERM", onSigterm);
|
|
44
|
+
try {
|
|
45
|
+
await subscribe({
|
|
46
|
+
...props,
|
|
47
|
+
signal: controller.signal
|
|
48
|
+
});
|
|
49
|
+
} finally {
|
|
50
|
+
process.removeListener("SIGINT", onSigint);
|
|
51
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** Resolve Realtime credentials and stream one live SQL query until interrupted. */
|
|
55
|
+
const subscribe = async (props, dependencies = {}) => {
|
|
56
|
+
const resolveBranchId = dependencies.resolveBranchId ?? branchIdFromProps;
|
|
57
|
+
const resolveEnv = dependencies.resolveEnv ?? resolveNeonEnvVars;
|
|
58
|
+
const connect = dependencies.connect ?? connectRealtime;
|
|
59
|
+
const branchId = await resolveBranchId(props);
|
|
60
|
+
const { vars } = await resolveEnv({
|
|
61
|
+
cwd: props.cwd ?? process.cwd(),
|
|
62
|
+
contextFile: props.contextFile,
|
|
63
|
+
projectId: props.projectId,
|
|
64
|
+
branchId,
|
|
65
|
+
apiKey: props.apiKey,
|
|
66
|
+
apiHost: props.apiHost,
|
|
67
|
+
services: ["postgres", "realtime"],
|
|
68
|
+
...props.runtimeApi ? { api: props.runtimeApi } : {}
|
|
69
|
+
});
|
|
70
|
+
const realtime = await connect({
|
|
71
|
+
url: requiredEnv(vars, "NEON_REALTIME_URL"),
|
|
72
|
+
secret: requiredEnv(vars, "NEON_REALTIME_SECRET"),
|
|
73
|
+
database: await databaseName(requiredEnv(vars, "DATABASE_URL"))
|
|
74
|
+
});
|
|
75
|
+
const render = createRenderer(props);
|
|
76
|
+
let subscription;
|
|
77
|
+
try {
|
|
78
|
+
if (props.changes) {
|
|
79
|
+
subscription = await realtime.subscribeChanges(props.query);
|
|
80
|
+
await consumeChanges(subscription, render, props.signal);
|
|
81
|
+
} else {
|
|
82
|
+
const materialized = await realtime.subscribeMaterialized(props.query);
|
|
83
|
+
subscription = materialized;
|
|
84
|
+
await consumeSnapshots(materialized, render, props.signal);
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
subscription?.unsubscribe();
|
|
88
|
+
realtime.close();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const connectRealtime = async (options) => {
|
|
92
|
+
await ensureWebSocket();
|
|
93
|
+
const { createRealtime, rawSql } = await import("@neon/realtime");
|
|
94
|
+
const realtime = createRealtime({
|
|
95
|
+
url: options.url,
|
|
96
|
+
secret: options.secret,
|
|
97
|
+
db: options.database
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
subscribeMaterialized: (query) => realtime.subscribe(rawSql(query)),
|
|
101
|
+
subscribeChanges: (query) => realtime.subscribe(rawSql(query), { materialize: false }),
|
|
102
|
+
close: () => realtime.close()
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
/** Node 20 does not expose WebSocket without an experimental flag. */
|
|
106
|
+
const ensureWebSocket = async () => {
|
|
107
|
+
if (typeof globalThis.WebSocket !== "undefined") return;
|
|
108
|
+
const { WebSocket } = await import("undici");
|
|
109
|
+
Object.defineProperty(globalThis, "WebSocket", {
|
|
110
|
+
configurable: true,
|
|
111
|
+
writable: true,
|
|
112
|
+
value: WebSocket
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const requiredEnv = (vars, key) => {
|
|
116
|
+
const value = vars[key];
|
|
117
|
+
if (value) return value;
|
|
118
|
+
throw new Error(`Could not resolve ${key} for the selected branch.`);
|
|
119
|
+
};
|
|
120
|
+
const databaseName = async (connectionString) => {
|
|
121
|
+
const { parse } = await import("pg-connection-string");
|
|
122
|
+
const database = parse(connectionString, { useLibpqCompat: true }).database;
|
|
123
|
+
if (database) return database;
|
|
124
|
+
throw new Error("DATABASE_URL must include a database name.");
|
|
125
|
+
};
|
|
126
|
+
const createRenderer = (props) => {
|
|
127
|
+
const out = props.out ?? process.stdout;
|
|
128
|
+
let renderedTableFrame = false;
|
|
129
|
+
return (event) => {
|
|
130
|
+
if (props.output === "json") {
|
|
131
|
+
out.write(`${JSON.stringify(event)}\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (props.output === "yaml") {
|
|
135
|
+
out.write(`---\n${YAML.stringify(event)}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (renderedTableFrame) out.write("\n");
|
|
139
|
+
renderedTableFrame = true;
|
|
140
|
+
renderTableEvent(out, event);
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
const renderTableEvent = (out, event) => {
|
|
144
|
+
if (event.type === "snapshot") {
|
|
145
|
+
const fields = rowFields(event.rows);
|
|
146
|
+
writer({
|
|
147
|
+
output: "table",
|
|
148
|
+
out
|
|
149
|
+
}).end(event.rows, {
|
|
150
|
+
fields,
|
|
151
|
+
humanTitle: "Query result",
|
|
152
|
+
emptyMessage: "Query returned no rows."
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (event.type === "reset") {
|
|
157
|
+
const rows = event.rows.map(({ rowId, row }) => ({
|
|
158
|
+
row_id: rowId,
|
|
159
|
+
row
|
|
160
|
+
}));
|
|
161
|
+
writer({
|
|
162
|
+
output: "table",
|
|
163
|
+
out
|
|
164
|
+
}).end(rows, {
|
|
165
|
+
fields: ["row_id", "row"],
|
|
166
|
+
humanTitle: "Reset",
|
|
167
|
+
emptyMessage: "Reset contains no rows."
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const rows = event.changes.map((change) => ({
|
|
172
|
+
type: change.type,
|
|
173
|
+
row_id: change.rowId,
|
|
174
|
+
row: change.type === "upsert" ? change.row : void 0
|
|
175
|
+
}));
|
|
176
|
+
writer({
|
|
177
|
+
output: "table",
|
|
178
|
+
out
|
|
179
|
+
}).end(rows, {
|
|
180
|
+
fields: [
|
|
181
|
+
"type",
|
|
182
|
+
"row_id",
|
|
183
|
+
"row"
|
|
184
|
+
],
|
|
185
|
+
humanTitle: `Batch (txids: ${event.txids.join(", ") || "none"})`,
|
|
186
|
+
emptyMessage: "Batch contains no row changes."
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const rowFields = (rows) => {
|
|
190
|
+
const seen = /* @__PURE__ */ new Set();
|
|
191
|
+
for (const row of rows) for (const key of Object.keys(row)) seen.add(key);
|
|
192
|
+
return [...seen];
|
|
193
|
+
};
|
|
194
|
+
const consumeSnapshots = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onReset(() => attempt(fail, () => {
|
|
195
|
+
const rows = subscription.getSnapshot().data;
|
|
196
|
+
if (rows !== void 0) render({
|
|
197
|
+
type: "snapshot",
|
|
198
|
+
rows
|
|
199
|
+
});
|
|
200
|
+
})), subscription.onBatch(() => attempt(fail, () => {
|
|
201
|
+
const rows = subscription.getSnapshot().data;
|
|
202
|
+
if (rows !== void 0) render({
|
|
203
|
+
type: "snapshot",
|
|
204
|
+
rows
|
|
205
|
+
});
|
|
206
|
+
}))], signal);
|
|
207
|
+
const consumeChanges = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onReset((rows) => attempt(fail, () => render({
|
|
208
|
+
type: "reset",
|
|
209
|
+
rows
|
|
210
|
+
}))), subscription.onBatch((changes, batch) => attempt(fail, () => render({
|
|
211
|
+
type: "batch",
|
|
212
|
+
txids: batch.txids,
|
|
213
|
+
changes
|
|
214
|
+
})))], signal);
|
|
215
|
+
const consumeSubscription = (subscription, listenForData, signal) => new Promise((resolve, reject) => {
|
|
216
|
+
let settled = false;
|
|
217
|
+
let detachData = [];
|
|
218
|
+
let detachState = () => void 0;
|
|
219
|
+
const onAbort = () => finish(resolve);
|
|
220
|
+
const finish = (settle, error) => {
|
|
221
|
+
if (settled) return;
|
|
222
|
+
settled = true;
|
|
223
|
+
for (const detach of detachData) detach();
|
|
224
|
+
detachState();
|
|
225
|
+
signal?.removeEventListener("abort", onAbort);
|
|
226
|
+
settle(error);
|
|
227
|
+
};
|
|
228
|
+
const fail = (error) => finish(reject, error);
|
|
229
|
+
const onState = (state) => {
|
|
230
|
+
if (state.status === "error") fail(state.error);
|
|
231
|
+
else if (state.status === "closed") finish(resolve);
|
|
232
|
+
};
|
|
233
|
+
detachData = listenForData(fail);
|
|
234
|
+
detachState = subscription.onStateChange(onState);
|
|
235
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
236
|
+
onState(subscription.getState());
|
|
237
|
+
if (signal?.aborted) onAbort();
|
|
238
|
+
});
|
|
239
|
+
const attempt = (fail, action) => {
|
|
240
|
+
try {
|
|
241
|
+
action();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
fail(error);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
//#endregion
|
|
247
|
+
export { builder, command, describe, handler, subscribe, subscribe_exports as t };
|
package/dist/config_edit.js
CHANGED
|
@@ -161,6 +161,52 @@ const setToggle = (parsed, key) => {
|
|
|
161
161
|
change: `set ${toggle.path}: false → true`
|
|
162
162
|
};
|
|
163
163
|
};
|
|
164
|
+
const realtimeValue = (edit) => edit.enabled && edit.allowedOrigins !== void 0 ? `{ allowedOrigins: ${JSON.stringify(edit.allowedOrigins)} }` : String(edit.enabled);
|
|
165
|
+
/** Set the complete Realtime desired state while preserving an existing settings object. */
|
|
166
|
+
const setRealtime = (parsed, edit) => {
|
|
167
|
+
const desired = realtimeValue(edit);
|
|
168
|
+
const declaration = `realtime: ${desired},`;
|
|
169
|
+
const snippet = [
|
|
170
|
+
"preview: {",
|
|
171
|
+
`${parsed.unit}${declaration}`,
|
|
172
|
+
"},"
|
|
173
|
+
];
|
|
174
|
+
const previewEntry = findEntry(parsed.root, "preview");
|
|
175
|
+
if (!previewEntry) return {
|
|
176
|
+
source: insertEntry(parsed, parsed.root, snippet, snippet, entryAfter(parsed, "preview")),
|
|
177
|
+
change: `added preview.realtime: ${desired}`
|
|
178
|
+
};
|
|
179
|
+
const preview = objectOf(parsed, previewEntry);
|
|
180
|
+
if (!preview) return unsupported("preview is set to an expression, not an object literal.", snippet);
|
|
181
|
+
const entry = findEntry(preview, "realtime");
|
|
182
|
+
if (!entry) return {
|
|
183
|
+
source: insertEntry(parsed, preview, [declaration], snippet),
|
|
184
|
+
change: `added preview.realtime: ${desired}`
|
|
185
|
+
};
|
|
186
|
+
const current = parsed.source.slice(entry.valueStart, entry.valueEnd);
|
|
187
|
+
if (entry.keyed && current === desired) return { source: parsed.source };
|
|
188
|
+
const object = objectOf(parsed, entry);
|
|
189
|
+
if (edit.enabled && edit.allowedOrigins === void 0 && object) return { source: parsed.source };
|
|
190
|
+
if (edit.enabled && edit.allowedOrigins !== void 0 && object) {
|
|
191
|
+
const origins = JSON.stringify(edit.allowedOrigins);
|
|
192
|
+
const allowedOrigins = findEntry(object, "allowedOrigins");
|
|
193
|
+
if (!allowedOrigins) return {
|
|
194
|
+
source: insertEntry(parsed, object, [`allowedOrigins: ${origins},`], snippet),
|
|
195
|
+
change: `added preview.realtime.allowedOrigins: ${origins}`
|
|
196
|
+
};
|
|
197
|
+
if (!allowedOrigins.keyed) return unsupported("preview.realtime.allowedOrigins is a computed property.", snippet);
|
|
198
|
+
if (parsed.source.slice(allowedOrigins.valueStart, allowedOrigins.valueEnd) === origins) return { source: parsed.source };
|
|
199
|
+
return {
|
|
200
|
+
source: parsed.source.slice(0, allowedOrigins.valueStart) + origins + parsed.source.slice(allowedOrigins.valueEnd),
|
|
201
|
+
change: `set preview.realtime.allowedOrigins to ${origins}`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!entry.keyed || current !== "true" && current !== "false" && !object) return unsupported("preview.realtime is set to an expression, not a boolean or object literal.", snippet);
|
|
205
|
+
return {
|
|
206
|
+
source: parsed.source.slice(0, entry.valueStart) + desired + parsed.source.slice(entry.valueEnd),
|
|
207
|
+
change: `set preview.realtime to ${desired}`
|
|
208
|
+
};
|
|
209
|
+
};
|
|
164
210
|
/**
|
|
165
211
|
* Whether `dataApi` verifies a third-party IdP, in which case it does not need Neon Auth. An
|
|
166
212
|
* `authProvider` that is neither literal is refused rather than assumed to be Neon's.
|
|
@@ -205,6 +251,9 @@ const apply = (source, edit) => {
|
|
|
205
251
|
steps.push(step);
|
|
206
252
|
};
|
|
207
253
|
switch (edit.kind) {
|
|
254
|
+
case "realtime":
|
|
255
|
+
run((parsed) => setRealtime(parsed, edit));
|
|
256
|
+
break;
|
|
208
257
|
case "service":
|
|
209
258
|
if (edit.service === "auth") run((parsed) => setToggle(parsed, "auth"));
|
|
210
259
|
else if (edit.service === "ai-gateway") run((parsed) => setToggle(parsed, "aiGateway"));
|
package/dist/config_services.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { authoredAiGateway, authoredBuckets, authoredFunctions } from "@neon/config";
|
|
2
2
|
//#region src/config_services.ts
|
|
3
|
+
const isClaimableService = (service) => service !== "realtime";
|
|
3
4
|
const isToggleEnabled = (toggle) => {
|
|
4
5
|
if (toggle === void 0) return false;
|
|
5
6
|
if (typeof toggle === "boolean") return toggle;
|
|
@@ -13,8 +14,10 @@ const declaredNeonServices = (config) => {
|
|
|
13
14
|
if (Object.keys(authoredBuckets(config) ?? {}).length > 0) services.push("object-storage");
|
|
14
15
|
if (Object.keys(authoredFunctions(config) ?? {}).length > 0) services.push("functions");
|
|
15
16
|
if (isToggleEnabled(authoredAiGateway(config))) services.push("ai-gateway");
|
|
17
|
+
if (config.preview?.realtime !== void 0 && config.preview.realtime !== false) services.push("realtime");
|
|
16
18
|
return services;
|
|
17
19
|
};
|
|
20
|
+
const declaredClaimableNeonServices = (config) => declaredNeonServices(config).filter(isClaimableService);
|
|
18
21
|
const dataApiSettingsToSnake = (settings) => {
|
|
19
22
|
const out = {};
|
|
20
23
|
if (settings.dbAggregatesEnabled !== void 0) out.db_aggregates_enabled = settings.dbAggregatesEnabled;
|
|
@@ -51,4 +54,4 @@ const claimableDataApiCreateBody = (config) => {
|
|
|
51
54
|
return withSettings({ auth_provider: "neon_auth" });
|
|
52
55
|
};
|
|
53
56
|
//#endregion
|
|
54
|
-
export { claimableDataApiCreateBody, declaredNeonServices };
|
|
57
|
+
export { claimableDataApiCreateBody, declaredClaimableNeonServices, declaredNeonServices, isClaimableService };
|
package/dist/config_template.js
CHANGED
|
@@ -76,11 +76,12 @@ const renderGaServices = (services) => {
|
|
|
76
76
|
const renderNeonConfig = (services) => {
|
|
77
77
|
const auth = services.includes("auth") || services.includes("data-api");
|
|
78
78
|
const dataApi = services.includes("data-api") ? " dataApi: true,\n" : "";
|
|
79
|
+
const realtimeDeclaration = services.includes("realtime") ? " preview: {\n realtime: true,\n },\n" : "";
|
|
79
80
|
return `import { defineConfig } from "${CONFIG_PACKAGE}/v1";
|
|
80
81
|
|
|
81
82
|
export default defineConfig({
|
|
82
83
|
// Declare your Neon services here
|
|
83
|
-
auth: ${auth},
|
|
84
|
+
${realtimeDeclaration} auth: ${auth},
|
|
84
85
|
${dataApi}${renderGaServices(services)} // Branch policy: per-branch tuning
|
|
85
86
|
branch: (branch) => {
|
|
86
87
|
if (branch.isDefault) {
|
package/dist/dev/env.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-
|
|
1
|
+
import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-4WGRW2nw.js";
|
|
2
2
|
export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, dropClaimableUnsupported, resolveDevEnv, resolveNeonEnvVars };
|
package/dist/env_services.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NEON_SERVICES } from "./neon_services.js";
|
|
2
|
-
import { f as parseFunctionBaseUrlKey, t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "./_chunks/env-
|
|
2
|
+
import { f as parseFunctionBaseUrlKey, t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "./_chunks/env-BpP3Dqpe.js";
|
|
3
3
|
//#region src/env_services.ts
|
|
4
4
|
const ENV_PULL_SERVICES = NEON_SERVICES;
|
|
5
5
|
const ENV_PULL_KEYS = [
|
|
@@ -7,6 +7,7 @@ const ENV_PULL_KEYS = [
|
|
|
7
7
|
NEON_ENV_VAR_KEYS.branch.name,
|
|
8
8
|
...Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
9
9
|
...Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
10
|
+
...Object.values(NEON_ENV_VAR_KEYS.realtime),
|
|
10
11
|
...Object.values(NEON_ENV_VAR_KEYS.storage),
|
|
11
12
|
...Object.values(NEON_ENV_VAR_KEYS.aiGateway)
|
|
12
13
|
];
|
|
@@ -16,6 +17,7 @@ const SERVICE_ENV_KEYS = {
|
|
|
16
17
|
postgres: Object.values(NEON_ENV_VAR_KEYS.postgres),
|
|
17
18
|
auth: Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
18
19
|
"data-api": Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
20
|
+
realtime: Object.values(NEON_ENV_VAR_KEYS.realtime),
|
|
19
21
|
"object-storage": Object.values(NEON_ENV_VAR_KEYS.storage),
|
|
20
22
|
"ai-gateway": Object.values(NEON_ENV_VAR_KEYS.aiGateway),
|
|
21
23
|
functions: []
|
|
@@ -44,6 +46,8 @@ const ENV_KEY_SERVICE = {
|
|
|
44
46
|
NEON_AUTH_BASE_URL: "auth",
|
|
45
47
|
NEON_AUTH_JWKS_URL: "auth",
|
|
46
48
|
NEON_DATA_API_URL: "data-api",
|
|
49
|
+
NEON_REALTIME_URL: "realtime",
|
|
50
|
+
NEON_REALTIME_SECRET: "realtime",
|
|
47
51
|
AWS_ACCESS_KEY_ID: "object-storage",
|
|
48
52
|
AWS_SECRET_ACCESS_KEY: "object-storage",
|
|
49
53
|
AWS_ENDPOINT_URL_S3: "object-storage",
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,13 @@ import { getCliName } from "./utils/cli_name.js";
|
|
|
12
12
|
import { ensureAuth } from "./commands/auth.js";
|
|
13
13
|
import { recoverFrom401 } from "./auth_recovery.js";
|
|
14
14
|
import { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageErrorHelp } from "./help.js";
|
|
15
|
+
import { stagingFeaturesEnabledForArgv } from "./utils/staging.js";
|
|
15
16
|
import { lazyCommands, preloadCommand } from "./commands/manifest.js";
|
|
16
17
|
import { rewriteUnknownAgentArg } from "./init/plan.js";
|
|
17
18
|
import { recoverFromSSO } from "./sso_recovery.js";
|
|
18
19
|
import { notifyIfUpdateAvailable } from "./update_notifier.js";
|
|
19
20
|
import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
|
|
21
|
+
import { collapseRepeatedFlags, declaredOptions } from "./utils/repeated_flags.js";
|
|
20
22
|
import yargs from "yargs";
|
|
21
23
|
import { hideBin } from "yargs/helpers";
|
|
22
24
|
//#region src/index.ts
|
|
@@ -28,6 +30,7 @@ const NO_SUBCOMMANDS_VERBS = [
|
|
|
28
30
|
"cs",
|
|
29
31
|
"connection-string",
|
|
30
32
|
"psql",
|
|
33
|
+
"subscribe",
|
|
31
34
|
"set-context",
|
|
32
35
|
"checkout",
|
|
33
36
|
"link",
|
|
@@ -47,7 +50,10 @@ const NO_SUBCOMMANDS_VERBS = [
|
|
|
47
50
|
"status"
|
|
48
51
|
];
|
|
49
52
|
let builder = yargs(hideBin(process.argv));
|
|
50
|
-
builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command> [options]").parserConfiguration({
|
|
53
|
+
builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command> [options]").parserConfiguration({
|
|
54
|
+
"populate--": true,
|
|
55
|
+
"parse-positional-numbers": false
|
|
56
|
+
}).help().option("output", {
|
|
51
57
|
alias: "o",
|
|
52
58
|
group: "Global options:",
|
|
53
59
|
describe: "Set output format",
|
|
@@ -120,6 +126,8 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
|
|
|
120
126
|
default: true
|
|
121
127
|
}
|
|
122
128
|
}).middleware((args) => {
|
|
129
|
+
collapseRepeatedFlags(args, declaredOptions(builder));
|
|
130
|
+
}, true).middleware((args) => {
|
|
123
131
|
fillInArgs(args);
|
|
124
132
|
}, true).middleware(resolveApiKeyFromEnv, true).middleware(initAnalyticsClientMiddleware, true).help(false).group("help", "Global options:").option("help", {
|
|
125
133
|
describe: "Show help",
|
|
@@ -129,7 +137,7 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
|
|
|
129
137
|
if (args.help) await showHelp(builder);
|
|
130
138
|
}, true).middleware(async (args) => {
|
|
131
139
|
if (args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0]) || args._.length > 1 && isBareParentCommand(builder, args._)) await showHelp(builder);
|
|
132
|
-
}).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(lazyCommands).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
|
|
140
|
+
}).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(lazyCommands({ staging: stagingFeaturesEnabledForArgv(hideBin(process.argv)) })).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
|
|
133
141
|
async function handleError(msg, err, canRetry) {
|
|
134
142
|
if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
|
|
135
143
|
await showHelp(builder);
|
package/dist/init/run.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getCliName } from "../utils/cli_name.js";
|
|
|
9
9
|
import { servicesFlagValue } from "../neon_services.js";
|
|
10
10
|
import { CONFIG_INIT_SERVICES, parseConfigInitServices } from "../config_template.js";
|
|
11
11
|
import { hasNeonConfigFile, neonConfigFilename } from "../project.js";
|
|
12
|
+
import { assertRealtimeAvailable } from "../utils/staging.js";
|
|
12
13
|
import { getAgentDisplayName } from "../mcp/agents.js";
|
|
13
14
|
import { mcpInstallableAgents } from "../mcp/targets.js";
|
|
14
15
|
import { missingPluginsCommands, pluginsInstallableAgents } from "../plugins/targets.js";
|
|
@@ -183,7 +184,7 @@ const runInit = async (props) => {
|
|
|
183
184
|
};
|
|
184
185
|
const servicesFlag = servicesFlagValue(props.services);
|
|
185
186
|
if (props.config === false && servicesFlag !== void 0) throw new Error(INIT_CONFIG_SERVICES_CONFLICT);
|
|
186
|
-
if (servicesFlag !== void 0
|
|
187
|
+
if (servicesFlag !== void 0 && parseConfigInitServices(servicesFlag).includes("realtime")) assertRealtimeAvailable(props.apiHost);
|
|
187
188
|
const linkInputs = {
|
|
188
189
|
...props.orgId ? { orgId: props.orgId } : {},
|
|
189
190
|
...props.projectId ? { projectId: props.projectId } : {},
|
package/dist/neon_services.js
CHANGED
|
@@ -23,7 +23,8 @@ const NEON_SERVICES = [
|
|
|
23
23
|
"data-api",
|
|
24
24
|
"functions",
|
|
25
25
|
"object-storage",
|
|
26
|
-
"ai-gateway"
|
|
26
|
+
"ai-gateway",
|
|
27
|
+
"realtime"
|
|
27
28
|
];
|
|
28
29
|
/** How output names each service. */
|
|
29
30
|
const NEON_SERVICE_LABELS = {
|
|
@@ -32,7 +33,8 @@ const NEON_SERVICE_LABELS = {
|
|
|
32
33
|
"data-api": "Data API",
|
|
33
34
|
"object-storage": "Object Storage",
|
|
34
35
|
functions: "Functions",
|
|
35
|
-
"ai-gateway": "AI Gateway"
|
|
36
|
+
"ai-gateway": "AI Gateway",
|
|
37
|
+
realtime: "Realtime"
|
|
36
38
|
};
|
|
37
39
|
/**
|
|
38
40
|
* Spellings that used to be canonical, and the service they now mean. Accepted so a scripted
|
|
@@ -73,7 +75,7 @@ const parseServices = (raw, options) => {
|
|
|
73
75
|
const unsupported = resolved.filter((name) => !allowed.some((service) => service === name));
|
|
74
76
|
if (unsupported.length > 0) throw new Error(`${unsupportedMessage(unsupported, flag)} ${supported}`);
|
|
75
77
|
for (const [used, canonical] of deprecated) onDeprecated?.(used, canonical);
|
|
76
|
-
return
|
|
78
|
+
return allowed.filter((service) => resolved.includes(service));
|
|
77
79
|
};
|
|
78
80
|
/**
|
|
79
81
|
* The sentences explaining why a selection was refused. A real Neon service this command
|
package/dist/parameters.gen.js
CHANGED
|
@@ -95,6 +95,11 @@ const projectCreateRequest = {
|
|
|
95
95
|
description: "The project name. If not specified, the name will be identical to the generated project ID",
|
|
96
96
|
demandOption: false
|
|
97
97
|
},
|
|
98
|
+
"project.realtime.allowed_origins": {
|
|
99
|
+
type: "array",
|
|
100
|
+
description: "The browser origins allowed to connect, each `http` or `https` with a host and optional port\nand no path. An empty list, or `*` alone, allows any origin. Omitted keeps the current or\ninherited value.\n",
|
|
101
|
+
demandOption: false
|
|
102
|
+
},
|
|
98
103
|
"project.branch.name": {
|
|
99
104
|
type: "string",
|
|
100
105
|
description: "The default branch name. If not specified, the default branch name, `main`, will be used.\n",
|
|
@@ -284,6 +289,11 @@ const branchCreateRequest = {
|
|
|
284
289
|
description: "Whether the branch is protected. Protected branches (and their computes) cannot be deleted, archived, or reset, and block deletion of the project. Can be gated by `protected_branches_only` in the IP allowlist. Paid plans only.\n",
|
|
285
290
|
demandOption: false
|
|
286
291
|
},
|
|
292
|
+
"branch.realtime.allowed_origins": {
|
|
293
|
+
type: "array",
|
|
294
|
+
description: "The browser origins allowed to connect, each `http` or `https` with a host and optional port\nand no path. An empty list, or `*` alone, allows any origin. Omitted keeps the current or\ninherited value.\n",
|
|
295
|
+
demandOption: false
|
|
296
|
+
},
|
|
287
297
|
"branch.archived": {
|
|
288
298
|
type: "boolean",
|
|
289
299
|
description: "Whether to create the branch in the archived state. When omitted, the branch is created as a normal (non-archived) branch.\n",
|
|
@@ -23,6 +23,26 @@ const reserveClosedPort = () => new Promise((resolve, reject) => {
|
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
|
+
/** Tokens yargs would turn into numbers. `1.50` and `1e3` do not survive the round trip. */
|
|
27
|
+
const NUMERIC_PSQL_ARGS = [
|
|
28
|
+
"-c",
|
|
29
|
+
"1",
|
|
30
|
+
"-p",
|
|
31
|
+
"5432",
|
|
32
|
+
"-v",
|
|
33
|
+
"1.50",
|
|
34
|
+
"1e3"
|
|
35
|
+
];
|
|
36
|
+
/** The arguments `mocks/bin/psql` received after the connection URI. */
|
|
37
|
+
const forwardedPsqlArgs = (stdout) => {
|
|
38
|
+
const match = /\{"psql-cli-args":(\[.*?\])\}/.exec(stdout);
|
|
39
|
+
if (!match) throw new Error(`mocks/bin/psql did not run. stdout: ${stdout}`);
|
|
40
|
+
const args = JSON.parse(match[1]);
|
|
41
|
+
if (!Array.isArray(args)) throw new Error(`psql-cli-args is not an array: ${match[1]}`);
|
|
42
|
+
return args.slice(1);
|
|
43
|
+
};
|
|
44
|
+
/** The CLI rejects `--output` given twice with different values, so a test's own wins. */
|
|
45
|
+
const passesOutput = (args) => args.some((arg) => arg === "-o" || arg === "--output" || arg.startsWith("--output="));
|
|
26
46
|
const test = test$1.extend({
|
|
27
47
|
runMockServer: async ({}, use) => {
|
|
28
48
|
let startedServer;
|
|
@@ -62,8 +82,7 @@ const test = test$1.extend({
|
|
|
62
82
|
const cp = fork(join(process.cwd(), "./dist/index.js"), [
|
|
63
83
|
"--api-host",
|
|
64
84
|
apiHost,
|
|
65
|
-
"--output",
|
|
66
|
-
options.output ?? (options.outputTable ? "table" : "yaml"),
|
|
85
|
+
...passesOutput(args) ? [] : ["--output", options.output ?? (options.outputTable ? "table" : "yaml")],
|
|
67
86
|
"--no-analytics",
|
|
68
87
|
...options.apiKey === false ? [] : ["--api-key", options.apiKey ?? "test-key"],
|
|
69
88
|
...args
|
|
@@ -110,4 +129,4 @@ const test = test$1.extend({
|
|
|
110
129
|
}
|
|
111
130
|
});
|
|
112
131
|
//#endregion
|
|
113
|
-
export { test };
|
|
132
|
+
export { NUMERIC_PSQL_ARGS, forwardedPsqlArgs, passesOutput, test };
|
|
@@ -78,6 +78,7 @@ const renderBranchGroups = (byBranch, paint) => {
|
|
|
78
78
|
const serviceLabel = (identifier, details) => {
|
|
79
79
|
if (identifier === "auth") return "Neon Auth";
|
|
80
80
|
if (identifier === "dataApi") return "Data API";
|
|
81
|
+
if (identifier === "realtime") return "Realtime";
|
|
81
82
|
if (identifier.startsWith("bucket:")) return `bucket ${identifier.slice(7)}`;
|
|
82
83
|
if (identifier.startsWith("function:")) return `function ${identifier.slice(9)}`;
|
|
83
84
|
if (identifier.startsWith("domain:")) {
|
package/dist/utils/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { log } from "../log.js";
|
|
2
2
|
import { loadProjectConfig } from "../project.js";
|
|
3
|
-
import { s as fetchEnvKeys } from "../_chunks/env-
|
|
3
|
+
import { s as fetchEnvKeys } from "../_chunks/env-BpP3Dqpe.js";
|
|
4
|
+
import { assertPolicyAvailable } from "./staging.js";
|
|
4
5
|
import { runHook } from "@neon/config-runtime";
|
|
5
6
|
//#region src/utils/hooks.ts
|
|
6
7
|
/**
|
|
@@ -149,6 +150,7 @@ const runAfter = async (label, run) => {
|
|
|
149
150
|
* postgres only, since nothing else is declared enabled).
|
|
150
151
|
*/
|
|
151
152
|
const resolveHookEnv = async (config, options) => {
|
|
153
|
+
assertPolicyAvailable(config, options.apiHost);
|
|
152
154
|
try {
|
|
153
155
|
const resolved = await fetchEnvKeys(config, options, null);
|
|
154
156
|
return shapeHookEnv(resolved);
|