@mittwald/cli 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
+ import { ListColumns } from "../../rendering/formatter/Table.js";
3
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
4
+ type ResponseItem = {
5
+ id: string;
6
+ kind: "mysql" | "redis";
7
+ name: string;
8
+ version: string;
9
+ description: string;
10
+ hostname: string;
11
+ isReady: boolean;
12
+ createdAt: string;
13
+ };
14
+ type Response = Awaited<ReturnType<MittwaldAPIV2Client["database"]["listMysqlDatabases"]>>;
15
+ export declare class List extends ListBaseCommand<typeof List, ResponseItem, Response> {
16
+ static description: string;
17
+ static args: {};
18
+ static flags: {
19
+ "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
20
+ output: import("@oclif/core/interfaces").OptionFlag<"json" | "txt" | "yaml" | "csv" | "tsv">;
21
+ extended: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ "no-header": import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ "no-truncate": import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ "no-relative-dates": import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ "csv-separator": import("@oclif/core/interfaces").OptionFlag<"," | ";">;
26
+ };
27
+ getData(): Promise<ResponseItem[]>;
28
+ protected getColumns(ignoredData: ResponseItem[]): ListColumns<ResponseItem>;
29
+ }
30
+ export {};
@@ -0,0 +1,68 @@
1
+ import { assertStatus } from "@mittwald/api-client-commons";
2
+ import { ListBaseCommand } from "../../lib/basecommands/ListBaseCommand.js";
3
+ import { projectFlags } from "../../lib/resources/project/flags.js";
4
+ export class List extends ListBaseCommand {
5
+ static description = "List all kinds of databases belonging to a project.";
6
+ static args = {};
7
+ static flags = {
8
+ ...ListBaseCommand.baseFlags,
9
+ ...projectFlags,
10
+ };
11
+ async getData() {
12
+ const projectId = await this.withProjectId(List);
13
+ const databases = [];
14
+ const mysqlResponse = await this.apiClient.database.listMysqlDatabases({
15
+ projectId,
16
+ });
17
+ assertStatus(mysqlResponse, 200);
18
+ const redisResponse = await this.apiClient.database.listRedisDatabases({
19
+ projectId,
20
+ });
21
+ assertStatus(redisResponse, 200);
22
+ databases.push(...mysqlResponse.data.map((d) => ({ ...d, kind: "mysql" })), ...redisResponse.data.map((d) => ({
23
+ ...d,
24
+ kind: "redis",
25
+ isReady: true,
26
+ })));
27
+ return databases;
28
+ }
29
+ getColumns(ignoredData) {
30
+ const { id, name, createdAt } = super.getColumns(ignoredData, {
31
+ shortIdKey: "name",
32
+ });
33
+ return {
34
+ id,
35
+ name,
36
+ version: {
37
+ header: "Version",
38
+ get(row) {
39
+ if (row.kind === "mysql") {
40
+ return `MySQL ${row.version}`;
41
+ }
42
+ else if (row.kind === "redis") {
43
+ return `Redis ${row.version}`;
44
+ }
45
+ else {
46
+ return "Unknown";
47
+ }
48
+ },
49
+ },
50
+ description: {
51
+ header: "Description",
52
+ },
53
+ hostname: {
54
+ header: "Hostname",
55
+ },
56
+ status: {
57
+ header: "Status",
58
+ get: (row) => {
59
+ if (!row.isReady) {
60
+ return "pending";
61
+ }
62
+ return "ready";
63
+ },
64
+ },
65
+ createdAt,
66
+ };
67
+ }
68
+ }
@@ -10,6 +10,7 @@ export declare class Dump extends ExecRenderBaseCommand<typeof Dump, Record<stri
10
10
  "ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
11
  "temporary-user": import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  "mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ "mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  };
15
16
  static args: {
@@ -68,5 +68,14 @@ function DumpSuccess({ database, output, }) {
68
68
  return (_jsxs(Success, { children: ["Dump of MySQL database ", _jsx(Value, { children: database }), " written to", " ", _jsx(Value, { children: output })] }));
69
69
  }
70
70
  function buildMySqlDumpArgs(d) {
71
- return ["-h", d.hostname, "-u", d.user, `-p${d.password}`, d.database];
71
+ return [
72
+ "-h",
73
+ d.hostname,
74
+ "-u",
75
+ d.user,
76
+ `-p${d.password}`,
77
+ "--default-character-set",
78
+ d.charset,
79
+ d.database,
80
+ ];
72
81
  }
@@ -10,6 +10,7 @@ export declare class Import extends ExecRenderBaseCommand<typeof Import, Record<
10
10
  "ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
11
  "temporary-user": import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  "mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ "mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  };
15
16
  static args: {
@@ -1,6 +1,6 @@
1
1
  import { Simplify } from "@mittwald/api-client-commons";
2
2
  import { MittwaldAPIV2, MittwaldAPIV2Client } from "@mittwald/api-client";
3
- import { ListColumns } from "../../../rendering/formatter/ListFormatter.js";
3
+ import { ListColumns } from "../../../rendering/formatter/Table.js";
4
4
  import { ListBaseCommand } from "../../../lib/basecommands/ListBaseCommand.js";
5
5
  type ResponseItem = Simplify<MittwaldAPIV2.Paths.V2ProjectsProjectIdMysqlDatabases.Get.Responses.$200.Content.ApplicationJson[number]>;
6
6
  type Response = Awaited<ReturnType<MittwaldAPIV2Client["database"]["listMysqlDatabases"]>>;
@@ -26,7 +26,7 @@ export class PortForward extends ExecRenderBaseCommand {
26
26
  async exec() {
27
27
  const databaseId = await withMySQLId(this.apiClient, this.flags, this.args);
28
28
  const p = makeProcessRenderer(this.flags, "Port-forwarding a MySQL database");
29
- const { sshUser, sshHost, hostname, database } = await getConnectionDetails(this.apiClient, databaseId, this.flags["ssh-user"], p);
29
+ const { sshUser, sshHost, hostname, database } = await getConnectionDetails(this.apiClient, databaseId, this.flags["ssh-user"], undefined, p);
30
30
  const { port } = this.flags;
31
31
  p.complete(_jsxs(Text, { children: ["Forwarding MySQL database ", _jsx(Value, { children: database }), " to local port", " ", _jsx(Value, { children: port }), ". Use CTRL+C to cancel."] }));
32
32
  const sshArgs = buildSSHClientFlags(sshUser, sshHost, this.flags, {
@@ -5,6 +5,7 @@ export declare class Shell extends ExecRenderBaseCommand<typeof Shell, Record<st
5
5
  static description: string;
6
6
  static flags: {
7
7
  "mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ "mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
9
  "ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
10
  "ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
11
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -24,7 +24,7 @@ export declare abstract class ListBaseCommand<T extends typeof BaseCommand, TIte
24
24
  protected sorter?: SorterFunction<TItem>;
25
25
  init(): Promise<void>;
26
26
  run(): Promise<void>;
27
- protected abstract getData(): Promise<TAPIResponse>;
27
+ protected abstract getData(): Promise<TAPIResponse | TItem[]>;
28
28
  protected mapData(data: SuccessfulResponse<TAPIResponse, 200>["data"]): TItem[] | Promise<TItem[]>;
29
29
  protected getColumns(data: TItem[], opts?: ColumnOpts<TItem>): ListColumns<TItem>;
30
30
  }
@@ -21,8 +21,14 @@ export class ListBaseCommand extends ExtendedBaseCommand {
21
21
  }
22
22
  async run() {
23
23
  const response = await this.getData();
24
- assertStatus(response, 200);
25
- const data = await this.mapData(response.data);
24
+ let data;
25
+ if (!Array.isArray(response)) {
26
+ assertStatus(response, 200);
27
+ data = await this.mapData(response.data);
28
+ }
29
+ else {
30
+ data = response;
31
+ }
26
32
  this.formatter.log(data, this.getColumns(data), this.flags);
27
33
  }
28
34
  mapData(data) {
@@ -159,7 +159,7 @@ export function provideSupportedFlags(requestedFlagNames, appName) {
159
159
  return flagsToReturn;
160
160
  }
161
161
  export async function autofillFlags(apiClient, process, necessaryFlags, flags, projectId, appName, defaults) {
162
- const ownUser = await apiClient.user.getOwnAccount();
162
+ const ownUser = await apiClient.user.getUser({ userId: "self" });
163
163
  assertStatus(ownUser, 200);
164
164
  // Version
165
165
  if (necessaryFlags.includes("version") && !flags.version) {
@@ -1,7 +1,7 @@
1
1
  import { assertStatus } from "@mittwald/api-client-commons";
2
2
  export async function getUser(apiClient, p) {
3
3
  return await p.runStep("fetching user", async () => {
4
- const r = await apiClient.user.getOwnAccount();
4
+ const r = await apiClient.user.getUser({ userId: "self" });
5
5
  assertStatus(r, 200);
6
6
  return r.data;
7
7
  });
@@ -4,6 +4,7 @@ import { MittwaldAPIV2Client } from "@mittwald/api-client";
4
4
  type Project = MittwaldAPIV2.Components.Schemas.ProjectProject;
5
5
  export interface MySQLConnectionFlags {
6
6
  "mysql-password": string | undefined;
7
+ "mysql-charset": string | undefined;
7
8
  "temporary-user"?: boolean;
8
9
  "ssh-user"?: string;
9
10
  }
@@ -14,6 +15,7 @@ export interface MySQLConnectionDetails {
14
15
  sshHost: string;
15
16
  sshUser: string;
16
17
  project: Project;
18
+ charset: string;
17
19
  }
18
20
  export type MySQLConnectionDetailsWithPassword = MySQLConnectionDetails & {
19
21
  password: string;
@@ -27,5 +29,5 @@ export type MySQLConnectionDetailsWithPassword = MySQLConnectionDetails & {
27
29
  */
28
30
  export declare function runWithConnectionDetails<TRes>(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: MySQLConnectionFlags, cb: (connectionDetails: MySQLConnectionDetailsWithPassword) => Promise<TRes>): Promise<TRes>;
29
31
  export declare function getConnectionDetailsWithPassword(apiClient: MittwaldAPIV2Client, databaseId: string, p: ProcessRenderer, flags: MySQLConnectionFlags): Promise<MySQLConnectionDetailsWithPassword>;
30
- export declare function getConnectionDetails(apiClient: MittwaldAPIV2Client, databaseId: string, sshUser: string | undefined, p: ProcessRenderer): Promise<MySQLConnectionDetails>;
32
+ export declare function getConnectionDetails(apiClient: MittwaldAPIV2Client, databaseId: string, sshUser: string | undefined, characterSet: string | undefined, p: ProcessRenderer): Promise<MySQLConnectionDetails>;
31
33
  export {};
@@ -19,12 +19,13 @@ export async function runWithConnectionDetails(apiClient, databaseId, p, flags,
19
19
  export async function getConnectionDetailsWithPassword(apiClient, databaseId, p, flags) {
20
20
  const password = flags["temporary-user"] ? "" : await getPassword(p, flags);
21
21
  const sshUser = flags["ssh-user"];
22
+ const characterSet = flags["mysql-charset"];
22
23
  return {
23
- ...(await getConnectionDetails(apiClient, databaseId, sshUser, p)),
24
+ ...(await getConnectionDetails(apiClient, databaseId, sshUser, characterSet, p)),
24
25
  password,
25
26
  };
26
27
  }
27
- export async function getConnectionDetails(apiClient, databaseId, sshUser, p) {
28
+ export async function getConnectionDetails(apiClient, databaseId, sshUser, characterSet, p) {
28
29
  const database = await getDatabase(apiClient, p, databaseId);
29
30
  const databaseUser = await getDatabaseUser(apiClient, p, databaseId);
30
31
  const project = await getProject(apiClient, p, database);
@@ -35,6 +36,7 @@ export async function getConnectionDetails(apiClient, databaseId, sshUser, p) {
35
36
  user: databaseUser.name,
36
37
  sshHost: sshConnectionData.host,
37
38
  sshUser: sshConnectionData.user,
39
+ charset: characterSet ?? database.characterSettings.characterSet,
38
40
  project,
39
41
  };
40
42
  }
@@ -1,10 +1,12 @@
1
1
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
2
  export declare const mysqlConnectionFlags: {
3
3
  "mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
4
+ "mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
4
5
  };
5
6
  export declare const mysqlConnectionFlagsWithTempUser: {
6
7
  "temporary-user": import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  "mysql-password": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ "mysql-charset": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
10
  };
9
11
  export declare const mysqlArgs: {
10
12
  "database-id": import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
@@ -12,6 +12,10 @@ NOTE: This is a security risk, as the password will be visible in the process li
12
12
  required: false,
13
13
  env: "MYSQL_PWD",
14
14
  }),
15
+ "mysql-charset": Flags.string({
16
+ summary: "the character set to use for the MySQL connection",
17
+ description: "The character set that should be used for the MySQL connection. If omitted, the database's default character set will be used (for newer databases, this should be utf8mb4 in most cases, but really might be anything).",
18
+ }),
15
19
  };
16
20
  export const mysqlConnectionFlagsWithTempUser = {
17
21
  ...mysqlConnectionFlags,
@@ -36,7 +36,7 @@ export function generateRandomPassword(length = 32) {
36
36
  const lowercase = "abcdefghijklmnopqrstuvwxyz";
37
37
  const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
38
38
  const digits = "0123456789";
39
- const specialChars = "#!~%^*_+-=?{}()<>|.,;";
39
+ const specialChars = "_";
40
40
  const allChars = lowercase + uppercase + digits + specialChars;
41
41
  // Ensure the password includes at least one of each required character type
42
42
  const passwordArray = [
@@ -1,11 +1,24 @@
1
1
  import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
2
  export default function useOwnAccount(client: MittwaldAPIV2Client): {
3
+ avatarRef?: string | undefined;
4
+ customerMemberships?: {
5
+ [k: string]: import("@mittwald/api-client").MittwaldAPIV2.Components.Schemas.UserCustomerMembership;
6
+ } | undefined;
3
7
  email?: string | undefined;
4
- mfaDetails?: {
5
- mfaConfirmed?: boolean;
6
- mfaInitialized?: boolean;
8
+ employeeInformation?: {
9
+ department: string;
10
+ } | undefined;
11
+ isEmployee?: boolean | undefined;
12
+ mfa?: {
13
+ active: boolean;
14
+ setup: boolean;
7
15
  } | undefined;
8
16
  passwordUpdatedAt?: string | undefined;
9
- person?: import("@mittwald/api-client").MittwaldAPIV2.Components.Schemas.CommonsPerson | undefined;
10
- userId?: string | undefined;
17
+ person: import("@mittwald/api-client").MittwaldAPIV2.Components.Schemas.CommonsPerson;
18
+ phoneNumber?: string | undefined;
19
+ projectMemberships?: {
20
+ [k: string]: import("@mittwald/api-client").MittwaldAPIV2.Components.Schemas.UserProjectMembership;
21
+ } | undefined;
22
+ registeredAt?: string | undefined;
23
+ userId: string;
11
24
  };
@@ -1,7 +1,7 @@
1
1
  import { assertStatus } from "@mittwald/api-client";
2
2
  import { usePromise } from "@mittwald/react-use-promise";
3
3
  export default function useOwnAccount(client) {
4
- const result = usePromise(() => client.user.getOwnAccount(), []);
4
+ const result = usePromise(() => client.user.getUser({ userId: "self" }), []);
5
5
  assertStatus(result, 200);
6
6
  return result.data;
7
7
  }
@@ -13,7 +13,7 @@ export async function getSSHConnectionForAppInstallation(client, appInstallation
13
13
  });
14
14
  assertStatus(projectResponse, 200);
15
15
  if (sshUser === undefined) {
16
- const userResponse = await client.user.getOwnAccount();
16
+ const userResponse = await client.user.getUser({ userId: "self" });
17
17
  assertStatus(userResponse, 200);
18
18
  sshUser = userResponse.data.email;
19
19
  }
@@ -3,7 +3,7 @@ export async function getSSHConnectionForProject(client, projectId, sshUser) {
3
3
  const projectResponse = await client.project.getProject({ projectId });
4
4
  assertStatus(projectResponse, 200);
5
5
  if (sshUser === undefined) {
6
- const userResponse = await client.user.getOwnAccount();
6
+ const userResponse = await client.user.getUser({ userId: "self" });
7
7
  assertStatus(userResponse, 200);
8
8
  sshUser = userResponse.data.email;
9
9
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import Duration from "./Duration.js";
3
+ describe("Duration class", () => {
4
+ describe("Static factory methods", () => {
5
+ test("fromZero should return a Duration of 0 milliseconds", () => {
6
+ const duration = Duration.fromZero();
7
+ expect(duration.milliseconds).toBe(0);
8
+ });
9
+ test("fromMilliseconds should return a Duration with the given milliseconds", () => {
10
+ const duration = Duration.fromMilliseconds(500);
11
+ expect(duration.milliseconds).toBe(500);
12
+ });
13
+ test("fromSeconds should return a Duration with the given seconds converted to milliseconds", () => {
14
+ const duration = Duration.fromSeconds(2);
15
+ expect(duration.milliseconds).toBe(2000);
16
+ });
17
+ test("fromString should parse valid duration strings", () => {
18
+ const duration = Duration.fromString("2s");
19
+ expect(duration.milliseconds).toBe(2000);
20
+ });
21
+ test("fromString should throw an error for invalid input", () => {
22
+ expect(() => Duration.fromString("invalid")).toThrow("could not parse duration: invalid");
23
+ });
24
+ });
25
+ describe("Instance methods", () => {
26
+ test("seconds should return duration in seconds", () => {
27
+ const duration = Duration.fromMilliseconds(3000);
28
+ expect(duration.seconds).toBe(3);
29
+ });
30
+ test("from should return a Date object offset by the duration", () => {
31
+ const baseDate = new Date("2023-01-01T00:00:00Z");
32
+ const duration = Duration.fromSeconds(10);
33
+ expect(duration.from(baseDate)).toEqual(new Date("2023-01-01T00:00:10Z"));
34
+ });
35
+ test("fromNow should return a future Date object", () => {
36
+ const duration = Duration.fromSeconds(5);
37
+ const now = new Date();
38
+ const future = duration.fromNow();
39
+ expect(future.getTime()).toBeGreaterThan(now.getTime());
40
+ });
41
+ test("add should correctly add two durations", () => {
42
+ const duration1 = Duration.fromSeconds(10);
43
+ const duration2 = Duration.fromSeconds(5);
44
+ const result = duration1.add(duration2);
45
+ expect(result.milliseconds).toBe(15000);
46
+ });
47
+ test("compare should return correct comparison results", () => {
48
+ const duration1 = Duration.fromSeconds(10);
49
+ const duration2 = Duration.fromSeconds(5);
50
+ expect(duration1.compare(duration2)).toBeGreaterThan(0);
51
+ expect(duration2.compare(duration1)).toBeLessThan(0);
52
+ expect(duration1.compare(duration1)).toBe(0);
53
+ });
54
+ test("toString should return formatted duration string", () => {
55
+ expect(Duration.fromMilliseconds(500).toString()).toBe("500ms");
56
+ expect(Duration.fromSeconds(3).toString()).toBe("3s");
57
+ });
58
+ });
59
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -59,7 +59,7 @@
59
59
  "marked": "^12.0.0",
60
60
  "marked-terminal": "^6.0.0",
61
61
  "open": "^10.0.3",
62
- "parse-duration": "^1.1.0",
62
+ "parse-duration": "^2.0.1",
63
63
  "pretty-bytes": "^6.1.0",
64
64
  "react": "^18.2.0",
65
65
  "semver": "^7.5.4",