Skip to content

Commit

Permalink
feat: upload images and mutation dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Feb 12, 2025
1 parent 24655a2 commit 3063509
Show file tree
Hide file tree
Showing 32 changed files with 575 additions and 101 deletions.
5 changes: 5 additions & 0 deletions components/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project will be documented in this file.

## [5.4.0]

- add a command to upload assets
- add a command to execute mutations with dependencies

## [5.3.0]

- fix tenantId that was not setup on the .env file after installation
Expand Down
4 changes: 2 additions & 2 deletions components/cli/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
YELLOW=$(shell echo "\033[00;33m")
RED=$(shell echo "\033[00;31m")
RESTORE=$(shell echo "\033[0m")
BUN=bun
# BUN=/opt/homebrew/Cellar/[email protected]/1.1.45/bin/bun
# BUN=bun
BUN=/opt/homebrew/Cellar/[email protected]/1.1.45/bin/bun

.DEFAULT_GOAL := list

Expand Down
2 changes: 1 addition & 1 deletion components/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@crystallize/cli",
"version": "5.3.0",
"version": "5.4.0",
"description": "Crystallize CLI",
"module": "src/index.ts",
"repository": "https://github.com/CrystallizeAPI/crystallize-cli",
Expand Down
4 changes: 0 additions & 4 deletions components/cli/src/command/boilerplate/install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Provider } from 'jotai';
import type { CredentialRetriever } from '../../domain/contracts/credential-retriever';
import type { Logger } from '../../domain/contracts/logger';
import type { QueryBus, CommandBus } from '../../domain/contracts/bus';
import type { createClient } from '@crystallize/js-api-client';
import type { FetchAvailableTenantIdentifier } from '../../domain/contracts/fetch-available-tenant-identifier';
import { logo } from '../..';

Expand All @@ -18,7 +17,6 @@ type Deps = {
flySystem: FlySystem;
installBoilerplateCommandStore: InstallBoilerplateStore;
credentialsRetriever: CredentialRetriever;
createCrystallizeClient: typeof createClient;
logger: Logger;
queryBus: QueryBus;
fetchAvailableTenantIdentifier: FetchAvailableTenantIdentifier;
Expand All @@ -32,7 +30,6 @@ export const createInstallBoilerplateCommand = ({
credentialsRetriever,
queryBus,
commandBus,
createCrystallizeClient,
fetchAvailableTenantIdentifier,
logLevels,
}: Deps): Command => {
Expand Down Expand Up @@ -78,7 +75,6 @@ export const createInstallBoilerplateCommand = ({
queryBus={queryBus}
commandBus={commandBus}
fetchAvailableTenantIdentifier={fetchAvailableTenantIdentifier}
createCrystallizeClient={createCrystallizeClient}
logger={logger}
store={atoms}
credentialsRetriever={credentialsRetriever}
Expand Down
78 changes: 78 additions & 0 deletions components/cli/src/command/images/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Command, Argument, Option } from 'commander';
import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-and-token-option';
import type { Logger } from '../../domain/contracts/logger';
import type { CommandBus } from '../../domain/contracts/bus';
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
import type { FlySystem } from '../../domain/contracts/fly-system';
import type { AsyncCreateClient } from '../../domain/contracts/credential-retriever';
import pc from 'picocolors';

type Deps = {
logger: Logger;
commandBus: CommandBus;
flySystem: FlySystem;
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
createCrystallizeClient: AsyncCreateClient;
};
export const createImageUploadCommand = ({
getAuthenticatedUserWithInteractivityIfPossible,
commandBus,
logger,
flySystem,
}: Deps): Command => {
const command = new Command('upload');
command.description('Upload images(s) to a Tenant.');
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to invite on.'));
command.addArgument(new Argument('<file>', 'The file or the folder that contains the Images.'));
command.addArgument(new Argument('[output-file]', 'An optional file that will contain the mapping path:key.'));
command.addOption(new Option('-f, --force', 'Force and override the output-file if it exits.'));

addInteractiveAndTokenOption(command);
command.action(async (tenantIdentifier: string, file: string, outputFile: string, flags) => {
if (outputFile && (await flySystem.isFileExists(outputFile)) && !flags.force) {
throw new Error(`File ${outputFile} already exist.`);
}

const { credentials } = await getAuthenticatedUserWithInteractivityIfPossible({
isInteractive: !flags.noInteractive,
token_id: flags.token_id,
token_secret: flags.token_secret,
});

const images: string[] = [];
if (await flySystem.isFileExists(file)) {
images.push(file);
}

for await (const image of flySystem.loopInDirectory(file)) {
images.push(image);
}

const intent = commandBus.createCommand('UploadImages', {
paths: images,
credentials,
tenant: {
identifier: tenantIdentifier,
},
});
const { result } = await commandBus.dispatch(intent);

if (!result) {
logger.error(`Failed to upload images.`);
return;
}
logger.success(`Images uploaded.`);
if (!outputFile) {
for (const [path, key] of Object.entries(result.keys)) {
logger.complete(`\t - ${path} -> ${pc.yellowBright(key)}`);
}
return;
}

await flySystem.saveFile(outputFile, JSON.stringify(result.keys, null, 2) + `\r\n`);
logger.complete(`Mapping saved at ${outputFile}`);
return;
});

return command;
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const createDumpContentModelMassOperationCommand = ({
command.description('Create a valid Mass Operation file on your machine.');
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to use.'));
command.addArgument(new Argument('<file>', 'The file that will contains the Operations.'));
command.addOption(new Option('-f, --force', 'Force and override the if it exits.'));
command.addOption(new Option('-f, --force', 'Force and override the file it it exits.'));
addInteractiveAndTokenOption(command);

command.action(async (tenantIdentifier: string, inputFile: string, flags) => {
Expand Down
71 changes: 71 additions & 0 deletions components/cli/src/command/mass-operation/execute-mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Argument, Command } from 'commander';
import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-and-token-option';
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
import type { Logger } from '../../domain/contracts/logger';
import type { FlySystem } from '../../domain/contracts/fly-system';
import { ZodError } from 'zod';
import type { CommandBus } from '../../domain/contracts/bus';

type Deps = {
logger: Logger;
flySystem: FlySystem;
commandBus: CommandBus;
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
};

export const createExecuteMutationsCommand = ({
logger,
flySystem,
commandBus,
getAuthenticatedUserWithInteractivityIfPossible: getAuthenticatedUser,
}: Deps): Command => {
const command = new Command('execute-mutations');
command.description('Execute mutations with different set (client-side).');
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to use.'));
command.addArgument(new Argument('<file>', 'The file that contains the Mutations.'));
command.addArgument(new Argument('[image-mapping-file]', 'An optional file for images mapping.'));
addInteractiveAndTokenOption(command);

command.action(async (tenantIdentifier: string, inputFile: string, imageMappingFile: string, flags) => {
if (!(await flySystem.isFileExists(inputFile))) {
throw new Error(`File ${inputFile} was not found.`);
}
try {
const { credentials } = await getAuthenticatedUser({
isInteractive: !flags.noInteractive,
token_id: flags.token_id,
token_secret: flags.token_secret,
});

let imageMapping: Record<string, string> = {};
if (imageMappingFile) {
imageMapping = await flySystem.loadJsonFile<Record<string, string>>(imageMappingFile);
}

const intent = commandBus.createCommand('ExecuteMutations', {
filePath: inputFile,
tenant: {
identifier: tenantIdentifier,
},
credentials,
placeholderMap: {
images: imageMapping,
},
});
const { result } = await commandBus.dispatch(intent);
if (!result) {
throw new Error('Failed to execute the Mutations file.');
}
// console.dir({ result }, { depth: null });
logger.success(`Mutations executed.`);
} catch (error) {
if (error instanceof ZodError) {
for (const issue of error.issues) {
logger.error(`[${issue.path.join('.')}]: ${issue.message}`);
}
}
throw error;
}
});
return command;
};
7 changes: 3 additions & 4 deletions components/cli/src/command/mass-operation/run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import { Argument, Command, Option } from 'commander';
import type { Logger } from '../../domain/contracts/logger';
import type { CommandBus } from '../../domain/contracts/bus';
import type { Operation, Operations } from '@crystallize/schema/mass-operation';
import type { CredentialRetriever } from '../../domain/contracts/credential-retriever';
import type { AsyncCreateClient, CredentialRetriever } from '../../domain/contracts/credential-retriever';
import pc from 'picocolors';
import { ZodError } from 'zod';
import type { createClient } from '@crystallize/js-api-client';
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-and-token-option';

type Deps = {
logger: Logger;
commandBus: CommandBus;
credentialsRetriever: CredentialRetriever;
createCrystallizeClient: typeof createClient;
createCrystallizeClient: AsyncCreateClient;
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
};

Expand Down Expand Up @@ -78,7 +77,7 @@ export const createRunMassOperationCommand = ({
throw new Error('Task not started. Please check the logs for more information.');
}

const crystallizeClient = createCrystallizeClient({
const crystallizeClient = await createCrystallizeClient({
tenantIdentifier,
accessTokenId: credentials.ACCESS_TOKEN_ID,
accessTokenSecret: credentials.ACCESS_TOKEN_SECRET,
Expand Down
2 changes: 0 additions & 2 deletions components/cli/src/command/tenant/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-
import type { Logger } from '../../domain/contracts/logger';
import type { CommandBus } from '../../domain/contracts/bus';
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
import type { createClient } from '@crystallize/js-api-client';

type Deps = {
logger: Logger;
commandBus: CommandBus;
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
createCrystallizeClient: typeof createClient;
};
export const createCreateInviteTokenCommand = ({
getAuthenticatedUserWithInteractivityIfPossible,
Expand Down
2 changes: 0 additions & 2 deletions components/cli/src/command/token/shop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-
import type { Logger } from '../../domain/contracts/logger';
import type { QueryBus } from '../../domain/contracts/bus';
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
import type { createClient } from '@crystallize/js-api-client';

type Deps = {
logger: Logger;
queryBus: QueryBus;
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
createCrystallizeClient: typeof createClient;
};
export const createGetShopAuthTokenCommand = ({
getAuthenticatedUserWithInteractivityIfPossible,
Expand Down
2 changes: 0 additions & 2 deletions components/cli/src/command/token/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-
import type { Logger } from '../../domain/contracts/logger';
import type { QueryBus } from '../../domain/contracts/bus';
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
import type { createClient } from '@crystallize/js-api-client';

type Deps = {
logger: Logger;
queryBus: QueryBus;
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
createCrystallizeClient: typeof createClient;
};
export const createGetStaticAuthTokenCommand = ({
getAuthenticatedUserWithInteractivityIfPossible,
Expand Down
21 changes: 20 additions & 1 deletion components/cli/src/content/completion_file.bash
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ _crystallize_completions() {
;;
mass-operation)
if [[ "${COMP_CWORD}" -eq 2 ]]; then
local options="run dump-content-model ${default_options}"
local options="run dump-content-model execute-mutations ${default_options}"
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
return 0
fi
Expand All @@ -53,6 +53,11 @@ _crystallize_completions() {
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
return 0
;;
execute-mutations)
local options="${i_login_options} ${default_options}"
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
return 0
;;
esac
;;
tenant)
Expand Down Expand Up @@ -98,6 +103,20 @@ _crystallize_completions() {
;;
esac
;;
image)
if [[ "${COMP_CWORD}" -eq 2 ]]; then
local options="upload ${default_options}"
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
return 0
fi
case "${subcmd}" in
upload)
local options="${i_login_options} --force ${default_options}"
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
return 0
;;
esac
;;
esac
}

Expand Down
11 changes: 7 additions & 4 deletions components/cli/src/core/create-credentials-retriever.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { createClient } from '@crystallize/js-api-client';
import type { CredentialRetriever, CredentialRetrieverOptions } from '../domain/contracts/credential-retriever';
import type {
AsyncCreateClient,
CredentialRetriever,
CredentialRetrieverOptions,
} from '../domain/contracts/credential-retriever';
import type { PimCredentials } from '../domain/contracts/models/credentials';
import type { FlySystem } from '../domain/contracts/fly-system';
import os from 'os';
Expand All @@ -8,7 +11,7 @@ type Deps = {
options?: CredentialRetrieverOptions;
fallbackFile: string;
flySystem: FlySystem;
createCrystallizeClient: typeof createClient;
createCrystallizeClient: AsyncCreateClient;
};
export const createCredentialsRetriever = ({
options,
Expand Down Expand Up @@ -58,7 +61,7 @@ export const createCredentialsRetriever = ({
};

const checkCredentials = async (credentials: PimCredentials) => {
const apiClient = createCrystallizeClient({
const apiClient = await createCrystallizeClient({
tenantIdentifier: '',
accessTokenId: credentials.ACCESS_TOKEN_ID,
accessTokenSecret: credentials.ACCESS_TOKEN_SECRET,
Expand Down
21 changes: 18 additions & 3 deletions components/cli/src/core/create-crystallize-client-builder.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { createClient, type ClientConfiguration, type CreateClientOptions } from '@crystallize/js-api-client';
import type { AsyncCreateClient } from '../domain/contracts/credential-retriever';

type Deps = {
crystallizeEnvironment: 'staging' | 'production';
};
export const createCrystallizeClientBuilder =
({ crystallizeEnvironment }: Deps): typeof createClient =>
(configuration: ClientConfiguration, options?: CreateClientOptions) =>
createClient(
({ crystallizeEnvironment }: Deps): AsyncCreateClient =>
async (configuration: ClientConfiguration, options?: CreateClientOptions) => {
if (configuration.tenantIdentifier.length > 0 && (configuration.tenantId || '').length === 0) {
const tempClient = createClient({
...configuration,
origin: crystallizeEnvironment === 'staging' ? '-dev.crystallize.digital' : '.crystallize.com',
});
const tenantInfo = await tempClient.nextPimApi(
`query { tenant(identifier:"${configuration.tenantIdentifier}") { ... on Tenant { id } } }`,
);
if (!tenantInfo.tenant.id) {
throw new Error(`Tenant Id for identifier ${configuration.tenantIdentifier} not found`);
}
configuration.tenantId = tenantInfo.tenant.id;
}
return createClient(
{
...configuration,
origin: crystallizeEnvironment === 'staging' ? '-dev.crystallize.digital' : '.crystallize.com',
},
options,
);
};
Loading

0 comments on commit 3063509

Please sign in to comment.