convex
Version:
Client for the Convex Cloud
364 lines (363 loc) • 10.9 kB
JavaScript
;
import chalk from "chalk";
import axios from "axios";
import equal from "deep-equal";
import path from "path";
import {
bundle,
databaseEntryPoints,
actionsEntryPoints,
actionsDir
} from "../../bundler/index.js";
import { version } from "../../index.js";
import axiosRetry from "axios-retry";
import {
deprecationCheckWarning,
formatSize,
functionsDir,
fatalServerErr
} from "./utils.js";
export { provisionHost, productionProvisionHost } from "./utils.js";
function isAuthInfo(object) {
return "applicationID" in object && typeof object.applicationID === "string" && "domain" in object && typeof object.domain === "string";
}
function isAuthInfos(object) {
return Array.isArray(object) && object.every((item) => isAuthInfo(item));
}
class ParseError extends Error {
}
export function parseProjectConfig(obj) {
if (typeof obj !== "object") {
throw new ParseError("Expected an object");
}
if (typeof obj.team !== "string") {
if (obj.instanceName && obj.origin) {
throw new ParseError(
'If upgrading from convex 0.1.8 or below, please delete "convex.json" and reinitialize using `npx convex reinit`'
);
}
throw new ParseError("Expected team to be a string");
}
if (typeof obj.project !== "string") {
throw new ParseError("Expected project to be a string");
}
if (typeof obj.prodUrl !== "string") {
throw new ParseError("Expected prodUrl to be a string");
}
if (typeof obj.functions !== "string") {
throw new ParseError("Expected functions to be a string");
}
obj.authInfo = obj.authInfo ?? [];
if (!isAuthInfos(obj.authInfo)) {
throw new ParseError("Expected authInfo to be type AuthInfo[]");
}
return obj;
}
function parseBackendConfig(obj) {
if (typeof obj !== "object") {
throw new ParseError("Expected an object");
}
if (typeof obj.functions !== "string") {
throw new ParseError("Expected functions to be a string");
}
obj.authInfo = obj.authInfo ?? [];
if (!isAuthInfos(obj.authInfo)) {
throw new ParseError("Expected authInfo to be type AuthInfo[]");
}
return obj;
}
export function configName() {
return "convex.json";
}
export async function configFilepath(ctx) {
const configFn = configName();
const preferredLocation = configFn;
const wrongLocation = path.join("src", configFn);
const preferredLocationExists = ctx.fs.exists(preferredLocation);
const wrongLocationExists = ctx.fs.exists(wrongLocation);
if (preferredLocationExists && wrongLocationExists) {
console.error(
chalk.red(
`Error: both ${preferredLocation} and ${wrongLocation} files exist!`
)
);
console.error(`Consolidate these and remove ${wrongLocation}.`);
return await ctx.fatalError(1, "fs");
}
if (!preferredLocationExists && wrongLocationExists) {
console.error(
chalk.red(
`Error: Please move ${wrongLocation} to the root of your project`
)
);
return await ctx.fatalError(1, "fs");
}
return preferredLocation;
}
export async function readProjectConfig(ctx) {
let projectConfig;
const configPath = await configFilepath(ctx);
try {
projectConfig = parseProjectConfig(
JSON.parse(ctx.fs.readUtf8File(configPath))
);
} catch (err) {
if (err instanceof ParseError || err instanceof SyntaxError) {
console.error(chalk.red(`Error: Parsing "${configPath}" failed`));
console.error(chalk.gray(err.toString()));
} else {
console.error(
chalk.red(`Error: Unable to read project config file "${configPath}"`)
);
console.error(
"Are you running this command from the root directory of a Convex project?"
);
if (err instanceof Error) {
console.error(chalk.gray(err.message));
}
}
return await ctx.fatalError(1, "fs", err);
}
return {
projectConfig,
configPath
};
}
export async function configFromProjectConfig(ctx, projectConfig, configPath, verbose) {
let modules;
try {
const baseDir = functionsDir(configPath, projectConfig);
const entryPoints = await databaseEntryPoints(ctx.fs, baseDir, verbose);
modules = await bundle(ctx.fs, baseDir, entryPoints, true, "browser");
if (verbose) {
console.log(
"Queries and mutations modules: ",
modules.map((m) => m.path)
);
}
const nodeEntryPoints = await actionsEntryPoints(ctx.fs, baseDir, verbose);
const nodeModules = await bundle(
ctx.fs,
baseDir,
nodeEntryPoints,
true,
"node",
path.join(actionsDir, "_deps")
);
if (verbose) {
console.log(
"Actions modules: ",
nodeModules.map((m) => m.path)
);
}
modules.push(...nodeModules);
} catch (err) {
console.error(chalk.red("Error: Unable to bundle Convex modules"));
if (err instanceof Error) {
console.error(chalk.gray(err.message));
}
return await ctx.fatalError(1, "fs", err);
}
return {
projectConfig,
modules,
udfServerVersion: version
};
}
export async function readConfig(ctx, verbose) {
const { projectConfig, configPath } = await readProjectConfig(ctx);
const config = await configFromProjectConfig(
ctx,
projectConfig,
configPath,
verbose
);
return { config, configPath };
}
export async function writeProjectConfig(ctx, projectConfig) {
const configPath = await configFilepath(ctx);
try {
const contents = JSON.stringify(projectConfig, void 0, 2) + "\n";
ctx.fs.writeUtf8File(configPath, contents, 420);
} catch (err) {
console.error(
chalk.red(
`Error: Unable to write project config file "${configPath}" in current directory`
)
);
console.error(
"Are you running this command from the root directory of a Convex project?"
);
return await ctx.fatalError(1, "fs", err);
}
ctx.fs.mkdir(functionsDir(configPath, projectConfig), {
allowExisting: true
});
}
export async function pullConfig(ctx, project, team, origin, adminKey) {
const client = axios.create();
axiosRetry(client, {
retries: 4,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return error.response?.status === 404 || false;
}
});
try {
const res = await client.post(
`${origin}/api/${version}/get_config`,
{ version, adminKey },
{
maxContentLength: Infinity
}
);
deprecationCheckWarning(ctx, res);
const { functions, authInfo } = parseBackendConfig(res.data.config);
const projectConfig = {
project,
team,
prodUrl: origin,
functions,
authInfo
};
return {
projectConfig,
modules: res.data.modules,
udfServerVersion: res.data.udfServerVersion
};
} catch (err) {
console.error(
chalk.red("Error: Unable to pull deployment config from", origin)
);
return await fatalServerErr(ctx, err);
}
}
export function configJSON(config, adminKey) {
const projectConfig = {
projectSlug: config.projectConfig.project,
teamSlug: config.projectConfig.team,
functions: config.projectConfig.functions,
authInfo: config.projectConfig.authInfo
};
return {
config: projectConfig,
modules: config.modules,
udfServerVersion: config.udfServerVersion,
adminKey
};
}
export async function pushConfig(ctx, config, adminKey, url) {
const serializedConfig = configJSON(config, adminKey);
try {
await axios.post(`${url}/api/${version}/push_config`, serializedConfig, {
maxContentLength: Infinity,
maxBodyLength: Infinity
});
} catch (err) {
console.error(chalk.red("Error: Unable to push deployment config to", url));
return await fatalServerErr(ctx, err);
}
}
function renderModule(module) {
const sourceMapSize = formatSize(module.sourceMap?.length ?? 0);
return module.path + ` (${formatSize(module.source.length)}, source map ${sourceMapSize})`;
}
function compareModules(oldModules, newModules) {
let diff = "";
const droppedModules = [];
for (const oldModule of oldModules) {
let matches = false;
for (const newModule of newModules) {
if (oldModule.path === newModule.path && oldModule.source === newModule.source && oldModule.sourceMap === newModule.sourceMap) {
matches = true;
break;
}
}
if (!matches) {
droppedModules.push(oldModule);
}
}
if (droppedModules.length > 0) {
diff += "Delete the following modules:\n";
for (const module of droppedModules) {
diff += "[-] " + renderModule(module) + "\n";
}
}
const addedModules = [];
for (const newModule of newModules) {
let matches = false;
for (const oldModule of oldModules) {
if (oldModule.path === newModule.path && oldModule.source === newModule.source && oldModule.sourceMap === newModule.sourceMap) {
matches = true;
break;
}
}
if (!matches) {
addedModules.push(newModule);
}
}
if (addedModules.length > 0) {
diff += "Add the following modules:\n";
for (const module of addedModules) {
diff += "[+] " + renderModule(module) + "\n";
}
}
return diff;
}
export function diffConfig(oldConfig, newConfig) {
let diff = compareModules(oldConfig.modules, newConfig.modules);
const droppedAuth = [];
for (const oldAuth of oldConfig.projectConfig.authInfo) {
let matches2 = false;
for (const newAuth of newConfig.projectConfig.authInfo) {
if (equal(oldAuth, newAuth)) {
matches2 = true;
break;
}
}
if (!matches2) {
droppedAuth.push(oldAuth);
}
}
if (droppedAuth.length > 0) {
diff += "Remove the following auth providers:\n";
for (const authInfo of droppedAuth) {
diff += "[-] " + JSON.stringify(authInfo) + "\n";
}
}
const addedAuth = [];
for (const newAuth of newConfig.projectConfig.authInfo) {
let matches2 = false;
for (const oldAuth of oldConfig.projectConfig.authInfo) {
if (equal(newAuth, oldAuth)) {
matches2 = true;
break;
}
}
if (!matches2) {
addedAuth.push(newAuth);
}
}
if (addedAuth.length > 0) {
diff += "Add the following auth providers:\n";
for (const auth of addedAuth) {
diff += "[+] " + JSON.stringify(auth) + "\n";
}
}
let versionMessage = "";
const matches = oldConfig.udfServerVersion === newConfig.udfServerVersion;
if (oldConfig.udfServerVersion && (!newConfig.udfServerVersion || !matches)) {
versionMessage += `[-] ${oldConfig.udfServerVersion}
`;
}
if (newConfig.udfServerVersion && (!oldConfig.udfServerVersion || !matches)) {
versionMessage += `[+] ${newConfig.udfServerVersion}
`;
}
if (versionMessage) {
diff += "Change the server's function version:\n";
diff += versionMessage;
}
return diff;
}
//# sourceMappingURL=config.js.map