@dbos-inc/dbos-cloud 0.8.41-preview → 0.8.43-preview

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/Gruntfile.js ADDED
@@ -0,0 +1,9 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
2
+ var nbgv = require('nerdbank-gitversioning')
3
+
4
+ module.exports = function (grunt) {
5
+ grunt.registerTask('setversion', function () {
6
+ var done = this.async();
7
+ nbgv.setPackageVersion().then(() => done());
8
+ });
9
+ };
@@ -0,0 +1,36 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "../cloudutils";
4
+ import path from "node:path";
5
+
6
+ export async function deleteApp(host: string): Promise<number> {
7
+ const logger = new GlobalLogger();
8
+ const userCredentials = getCloudCredentials();
9
+ const bearerToken = "Bearer " + userCredentials.token;
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ const packageJson = require(path.join(process.cwd(), 'package.json')) as { name: string };
13
+ const appName = packageJson.name;
14
+ logger.info(`Loaded application name from package.json: ${appName}`)
15
+ logger.info(`Deleting application: ${appName}`)
16
+
17
+ try {
18
+ await axios.delete(`https://${host}/${userCredentials.userName}/application/${appName}`, {
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Authorization: bearerToken,
22
+ },
23
+ });
24
+
25
+ logger.info(`Successfully deleted application: ${appName}`);
26
+ return 0;
27
+ } catch (e) {
28
+ if (axios.isAxiosError(e) && e.response) {
29
+ logger.error(`Failed to delete application ${appName}: ${e.response?.data}`);
30
+ return 1;
31
+ } else {
32
+ logger.error(`Failed to delete application ${appName}: ${(e as Error).message}`);
33
+ return 1;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,159 @@
1
+ import axios from "axios";
2
+ import { execSync } from "child_process";
3
+ import { writeFileSync, existsSync } from 'fs';
4
+ import { GlobalLogger } from "../../../src/telemetry/logs";
5
+ import { getCloudCredentials, runCommand } from "../cloudutils";
6
+ import { createDirectory, readFileSync, sleep } from "../../../src/utils";
7
+ import path from "path";
8
+ import { Application } from "./types";
9
+
10
+ const deployDirectoryName = "dbos_deploy";
11
+
12
+ type DeployOutput = {
13
+ ApplicationName: string;
14
+ ApplicationVersion: string;
15
+ }
16
+
17
+ export async function deployAppCode(host: string, docker: boolean): Promise<number> {
18
+ const logger = new GlobalLogger();
19
+ const userCredentials = getCloudCredentials();
20
+ const bearerToken = "Bearer " + userCredentials.token;
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
23
+ const packageJson = require(path.join(process.cwd(), 'package.json')) as { name: string };
24
+ const appName = packageJson.name;
25
+ logger.info(`Loaded application name from package.json: ${appName}`)
26
+
27
+ createDirectory(deployDirectoryName);
28
+
29
+ // Verify that package-lock.json exists
30
+ if (!existsSync(path.join(process.cwd(), 'package-lock.json'))) {
31
+ logger.error("package-lock.json not found. Please run 'npm install' before deploying.")
32
+ return 1;
33
+ }
34
+
35
+ if (docker) {
36
+ // Build the application inside a Docker container using the same base image as our cloud setup
37
+ logger.info(`Building ${appName} using Docker`)
38
+ const dockerSuccess = await buildAppInDocker(appName);
39
+ if (!dockerSuccess) {
40
+ return 1;
41
+ }
42
+ } else {
43
+ // Zip the current directory and deploy from there. Requires app to have already been built. Only for testing.
44
+ execSync(`zip -ry ${deployDirectoryName}/${appName}.zip ./* -x ${deployDirectoryName}/* > /dev/null`);
45
+ }
46
+
47
+ try {
48
+ const zipData = readFileSync(`${deployDirectoryName}/${appName}.zip`, "base64");
49
+
50
+ // Submit the deploy request
51
+ logger.info(`Submitting deploy request for ${appName}`)
52
+ const response = await axios.post(
53
+ `https://${host}/${userCredentials.userName}/application/${appName}`,
54
+ {
55
+ application_archive: zipData,
56
+ },
57
+ {
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ Authorization: bearerToken,
61
+ },
62
+ }
63
+ );
64
+ const deployOutput = response.data as DeployOutput;
65
+ logger.info(`Submitted deploy request for ${appName}. Assigned version: ${deployOutput.ApplicationVersion}`);
66
+
67
+ // Wait for the application to become available
68
+ let count = 0
69
+ let applicationAvailable = false
70
+ while (!applicationAvailable) {
71
+ count += 1
72
+ if (count % 5 === 0) {
73
+ logger.info(`Waiting for ${appName} with version ${deployOutput.ApplicationVersion} to be available`);
74
+ if (count > 20) {
75
+ logger.info(`If ${appName} takes too long to become available, check its logs at...`);
76
+ }
77
+ }
78
+
79
+ // Retrieve the application status, check if it is "AVAILABLE"
80
+ const list = await axios.get(
81
+ `https://${host}/${userCredentials.userName}/application`,
82
+ {
83
+ headers: {
84
+ Authorization: bearerToken,
85
+ },
86
+ }
87
+ );
88
+ const applications: Application[] = list.data as Application[];
89
+ for (const application of applications) {
90
+ if (application.Name === appName && application.Status === "AVAILABLE") {
91
+ applicationAvailable = true
92
+ }
93
+ }
94
+ await sleep(1000)
95
+ }
96
+ logger.info(`Application ${appName} successfuly deployed`)
97
+ logger.info(`Access your application at https://${host}/${userCredentials.userName}/application/${appName}`)
98
+ return 0;
99
+ } catch (e) {
100
+ if (axios.isAxiosError(e) && e.response) {
101
+ logger.error(`Failed to deploy application ${appName}: ${e.response?.data}`);
102
+ return 1;
103
+ } else {
104
+ logger.error(`Failed to deploy application ${appName}: ${(e as Error).message}`);
105
+ return 1;
106
+ }
107
+ }
108
+ }
109
+
110
+ async function buildAppInDocker(appName: string): Promise<boolean> {
111
+ const logger = new GlobalLogger();
112
+
113
+ // Verify Docker is running
114
+ try {
115
+ execSync(`docker > /dev/null 2>&1`)
116
+ } catch (e) {
117
+ logger.error("Docker not available. To deploy, please start the Docker daemon and make the `docker` command runnable without sudo.")
118
+ return false
119
+ }
120
+
121
+ const dockerFileName = `${deployDirectoryName}/Dockerfile.dbos`;
122
+ const containerName = `dbos-builder-${appName}`;
123
+
124
+ // Dockerfile content
125
+ const dockerFileContent = `
126
+ FROM node:lts-bookworm-slim
127
+ RUN apt update
128
+ RUN apt install -y zip
129
+ WORKDIR /app
130
+ COPY . .
131
+ RUN npm clean-install
132
+ RUN npm run build
133
+ RUN npm prune --omit=dev
134
+ RUN zip -ry ${appName}.zip ./* -x "${appName}.zip" -x "${deployDirectoryName}/*" > /dev/null
135
+ `;
136
+ const dockerIgnoreContent = `
137
+ node_modules/
138
+ ${deployDirectoryName}/
139
+ dist/
140
+ `;
141
+ try {
142
+ // Write the Dockerfile and .dockerignore
143
+ writeFileSync(dockerFileName, dockerFileContent);
144
+ writeFileSync(`${deployDirectoryName}/Dockerfile.dbos.dockerignore`, dockerIgnoreContent);
145
+ // Build the Docker image. As build takes a long time, use runCommand to stream its output to stdout.
146
+ await runCommand('docker', ['build', '-t', appName, '-f', dockerFileName, '.'])
147
+ // Run the container
148
+ execSync(`docker run -d --name ${containerName} ${appName}`);
149
+ // Copy the archive from the container to the local deploy directory
150
+ execSync(`docker cp ${containerName}:/app/${appName}.zip ${deployDirectoryName}/${appName}.zip`);
151
+ // Stop and remove the container
152
+ execSync(`docker stop ${containerName}`);
153
+ execSync(`docker rm ${containerName}`);
154
+ return true;
155
+ } catch (e) {
156
+ logger.error(`Failed to build application ${appName}: ${(e as Error).message}`);
157
+ return false;
158
+ }
159
+ }
@@ -0,0 +1,36 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "../cloudutils";
4
+ import path from "node:path";
5
+
6
+ export async function getAppLogs(host: string): Promise<number> {
7
+ const logger = new GlobalLogger();
8
+ const userCredentials = getCloudCredentials();
9
+ const bearerToken = "Bearer " + userCredentials.token;
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ const packageJson = require(path.join(process.cwd(), 'package.json')) as { name: string };
13
+ const appName = packageJson.name;
14
+ logger.info(`Retrieving logs for application: ${appName}`)
15
+
16
+ try {
17
+ const res = await axios.get(`https://${host}/${userCredentials.userName}/logs/application/${appName}`, {
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ Authorization: bearerToken,
21
+ },
22
+ });
23
+
24
+ logger.info(`Successfully retrieved logs of application: ${appName}`);
25
+ logger.info(res.data)
26
+ return 0;
27
+ } catch (e) {
28
+ if (axios.isAxiosError(e) && e.response) {
29
+ logger.error(`Failed to retrieve logs of application ${appName}: ${e.response?.data}`);
30
+ return 1;
31
+ } else {
32
+ logger.error(`Failed to retrieve logs of application ${appName}: ${(e as Error).message}`);
33
+ return 1;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ export { registerApp } from './register-app';
2
+ export { listApps } from './list-apps';
3
+ export { deleteApp } from './delete-app';
4
+ export { deployAppCode } from './deploy-app-code';
5
+ export { getAppLogs } from './get-app-logs';
6
+ export { updateApp } from './update-app';
7
+
@@ -0,0 +1,41 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "../cloudutils";
4
+ import { Application } from "./types";
5
+
6
+ export async function listApps(host: string): Promise<number> {
7
+ const logger = new GlobalLogger();
8
+ const userCredentials = getCloudCredentials();
9
+ const bearerToken = "Bearer " + userCredentials.token;
10
+
11
+ try {
12
+ const list = await axios.get(
13
+ `https://${host}/${userCredentials.userName}/application`,
14
+ {
15
+ headers: {
16
+ Authorization: bearerToken,
17
+ },
18
+ }
19
+ );
20
+ const data: Application[] = list.data as Application[];
21
+ if (data.length === 0) {
22
+ logger.info("No applications found");
23
+ return 1;
24
+ }
25
+ const formattedData: Application[] = []
26
+ for (const application of data) {
27
+ formattedData.push({ "Name": application.Name, "ID": application.ID, "Version": application.Version, "DatabaseName": application.DatabaseName, "MaxVMs": application.MaxVMs, "Status": application.Status });
28
+ }
29
+ logger.info(`Listing applications for ${userCredentials.userName}`)
30
+ console.log(JSON.stringify(formattedData));
31
+ return 0;
32
+ } catch (e) {
33
+ if (axios.isAxiosError(e) && e.response) {
34
+ logger.error(`Failed to list applications: ${e.response?.data}`);
35
+ return 1;
36
+ } else {
37
+ logger.error(`Failed to list applications: ${(e as Error).message}`);
38
+ return 1;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,45 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "../cloudutils";
4
+ import path from "node:path";
5
+
6
+ export async function registerApp(dbname: string, host: string, machines: number): Promise<number> {
7
+ const logger = new GlobalLogger();
8
+ const userCredentials = getCloudCredentials();
9
+ const bearerToken = "Bearer " + userCredentials.token;
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ const packageJson = require(path.join(process.cwd(), 'package.json')) as { name: string };
13
+ const appName = packageJson.name;
14
+ logger.info(`Loaded application name from package.json: ${appName}`)
15
+ logger.info(`Registering application: ${appName}`)
16
+
17
+ try {
18
+ const register = await axios.put(
19
+ `https://${host}/${userCredentials.userName}/application`,
20
+ {
21
+ name: appName,
22
+ database: dbname,
23
+ max_vms: machines,
24
+ },
25
+ {
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ Authorization: bearerToken,
29
+ },
30
+ }
31
+ );
32
+ const uuid = register.data as string;
33
+ logger.info(`Successfully registered: ${appName}`);
34
+ logger.info(`${appName} ID: ${uuid}`);
35
+ return 0;
36
+ } catch (e) {
37
+ if (axios.isAxiosError(e) && e.response) {
38
+ logger.error(`Failed to register application ${appName}: ${e.response?.data}`);
39
+ return 1;
40
+ } else {
41
+ logger.error(`Failed to register application ${appName}: ${(e as Error).message}`);
42
+ return 1;
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,8 @@
1
+ export type Application = {
2
+ Name: string;
3
+ ID: string;
4
+ DatabaseName: string;
5
+ Status: string;
6
+ Version: string;
7
+ MaxVMs: string;
8
+ };
@@ -0,0 +1,47 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "../cloudutils";
4
+ import { Application } from "./types";
5
+ import path from "node:path";
6
+
7
+ export async function updateApp(host: string, machines: number): Promise<number> {
8
+ const logger = new GlobalLogger();
9
+ const userCredentials = getCloudCredentials();
10
+ const bearerToken = "Bearer " + userCredentials.token;
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
13
+ const packageJson = require(path.join(process.cwd(), 'package.json')) as { name: string };
14
+ const appName = packageJson.name;
15
+ logger.info(`Loaded application name from package.json: ${appName}`)
16
+ logger.info(`Updating application: ${appName}`)
17
+
18
+ try {
19
+ logger.info(`Updating application ${appName} to ${machines} machines`);
20
+ const update = await axios.patch(
21
+ `https://${host}/${userCredentials.userName}/application/${appName}`,
22
+ {
23
+ name: appName,
24
+ max_vms: machines
25
+ },
26
+ {
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ Authorization: bearerToken,
30
+ },
31
+ }
32
+ );
33
+ const application: Application = update.data as Application;
34
+ logger.info(`Successfully updated: ${application.Name}`);
35
+ console.log(JSON.stringify({ "Name": application.Name, "ID": application.ID, "Version": application.Version, "MaxVMs": application.MaxVMs }));
36
+ return 0;
37
+ } catch (e) {
38
+ if (axios.isAxiosError(e) && e.response) {
39
+ logger.error(`Failed to update application ${appName}: ${e.response?.data}`);
40
+ return 1;
41
+ } else {
42
+ (e as Error).message = `Failed to update application ${appName}: ${(e as Error).message}`;
43
+ logger.error(e);
44
+ return 1;
45
+ }
46
+ }
47
+ }
package/cloudutils.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { DBOSCloudCredentials, dbosEnvPath } from "./login";
2
+ import fs from "fs";
3
+ import { spawn, StdioOptions } from 'child_process';
4
+ import { GlobalLogger } from "../../src/telemetry/logs";
5
+
6
+ export function getCloudCredentials(): DBOSCloudCredentials {
7
+ const logger = new GlobalLogger();
8
+ if (!credentialsExist()) {
9
+ logger.error("Error: not logged in")
10
+ process.exit(1)
11
+ }
12
+ const userCredentials = JSON.parse(fs.readFileSync(`./${dbosEnvPath}/credentials`).toString("utf-8")) as DBOSCloudCredentials;
13
+ return {
14
+ userName: userCredentials.userName,
15
+ token: userCredentials.token.replace(/\r|\n/g, ""), // Trim the trailing /r /n.
16
+ };
17
+ }
18
+
19
+ export function credentialsExist(): boolean {
20
+ return fs.existsSync(`./${dbosEnvPath}/credentials`);
21
+ }
22
+
23
+ // Run a command, streaming its output to stdout
24
+ export function runCommand(command: string, args: string[] = []): Promise<number> {
25
+ return new Promise((resolve, reject) => {
26
+ const stdio: StdioOptions = 'inherit';
27
+
28
+ const process = spawn(command, args, { stdio });
29
+
30
+ process.on('close', (code) => {
31
+ if (code === 0) {
32
+ resolve(code);
33
+ } else {
34
+ reject(new Error(`Command "${command}" exited with code ${code}`));
35
+ }
36
+ });
37
+
38
+ process.on('error', (error) => {
39
+ reject(error);
40
+ });
41
+ });
42
+ }
package/login.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { GlobalLogger } from "../../src/telemetry/logs";
2
+ import axios from "axios";
3
+ import { sleep } from "../../src/utils";
4
+ import jwt, { JwtPayload } from 'jsonwebtoken';
5
+ import jwksClient from 'jwks-rsa';
6
+ import { execSync } from "child_process";
7
+ import fs from "fs";
8
+
9
+ export const dbosEnvPath = ".dbos";
10
+ export const DBOSClientID = 'G38fLmVErczEo9ioCFjVIHea6yd0qMZu'
11
+ export const DBOSCloudIdentifier = 'dbos-cloud-api'
12
+
13
+ export interface DBOSCloudCredentials {
14
+ token: string;
15
+ userName: string;
16
+ }
17
+
18
+ interface DeviceCodeResponse {
19
+ device_code: string;
20
+ user_code: string;
21
+ verification_uri: string;
22
+ verification_uri_complete: string;
23
+ expires_in: number;
24
+ interval: number;
25
+ }
26
+
27
+ interface TokenResponse {
28
+ access_token: string;
29
+ token_type: string;
30
+ expires_in: number;
31
+ }
32
+
33
+ const client = jwksClient({
34
+ jwksUri: 'https://dbos-inc.us.auth0.com/.well-known/jwks.json'
35
+ });
36
+
37
+ async function getSigningKey(kid: string): Promise<string> {
38
+ const key = await client.getSigningKey(kid);
39
+ return key.getPublicKey();
40
+ }
41
+
42
+ async function verifyToken(token: string): Promise<JwtPayload> {
43
+ const decoded = jwt.decode(token, { complete: true });
44
+
45
+ if (!decoded || typeof decoded === 'string' || !decoded.header.kid) {
46
+ throw new Error('Invalid token');
47
+ }
48
+
49
+ const signingKey = await getSigningKey(decoded.header.kid);
50
+
51
+ return new Promise((resolve, reject) => {
52
+ jwt.verify(token, signingKey, { algorithms: ['RS256'] }, (err, verifiedToken) => {
53
+ if (err) {
54
+ reject(err);
55
+ } else {
56
+ resolve(verifiedToken as JwtPayload);
57
+ }
58
+ });
59
+ });
60
+ }
61
+
62
+ export async function login(username: string): Promise<number> {
63
+ const logger = new GlobalLogger();
64
+ logger.info(`Logging in!`);
65
+
66
+ const deviceCodeRequest = {
67
+ method: 'POST',
68
+ url: 'https://dbos-inc.us.auth0.com/oauth/device/code',
69
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
70
+ data: { client_id: DBOSClientID, scope: 'sub', audience: DBOSCloudIdentifier }
71
+ };
72
+ let deviceCodeResponse: DeviceCodeResponse | undefined;
73
+ try {
74
+ const response = await axios.request(deviceCodeRequest);
75
+ deviceCodeResponse = response.data as DeviceCodeResponse;
76
+ } catch (e) {
77
+ (e as Error).message = `failed to log in: ${(e as Error).message}`;
78
+ logger.error(e);
79
+ }
80
+ if (!deviceCodeResponse) {
81
+ return 1;
82
+ }
83
+ console.log(`Login URL: ${deviceCodeResponse.verification_uri_complete}`);
84
+
85
+ const tokenRequest = {
86
+ method: 'POST',
87
+ url: 'https://dbos-inc.us.auth0.com/oauth/token',
88
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
89
+ data: new URLSearchParams({
90
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
91
+ device_code: deviceCodeResponse.device_code,
92
+ client_id: DBOSClientID
93
+ })
94
+ };
95
+ let tokenResponse: TokenResponse | undefined;
96
+ let elapsedTimeSec = 0;
97
+ while (elapsedTimeSec < deviceCodeResponse.expires_in) {
98
+ try {
99
+ await sleep(deviceCodeResponse.interval * 1000)
100
+ elapsedTimeSec += deviceCodeResponse.interval;
101
+ const response = await axios.request(tokenRequest);
102
+ tokenResponse = response.data as TokenResponse;
103
+ break;
104
+ } catch (e) {
105
+ logger.info(`Waiting for login...`);
106
+ }
107
+ }
108
+ if (!tokenResponse) {
109
+ return 1;
110
+ }
111
+
112
+ await verifyToken(tokenResponse.access_token);
113
+ const credentials: DBOSCloudCredentials = {
114
+ token: tokenResponse.access_token,
115
+ userName: username,
116
+ };
117
+ execSync(`mkdir -p ${dbosEnvPath}`);
118
+ fs.writeFileSync(`${dbosEnvPath}/credentials`, JSON.stringify(credentials), "utf-8");
119
+ logger.info(`Successfully logged in as user: ${credentials.userName}`);
120
+ logger.info(`You can view your credentials in: ./${dbosEnvPath}/credentials`);
121
+ return 0;
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbos-inc/dbos-cloud",
3
- "version": "0.8.41-preview",
3
+ "version": "0.8.43-preview",
4
4
  "description": "Tool for performing application deployment to DBOS cloud",
5
5
  "license": "MIT",
6
6
  "repository": {
package/register.ts ADDED
@@ -0,0 +1,35 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "./cloudutils";
4
+
5
+ export async function registerUser(username: string, host: string): Promise<number> {
6
+ const userCredentials = getCloudCredentials();
7
+ const bearerToken = "Bearer " + userCredentials.token;
8
+ const userName = userCredentials.userName;
9
+ const logger = new GlobalLogger();
10
+ try {
11
+ // First, register the user.
12
+ const register = await axios.put(
13
+ `https://${host}/user`,
14
+ {
15
+ name: userName,
16
+ },
17
+ {
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ Authorization: bearerToken,
21
+ },
22
+ }
23
+ );
24
+ const userUUID = register.data as string;
25
+ logger.info(`Registered user ${userName}, UUID: ${userUUID}`);
26
+ } catch (e) {
27
+ if (axios.isAxiosError(e) && e.response) {
28
+ logger.error(`Failed to register user ${userName}: ${e.response.data}`);
29
+ } else {
30
+ logger.error(`Failed to register user ${userName}: ${(e as Error).message}`);
31
+ }
32
+ return 1;
33
+ }
34
+ return 0;
35
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ /* Visit https://aka.ms/tsconfig to read more about this file */
2
+ {
3
+ "compilerOptions": {
4
+ "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
5
+ "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
6
+ "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
7
+ "module": "commonjs", /* Specify what module code is generated. */
8
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
9
+ "declarationMap": true, /* Create sourcemaps for d.ts files. */
10
+ "sourceMap": true, /* Create source map files for emitted JavaScript files. */
11
+ "outDir": "./dist", /* Specify an output folder for all emitted files. */
12
+ "newLine": "lf", /* Set the newline character for emitting files. */
13
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
14
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
15
+ "strict": true, /* Enable all strict type-checking options. */
16
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
17
+ "paths": {
18
+ "../../src/telemetry/*": ["../../src/telemetry/*"],
19
+ "../../src/utils": ["../../src/utils"],
20
+ "../../src/user_database": ["../../src/user_database"],
21
+ "../../src/system_database": ["../../src/system_database"],
22
+ "../../src/dbos-runtime/*": ["../../src/dbos-runtime/*"]
23
+ }
24
+ },
25
+ "include": [ /* Specifies an array of filenames or patterns to include in the program. */
26
+ ".",
27
+ "applications"
28
+ ],
29
+ /* "references": [{"path": "../.."}]*/
30
+ }
package/userdb.ts ADDED
@@ -0,0 +1,279 @@
1
+ import axios from "axios";
2
+ import { GlobalLogger } from "../../src/telemetry/logs";
3
+ import { getCloudCredentials } from "./cloudutils";
4
+ import { readFileSync, sleep } from "../../src/utils";
5
+ import { ConfigFile, loadConfigFile, dbosConfigFilePath } from "../../src/dbos-runtime/config";
6
+ import { execSync } from "child_process";
7
+ import { UserDatabaseName } from "../../src/user_database";
8
+ import { Client, PoolConfig } from "pg";
9
+ import { ExistenceCheck } from "../../src/system_database";
10
+ import { systemDBSchema } from "../../schemas/system_db_schema";
11
+ import { createUserDBSchema, userDBSchema } from "../../schemas/user_db_schema";
12
+
13
+ export interface UserDBInstance {
14
+ readonly DBName: string;
15
+ readonly Status: string;
16
+ readonly HostName: string;
17
+ readonly Port: number;
18
+ }
19
+
20
+ export async function createUserDb(host: string, dbName: string, adminName: string, adminPassword: string, sync: boolean) {
21
+ const logger = new GlobalLogger();
22
+ const userCredentials = getCloudCredentials();
23
+ const bearerToken = "Bearer " + userCredentials.token;
24
+
25
+ try {
26
+ await axios.post(
27
+ `https://${host}/${userCredentials.userName}/databases/userdb`,
28
+ { Name: dbName, AdminName: adminName, AdminPassword: adminPassword },
29
+ {
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Authorization: bearerToken,
33
+ },
34
+ }
35
+ );
36
+
37
+ logger.info(`Successfully started creating database: ${dbName}`);
38
+
39
+ if (sync) {
40
+ let status = "";
41
+ while (status != "available") {
42
+ await sleep(30000);
43
+ const userDBInfo = await getUserDBInfo(host, dbName);
44
+ logger.info(userDBInfo);
45
+ status = userDBInfo.Status;
46
+ }
47
+ }
48
+ } catch (e) {
49
+ if (axios.isAxiosError(e) && e.response) {
50
+ logger.error(`Error creating database ${dbName}: ${e.response?.data}`);
51
+ } else {
52
+ logger.error(`Error creating database ${dbName}: ${(e as Error).message}`);
53
+ }
54
+ }
55
+ }
56
+
57
+ export async function deleteUserDb(host: string, dbName: string) {
58
+ const logger = new GlobalLogger();
59
+ const userCredentials = getCloudCredentials();
60
+ const bearerToken = "Bearer " + userCredentials.token;
61
+
62
+ try {
63
+ await axios.delete(`https://${host}/${userCredentials.userName}/databases/userdb/${dbName}`, {
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ Authorization: bearerToken,
67
+ },
68
+ });
69
+ logger.info(`Database deleted: ${dbName}`);
70
+ } catch (e) {
71
+ if (axios.isAxiosError(e) && e.response) {
72
+ logger.error(`Error deleting database ${dbName}: ${e.response?.data}`);
73
+ } else {
74
+ logger.error(`Error deleting database ${dbName}: ${(e as Error).message}`);
75
+ }
76
+ }
77
+ }
78
+
79
+ export async function getUserDb(host: string, dbName: string) {
80
+ const logger = new GlobalLogger();
81
+
82
+ try {
83
+ const userDBInfo = await getUserDBInfo(host, dbName);
84
+ logger.info(userDBInfo);
85
+ } catch (e) {
86
+ if (axios.isAxiosError(e) && e.response) {
87
+ logger.error(`Error getting database ${dbName}: ${e.response?.data}`);
88
+ } else {
89
+ logger.error(`Error getting database ${dbName}: ${(e as Error).message}`);
90
+ }
91
+ }
92
+ }
93
+
94
+ export async function migrate(): Promise<number> {
95
+ const logger = new GlobalLogger();
96
+
97
+ // Read the configuration YAML file
98
+ const configFile: ConfigFile | undefined = loadConfigFile(dbosConfigFilePath);
99
+ if (!configFile) {
100
+ logger.error(`Failed to parse ${dbosConfigFilePath}`);
101
+ return 1;
102
+ }
103
+
104
+ const userDBName = configFile.database.user_database;
105
+
106
+ logger.info(`Creating database ${userDBName} if it does not already exist`);
107
+ const createDB = `createdb -h ${configFile.database.hostname} -p ${configFile.database.port} ${userDBName} -U ${configFile.database.username} -ew ${userDBName}`;
108
+ try {
109
+ process.env.PGPASSWORD = configFile.database.password;
110
+ const createDBOutput = execSync(createDB, { env: process.env }).toString();
111
+ if (createDBOutput.includes(`database "${userDBName}" already exists`)) {
112
+ logger.info(`Database ${userDBName} already exists`);
113
+ } else {
114
+ logger.info(createDBOutput);
115
+ }
116
+ } catch (e) {
117
+ if (e instanceof Error) {
118
+ if (e.message.includes(`database "${userDBName}" already exists`)) {
119
+ logger.info(`Database ${userDBName} already exists`);
120
+ } else {
121
+ logger.error(`Error creating database: ${e.message}`);
122
+ return 1;
123
+ }
124
+ } else {
125
+ logger.error(e);
126
+ return 1;
127
+ }
128
+ }
129
+
130
+ const dbType = configFile.database.user_dbclient || UserDatabaseName.KNEX;
131
+ const migrationScript = `node_modules/.bin/${dbType}`;
132
+ const migrationCommands = configFile.database.migrate;
133
+
134
+ try {
135
+ migrationCommands?.forEach((cmd) => {
136
+ const command = `node ${migrationScript} ${cmd}`;
137
+ logger.info(`Executing migration command: ${command}`);
138
+ const migrateCommandOutput = execSync(command).toString();
139
+ logger.info(migrateCommandOutput);
140
+ });
141
+ } catch (e) {
142
+ logger.error("Error running migration");
143
+ if (e instanceof Error) {
144
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
145
+ const stderr = (e as any).stderr;
146
+ if (stderr && Buffer.isBuffer(stderr) && stderr.length > 0) {
147
+ logger.error(`Standard Error: ${stderr.toString().trim()}`);
148
+ }
149
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
150
+ const stdout = (e as any).stdout;
151
+ if (stdout && Buffer.isBuffer(stdout) && stdout.length > 0) {
152
+ logger.error(`Standard Output: ${stdout.toString().trim()}`);
153
+ }
154
+ if (e.message) {
155
+ logger.error(e.message);
156
+ }
157
+ } else {
158
+ logger.error(e);
159
+ }
160
+ return 1;
161
+ }
162
+
163
+ logger.info("Creating DBOS tables and system database.");
164
+ try {
165
+ await createDBOSTables(configFile);
166
+ } catch (e) {
167
+ if (e instanceof Error) {
168
+ logger.error(`Error creating DBOS system database: ${e.message}`);
169
+ } else {
170
+ logger.error(e);
171
+ }
172
+ return 1;
173
+ }
174
+ return 0;
175
+ }
176
+
177
+ export function rollbackMigration(): number {
178
+ const logger = new GlobalLogger();
179
+
180
+ // read the yaml file
181
+ const configFile: ConfigFile | undefined = loadConfigFile(dbosConfigFilePath);
182
+ if (!configFile) {
183
+ logger.error(`failed to parse ${dbosConfigFilePath}`);
184
+ return 1;
185
+ }
186
+
187
+ let dbType = configFile.database.user_dbclient;
188
+ if (dbType == undefined) {
189
+ dbType = "knex";
190
+ }
191
+
192
+ const rollbackcommands = configFile.database.rollback;
193
+
194
+ try {
195
+ rollbackcommands?.forEach((cmd) => {
196
+ const command = "npx " + dbType + " " + cmd;
197
+ logger.info("Executing " + command);
198
+ execSync(command);
199
+ });
200
+ } catch (e) {
201
+ logger.error("Error rolling back migration. ");
202
+ return 1;
203
+ }
204
+ return 0;
205
+ }
206
+
207
+ export async function getUserDBInfo(host: string, dbName: string): Promise<UserDBInstance> {
208
+ const userCredentials = getCloudCredentials();
209
+ const bearerToken = "Bearer " + userCredentials.token;
210
+
211
+ const res = await axios.get(`https://${host}/${userCredentials.userName}/databases/userdb/info/${dbName}`, {
212
+ headers: {
213
+ "Content-Type": "application/json",
214
+ Authorization: bearerToken,
215
+ },
216
+ });
217
+
218
+ return res.data as UserDBInstance;
219
+ }
220
+
221
+ // Create DBOS system DB and tables.
222
+ // TODO: replace this with knex to manage schema.
223
+ async function createDBOSTables(configFile: ConfigFile) {
224
+ const logger = new GlobalLogger();
225
+
226
+ const userPoolConfig: PoolConfig = {
227
+ host: configFile.database.hostname,
228
+ port: configFile.database.port,
229
+ user: configFile.database.username,
230
+ password: configFile.database.password,
231
+ connectionTimeoutMillis: configFile.database.connectionTimeoutMillis || 3000,
232
+ database: configFile.database.user_database,
233
+ };
234
+
235
+ if (configFile.database.ssl_ca) {
236
+ userPoolConfig.ssl = { ca: [readFileSync(configFile.database.ssl_ca)], rejectUnauthorized: true };
237
+ }
238
+
239
+ const systemPoolConfig = { ...userPoolConfig };
240
+ systemPoolConfig.database = `${userPoolConfig.database}_dbos_sys`;
241
+
242
+ const pgUserClient = new Client(userPoolConfig);
243
+ await pgUserClient.connect();
244
+
245
+ // Create DBOS table/schema in user DB.
246
+ const schemaExists = await pgUserClient.query<ExistenceCheck>(`SELECT EXISTS (SELECT FROM information_schema.schemata WHERE schema_name = 'dbos')`);
247
+ if (!schemaExists.rows[0].exists) {
248
+ await pgUserClient.query(createUserDBSchema);
249
+ await pgUserClient.query(userDBSchema);
250
+ }
251
+
252
+ // Create the DBOS system database.
253
+ const dbExists = await pgUserClient.query<ExistenceCheck>(`SELECT EXISTS (SELECT FROM pg_database WHERE datname = '${systemPoolConfig.database}')`);
254
+ if (!dbExists.rows[0].exists) {
255
+ await pgUserClient.query(`CREATE DATABASE ${systemPoolConfig.database}`);
256
+ }
257
+
258
+ // Load the DBOS system schema.
259
+ const pgSystemClient = new Client(systemPoolConfig);
260
+ await pgSystemClient.connect();
261
+
262
+ try {
263
+ const tableExists = await pgSystemClient.query<ExistenceCheck>(`SELECT EXISTS (SELECT FROM information_schema.schemata WHERE schema_name = 'dbos')`);
264
+ if (!tableExists.rows[0].exists) {
265
+ await pgSystemClient.query(systemDBSchema);
266
+ }
267
+ } catch (e) {
268
+ const tableExists = await pgSystemClient.query<ExistenceCheck>(`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'dbos' AND table_name = 'operation_outputs')`);
269
+ if (tableExists.rows[0].exists) {
270
+ // If the table has been created by someone else. Ignore the error.
271
+ logger.warn(`System tables creation failed, may conflict with concurrent tasks: ${(e as Error).message}`);
272
+ } else {
273
+ throw e;
274
+ }
275
+ } finally {
276
+ await pgSystemClient.end();
277
+ await pgUserClient.end();
278
+ }
279
+ }