diff --git a/.github/workflows/sdk-build-validation.yml b/.github/workflows/sdk-build-validation.yml index 8b68951c5..20aad56f8 100644 --- a/.github/workflows/sdk-build-validation.yml +++ b/.github/workflows/sdk-build-validation.yml @@ -150,7 +150,7 @@ jobs: ;; cli) npm install - npm run linux-x64 || echo "Build completed with TypeScript warnings/errors, but binaries were generated" + npm run linux-x64 ;; react-native) npm install diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5a95e2f5c..47c2f894a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,8 +16,8 @@ jobs: sdk: [ Android5Java17, Android14Java17, - CLINode16, CLINode18, + CLINode20, DartBeta, DartStable, DotNet60, diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 38ae3eb65..2f9c5be83 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -296,6 +296,11 @@ public function getFiles(): array 'destination' => 'lib/types.ts', 'template' => 'cli/lib/types.ts.twig', ], + [ + 'scope' => 'enum', + 'destination' => 'lib/enums/{{ enum.name | caseKebab }}.ts', + 'template' => 'cli/lib/enums/enum.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/commands/init.ts', @@ -384,7 +389,7 @@ public function getTypeName(array $parameter, array $spec = []): string self::TYPE_STRING => 'string', self::TYPE_FILE => 'string', self::TYPE_BOOLEAN => 'boolean', - self::TYPE_OBJECT => 'object', + self::TYPE_OBJECT => 'string', self::TYPE_ARRAY => (!empty(($parameter['array'] ?? [])['type']) && !\is_array($parameter['array']['type'])) ? $this->getTypeName($parameter['array']) . '[]' : 'any[]', diff --git a/templates/cli/base/params.twig b/templates/cli/base/params.twig index 9d9dfcc15..affeea455 100644 --- a/templates/cli/base/params.twig +++ b/templates/cli/base/params.twig @@ -53,10 +53,13 @@ const nodeStream = fs.createReadStream(filePath); const stream = convertReadStreamToReadableStream(nodeStream); - if (typeof filePath !== 'undefined') { - {{ parameter.name | caseCamel | escapeKeyword }} = { type: 'file', stream, filename: pathLib.basename(filePath), size: fs.statSync(filePath).size }; - payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }} - } + const {{ parameter.name | caseCamel | escapeKeyword }}Upload: FileInput = { + type: 'file', + stream, + filename: pathLib.basename(filePath), + size: fs.statSync(filePath).size + }; + payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}Upload; {% elseif parameter.type == 'boolean' %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; @@ -74,7 +77,7 @@ payload['{{ parameter.name }}'] = JSON.parse({{ parameter.name | caseCamel | escapeKeyword}}); } {% elseif parameter.type == 'array' %} - {{ parameter.name | caseCamel | escapeKeyword}} = {{ parameter.name | caseCamel | escapeKeyword}} === true ? [] : {{ parameter.name | caseCamel | escapeKeyword}}; + {{ parameter.name | caseCamel | escapeKeyword}} = ({{ parameter.name | caseCamel | escapeKeyword}} as unknown) === true ? [] : {{ parameter.name | caseCamel | escapeKeyword}}; if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword}}; } diff --git a/templates/cli/base/requests/file.twig b/templates/cli/base/requests/file.twig index afbef914c..7d8cf88bc 100644 --- a/templates/cli/base/requests/file.twig +++ b/templates/cli/base/requests/file.twig @@ -1,6 +1,6 @@ {% for parameter in method.parameters.all %} {% if parameter.type == 'file' %} - const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; + const size = {{ parameter.name | caseCamel | escapeKeyword }}Upload.size; const apiHeaders = { {% for parameter in method.parameters.header %} @@ -59,7 +59,7 @@ apiHeaders['x-{{spec.title | caseLower }}-id'] = id; } - payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; + payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}Upload.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}Upload.filename }; response = await client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); @@ -82,7 +82,7 @@ currentPosition = 0; } - for await (const chunk of {{ parameter.name | caseCamel | escapeKeyword }}.stream) { + for await (const chunk of {{ parameter.name | caseCamel | escapeKeyword }}Upload.stream) { for(const b of chunk) { uploadableChunk[currentPosition] = b; diff --git a/templates/cli/index.ts.twig b/templates/cli/index.ts.twig index f51dc78eb..17dde8a0b 100644 --- a/templates/cli/index.ts.twig +++ b/templates/cli/index.ts.twig @@ -5,7 +5,7 @@ const oldWidth = process.stdout.columns; process.stdout.columns = 100; /** ---------------------------------------------- */ -import program = require('commander'); +import { program } from 'commander'; import chalk = require('chalk'); const { version } = require('../package.json'); import { commandDescriptions, cliConfig } from './lib/parser'; diff --git a/templates/cli/lib/client.ts.twig b/templates/cli/lib/client.ts.twig index e68bb3089..b4ee24de0 100644 --- a/templates/cli/lib/client.ts.twig +++ b/templates/cli/lib/client.ts.twig @@ -1,5 +1,5 @@ import os = require('os'); -import { fetch, FormData, Agent } from 'undici'; +import { fetch, FormData, Agent, File } from 'undici'; import JSONbig = require('json-bigint'); import {{ spec.title | caseUcfirst }}Exception = require('./exception'); import { globalConfig } from './config'; @@ -9,7 +9,7 @@ import type { Headers, RequestParams, ResponseType, FileUpload } from './types'; const JSONBigInt = JSONbig({ storeAsString: false }); class Client { - private readonly CHUNK_SIZE = 5 * 1024 * 1024; // 5MB + readonly CHUNK_SIZE = 5 * 1024 * 1024; // 5MB private endpoint: string; private headers: Headers; private selfSigned: boolean; @@ -122,7 +122,7 @@ class Client { for (const [key, value] of Object.entries(flatParams)) { if (value && typeof value === 'object' && 'type' in value && value.type === 'file') { const fileUpload = value as FileUpload; - formData.append(key, fileUpload.file as any, fileUpload.filename); + formData.append(key, fileUpload.file, fileUpload.filename); } else { formData.append(key, value as string); } diff --git a/templates/cli/lib/commands/command.ts.twig b/templates/cli/lib/commands/command.ts.twig index 1afc3fde3..fd124f111 100644 --- a/templates/cli/lib/commands/command.ts.twig +++ b/templates/cli/lib/commands/command.ts.twig @@ -1,7 +1,7 @@ import fs = require('fs'); import pathLib = require('path'); import tar = require('tar'); -import ignore = require('ignore'); +import ignore from 'ignore'; import { promisify } from 'util'; import Client from '../client'; import { getAllFiles, showConsoleLink } from '../utils'; @@ -11,6 +11,18 @@ import { parse, actionRunner, parseInteger, parseBool, commandDescriptions, succ import { localConfig, globalConfig } from '../config'; import { File } from 'undici'; import { ReadableStream } from 'stream/web'; +import type { UploadProgress, FileInput } from '../types'; +{% set addedEnums = [] %} +{% for method in service.methods %} +{% for parameter in method.parameters.all %} +{% if parameter.enumValues is not empty %} +{% if parameter.enumName not in addedEnums %} +import { {{ parameter.enumName | caseUcfirst }} } from '../enums/{{ parameter.enumName | caseKebab }}'; +{% set addedEnums = addedEnums|merge([parameter.enumName]) %} +{% endif %} +{% endif %} +{% endfor %} +{% endfor %} function convertReadStreamToReadableStream(readStream: fs.ReadStream): ReadableStream { return new ReadableStream({ @@ -55,7 +67,7 @@ interface {{ service.name | caseUcfirst }}{{ method.name | caseUcfirst }}Request parseOutput?: boolean; sdk?: Client; {% if 'multipart/form-data' in method.consumes %} - onProgress?: (progress: number) => void; + onProgress?: (progress: UploadProgress) => void; {% endif %} {% if method.type == 'location' %} destination?: string; @@ -73,7 +85,7 @@ export const {{ service.name }}{{ method.name | caseUcfirst }} = async ({ {%- block baseParams -%}parseOutput = true, overrideForCli = false, sdk = undefined {%- endblock -%} - {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} + {%- if 'multipart/form-data' in method.consumes -%},onProgress = (progress: any) => {}{%- endif -%} {%- if method.type == 'location' -%}, destination{%- endif -%} {% if hasConsolePreview(method.name,service.name) %}, console: showConsole{%- endif -%} diff --git a/templates/cli/lib/commands/generic.ts.twig b/templates/cli/lib/commands/generic.ts.twig index ce4138961..b76c9102a 100644 --- a/templates/cli/lib/commands/generic.ts.twig +++ b/templates/cli/lib/commands/generic.ts.twig @@ -53,7 +53,7 @@ export const loginCommand = async ({ email, password, endpoint, mfa, code }: Log const id = ID.unique(); - globalConfig.addSession(id, {}); + globalConfig.addSession(id, { endpoint: configEndpoint }); globalConfig.setCurrentSession(id); globalConfig.setEndpoint(configEndpoint); globalConfig.setEmail(answers.email); @@ -281,12 +281,12 @@ export const client = new Command("client") if (selfSigned || globalConfig.getSelfSigned()) { clientInstance.setSelfSigned(true); } - let response = await clientInstance.call('GET', '/health/version'); + let response = await clientInstance.call<{ version?: string }>('GET', '/health/version'); if (!response.version) { throw new Error(); } globalConfig.setCurrentSession(id); - globalConfig.addSession(id, {}); + globalConfig.addSession(id, { endpoint }); globalConfig.setEndpoint(endpoint); } catch (_) { throw new Error("Invalid endpoint or your Appwrite server is not running as expected."); @@ -322,8 +322,8 @@ export const migrate = async (): Promise => { return; } - const endpoint = globalConfig.get('endpoint'); - const cookie = globalConfig.get('cookie'); + const endpoint = globalConfig.get('endpoint') as string; + const cookie = globalConfig.get('cookie') as string; const id = ID.unique(); const data = { diff --git a/templates/cli/lib/commands/init.ts.twig b/templates/cli/lib/commands/init.ts.twig index cac88d3b6..55a64a9aa 100644 --- a/templates/cli/lib/commands/init.ts.twig +++ b/templates/cli/lib/commands/init.ts.twig @@ -41,7 +41,7 @@ const initResources = async (): Promise => { collection: initCollection } - const answers = await inquirer.prompt(questionsInitResources[0]); + const answers = await inquirer.prompt([questionsInitResources[0]]); const action = actions[answers.resource]; if (action !== undefined) { @@ -85,9 +85,9 @@ const initProject = async ({ organizationId, projectId, projectName }: InitProje answers.project = {}; answers.organization = {}; - answers.organization = organizationId ?? (await inquirer.prompt(questionsInitProject[2])).organization; - answers.project.name = projectName ?? (await inquirer.prompt(questionsInitProject[3])).project; - answers.project = projectId ?? (await inquirer.prompt(questionsInitProject[4])).id; + answers.organization = organizationId ?? (await inquirer.prompt([questionsInitProject[2]])).organization; + answers.project.name = projectName ?? (await inquirer.prompt([questionsInitProject[3]])).project; + answers.project = projectId ?? (await inquirer.prompt([questionsInitProject[4]])).id; try { await projectsGet({ projectId, parseOutput: false }); diff --git a/templates/cli/lib/commands/pull.ts.twig b/templates/cli/lib/commands/pull.ts.twig index e0e173985..523bc25bb 100644 --- a/templates/cli/lib/commands/pull.ts.twig +++ b/templates/cli/lib/commands/pull.ts.twig @@ -54,7 +54,7 @@ export const pullResources = async ({ await action({ returnOnZero: true }); } } else { - const answers = await inquirer.prompt(questionsPullResources[0]); + const answers = await inquirer.prompt([questionsPullResources[0]]); const action = actions[answers.resource]; if (action !== undefined) { diff --git a/templates/cli/lib/commands/push.ts.twig b/templates/cli/lib/commands/push.ts.twig index d59f88f40..dd46125a3 100644 --- a/templates/cli/lib/commands/push.ts.twig +++ b/templates/cli/lib/commands/push.ts.twig @@ -92,6 +92,8 @@ import { projectsUpdateSessionAlerts, projectsUpdateMockNumbers, } from "./projects"; +import { ApiService } from '../enums/api-service'; +import { AuthMethod } from '../enums/auth-method'; import { checkDeployConditions } from '../utils'; const JSONbigNative = JSONbig({ storeAsString: false }); @@ -622,7 +624,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an key: attribute.key, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'url': @@ -632,7 +633,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an key: attribute.key, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'ip': @@ -642,7 +642,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an key: attribute.key, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'enum': @@ -653,7 +652,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an elements: attribute.elements, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) default: @@ -661,10 +659,8 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an databaseId, collectionId, key: attribute.key, - size: attribute.size, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) @@ -678,7 +674,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an min: attribute.min, max: attribute.max, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'double': @@ -690,7 +685,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an min: attribute.min, max: attribute.max, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'boolean': @@ -700,7 +694,6 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an key: attribute.key, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'datetime': @@ -710,18 +703,13 @@ const updateAttribute = (databaseId: string, collectionId: string, attribute: an key: attribute.key, required: attribute.required, xdefault: attribute.default, - array: attribute.array, parseOutput: false }) case 'relationship': return databasesUpdateRelationshipAttribute({ databaseId, collectionId, - relatedCollectionId: attribute.relatedTable ?? attribute.relatedCollection, - type: attribute.relationType, - twoWay: attribute.twoWay, key: attribute.key, - twoWayKey: attribute.twoWayKey, onDelete: attribute.onDelete, parseOutput: false }) @@ -1034,7 +1022,7 @@ const pushResources = async (): Promise => { await action({ returnOnZero: true }); } } else { - const answers = await inquirer.prompt(questionsPushResources[0]); + const answers = await inquirer.prompt(questionsPushResources); const action = actions[answers.resource]; if (action !== undefined) { @@ -1093,7 +1081,7 @@ const pushSettings = async (): Promise => { for (let [service, status] of Object.entries(settings.services)) { await projectsUpdateServiceStatus({ projectId, - service, + service: service as ApiService, status, parseOutput: false }); @@ -1119,7 +1107,7 @@ const pushSettings = async (): Promise => { for (let [method, status] of Object.entries(settings.auth.methods)) { await projectsUpdateAuthStatus({ projectId, - method, + method: method as AuthMethod, status, parseOutput: false }); @@ -1157,7 +1145,7 @@ const pushSite = async({ siteId, async: asyncDeploy, code, withVariables }: Push } if (siteIds.length <= 0) { - const answers = await inquirer.prompt(questionsPushSites[0]); + const answers = await inquirer.prompt(questionsPushSites); if (answers.sites) { siteIds.push(...answers.sites); } @@ -1241,8 +1229,6 @@ const pushSite = async({ siteId, async: asyncDeploy, code, withVariables }: Push buildCommand: site.buildCommand, installCommand: site.installCommand, outputDirectory: site.outputDirectory, - fallbackFile: site.fallbackFile, - vars: JSON.stringify(response.vars), parseOutput: false }); } catch (e: any) { @@ -1269,7 +1255,6 @@ const pushSite = async({ siteId, async: asyncDeploy, code, withVariables }: Push buildCommand: site.buildCommand, installCommand: site.installCommand, outputDirectory: site.outputDirectory, - fallbackFile: site.fallbackFile, adapter: site.adapter, timeout: site.timeout, enabled: site.enabled, @@ -1356,10 +1341,6 @@ const pushSite = async({ siteId, async: asyncDeploy, code, withVariables }: Push updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); response = await sitesCreateDeployment({ siteId: site['$id'], - buildCommand: site.buildCommand, - installCommand: site.installCommand, - outputDirectory: site.outputDirectory, - fallbackFile: site.fallbackFile, code: site.path, activate: true, parseOutput: false @@ -1488,7 +1469,7 @@ const pushFunction = async ({ functionId, async: asyncDeploy, code, withVariable } if (functionIds.length <= 0) { - const answers = await inquirer.prompt(questionsPushFunctions[0]); + const answers = await inquirer.prompt(questionsPushFunctions); if (answers.functions) { functionIds.push(...answers.functions); } @@ -1571,7 +1552,6 @@ const pushFunction = async ({ functionId, async: asyncDeploy, code, withVariable entrypoint: func.entrypoint, commands: func.commands, scopes: func.scopes, - vars: JSON.stringify(response.vars), parseOutput: false }); } catch (e: any) { @@ -1665,7 +1645,6 @@ const pushFunction = async ({ functionId, async: asyncDeploy, code, withVariable await Promise.all(envVariables.map(async (variable) => { await functionsCreateVariable({ functionId: func['$id'], - variableId: ID.unique(), key: variable.key, value: variable.value, parseOutput: false, @@ -2314,7 +2293,7 @@ const pushBucket = async ({ returnOnZero }: PushBucketOptions = { returnOnZero: } if (bucketIds.length === 0) { - const answers = await inquirer.prompt(questionsPushBuckets[0]) + const answers = await inquirer.prompt(questionsPushBuckets) if (answers.buckets) { bucketIds.push(...answers.buckets); } @@ -2403,7 +2382,7 @@ const pushTeam = async ({ returnOnZero }: PushTeamOptions = { returnOnZero: fals } if (teamIds.length === 0) { - const answers = await inquirer.prompt(questionsPushTeams[0]) + const answers = await inquirer.prompt(questionsPushTeams) if (answers.teams) { teamIds.push(...answers.teams); } @@ -2476,7 +2455,7 @@ const pushMessagingTopic = async ({ returnOnZero }: PushMessagingTopicOptions = } if (topicsIds.length === 0) { - const answers = await inquirer.prompt(questionsPushMessagingTopics[0]) + const answers = await inquirer.prompt(questionsPushMessagingTopics) if (answers.topics) { topicsIds.push(...answers.topics); } diff --git a/templates/cli/lib/commands/run.ts.twig b/templates/cli/lib/commands/run.ts.twig index 8caf2feb8..d3f688ddc 100644 --- a/templates/cli/lib/commands/run.ts.twig +++ b/templates/cli/lib/commands/run.ts.twig @@ -1,7 +1,7 @@ import { Tail } from 'tail'; import { parse as parseDotenv } from 'dotenv'; import chalk from 'chalk'; -import ignore = require('ignore'); +import ignore from 'ignore'; import tar = require('tar'); import fs = require('fs'); import chokidar from 'chokidar'; @@ -28,7 +28,7 @@ interface RunFunctionOptions { const runFunction = async ({ port, functionId, withVariables, reload, userId }: RunFunctionOptions = {}): Promise => { // Selection if(!functionId) { - const answers = await inquirer.prompt(questionsRunFunctions[0]); + const answers = await inquirer.prompt([questionsRunFunctions[0]]); functionId = answers.function; } @@ -250,13 +250,12 @@ const runFunction = async ({ port, functionId, withVariables, reload, userId }: await tar .extract({ keep: true, - gzip: true, sync: true, cwd: hotSwapPath, file: buildPath }); - const ignorer = ignore.default(); + const ignorer = ignore(); ignorer.add('.appwrite'); if (func.ignore) { ignorer.add(func.ignore); diff --git a/templates/cli/lib/config.ts.twig b/templates/cli/lib/config.ts.twig index 7369d90c1..8b84ba432 100644 --- a/templates/cli/lib/config.ts.twig +++ b/templates/cli/lib/config.ts.twig @@ -3,7 +3,22 @@ import fs = require('fs'); import _path = require('path'); import process = require('process'); import JSONbig = require('json-bigint'); -import type { ConfigData, SessionData, GlobalConfigData, ProjectConfigData } from './types'; +import type { + BucketConfig, + CollectionConfig, + ConfigData, + Entity, + FunctionConfig, + GlobalConfigData, + ProjectConfigData, + ProjectSettings, + RawProjectSettings, + SessionData, + SiteConfig, + TableConfig, + TeamConfig, + TopicConfig +} from './types'; const JSONBigInt = JSONbig({ storeAsString: false }); @@ -102,7 +117,7 @@ function whitelistKeys( } class Config { - protected path: string; + readonly path: string; protected data: T; constructor(path: string) { @@ -165,19 +180,19 @@ class Config { return JSONBigInt.stringify(this.data, null, 4); } - protected _getDBEntities(entityType: string): any[] { + protected _getDBEntities(entityType: string): Entity[] { if (!this.has(entityType)) { return []; } - return this.get(entityType); + return this.get(entityType) as Entity[]; } - protected _getDBEntity(entityType: string, $id: string): any { + protected _getDBEntity(entityType: string, $id: string): Entity | Record { if (!this.has(entityType)) { return {}; } - const entities = this.get(entityType); + const entities = this.get(entityType) as Entity[]; for (let i = 0; i < entities.length; i++) { if (entities[i]['$id'] == $id) { return entities[i]; @@ -189,26 +204,26 @@ class Config { protected _addDBEntity( entityType: string, - props: any, + props: Entity, keysSet: Set, nestedKeys: Record> = {} ): void { props = whitelistKeys(props, keysSet, nestedKeys); if (!this.has(entityType)) { - this.set(entityType, []); + (this.set as (key: string, value: Entity[]) => void)(entityType, []); } - const entities = this.get(entityType); + const entities = this.get(entityType) as Entity[]; for (let i = 0; i < entities.length; i++) { if (entities[i]['$id'] == props['$id']) { entities[i] = props; - this.set(entityType, entities); + (this.set as (key: string, value: Entity[]) => void)(entityType, entities); return; } } entities.push(props); - this.set(entityType, entities); + (this.set as (key: string, value: Entity[]) => void)(entityType, entities); } } @@ -253,26 +268,26 @@ class Local extends Config { } getEndpoint(): string { - return this.get('endpoint' as any) || ''; + return (this.get('endpoint' as keyof ProjectConfigData) as string) || ''; } setEndpoint(endpoint: string): void { this.set('endpoint' as any, endpoint); } - getSites(): any[] { + getSites(): SiteConfig[] { if (!this.has('sites')) { return []; } - return this.get('sites' as any); + return this.get('sites') ?? []; } - getSite($id: string): any { + getSite($id: string): SiteConfig | Record { if (!this.has('sites')) { return {}; } - const sites = this.get('sites' as any); + const sites = this.get('sites') ?? []; for (let i = 0; i < sites.length; i++) { if (sites[i]['$id'] == $id) { return sites[i]; @@ -282,44 +297,44 @@ class Local extends Config { return {}; } - addSite(props: any): void { + addSite(props: SiteConfig): void { props = whitelistKeys(props, KeysSite, { vars: KeysVars, }); if (!this.has('sites')) { - this.set('sites' as any, []); + this.set('sites', []); } - const sites = this.get('sites' as any); + const sites = this.get('sites') ?? []; for (let i = 0; i < sites.length; i++) { if (sites[i]['$id'] == props['$id']) { sites[i] = { ...sites[i], ...props, }; - this.set('sites' as any, sites); + this.set('sites', sites); return; } } sites.push(props); - this.set('sites' as any, sites); + this.set('sites', sites); } - getFunctions(): any[] { + getFunctions(): FunctionConfig[] { if (!this.has('functions')) { return []; } - return this.get('functions' as any); + return this.get('functions') ?? []; } - getFunction($id: string): any { + getFunction($id: string): FunctionConfig | Record { if (!this.has('functions')) { return {}; } - const functions = this.get('functions' as any); + const functions = this.get('functions') ?? []; for (let i = 0; i < functions.length; i++) { if (functions[i]['$id'] == $id) { return functions[i]; @@ -329,44 +344,44 @@ class Local extends Config { return {}; } - addFunction(props: any): void { + addFunction(props: FunctionConfig): void { props = whitelistKeys(props, KeysFunction, { vars: KeysVars, }); if (!this.has('functions')) { - this.set('functions' as any, []); + this.set('functions', []); } - const functions = this.get('functions' as any); + const functions = this.get('functions') ?? []; for (let i = 0; i < functions.length; i++) { if (functions[i]['$id'] == props['$id']) { functions[i] = { ...functions[i], ...props, }; - this.set('functions' as any, functions); + this.set('functions', functions); return; } } functions.push(props); - this.set('functions' as any, functions); + this.set('functions', functions); } - getCollections(): any[] { + getCollections(): CollectionConfig[] { if (!this.has('collections')) { return []; } - return this.get('collections' as any); + return this.get('collections') ?? []; } - getCollection($id: string): any { + getCollection($id: string): CollectionConfig | Record { if (!this.has('collections')) { return {}; } - const collections = this.get('collections' as any); + const collections = this.get('collections') ?? []; for (let i = 0; i < collections.length; i++) { if (collections[i]['$id'] == $id) { return collections[i]; @@ -376,41 +391,41 @@ class Local extends Config { return {}; } - addCollection(props: any): void { + addCollection(props: CollectionConfig): void { props = whitelistKeys(props, KeysCollection, { attributes: KeysAttributes, indexes: KeyIndexes, }); if (!this.has('collections')) { - this.set('collections' as any, []); + this.set('collections', []); } - const collections = this.get('collections' as any); + const collections = this.get('collections') ?? []; for (let i = 0; i < collections.length; i++) { if (collections[i]['$id'] == props['$id'] && collections[i]['databaseId'] == props['databaseId']) { collections[i] = props; - this.set('collections' as any, collections); + this.set('collections', collections); return; } } collections.push(props); - this.set('collections' as any, collections); + this.set('collections', collections); } - getTables(): any[] { + getTables(): TableConfig[] { if (!this.has('tables')) { return []; } - return this.get('tables' as any); + return this.get('tables') ?? []; } - getTable($id: string): any { + getTable($id: string): TableConfig | Record { if (!this.has('tables')) { return {}; } - const tables = this.get('tables' as any); + const tables = this.get('tables') ?? []; for (let i = 0; i < tables.length; i++) { if (tables[i]['$id'] == $id) { return tables[i]; @@ -420,41 +435,41 @@ class Local extends Config { return {}; } - addTable(props: any): void { + addTable(props: TableConfig): void { props = whitelistKeys(props, KeysTable, { columns: KeysColumns, indexes: KeyIndexesColumns, }); if (!this.has('tables')) { - this.set('tables' as any, []); + this.set('tables', []); } - const tables = this.get('tables' as any); + const tables = this.get('tables') ?? []; for (let i = 0; i < tables.length; i++) { if (tables[i]['$id'] == props['$id'] && tables[i]['databaseId'] == props['databaseId']) { tables[i] = props; - this.set('tables' as any, tables); + this.set('tables', tables); return; } } tables.push(props); - this.set('tables' as any, tables); + this.set('tables', tables); } - getBuckets(): any[] { + getBuckets(): BucketConfig[] { if (!this.has('buckets')) { return []; } - return this.get('buckets' as any); + return this.get('buckets') ?? []; } - getBucket($id: string): any { + getBucket($id: string): BucketConfig | Record { if (!this.has('buckets')) { return {}; } - const buckets = this.get('buckets' as any); + const buckets = this.get('buckets') ?? []; for (let i = 0; i < buckets.length; i++) { if (buckets[i]['$id'] == $id) { return buckets[i]; @@ -464,64 +479,64 @@ class Local extends Config { return {}; } - addBucket(props: any): void { + addBucket(props: BucketConfig): void { props = whitelistKeys(props, KeysStorage); if (!this.has('buckets')) { - this.set('buckets' as any, []); + this.set('buckets', []); } - const buckets = this.get('buckets' as any); + const buckets = this.get('buckets') ?? []; for (let i = 0; i < buckets.length; i++) { if (buckets[i]['$id'] == props['$id']) { buckets[i] = props; - this.set('buckets' as any, buckets); + this.set('buckets', buckets); return; } } buckets.push(props); - this.set('buckets' as any, buckets); + this.set('buckets', buckets); } - getMessagingTopics(): any[] { + getMessagingTopics(): TopicConfig[] { if (!this.has('topics')) { return []; } - return this.get('topics' as any); + return this.get('topics') ?? []; } - getMessagingTopic($id: string): any { + getMessagingTopic($id: string): TopicConfig | Record { if (!this.has('topics')) { return {}; } - const topic = this.get('topics' as any); - for (let i = 0; i < topic.length; i++) { - if (topic[i]['$id'] == $id) { - return topic[i]; + const topics = this.get('topics') ?? []; + for (let i = 0; i < topics.length; i++) { + if (topics[i]['$id'] == $id) { + return topics[i]; } } return {}; } - addMessagingTopic(props: any): void { + addMessagingTopic(props: TopicConfig): void { props = whitelistKeys(props, KeysTopics); if (!this.has('topics')) { - this.set('topics' as any, []); + this.set('topics', []); } - const topics = this.get('topics' as any); + const topics = this.get('topics') ?? []; for (let i = 0; i < topics.length; i++) { if (topics[i]['$id'] === props['$id']) { topics[i] = props; - this.set('topics' as any, topics); + this.set('topics', topics); return; } } topics.push(props); - this.set('topics' as any, topics); + this.set('topics', topics); } getTablesDBs(): any[] { @@ -548,19 +563,19 @@ class Local extends Config { this._addDBEntity('databases', props, KeysDatabase); } - getTeams(): any[] { + getTeams(): TeamConfig[] { if (!this.has('teams')) { return []; } - return this.get('teams' as any); + return this.get('teams') ?? []; } - getTeam($id: string): any { + getTeam($id: string): TeamConfig | Record { if (!this.has('teams')) { return {}; } - const teams = this.get('teams' as any); + const teams = this.get('teams') ?? []; for (let i = 0; i < teams.length; i++) { if (teams[i]['$id'] == $id) { return teams[i]; @@ -570,51 +585,51 @@ class Local extends Config { return {}; } - addTeam(props: any): void { + addTeam(props: TeamConfig): void { props = whitelistKeys(props, KeysTeams); if (!this.has('teams')) { - this.set('teams' as any, []); + this.set('teams', []); } - const teams = this.get('teams' as any); + const teams = this.get('teams') ?? []; for (let i = 0; i < teams.length; i++) { if (teams[i]['$id'] == props['$id']) { teams[i] = props; - this.set('teams' as any, teams); + this.set('teams', teams); return; } } teams.push(props); - this.set('teams' as any, teams); + this.set('teams', teams); } - getProject(): { projectId?: string; projectName?: string; projectSettings?: any } { + getProject(): { projectId?: string; projectName?: string; projectSettings?: ProjectSettings } { if (!this.has('projectId')) { return {}; } return { - projectId: this.get('projectId' as any), - projectName: this.get('projectName' as any), - projectSettings: this.get('settings' as any), + projectId: this.get('projectId'), + projectName: this.get('projectName'), + projectSettings: this.get('settings'), }; } - setProject(projectId: string, projectName: string = '', projectSettings: any = undefined): void { - this.set('projectId' as any, projectId); + setProject(projectId: string, projectName: string = '', projectSettings?: RawProjectSettings): void { + this.set('projectId', projectId); if (projectName !== '') { - this.set('projectName' as any, projectName); + this.set('projectName', projectName); } if (projectSettings === undefined) { return; } - this.set('settings' as any, this.createSettingsObject(projectSettings)); + this.set('settings', this.createSettingsObject(projectSettings)); } - createSettingsObject(projectSettings: any): any { + createSettingsObject(projectSettings: RawProjectSettings): ProjectSettings { return { services: { account: projectSettings.serviceStatusForAccount, @@ -658,17 +673,17 @@ class Local extends Config { class Global extends Config { static CONFIG_FILE_PATH = '.{{ spec.title|caseLower }}/prefs.json'; - static PREFERENCE_CURRENT = 'current'; - static PREFERENCE_ENDPOINT = 'endpoint'; - static PREFERENCE_EMAIL = 'email'; - static PREFERENCE_SELF_SIGNED = 'selfSigned'; - static PREFERENCE_COOKIE = 'cookie'; - static PREFERENCE_PROJECT = 'project'; - static PREFERENCE_KEY = 'key'; - static PREFERENCE_LOCALE = 'locale'; - static PREFERENCE_MODE = 'mode'; - - static IGNORE_ATTRIBUTES = [ + static PREFERENCE_CURRENT = 'current' as const; + static PREFERENCE_ENDPOINT = 'endpoint' as const; + static PREFERENCE_EMAIL = 'email' as const; + static PREFERENCE_SELF_SIGNED = 'selfSigned' as const; + static PREFERENCE_COOKIE = 'cookie' as const; + static PREFERENCE_PROJECT = 'project' as const; + static PREFERENCE_KEY = 'key' as const; + static PREFERENCE_LOCALE = 'locale' as const; + static PREFERENCE_MODE = 'mode' as const; + + static IGNORE_ATTRIBUTES: readonly string[] = [ Global.PREFERENCE_CURRENT, Global.PREFERENCE_SELF_SIGNED, Global.PREFERENCE_ENDPOINT, @@ -693,12 +708,12 @@ class Global extends Config { if (!this.has(Global.PREFERENCE_CURRENT)) { return ''; } - return this.get(Global.PREFERENCE_CURRENT as any); + return this.get(Global.PREFERENCE_CURRENT); } setCurrentSession(session: string): void { if (session !== undefined) { - this.set(Global.PREFERENCE_CURRENT as any, session); + this.set(Global.PREFERENCE_CURRENT, session); } } diff --git a/templates/cli/lib/emulation/docker.ts.twig b/templates/cli/lib/emulation/docker.ts.twig index 8acd672b2..6eb07da16 100644 --- a/templates/cli/lib/emulation/docker.ts.twig +++ b/templates/cli/lib/emulation/docker.ts.twig @@ -1,4 +1,4 @@ -import ignore = require('ignore'); +import ignore from 'ignore'; import net = require('net'); import chalk from 'chalk'; import childProcess = require('child_process'); @@ -8,15 +8,7 @@ import fs = require('fs'); import { log, error, success } from '../parser'; import { openRuntimesVersion, systemTools, Queue } from './utils'; import { getAllFiles } from '../utils'; - -interface FunctionConfig { - $id: string; - runtime: string; - path: string; - entrypoint: string; - commands: string; - ignore?: string; -} +import type { FunctionConfig } from '../types'; export async function dockerStop(id: string): Promise { const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { @@ -59,7 +51,7 @@ export async function dockerBuild(func: FunctionConfig, variables: Record { - [key: string]: T; - total: number; -} +// Overload for when wrapper is empty string - returns array +export function paginate( + action: (args: PaginateArgs) => Promise, + args: PaginateArgs, + limit: number, + wrapper: '', + queries?: string[] +): Promise; -export const paginate = async ( +// Overload for when wrapper is specified - returns object with that key +export function paginate( + action: (args: PaginateArgs) => Promise, + args: PaginateArgs, + limit: number, + wrapper: K, + queries?: string[] +): Promise & { total: number }>; + +// Implementation +export async function paginate( action: (args: PaginateArgs) => Promise, args: PaginateArgs = {}, limit: number = 100, wrapper: string = '', queries: string[] = [] -): Promise> => { +): Promise & { total: number })> { let pageNumber = 0; let results: T[] = []; let total = 0; @@ -59,5 +73,5 @@ export const paginate = async ( return { [wrapper]: results, total, - } as PaginateResponse; -}; + } as Record & { total: number }; +} diff --git a/templates/cli/lib/questions.ts.twig b/templates/cli/lib/questions.ts.twig index 11709ace5..a0f35e257 100644 --- a/templates/cli/lib/questions.ts.twig +++ b/templates/cli/lib/questions.ts.twig @@ -37,7 +37,7 @@ interface Question { message: string; default?: any; when?: ((answers: Answers) => boolean | Promise) | boolean; - choices?: (() => Promise | Choice[]) | Choice[]; + choices?: ((answers: Answers) => Promise | Choice[]) | (() => Promise | Choice[]) | Choice[] | string[]; validate?: (value: any) => boolean | string | Promise; mask?: string; } @@ -250,7 +250,7 @@ export const questionsInitProject: Question[] = [ message: "Select your Appwrite Cloud region", choices: async () => { let client = await sdkForConsole(true); - let response = await client.call("GET", "/console/regions"); + let response = await client.call<{ regions: any[] }>("GET", "/console/regions"); let regions = response.regions || []; if (!regions.length) { throw new Error("No regions found. Please check your network or Appwrite Cloud availability."); @@ -711,7 +711,7 @@ export const questionGetEndpoint: Question[] = [ } let client = new Client().setEndpoint(value); try { - let response = await client.call('get', '/health/version'); + let response = await client.call<{ version?: string }>('get', '/health/version'); if (response.version) { return true; } else { diff --git a/templates/cli/lib/types.ts.twig b/templates/cli/lib/types.ts.twig index dee317a94..6a2e6ffa7 100644 --- a/templates/cli/lib/types.ts.twig +++ b/templates/cli/lib/types.ts.twig @@ -1,12 +1,7 @@ -export interface CliConfig { - verbose: boolean; - json: boolean; - force: boolean; - all: boolean; - ids: string[]; - report: boolean; - reportData: Record; -} +import type { File } from 'undici'; +import type { ReadableStream } from 'node:stream/web'; + +export type ResponseType = 'json' | 'arraybuffer'; export interface Headers { [key: string]: string; @@ -18,16 +13,52 @@ export interface RequestParams { export interface FileUpload { type: 'file'; - file: Buffer | ReadableStream; + file: File; filename: string; } -export type ResponseType = 'json' | 'arraybuffer'; +export interface FileInput { + type: 'file'; + stream: ReadableStream; + filename: string; + size: number; +} + +export interface UploadProgress { + $id: string; + progress: number; + sizeUploaded: number; + chunksTotal: number; + chunksUploaded: number; +} export interface ConfigData { [key: string]: unknown; } +export interface Entity { + $id: string; + [key: string]: unknown; +} + +export interface ParsedData { + [key: string]: unknown; +} + +export interface CommandDescription { + [key: string]: string; +} + +export interface CliConfig { + verbose: boolean; + json: boolean; + force: boolean; + all: boolean; + ids: string[]; + report: boolean; + reportData: Record; +} + export interface SessionData { endpoint: string; email?: string; @@ -35,7 +66,7 @@ export interface SessionData { cookie?: string; } -export interface GlobalConfigData { +export interface GlobalConfigData extends ConfigData { sessions: { [key: string]: SessionData; }; @@ -43,33 +74,106 @@ export interface GlobalConfigData { cookie?: string; } -export interface ProjectConfigData { - projectId?: string; - projectName?: string; - functions?: FunctionConfig[]; - collections?: CollectionConfig[]; - databases?: DatabaseConfig[]; - buckets?: BucketConfig[]; - teams?: TeamConfig[]; - topics?: TopicConfig[]; +export interface ProjectSettings { + services?: { + account?: boolean; + avatars?: boolean; + databases?: boolean; + locale?: boolean; + health?: boolean; + storage?: boolean; + teams?: boolean; + users?: boolean; + sites?: boolean; + functions?: boolean; + graphql?: boolean; + messaging?: boolean; + }; + auth?: { + methods?: { + jwt?: boolean; + phone?: boolean; + invites?: boolean; + anonymous?: boolean; + 'email-otp'?: boolean; + 'magic-url'?: boolean; + 'email-password'?: boolean; + }; + security?: { + duration?: number; + limit?: number; + sessionsLimit?: number; + passwordHistory?: number; + passwordDictionary?: boolean; + personalDataCheck?: boolean; + sessionAlerts?: boolean; + mockNumbers?: string[]; + }; + }; } -export interface FunctionConfig { +export interface RawProjectSettings { + serviceStatusForAccount?: boolean; + serviceStatusForAvatars?: boolean; + serviceStatusForDatabases?: boolean; + serviceStatusForLocale?: boolean; + serviceStatusForHealth?: boolean; + serviceStatusForStorage?: boolean; + serviceStatusForTeams?: boolean; + serviceStatusForUsers?: boolean; + serviceStatusForSites?: boolean; + serviceStatusForFunctions?: boolean; + serviceStatusForGraphql?: boolean; + serviceStatusForMessaging?: boolean; + authJWT?: boolean; + authPhone?: boolean; + authInvites?: boolean; + authAnonymous?: boolean; + authEmailOtp?: boolean; + authUsersAuthMagicURL?: boolean; + authEmailPassword?: boolean; + authDuration?: number; + authLimit?: number; + authSessionsLimit?: number; + authPasswordHistory?: number; + authPasswordDictionary?: boolean; + authPersonalDataCheck?: boolean; + authSessionAlerts?: boolean; + authMockNumbers?: string[]; +} + +export interface DatabaseConfig { $id: string; name: string; - runtime: string; - path: string; - entrypoint: string; - execute?: string[]; enabled?: boolean; - logging?: boolean; - events?: string[]; - schedule?: string; - timeout?: number; - vars?: Record; - commands?: string; - scopes?: string[]; - specification?: string; +} + +export interface AttributeConfig { + key: string; + type: string; + required?: boolean; + array?: boolean; + size?: number; + default?: unknown; + min?: number; + max?: number; + format?: string; + elements?: string[]; + relatedCollection?: string; + relationType?: string; + twoWay?: boolean; + twoWayKey?: string; + onDelete?: string; + side?: string; + encrypt?: boolean; +} + +export interface IndexConfig { + key: string; + type: string; + status?: string; + attributes?: string[]; + orders?: string[]; } export interface CollectionConfig { @@ -83,7 +187,7 @@ export interface CollectionConfig { indexes?: IndexConfig[]; } -export interface AttributeConfig { +export interface ColumnConfig { key: string; type: string; required?: boolean; @@ -94,7 +198,7 @@ export interface AttributeConfig { max?: number; format?: string; elements?: string[]; - relatedCollection?: string; + relatedTable?: string; relationType?: string; twoWay?: boolean; twoWayKey?: string; @@ -103,18 +207,23 @@ export interface AttributeConfig { encrypt?: boolean; } -export interface IndexConfig { +export interface TableIndexConfig { key: string; type: string; status?: string; - attributes?: string[]; + columns?: string[]; orders?: string[]; } -export interface DatabaseConfig { +export interface TableConfig { $id: string; + $permissions?: string[]; + databaseId: string; name: string; enabled?: boolean; + rowSecurity?: boolean; + columns?: ColumnConfig[]; + indexes?: TableIndexConfig[]; } export interface BucketConfig { @@ -130,6 +239,44 @@ export interface BucketConfig { antivirus?: boolean; } +export interface FunctionConfig { + $id: string; + name: string; + runtime: string; + path: string; + entrypoint: string; + execute?: string[]; + enabled?: boolean; + logging?: boolean; + events?: string[]; + schedule?: string; + timeout?: number; + vars?: Record; + commands?: string; + scopes?: string[]; + specification?: string; + ignore?: string; +} + +export interface SiteConfig { + $id: string; + name: string; + path: string; + enabled?: boolean; + logging?: boolean; + timeout?: number; + framework: string; + buildRuntime?: string; + adapter?: string; + installCommand?: string; + buildCommand?: string; + outputDirectory?: string; + fallbackFile?: string; + specification?: string; + vars?: Record; + ignore?: string; +} + export interface TeamConfig { $id: string; name: string; @@ -141,10 +288,16 @@ export interface TopicConfig { subscribe?: string[]; } -export interface CommandDescription { - [key: string]: string; -} - -export interface ParsedData { - [key: string]: unknown; +export interface ProjectConfigData extends ConfigData { + projectId?: string; + projectName?: string; + settings?: ProjectSettings; + functions?: FunctionConfig[]; + collections?: CollectionConfig[]; + databases?: DatabaseConfig[]; + buckets?: BucketConfig[]; + teams?: TeamConfig[]; + topics?: TopicConfig[]; + sites?: SiteConfig[]; + tables?: TableConfig[]; } diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 32267f69a..b0a082264 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -14,7 +14,7 @@ "url": "{{ sdk.gitURL }}" }, "scripts": { - "build": "tsc || true", + "build": "tsc", "prepublishOnly": "npm run build", "test": "echo \"Error: no test specified\" && exit 1", "linux-x64": "npm run build && pkg -t node18-linux-x64 -o build/appwrite-cli-linux-x64 package.json", diff --git a/tests/CLINode16Test.php b/tests/CLINode20Test.php similarity index 86% rename from tests/CLINode16Test.php rename to tests/CLINode20Test.php index 7a8dd1292..f9a9a7b78 100644 --- a/tests/CLINode16Test.php +++ b/tests/CLINode20Test.php @@ -5,7 +5,7 @@ use Appwrite\SDK\Language; use Appwrite\SDK\Language\CLI; -class CLINode16Test extends Base +class CLINode20Test extends Base { protected string $sdkName = 'cli'; protected string $sdkPlatform = 'server'; @@ -15,12 +15,12 @@ class CLINode16Test extends Base protected string $language = 'cli'; protected string $class = 'Appwrite\SDK\Language\CLI'; protected array $build = [ - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine npm install', - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine npm run build', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:20-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:20-alpine npm run build', 'cp tests/languages/cli/test.js tests/sdks/cli/test.js' ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine node test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:20-alpine node test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, @@ -37,7 +37,7 @@ public function getLanguage(): Language $language->setLogo(json_encode(" _ _ _ ___ __ _____ /_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \ - //_\\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ + //_\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ |_| |_| @@ -46,7 +46,7 @@ public function getLanguage(): Language $language->setLogoUnescaped(" _ _ _ ___ __ _____ /_\ _ __ _ ____ ___ __(_) |_ ___ / __\ / / \_ \ - //_\\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ + //_\\| '_ \| '_ \ \ /\ / / '__| | __/ _ \ / / / / / /\/ / _ \ |_) | |_) \ V V /| | | | || __/ / /___/ /___/\/ /_ \_/ \_/ .__/| .__/ \_/\_/ |_| |_|\__\___| \____/\____/\____/ |_| |_| ");