Skip to content

Commit b08b5d1

Browse files
authored
adding slas clients and commands (#3)
* adding slas clients and commands * remove unused file
1 parent 801f90e commit b08b5d1

File tree

18 files changed

+4309
-102
lines changed

18 files changed

+4309
-102
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {Args, Flags} from '@oclif/core';
2+
import {randomUUID} from 'node:crypto';
3+
import {
4+
SlasClientCommand,
5+
type Client,
6+
type ClientOutput,
7+
parseMultiple,
8+
normalizeClientResponse,
9+
printClientDetails,
10+
formatApiError,
11+
} from '../../../utils/slas/client.js';
12+
import {t} from '../../../i18n/index.js';
13+
14+
export default class SlasClientCreate extends SlasClientCommand<typeof SlasClientCreate> {
15+
static args = {
16+
clientId: Args.string({
17+
description: 'SLAS client ID to create or update (generates UUID if omitted)',
18+
required: false,
19+
}),
20+
};
21+
22+
static description = t('commands.slas.client.create.description', 'Create or update a SLAS client');
23+
24+
static enableJsonFlag = true;
25+
26+
static examples = [
27+
'<%= config.bin %> <%= command.id %> --tenant-id abcd_123 --channels RefArch --scopes sfcc.shopper-products,sfcc.shopper-search --redirect-uri http://localhost:3000/callback',
28+
'<%= config.bin %> <%= command.id %> my-client-id --tenant-id abcd_123 --name "My Client" --channels RefArch --scopes sfcc.shopper-products --redirect-uri http://localhost:3000/callback --public',
29+
'<%= config.bin %> <%= command.id %> my-client-id --tenant-id abcd_123 --name "My Client" --channels RefArch --scopes sfcc.shopper-products --redirect-uri http://localhost:3000/callback --json',
30+
];
31+
32+
static flags = {
33+
...SlasClientCommand.baseFlags,
34+
name: Flags.string({
35+
description: 'Display name for the client (generates timestamped name if omitted)',
36+
}),
37+
channels: Flags.string({
38+
description: 'Site IDs/channels (comma-separated or multiple flags)',
39+
required: true,
40+
multiple: true,
41+
}),
42+
scopes: Flags.string({
43+
description: 'OAuth scopes for the client (comma-separated or multiple flags)',
44+
required: true,
45+
multiple: true,
46+
}),
47+
'redirect-uri': Flags.string({
48+
description: 'Redirect URIs (comma-separated or multiple flags)',
49+
required: true,
50+
multiple: true,
51+
}),
52+
'callback-uri': Flags.string({
53+
description: 'Callback URIs for passwordless login (comma-separated or multiple flags)',
54+
multiple: true,
55+
}),
56+
secret: Flags.string({
57+
description: 'Client secret for private clients (if omitted, one will be generated)',
58+
}),
59+
public: Flags.boolean({
60+
description: 'Create a public client (default is private)',
61+
default: false,
62+
}),
63+
};
64+
65+
async run(): Promise<ClientOutput> {
66+
this.requireOAuthCredentials();
67+
68+
const {
69+
'tenant-id': tenantId,
70+
name,
71+
channels,
72+
scopes,
73+
'redirect-uri': redirectUri,
74+
'callback-uri': callbackUri,
75+
secret,
76+
public: isPublic,
77+
} = this.flags;
78+
79+
// Use provided client ID or generate a UUID
80+
const clientId = this.args.clientId ?? randomUUID().toLowerCase();
81+
82+
// Use provided name or generate a timestamped name
83+
const clientName = name ?? `b2c-cli client ${new Date().toISOString()}`;
84+
85+
const parsedChannels = parseMultiple(channels);
86+
const parsedScopes = parseMultiple(scopes);
87+
const parsedRedirectUri = parseMultiple(redirectUri);
88+
const parsedCallbackUri = callbackUri ? parseMultiple(callbackUri) : undefined;
89+
90+
if (!this.jsonEnabled()) {
91+
this.log(t('commands.slas.client.create.creating', 'Creating/updating SLAS client {{clientId}}...', {clientId}));
92+
}
93+
94+
const slasClient = this.getSlasClient();
95+
96+
// eslint-disable-next-line new-cap
97+
const {data, error, response} = await slasClient.PUT('/tenants/{tenantId}/clients/{clientId}', {
98+
params: {
99+
path: {tenantId, clientId},
100+
},
101+
body: {
102+
clientId,
103+
name: clientName,
104+
channels: parsedChannels,
105+
scopes: parsedScopes,
106+
redirectUri: parsedRedirectUri,
107+
callbackUri: parsedCallbackUri,
108+
// For private clients, use provided secret or generate one with sk_ prefix
109+
// For public clients, secret is ignored but still required by the schema
110+
secret: secret ?? (isPublic ? '' : `sk_${randomUUID().replaceAll('-', '')}`),
111+
isPrivateClient: !isPublic,
112+
},
113+
});
114+
115+
if (error) {
116+
this.error(
117+
t('commands.slas.client.create.error', 'Failed to create/update SLAS client: {{message}}', {
118+
message: formatApiError(error),
119+
}),
120+
);
121+
}
122+
123+
const client = data as Client;
124+
const wasCreated = response.status === 201;
125+
126+
const output = normalizeClientResponse(client);
127+
// Use our parsed values as fallback if API doesn't return them
128+
if (output.scopes.length === 0) output.scopes = parsedScopes;
129+
if (output.channels.length === 0) output.channels = parsedChannels;
130+
if (!output.redirectUri) output.redirectUri = parsedRedirectUri.join(', ');
131+
132+
if (this.jsonEnabled()) {
133+
return output;
134+
}
135+
136+
// Human-readable output
137+
if (wasCreated) {
138+
this.log(t('commands.slas.client.create.created', 'SLAS client created successfully.'));
139+
} else {
140+
this.log(t('commands.slas.client.create.updated', 'SLAS client updated successfully.'));
141+
}
142+
143+
printClientDetails(output);
144+
145+
return output;
146+
}
147+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {Args} from '@oclif/core';
2+
import {SlasClientCommand, formatApiError} from '../../../utils/slas/client.js';
3+
import {t} from '../../../i18n/index.js';
4+
5+
interface DeleteOutput {
6+
clientId: string;
7+
deleted: boolean;
8+
}
9+
10+
export default class SlasClientDelete extends SlasClientCommand<typeof SlasClientDelete> {
11+
static args = {
12+
clientId: Args.string({
13+
description: 'SLAS client ID to delete',
14+
required: true,
15+
}),
16+
};
17+
18+
static description = t('commands.slas.client.delete.description', 'Delete a SLAS client');
19+
20+
static enableJsonFlag = true;
21+
22+
static examples = [
23+
'<%= config.bin %> <%= command.id %> my-client-id --tenant-id abcd_123',
24+
'<%= config.bin %> <%= command.id %> my-client-id --tenant-id abcd_123 --json',
25+
];
26+
27+
static flags = {
28+
...SlasClientCommand.baseFlags,
29+
};
30+
31+
async run(): Promise<DeleteOutput> {
32+
this.requireOAuthCredentials();
33+
34+
const {'tenant-id': tenantId} = this.flags;
35+
const {clientId} = this.args;
36+
37+
if (!this.jsonEnabled()) {
38+
this.log(t('commands.slas.client.delete.deleting', 'Deleting SLAS client {{clientId}}...', {clientId}));
39+
}
40+
41+
const slasClient = this.getSlasClient();
42+
43+
// eslint-disable-next-line new-cap
44+
const {error} = await slasClient.DELETE('/tenants/{tenantId}/clients/{clientId}', {
45+
params: {
46+
path: {tenantId, clientId},
47+
},
48+
});
49+
50+
if (error) {
51+
this.error(
52+
t('commands.slas.client.delete.error', 'Failed to delete SLAS client: {{message}}', {
53+
message: formatApiError(error),
54+
}),
55+
);
56+
}
57+
58+
const output: DeleteOutput = {
59+
clientId,
60+
deleted: true,
61+
};
62+
63+
if (this.jsonEnabled()) {
64+
return output;
65+
}
66+
67+
this.log(t('commands.slas.client.delete.success', 'SLAS client {{clientId}} deleted successfully.', {clientId}));
68+
69+
return output;
70+
}
71+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {Args} from '@oclif/core';
2+
import {
3+
SlasClientCommand,
4+
type Client,
5+
type ClientOutput,
6+
normalizeClientResponse,
7+
printClientDetails,
8+
formatApiError,
9+
} from '../../../utils/slas/client.js';
10+
import {t} from '../../../i18n/index.js';
11+
12+
export default class SlasClientGet extends SlasClientCommand<typeof SlasClientGet> {
13+
static args = {
14+
clientId: Args.string({
15+
description: 'SLAS client ID to retrieve',
16+
required: true,
17+
}),
18+
};
19+
20+
static description = t('commands.slas.client.get.description', 'Get a SLAS client');
21+
22+
static enableJsonFlag = true;
23+
24+
static examples = [
25+
'<%= config.bin %> <%= command.id %> my-client-id --tenant-id abcd_123',
26+
'<%= config.bin %> <%= command.id %> my-client-id --tenant-id abcd_123 --json',
27+
];
28+
29+
static flags = {
30+
...SlasClientCommand.baseFlags,
31+
};
32+
33+
async run(): Promise<ClientOutput> {
34+
this.requireOAuthCredentials();
35+
36+
const {'tenant-id': tenantId} = this.flags;
37+
const {clientId} = this.args;
38+
39+
if (!this.jsonEnabled()) {
40+
this.log(t('commands.slas.client.get.fetching', 'Fetching SLAS client {{clientId}}...', {clientId}));
41+
}
42+
43+
const slasClient = this.getSlasClient();
44+
45+
// eslint-disable-next-line new-cap
46+
const {data, error} = await slasClient.GET('/tenants/{tenantId}/clients/{clientId}', {
47+
params: {
48+
path: {tenantId, clientId},
49+
},
50+
});
51+
52+
if (error) {
53+
this.error(
54+
t('commands.slas.client.get.error', 'Failed to get SLAS client: {{message}}', {
55+
message: formatApiError(error),
56+
}),
57+
);
58+
}
59+
60+
const output = normalizeClientResponse(data as Client);
61+
62+
if (this.jsonEnabled()) {
63+
return output;
64+
}
65+
66+
printClientDetails(output, false);
67+
68+
return output;
69+
}
70+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {ux} from '@oclif/core';
2+
import cliui from 'cliui';
3+
import {
4+
SlasClientCommand,
5+
type Client,
6+
type ClientOutput,
7+
normalizeClientResponse,
8+
formatApiError,
9+
} from '../../../utils/slas/client.js';
10+
import {t} from '../../../i18n/index.js';
11+
12+
interface ClientListOutput {
13+
clients: ClientOutput[];
14+
}
15+
16+
export default class SlasClientList extends SlasClientCommand<typeof SlasClientList> {
17+
static description = t('commands.slas.client.list.description', 'List SLAS clients for a tenant');
18+
19+
static enableJsonFlag = true;
20+
21+
static examples = [
22+
'<%= config.bin %> <%= command.id %> --tenant-id abcd_123',
23+
'<%= config.bin %> <%= command.id %> --tenant-id abcd_123 --json',
24+
];
25+
26+
static flags = {
27+
...SlasClientCommand.baseFlags,
28+
};
29+
30+
async run(): Promise<ClientListOutput> {
31+
this.requireOAuthCredentials();
32+
33+
const {'tenant-id': tenantId} = this.flags;
34+
35+
if (!this.jsonEnabled()) {
36+
this.log(t('commands.slas.client.list.fetching', 'Fetching SLAS clients for tenant {{tenantId}}...', {tenantId}));
37+
}
38+
39+
const slasClient = this.getSlasClient();
40+
41+
// eslint-disable-next-line new-cap
42+
const {data, error} = await slasClient.GET('/tenants/{tenantId}/clients', {
43+
params: {
44+
path: {tenantId},
45+
},
46+
});
47+
48+
if (error) {
49+
this.error(
50+
t('commands.slas.client.list.error', 'Failed to list SLAS clients: {{message}}', {
51+
message: formatApiError(error),
52+
}),
53+
);
54+
}
55+
56+
const clients = ((data as {data?: Client[]})?.data ?? []).map((client) => normalizeClientResponse(client));
57+
const output: ClientListOutput = {clients};
58+
59+
if (this.jsonEnabled()) {
60+
return output;
61+
}
62+
63+
if (clients.length === 0) {
64+
this.log(t('commands.slas.client.list.noClients', 'No SLAS clients found.'));
65+
return output;
66+
}
67+
68+
this.printClientsTable(clients);
69+
70+
return output;
71+
}
72+
73+
private printClientsTable(clients: ClientOutput[]): void {
74+
const ui = cliui({width: process.stdout.columns || 80});
75+
76+
// Header
77+
ui.div(
78+
{text: 'Client ID', width: 40, padding: [0, 2, 0, 0]},
79+
{text: 'Name', width: 30, padding: [0, 2, 0, 0]},
80+
{text: 'Private', padding: [0, 0, 0, 0]},
81+
);
82+
83+
// Separator
84+
ui.div({text: '─'.repeat(80), padding: [0, 0, 0, 0]});
85+
86+
// Rows
87+
for (const client of clients) {
88+
ui.div(
89+
{text: client.clientId, width: 40, padding: [0, 2, 0, 0]},
90+
{text: client.name, width: 30, padding: [0, 2, 0, 0]},
91+
{text: String(client.isPrivateClient), padding: [0, 0, 0, 0]},
92+
);
93+
}
94+
95+
ux.stdout(ui.toString());
96+
}
97+
}

0 commit comments

Comments
 (0)