@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 +9 -0
- package/applications/delete-app.ts +36 -0
- package/applications/deploy-app-code.ts +159 -0
- package/applications/get-app-logs.ts +36 -0
- package/applications/index.ts +7 -0
- package/applications/list-apps.ts +41 -0
- package/applications/register-app.ts +45 -0
- package/applications/types.ts +8 -0
- package/applications/update-app.ts +47 -0
- package/cloudutils.ts +42 -0
- package/login.ts +122 -0
- package/package.json +1 -1
- package/register.ts +35 -0
- package/tsconfig.json +30 -0
- package/userdb.ts +279 -0
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,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
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
|
+
}
|