@norbix.ai/cli 1.0.0 → 1.1.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.
- package/README.md +2 -1
- package/dist/commands/push/campaign/batch.js +39 -0
- package/dist/commands/push/campaign/batches.js +24 -0
- package/dist/commands/push/campaign/create.js +102 -0
- package/dist/commands/push/campaign/delete.js +27 -0
- package/dist/commands/push/campaign/message.js +28 -0
- package/dist/commands/push/campaign/messages.js +26 -0
- package/dist/commands/push/device.js +16 -0
- package/dist/commands/push/devices.js +33 -0
- package/dist/commands/push/disable-dependencies.js +12 -0
- package/dist/commands/push/disable.js +24 -0
- package/dist/commands/push/enable.js +12 -0
- package/dist/commands/push/integration/app-request.js +34 -0
- package/dist/commands/push/integration/confirm-delivery.js +19 -0
- package/dist/commands/push/integration/save.js +41 -0
- package/dist/commands/push/stop.js +4 -6
- package/dist/commands/push/template/create.js +20 -0
- package/dist/commands/push/template/render.js +28 -0
- package/dist/commands/push/template/tokens.js +16 -0
- package/dist/commands/push/template/update.js +25 -0
- package/dist/lib/push.js +70 -0
- package/oclif.manifest.json +2518 -416
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,7 +48,8 @@ norbix scheduler list
|
|
|
48
48
|
| `norbix apikeys list/regenerate` | Show or regenerate project API keys |
|
|
49
49
|
| `norbix webhooks show/secret/enable/disable/remove` | Inspect the webhook integration |
|
|
50
50
|
| `norbix email templates/template/clone/archive/unarchive/delete/campaigns/campaign/stop` | Email templates and campaigns |
|
|
51
|
-
| `norbix push ...`
|
|
51
|
+
| `norbix push ...` | Push module, integrations, devices, templates, campaigns — every push endpoint, see [docs/push.md](docs/push.md) |
|
|
52
|
+
| `norbix sms ...` | SMS — same commands as email |
|
|
52
53
|
| `norbix account profile/status/usage/projects/team/regions/billing-portal` | Account-level info |
|
|
53
54
|
| `norbix payments integrations/triggers/trigger/enable/disable` | Payment integrations and triggers |
|
|
54
55
|
| `norbix integrations <module>` | List integrations of any module |
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushCampaignBatch extends BaseCommand {
|
|
4
|
+
static description = `List the notifications in one batch — or show one of them
|
|
5
|
+
|
|
6
|
+
Give a notification ID as the third argument to see just that notification.`;
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> push campaign batch 66b2f0a1... b7c3...',
|
|
9
|
+
'<%= config.bin %> push campaign batch 66b2f0a1... b7c3... n9d4...',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
id: Args.string({ required: true, description: 'Campaign ID' }),
|
|
13
|
+
batchId: Args.string({ required: true, description: 'Batch ID (from `push campaign batches`)' }),
|
|
14
|
+
notificationId: Args.string({ description: 'Notification ID' }),
|
|
15
|
+
};
|
|
16
|
+
static flags = {
|
|
17
|
+
'page-size': Flags.integer({ description: 'Items per page' }),
|
|
18
|
+
after: Flags.string({ description: 'Cursor from the previous page' }),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { args, flags } = await this.parse(PushCampaignBatch);
|
|
22
|
+
const client = this.client(flags);
|
|
23
|
+
const n = client.hub.notifications;
|
|
24
|
+
const res = args.notificationId
|
|
25
|
+
? await n.getPushCampaignBatchNotification({
|
|
26
|
+
id: args.id,
|
|
27
|
+
batchId: args.batchId,
|
|
28
|
+
notificationId: args.notificationId,
|
|
29
|
+
})
|
|
30
|
+
: await n.getPushCampaignBatchNotifications({
|
|
31
|
+
id: args.id,
|
|
32
|
+
batchId: args.batchId,
|
|
33
|
+
pageSize: flags['page-size'],
|
|
34
|
+
startingAfter: flags.after,
|
|
35
|
+
});
|
|
36
|
+
this.print(res);
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushCampaignBatches extends BaseCommand {
|
|
4
|
+
static description = 'List the send batches of a push campaign';
|
|
5
|
+
static examples = ['<%= config.bin %> push campaign batches 66b2f0a1...'];
|
|
6
|
+
static args = {
|
|
7
|
+
id: Args.string({ required: true, description: 'Campaign ID' }),
|
|
8
|
+
};
|
|
9
|
+
static flags = {
|
|
10
|
+
'page-size': Flags.integer({ description: 'Items per page' }),
|
|
11
|
+
after: Flags.string({ description: 'Cursor from the previous page' }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { args, flags } = await this.parse(PushCampaignBatches);
|
|
15
|
+
const client = this.client(flags);
|
|
16
|
+
const res = await client.hub.notifications.getPushCampaignBatches({
|
|
17
|
+
id: args.id,
|
|
18
|
+
pageSize: flags['page-size'],
|
|
19
|
+
startingAfter: flags.after,
|
|
20
|
+
});
|
|
21
|
+
this.print(res);
|
|
22
|
+
return res;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
import { parseTokens, readJsonObject } from '../../../lib/push.js';
|
|
4
|
+
/** --audience value → the `source` discriminator the server routes the body by. */
|
|
5
|
+
const AUDIENCES = {
|
|
6
|
+
'all-users': 'allUsers',
|
|
7
|
+
users: 'specifiedUsers',
|
|
8
|
+
'account-users': 'accountUsers',
|
|
9
|
+
collection: 'collection',
|
|
10
|
+
devices: 'devices',
|
|
11
|
+
};
|
|
12
|
+
export default class PushCampaignCreate extends BaseCommand {
|
|
13
|
+
static description = `Create a push campaign
|
|
14
|
+
|
|
15
|
+
--audience picks who receives it:
|
|
16
|
+
all-users every user; narrow with --role / --tag
|
|
17
|
+
users the users named with --user
|
|
18
|
+
account-users the account users named with --user
|
|
19
|
+
collection the users referenced by --field in the --schema collection
|
|
20
|
+
devices raw device tokens, --device <token>:<family> (Ios, Android, Chrome, Safari, Expo)
|
|
21
|
+
|
|
22
|
+
Without --at the campaign is sent right away. Anything the flags do not cover
|
|
23
|
+
goes in --config as a JSON object and is merged into the campaign.`;
|
|
24
|
+
static examples = [
|
|
25
|
+
'<%= config.bin %> push campaign create --template 66b2f0a1... --audience all-users --tag beta',
|
|
26
|
+
'<%= config.bin %> push campaign create --template 66b2f0a1... --audience users --user usr_1 --user usr_2',
|
|
27
|
+
'<%= config.bin %> push campaign create --template 66b2f0a1... --audience devices --device dGVzdA==:Ios',
|
|
28
|
+
];
|
|
29
|
+
static flags = {
|
|
30
|
+
template: Flags.string({ required: true, description: 'Template ID' }),
|
|
31
|
+
audience: Flags.string({ required: true, description: 'Who receives it', options: Object.keys(AUDIENCES) }),
|
|
32
|
+
user: Flags.string({ description: 'User ID (users / account-users; repeat for several)', multiple: true }),
|
|
33
|
+
role: Flags.string({ description: 'Role name (all-users / collection; repeat for several)', multiple: true }),
|
|
34
|
+
tag: Flags.string({ description: 'User tag (all-users; repeat for several)', multiple: true }),
|
|
35
|
+
schema: Flags.string({ description: 'Collection name (collection)' }),
|
|
36
|
+
field: Flags.string({ description: 'Field that holds the recipient (collection; repeat for several)', multiple: true }),
|
|
37
|
+
'field-type': Flags.string({ description: 'What --field holds (collection)', options: ['User', 'Email'] }),
|
|
38
|
+
device: Flags.string({ description: 'Device as <token>:<family> (devices; repeat for several)', multiple: true }),
|
|
39
|
+
integration: Flags.string({ description: 'Push integration ID (defaults to the project default)' }),
|
|
40
|
+
language: Flags.string({ description: 'Template language to send' }),
|
|
41
|
+
notes: Flags.string({ description: 'Internal notes' }),
|
|
42
|
+
at: Flags.string({ description: 'Send later: ISO 8601 date-time or Unix seconds' }),
|
|
43
|
+
token: Flags.string({ description: 'Token value as key=value (repeat for several)', multiple: true }),
|
|
44
|
+
'database-integration': Flags.string({ description: 'Database integration ID (defaults to the project default)' }),
|
|
45
|
+
config: Flags.string({ description: 'Extra campaign fields as a JSON object, @file or -' }),
|
|
46
|
+
};
|
|
47
|
+
async run() {
|
|
48
|
+
const { flags } = await this.parse(PushCampaignCreate);
|
|
49
|
+
const client = this.client(flags);
|
|
50
|
+
const extra = flags.config ? await readJsonObject(flags.config, 'config') : {};
|
|
51
|
+
const campaign = {
|
|
52
|
+
...extra,
|
|
53
|
+
source: AUDIENCES[flags.audience],
|
|
54
|
+
templateId: flags.template,
|
|
55
|
+
integrationId: flags.integration,
|
|
56
|
+
language: flags.language,
|
|
57
|
+
notes: flags.notes,
|
|
58
|
+
campaignTime: flags.at ? toUnixSeconds(flags.at) : undefined,
|
|
59
|
+
mappedTokens: parseTokens(flags.token),
|
|
60
|
+
...this.audienceFields(flags),
|
|
61
|
+
};
|
|
62
|
+
const res = await client.hub.notifications.createPushCampaign({
|
|
63
|
+
campaign,
|
|
64
|
+
databaseIntegrationId: flags['database-integration'],
|
|
65
|
+
});
|
|
66
|
+
this.print(res);
|
|
67
|
+
return res;
|
|
68
|
+
}
|
|
69
|
+
audienceFields(flags) {
|
|
70
|
+
switch (flags.audience) {
|
|
71
|
+
case 'all-users':
|
|
72
|
+
return { rolesNames: flags.role, userTags: flags.tag };
|
|
73
|
+
case 'users':
|
|
74
|
+
case 'account-users':
|
|
75
|
+
if (!flags.user?.length)
|
|
76
|
+
this.error(`--audience ${flags.audience} needs at least one --user`);
|
|
77
|
+
return { userRecipients: flags.user };
|
|
78
|
+
case 'collection':
|
|
79
|
+
if (!flags.schema || !flags.field?.length)
|
|
80
|
+
this.error('--audience collection needs --schema and --field');
|
|
81
|
+
return { schemaName: flags.schema, fields: flags.field, fieldType: flags['field-type'], roleNames: flags.role };
|
|
82
|
+
default:
|
|
83
|
+
if (!flags.device?.length)
|
|
84
|
+
this.error('--audience devices needs at least one --device <token>:<family>');
|
|
85
|
+
return { devices: flags.device.map((d) => this.device(d)) };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
device(value) {
|
|
89
|
+
const at = value.lastIndexOf(':');
|
|
90
|
+
if (at < 1)
|
|
91
|
+
this.error(`--device must look like <token>:<family>, got "${value}"`);
|
|
92
|
+
return { token: value.slice(0, at), deliveryFamily: value.slice(at + 1) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function toUnixSeconds(value) {
|
|
96
|
+
if (/^\d+$/.test(value))
|
|
97
|
+
return Number(value);
|
|
98
|
+
const ms = Date.parse(value);
|
|
99
|
+
if (Number.isNaN(ms))
|
|
100
|
+
throw new Error(`--at is not a date: "${value}"`);
|
|
101
|
+
return Math.floor(ms / 1000);
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../../base.js';
|
|
4
|
+
export default class PushCampaignDelete extends BaseCommand {
|
|
5
|
+
static description = 'Delete a push campaign';
|
|
6
|
+
static examples = ['<%= config.bin %> push campaign delete 66b2f0a1... --yes'];
|
|
7
|
+
static args = {
|
|
8
|
+
id: Args.string({ required: true, description: 'Campaign ID' }),
|
|
9
|
+
};
|
|
10
|
+
static flags = {
|
|
11
|
+
yes: Flags.boolean({ char: 'y', description: 'Skip the confirmation prompt', default: false }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { args, flags } = await this.parse(PushCampaignDelete);
|
|
15
|
+
const client = this.client(flags);
|
|
16
|
+
if (!flags.yes && process.stdout.isTTY) {
|
|
17
|
+
const ok = await confirm({ message: `Delete push campaign ${args.id}?`, default: false });
|
|
18
|
+
if (!ok)
|
|
19
|
+
return this.print('Cancelled.');
|
|
20
|
+
}
|
|
21
|
+
// The route is `/campaigns/{Id}` but the generated request type is empty,
|
|
22
|
+
// so the id goes in as `Id` — the transport fills tokens by exact name.
|
|
23
|
+
const res = await client.hub.notifications.deletePushCampaign({ Id: args.id });
|
|
24
|
+
this.print(`Campaign ${args.id} deleted.`);
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushCampaignMessage extends BaseCommand {
|
|
4
|
+
static description = 'Show one push message a campaign sent';
|
|
5
|
+
static examples = ['<%= config.bin %> push campaign message 66b2f0a1... n9d4... --batch b7c3...'];
|
|
6
|
+
static args = {
|
|
7
|
+
id: Args.string({ required: true, description: 'Campaign ID' }),
|
|
8
|
+
messageId: Args.string({ required: true, description: 'Message (notification) ID' }),
|
|
9
|
+
};
|
|
10
|
+
static flags = {
|
|
11
|
+
batch: Flags.string({ required: true, description: 'Batch the message belongs to' }),
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { args, flags } = await this.parse(PushCampaignMessage);
|
|
15
|
+
const client = this.client(flags);
|
|
16
|
+
// The gateway route now names its token `{notificationId}`, and the
|
|
17
|
+
// regenerated request type has the matching field — so the workaround
|
|
18
|
+
// this command used to carry (an extra `id` plus a cast, because the
|
|
19
|
+
// token and the field had different names) is gone.
|
|
20
|
+
const res = await client.hub.notifications.getPushCampaignMessage({
|
|
21
|
+
campaignId: args.id,
|
|
22
|
+
notificationId: args.messageId,
|
|
23
|
+
campaignBatchId: flags.batch,
|
|
24
|
+
});
|
|
25
|
+
this.print(res);
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushCampaignMessages extends BaseCommand {
|
|
4
|
+
static description = 'List the push messages a campaign sent';
|
|
5
|
+
static examples = ['<%= config.bin %> push campaign messages 66b2f0a1... --batch b7c3...'];
|
|
6
|
+
static args = {
|
|
7
|
+
id: Args.string({ required: true, description: 'Campaign ID' }),
|
|
8
|
+
};
|
|
9
|
+
static flags = {
|
|
10
|
+
batch: Flags.string({ description: 'Only this batch (from `push campaign batches`)' }),
|
|
11
|
+
'page-size': Flags.integer({ description: 'Items per page' }),
|
|
12
|
+
after: Flags.string({ description: 'Cursor from the previous page' }),
|
|
13
|
+
};
|
|
14
|
+
async run() {
|
|
15
|
+
const { args, flags } = await this.parse(PushCampaignMessages);
|
|
16
|
+
const client = this.client(flags);
|
|
17
|
+
const res = await client.hub.notifications.getPushCampaignMessages({
|
|
18
|
+
campaignId: args.id,
|
|
19
|
+
campaignBatchId: flags.batch,
|
|
20
|
+
pageSize: flags['page-size'],
|
|
21
|
+
startingAfter: flags.after,
|
|
22
|
+
});
|
|
23
|
+
this.print(res);
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
export default class PushDevice extends BaseCommand {
|
|
4
|
+
static description = 'Show one registered push device, with the user it belongs to';
|
|
5
|
+
static examples = ['<%= config.bin %> push device pnd_123'];
|
|
6
|
+
static args = {
|
|
7
|
+
id: Args.string({ required: true, description: 'Device ID' }),
|
|
8
|
+
};
|
|
9
|
+
async run() {
|
|
10
|
+
const { args, flags } = await this.parse(PushDevice);
|
|
11
|
+
const client = this.client(flags);
|
|
12
|
+
const res = await client.hub.notifications.getPushDevice({ id: args.id });
|
|
13
|
+
this.print(res);
|
|
14
|
+
return res;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
export default class PushDevices extends BaseCommand {
|
|
4
|
+
static description = `List the devices registered for push
|
|
5
|
+
|
|
6
|
+
Narrow the list with --user, --token or --platform. Devices are stored
|
|
7
|
+
inside their user, so a page is a page of users carrying every matching
|
|
8
|
+
device they hold — follow hasMore rather than stopping at a short page.`;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> push devices',
|
|
11
|
+
'<%= config.bin %> push devices --platform ios',
|
|
12
|
+
'<%= config.bin %> push devices --user usr_123',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
user: Flags.string({ description: 'Only the devices of this user' }),
|
|
16
|
+
token: Flags.string({ description: 'Only the device registered with this provider token' }),
|
|
17
|
+
platform: Flags.string({
|
|
18
|
+
description: 'Only devices of this platform',
|
|
19
|
+
options: ['ios', 'android', 'chrome', 'safari', 'expo'],
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { flags } = await this.parse(PushDevices);
|
|
24
|
+
const client = this.client(flags);
|
|
25
|
+
const res = await client.hub.notifications.getPushDevices({
|
|
26
|
+
userId: flags.user,
|
|
27
|
+
deviceKey: flags.token,
|
|
28
|
+
platform: flags.platform,
|
|
29
|
+
});
|
|
30
|
+
this.print(res);
|
|
31
|
+
return res;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class PushDisableDependencies extends BaseCommand {
|
|
3
|
+
static description = 'List what still depends on push — check this before `push disable`';
|
|
4
|
+
static examples = ['<%= config.bin %> push disable-dependencies'];
|
|
5
|
+
async run() {
|
|
6
|
+
const { flags } = await this.parse(PushDisableDependencies);
|
|
7
|
+
const client = this.client(flags);
|
|
8
|
+
const res = await client.hub.notifications.getPushDisableDependencies({});
|
|
9
|
+
this.print(res);
|
|
10
|
+
return res;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
export default class PushDisable extends BaseCommand {
|
|
5
|
+
static description = `Turn off the push module for the project
|
|
6
|
+
|
|
7
|
+
Run \`push disable-dependencies\` first to see what still depends on push.`;
|
|
8
|
+
static examples = ['<%= config.bin %> push disable --yes'];
|
|
9
|
+
static flags = {
|
|
10
|
+
yes: Flags.boolean({ char: 'y', description: 'Skip the confirmation prompt', default: false }),
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(PushDisable);
|
|
14
|
+
const client = this.client(flags);
|
|
15
|
+
if (!flags.yes && process.stdout.isTTY) {
|
|
16
|
+
const ok = await confirm({ message: 'Turn off push for this project?', default: false });
|
|
17
|
+
if (!ok)
|
|
18
|
+
return this.print('Cancelled.');
|
|
19
|
+
}
|
|
20
|
+
const res = await client.hub.notifications.disablePush({});
|
|
21
|
+
this.print('Push disabled.');
|
|
22
|
+
return res;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class PushEnable extends BaseCommand {
|
|
3
|
+
static description = 'Turn on the push module for the project';
|
|
4
|
+
static examples = ['<%= config.bin %> push enable'];
|
|
5
|
+
async run() {
|
|
6
|
+
const { flags } = await this.parse(PushEnable);
|
|
7
|
+
const client = this.client(flags);
|
|
8
|
+
const res = await client.hub.notifications.enablePush({});
|
|
9
|
+
this.print('Push enabled.');
|
|
10
|
+
return res;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushIntegrationAppRequest extends BaseCommand {
|
|
4
|
+
static description = `Register a Norbix app push integration request (managed service)
|
|
5
|
+
|
|
6
|
+
This is one step of the managed-app pairing flow: the Norbix mobile app shows
|
|
7
|
+
a request ID, a PIN and a public key, and this call registers them for the
|
|
8
|
+
account. It is account-scoped, so an account ID must be configured or passed
|
|
9
|
+
with --account.`;
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> push integration app-request --user usr_1 --request-id req_1 --pin 123456 --valid-till 2026-09-20T12:00:00Z --public-key MIIB...',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
user: Flags.string({ required: true, description: 'User ID the app is paired to' }),
|
|
15
|
+
'request-id': Flags.string({ required: true, description: 'Request ID shown by the app' }),
|
|
16
|
+
pin: Flags.integer({ required: true, description: 'PIN shown by the app' }),
|
|
17
|
+
'valid-till': Flags.string({ required: true, description: 'When the request expires (ISO 8601)' }),
|
|
18
|
+
'public-key': Flags.string({ required: true, description: "The app's public key" }),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(PushIntegrationAppRequest);
|
|
22
|
+
const client = this.client(flags);
|
|
23
|
+
const res = await client.hub.notifications.registerCodeMashAppPushIntegration({
|
|
24
|
+
accountId: this.resolveContext(flags).accountId,
|
|
25
|
+
userId: flags.user,
|
|
26
|
+
requestId: flags['request-id'],
|
|
27
|
+
pin: flags.pin,
|
|
28
|
+
validTill: flags['valid-till'],
|
|
29
|
+
publicKey: flags['public-key'],
|
|
30
|
+
});
|
|
31
|
+
this.print('App push integration request registered.');
|
|
32
|
+
return res;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushIntegrationConfirmDelivery extends BaseCommand {
|
|
4
|
+
static description = `Confirm that a test push reached a real person
|
|
5
|
+
|
|
6
|
+
After \`push integration test\`, run this once you have seen the push on the
|
|
7
|
+
device. It marks the integration as proven to deliver.`;
|
|
8
|
+
static examples = ['<%= config.bin %> push integration confirm-delivery 66b2f0a1...'];
|
|
9
|
+
static args = {
|
|
10
|
+
id: Args.string({ required: true, description: 'Integration ID' }),
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { args, flags } = await this.parse(PushIntegrationConfirmDelivery);
|
|
14
|
+
const client = this.client(flags);
|
|
15
|
+
const res = await client.hub.notifications.confirmPushIntegrationHumanDelivery({ integrationId: args.id });
|
|
16
|
+
this.print(`Delivery confirmed for integration ${args.id}.`);
|
|
17
|
+
return res;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
import { readJsonObject } from '../../../lib/push.js';
|
|
4
|
+
/** The provider names the server's integration converter accepts. */
|
|
5
|
+
const PROVIDERS = ['Fake', 'AppleApns', 'AndroidFirebase', 'SafariPush', 'ChromeWeb', 'FirefoxWeb', 'EdgeWeb', 'ChromePush'];
|
|
6
|
+
export default class PushIntegrationSave extends BaseCommand {
|
|
7
|
+
static description = `Create or update a push integration
|
|
8
|
+
|
|
9
|
+
--provider picks the body shape. Fake needs nothing else: it accepts every send
|
|
10
|
+
and contacts no push service, so use it while you develop. The real providers
|
|
11
|
+
need their credentials — put the provider-specific fields in --config (inline
|
|
12
|
+
JSON, @file.json, or - for stdin). Pass --id to update an existing integration.`;
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> push integration save --provider Fake',
|
|
15
|
+
'<%= config.bin %> push integration save --provider AndroidFirebase --name Android --config @firebase.json',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
provider: Flags.string({ required: true, description: 'Push provider', options: PROVIDERS }),
|
|
19
|
+
name: Flags.string({ description: 'Integration name (Fake ignores it and uses its own)' }),
|
|
20
|
+
id: Flags.string({ description: 'Integration ID — set it to update instead of create' }),
|
|
21
|
+
disabled: Flags.boolean({ description: 'Save it turned off', default: false }),
|
|
22
|
+
config: Flags.string({ description: 'Provider-specific fields as a JSON object, @file or -' }),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(PushIntegrationSave);
|
|
26
|
+
const client = this.client(flags);
|
|
27
|
+
const extra = flags.config ? await readJsonObject(flags.config, 'config') : {};
|
|
28
|
+
const integration = {
|
|
29
|
+
...extra,
|
|
30
|
+
provider: flags.provider,
|
|
31
|
+
integrationId: flags.id,
|
|
32
|
+
integrationName: flags.name ?? '',
|
|
33
|
+
isEnabled: !flags.disabled,
|
|
34
|
+
};
|
|
35
|
+
// The provider-specific fields are not on the generated base type, so the
|
|
36
|
+
// body is cast: the server picks the shape from `provider`.
|
|
37
|
+
const res = await client.hub.notifications.savePushIntegration({ integration });
|
|
38
|
+
this.print(res);
|
|
39
|
+
return res;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -18,12 +18,10 @@ export default class PushStop extends BaseCommand {
|
|
|
18
18
|
if (!ok)
|
|
19
19
|
return this.print('Cancelled.');
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
const res = await n.stopPushCampaign({ id: args.id });
|
|
21
|
+
// The route is `/campaigns/{Id}/stop` and the transport fills tokens by
|
|
22
|
+
// exact name, but the generated request type has no field at all — so the
|
|
23
|
+
// id goes in as `Id`. Sending `id` throws NORBIX_MISSING_PATH_PARAM.
|
|
24
|
+
const res = await client.hub.notifications.stopPushCampaign({ Id: args.id });
|
|
27
25
|
this.print(`Campaign ${args.id} stopped.`);
|
|
28
26
|
return res;
|
|
29
27
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
import { templateBody, templateFlags } from '../../../lib/push.js';
|
|
3
|
+
export default class PushTemplateCreate extends BaseCommand {
|
|
4
|
+
static description = `Create a push template
|
|
5
|
+
|
|
6
|
+
One language: --title and --body. Several languages: --translations with a
|
|
7
|
+
JSON array of {language, content: {title, body}}.`;
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> push template create --name Welcome --title "Hi @Model.Name" --body "Thanks for joining"',
|
|
10
|
+
'<%= config.bin %> push template create --name Welcome --translations @welcome.json',
|
|
11
|
+
];
|
|
12
|
+
static flags = templateFlags;
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(PushTemplateCreate);
|
|
15
|
+
const client = this.client(flags);
|
|
16
|
+
const res = await client.hub.notifications.createPushTemplate((await templateBody(flags)));
|
|
17
|
+
this.print(res);
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
import { parseTokens } from '../../../lib/push.js';
|
|
4
|
+
export default class PushTemplateRender extends BaseCommand {
|
|
5
|
+
static description = `Render one push field (title, body or subtitle) with token values
|
|
6
|
+
|
|
7
|
+
Use it to check a Razor template before you save it. When the template needs a
|
|
8
|
+
token you did not pass, the answer lists the missing ones.`;
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> push template render --code "Hi @Model.Name" --token Name=Ada',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
code: Flags.string({ required: true, description: 'Razor source of the field' }),
|
|
14
|
+
token: Flags.string({ description: 'Token value as key=value (repeat for several)', multiple: true }),
|
|
15
|
+
preview: Flags.boolean({ description: 'Render for a preview (less strict)', default: false }),
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(PushTemplateRender);
|
|
19
|
+
const client = this.client(flags);
|
|
20
|
+
const res = await client.hub.notifications.renderPush({
|
|
21
|
+
code: flags.code,
|
|
22
|
+
tokens: parseTokens(flags.token),
|
|
23
|
+
isForPreview: flags.preview,
|
|
24
|
+
});
|
|
25
|
+
this.print(res);
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
export default class PushTemplateTokens extends BaseCommand {
|
|
4
|
+
static description = 'List the tokens a push template uses — the values a campaign must fill';
|
|
5
|
+
static examples = ['<%= config.bin %> push template tokens 66b2f0a1...'];
|
|
6
|
+
static args = {
|
|
7
|
+
id: Args.string({ required: true, description: 'Template ID' }),
|
|
8
|
+
};
|
|
9
|
+
async run() {
|
|
10
|
+
const { args, flags } = await this.parse(PushTemplateTokens);
|
|
11
|
+
const client = this.client(flags);
|
|
12
|
+
const res = await client.hub.notifications.getPushMessageContentTokens({ id: args.id });
|
|
13
|
+
this.print(res);
|
|
14
|
+
return res;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../../base.js';
|
|
3
|
+
import { templateBody, templateFlags } from '../../../lib/push.js';
|
|
4
|
+
export default class PushTemplateUpdate extends BaseCommand {
|
|
5
|
+
static description = `Replace a push template's name, channel, tags and content
|
|
6
|
+
|
|
7
|
+
The whole template is sent, so pass every language you want to keep.`;
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> push template update 66b2f0a1... --name Welcome --title "Hi" --body "Thanks for joining"',
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
id: Args.string({ required: true, description: 'Template ID' }),
|
|
13
|
+
};
|
|
14
|
+
static flags = templateFlags;
|
|
15
|
+
async run() {
|
|
16
|
+
const { args, flags } = await this.parse(PushTemplateUpdate);
|
|
17
|
+
const client = this.client(flags);
|
|
18
|
+
const res = await client.hub.notifications.updatePushTemplate({
|
|
19
|
+
...(await templateBody(flags)),
|
|
20
|
+
viewId: args.id,
|
|
21
|
+
});
|
|
22
|
+
this.print(`Template ${args.id} updated.`);
|
|
23
|
+
return res;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/lib/push.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { readJsonInput } from './json.js';
|
|
4
|
+
/**
|
|
5
|
+
* Read a JSON object from a flag: inline JSON, `-` for stdin, or `@path` for a
|
|
6
|
+
* file. Used by the push commands whose body is too rich for flags alone
|
|
7
|
+
* (template translations, provider credentials, campaign audiences).
|
|
8
|
+
*/
|
|
9
|
+
export async function readJsonObject(value, flagName) {
|
|
10
|
+
const raw = value.startsWith('@') ? readFileSync(value.slice(1), 'utf8') : value;
|
|
11
|
+
const parsed = JSON.parse(await readJsonInput(raw, flagName));
|
|
12
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
13
|
+
throw new Error(`--${flagName} must be a JSON object`);
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
/** Turn repeated `--token key=value` flags into token mappings with the Custom resolver. */
|
|
18
|
+
export function parseTokens(values) {
|
|
19
|
+
if (!values?.length)
|
|
20
|
+
return undefined;
|
|
21
|
+
return values.map((pair) => {
|
|
22
|
+
const at = pair.indexOf('=');
|
|
23
|
+
if (at < 1)
|
|
24
|
+
throw new Error(`--token must look like key=value, got "${pair}"`);
|
|
25
|
+
return { key: pair.slice(0, at), value: pair.slice(at + 1), resolver: 'Custom' };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the template body. Either --translations (a JSON array, @file or -)
|
|
30
|
+
* for several languages, or --title + --body for one language.
|
|
31
|
+
*/
|
|
32
|
+
export async function templateBody(flags) {
|
|
33
|
+
let translations;
|
|
34
|
+
if (flags.translations) {
|
|
35
|
+
const raw = flags.translations.startsWith('@') ? readFileSync(flags.translations.slice(1), 'utf8') : flags.translations;
|
|
36
|
+
translations = JSON.parse(await readJsonInput(raw, 'translations'));
|
|
37
|
+
if (!Array.isArray(translations))
|
|
38
|
+
throw new Error('--translations must be a JSON array');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
if (!flags.title || !flags.body) {
|
|
42
|
+
throw new Error('Pass --title and --body, or --translations for several languages.');
|
|
43
|
+
}
|
|
44
|
+
translations = [{ language: flags.language, content: { title: flags.title, body: flags.body } }];
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
templateName: flags.name,
|
|
48
|
+
description: flags.description,
|
|
49
|
+
communicationChannel: flags.channel,
|
|
50
|
+
tags: flags.tag,
|
|
51
|
+
translations,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export const templateFlags = {
|
|
55
|
+
name: Flags.string({ required: true, description: 'Template name' }),
|
|
56
|
+
description: Flags.string({ description: 'What the template is for' }),
|
|
57
|
+
channel: Flags.string({
|
|
58
|
+
description: 'Communication channel',
|
|
59
|
+
options: ['Transactional', 'Marketing', 'System'],
|
|
60
|
+
default: 'Transactional',
|
|
61
|
+
}),
|
|
62
|
+
tag: Flags.string({ description: 'Tag (repeat for several)', multiple: true }),
|
|
63
|
+
language: Flags.string({ description: 'Language of --title / --body', default: 'en' }),
|
|
64
|
+
title: Flags.string({ description: 'Push title (Razor allowed)' }),
|
|
65
|
+
body: Flags.string({ description: 'Push body (Razor allowed)' }),
|
|
66
|
+
translations: Flags.string({
|
|
67
|
+
description: 'All languages as a JSON array of {language, content: {title, body}} — inline, @file or -',
|
|
68
|
+
exclusive: ['title', 'body'],
|
|
69
|
+
}),
|
|
70
|
+
};
|