Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/SDK/SDK.php
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ public function generate(string $target): void
'webAuth' => $this->hasWebAuth($methods),
],
'methods' => $methods,
'isConsoleOnly' => $this->isConsoleOnly($methods),
];

if ($this->exclude($file, $params)) {
Expand Down Expand Up @@ -1014,6 +1015,7 @@ public function generate(string $target): void
'location' => $this->hasLocation($methods),
'webAuth' => $this->hasWebAuth($methods),
],
'isConsoleOnly' => $this->isConsoleOnly($methods),
];

foreach ($methods as $method) {
Expand Down Expand Up @@ -1273,6 +1275,22 @@ protected function hasWebAuth(array $methods): bool
return false;
}

protected function isConsoleOnly(array $methods): bool
{
if (empty($methods)) {
return false;
}

foreach ($methods as $method) {
$platforms = $method['platforms'] ?? [];
if ($platforms !== ['console']) {
return false;
}
}

return true;
}

/**
* @param TemplateWrapper $template
* @param string $destination
Expand Down
1 change: 1 addition & 0 deletions src/Spec/Swagger2.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ protected function parseMethod(string $methodName, string $pathName, array $meth
'security' => [$methodSecurity] ?? [],
'consumes' => $method['consumes'] ?? [],
'cookies' => $method['x-appwrite']['cookies'] ?? false,
'platforms' => $method['x-appwrite']['platforms'] ?? [],
'type' => $method['x-appwrite']['type'] ?? false,
'deprecated' => $method['deprecated'] ?? false,
'headers' => [],
Expand Down
4 changes: 4 additions & 0 deletions templates/cli/cli.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) {
.version(version, '-v, --version', 'Output the version number')
.option('-V, --verbose', 'Show complete error log')
.option('-j, --json', 'Output in JSON format')
.option('-R, --raw', 'Output full raw JSON (no filtering)')
.hook('preAction', migrate)
.option('-f,--force', 'Flag to confirm all warnings')
.option('-a,--all', 'Flag to push all resources')
Expand All @@ -92,6 +93,9 @@ if (process.argv.includes('-v') || process.argv.includes('--version')) {
.on('option:json', () => {
cliConfig.json = true;
})
.on('option:raw', () => {
cliConfig.raw = true;
})
.on('option:verbose', () => {
cliConfig.verbose = true;
})
Expand Down
21 changes: 20 additions & 1 deletion templates/cli/lib/commands/services/services.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@ import fs from "fs";
{% if hasFileParam %}
import { resolveFileParam } from "../utils/deployment.js";
{% endif %}
{% if service.isConsoleOnly %}
import { sdkForConsole } from "../../sdks.js";
{% else %}
import { sdkForProject } from "../../sdks.js";
{% endif %}
import {
actionRunner,
commandDescriptions,
success,
parse,
parseBool,
parseInteger,
{% if service.name == 'teams' %}
hint,
{% endif %}
} from "../../parser.js";
{% if sdk.test %}
{{ include('cli/base/mock.twig') }}
Expand All @@ -40,7 +47,19 @@ let {{ service.name | caseCamel }}Client: {{ service.name | caseUcfirst }} | nul

const get{{ service.name | caseUcfirst }}Client = async (): Promise<{{ service.name | caseUcfirst }}> => {
if (!{{ service.name | caseCamel }}Client) {
const sdkClient = await sdkForProject();
{% if service.name == 'teams' %}
let sdkClient;
try {
sdkClient = await sdkForProject();
} catch (e) {
if (e instanceof Error && e.message.includes("Project is not set")) {
hint(`To manage console-level teams, use the 'organizations' command instead.`);
}
throw e;
}
{% else %}
const sdkClient = await {{ service.isConsoleOnly ? 'sdkForConsole' : 'sdkForProject' }}();
{% endif %}
{{ service.name | caseCamel }}Client = new {{ service.name | caseUcfirst }}(sdkClient);
}
return {{ service.name | caseCamel }}Client;
Expand Down
176 changes: 149 additions & 27 deletions templates/cli/lib/parser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// @ts-expect-error BigInt toJSON polyfill for JSON.stringify support
BigInt.prototype.toJSON = function () { return this.toString(); };

import chalk from "chalk";
import { InvalidArgumentError } from "commander";
import Table from "cli-table3";
Expand All @@ -18,6 +21,7 @@ import {
const cliConfig: CliConfig = {
verbose: false,
json: false,
raw: false,
force: false,
all: false,
ids: [],
Expand Down Expand Up @@ -54,36 +58,118 @@ const extractReportCommandArgs = (value: unknown): string[] => {
return reportData.data.args;
};

const filterObject = (obj: JsonObject): JsonObject => {
const result: JsonObject = {};
for (const key of Object.keys(obj)) {
const value = obj[key];
if (typeof value === "function") continue;
if (value == null) continue;
if (value?.constructor?.name === "BigNumber") {
result[key] = String(value);
continue;
}
if (typeof value === "object") continue;
if (typeof value === "string" && value.trim() === "") continue;
result[key] = value;
}
return result;
};

const filterData = (data: JsonObject): JsonObject => {
const result: JsonObject = {};
for (const key of Object.keys(data)) {
const value = data[key];
if (typeof value === "function") continue;
if (value == null) continue;
if (value?.constructor?.name === "BigNumber") {
result[key] = String(value);
continue;
}
if (Array.isArray(value)) {
result[key] = value.map((item) => {
if (item?.constructor?.name === "BigNumber") return String(item);
return item && typeof item === "object" && !Array.isArray(item)
? filterObject(item as JsonObject)
: item;
});
} else if (typeof value === "object") {
continue;
} else if (typeof value === "string" && value.trim() === "") {
continue;
} else {
result[key] = value;
}
}
return result;
};

export const parse = (data: JsonObject): void => {
if (cliConfig.json) {
if (cliConfig.raw) {
drawJSON(data);
return;
}

for (const key in data) {
if (data[key] === null) {
if (cliConfig.json) {
drawJSON(filterData(data));
return;
}

const keys = Object.keys(data).filter((k) => typeof data[k] !== "function");
let printedScalar = false;

for (const key of keys) {
const value = data[key];
if (value === null) {
console.log(`${chalk.yellow.bold(key)} : null`);
} else if (Array.isArray(data[key])) {
printedScalar = true;
} else if (Array.isArray(value)) {
if (printedScalar) console.log("");
console.log(`${chalk.yellow.bold.underline(key)}`);
if (typeof data[key][0] === "object") {
drawTable(data[key]);
if (typeof value[0] === "object") {
drawTable(value);
} else {
drawJSON(data[key]);
drawJSON(value);
}
} else if (typeof data[key] === "object") {
if (data[key]?.constructor?.name === "BigNumber") {
console.log(`${chalk.yellow.bold(key)} : ${data[key]}`);
printedScalar = false;
} else if (typeof value === "object") {
if (printedScalar) console.log("");
if (value?.constructor?.name === "BigNumber") {
console.log(`${chalk.yellow.bold(key)} : ${value}`);
printedScalar = true;
} else {
console.log(`${chalk.yellow.bold.underline(key)}`);
const tableRow = toJsonObject(data[key]) ?? {};
const tableRow = toJsonObject(value) ?? {};
drawTable([tableRow]);
printedScalar = false;
}
} else {
console.log(`${chalk.yellow.bold(key)} : ${data[key]}`);
console.log(`${chalk.yellow.bold(key)} : ${value}`);
printedScalar = true;
}
}
};

const MAX_COL_WIDTH = 40;

const formatCellValue = (value: unknown): string => {
if (value == null) return "-";
if (Array.isArray(value)) {
if (value.length === 0) return "[]";
return `[${value.length} items]`;
}
if (typeof value === "object") {
if (value?.constructor?.name === "BigNumber") return String(value);
const keys = Object.keys(value as Record<string, unknown>);
if (keys.length === 0) return "{}";
return `{${keys.length} keys}`;
}
const str = String(value);
if (str.length > MAX_COL_WIDTH) {
return str.slice(0, MAX_COL_WIDTH - 1) + "…";
}
return str;
};

export const drawTable = (data: Array<JsonObject | null | undefined>): void => {
if (data.length == 0) {
console.log("[]");
Expand All @@ -95,23 +181,67 @@ export const drawTable = (data: Array<JsonObject | null | undefined>): void => {
// Create an object with all the keys in it
const obj = rows.reduce((res, item) => ({ ...res, ...item }), {});
// Get those keys as an array
const keys = Object.keys(obj);
if (keys.length === 0) {
const allKeys = Object.keys(obj);
if (allKeys.length === 0) {
drawJSON(data);
return;
}

// If too many columns, show condensed key-value output with only scalar, non-empty fields
const maxColumns = 6;
if (allKeys.length > maxColumns) {
// Collect visible entries per row to compute alignment
const rowEntries = rows.map((row) => {
const entries: Array<[string, string]> = [];
for (const key of Object.keys(row)) {
const value = row[key];
if (typeof value === "function") continue;
if (value == null) continue;
if (value?.constructor?.name === "BigNumber") {
entries.push([key, String(value)]);
continue;
}
if (typeof value === "object") continue;
if (typeof value === "string" && value.trim() === "") continue;
entries.push([key, String(value)]);
}
return entries;
});

const flatEntries = rowEntries.flat();
if (flatEntries.length === 0) {
drawJSON(data);
return;
}

const maxKeyLen = Math.max(...flatEntries.map(([key]) => key.length));

const separatorLen = Math.min(maxKeyLen + 2 + MAX_COL_WIDTH, process.stdout.columns || 80);

rowEntries.forEach((entries, idx) => {
if (idx > 0) console.log(chalk.cyan("─".repeat(separatorLen)));
for (const [key, value] of entries) {
const paddedKey = key.padEnd(maxKeyLen);
console.log(`${chalk.yellow.bold(paddedKey)} ${value}`);
}
});
return;
}

const columns = allKeys;

// Create an object with all keys set to the default value ''
const def = keys.reduce((result: Record<string, string>, key) => {
const def = allKeys.reduce((result: Record<string, string>, key) => {
result[key] = "-";
return result;
}, {});
// Use object destructuring to replace all default values with the ones we have
const normalizedData = rows.map((item) => ({ ...def, ...item }));

const columns = Object.keys(normalizedData[0]);

const table = new Table({
head: columns.map((c) => chalk.cyan.italic.bold(c)),
colWidths: columns.map(() => null) as (number | null)[],
wordWrap: false,
chars: {
top: " ",
"top-mid": " ",
Expand All @@ -134,15 +264,7 @@ export const drawTable = (data: Array<JsonObject | null | undefined>): void => {
normalizedData.forEach((row) => {
const rowValues: string[] = [];
for (const key of columns) {
if (row[key] == null) {
rowValues.push("-");
} else if (Array.isArray(row[key])) {
rowValues.push(JSON.stringify(row[key]));
} else if (typeof row[key] === "object") {
rowValues.push(JSON.stringify(row[key]));
} else {
rowValues.push(String(row[key]));
}
rowValues.push(formatCellValue(row[key]));
}
table.push(rowValues);
});
Expand Down Expand Up @@ -291,7 +413,7 @@ export const commandDescriptions: Record<string, string> = {
locale: `The locale command allows you to customize your app based on your users' location.`,
sites: `The sites command allows you to view, create and manage your Appwrite Sites.`,
storage: `The storage command allows you to manage your project files.`,
teams: `The teams command allows you to group users of your project to enable them to share read and write access to your project resources.`,
teams: `The teams command allows you to group users of your project to enable them to share read and write access to your project resources. Requires a linked project. To manage console-level teams, use the 'organizations' command instead.`,
update: `The update command allows you to update the ${SDK_TITLE} CLI to the latest version.`,
users: `The users command allows you to manage your project users.`,
projects: `The projects command allows you to manage your projects, add platforms, manage API keys, Dev Keys etc.`,
Expand Down
4 changes: 2 additions & 2 deletions templates/cli/lib/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,14 @@ export const questionsInitProject: Question[] = [
name: "project",
message: "What would you like to name your project?",
default: "My Awesome Project",
when: (answer: Answers) => answer.start !== "existing",
when: (answer: Answers) => whenOverride(answer) && answer.start !== "existing",
},
{
type: "input",
name: "id",
message: "What ID would you like to have for your project?",
default: "unique()",
when: (answer: Answers) => answer.start !== "existing",
when: (answer: Answers) => whenOverride(answer) && answer.start !== "existing",
},
{
type: "search-list",
Expand Down
1 change: 1 addition & 0 deletions templates/cli/lib/sdks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ export const sdkForProject = async (): Promise<Client> => {
`Session not found. Please run \`${EXECUTABLE_NAME} login\` to create a session.`,
);
};

1 change: 1 addition & 0 deletions templates/cli/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface CommandDescription {
export interface CliConfig {
verbose: boolean;
json: boolean;
raw: boolean;
force: boolean;
all: boolean;
ids: string[];
Expand Down
Loading