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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
13 changes: 0 additions & 13 deletions .eslintrc.json

This file was deleted.

36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches: [master]
pull_request:

permissions:
contents: read

jobs:
test:
name: Node ${{ matrix.node }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
strategy:
fail-fast: false
matrix:
node: [24, 26]
steps:
- uses: step-security/harden-runner@46b06cea07f9e64621fdf066ba77ac6915163d43 # v2.22.3
with:
egress-policy: audit
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci --ignore-scripts
- run: npm run lint
- run: npm test
- run: npm pack --dry-run
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.DS_Store
*.key
*.tgz
node_modules
.pushapi-config.json
.pushapi.buffer.*
4 changes: 0 additions & 4 deletions .jshintrc

This file was deleted.

1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
140 changes: 97 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,132 @@
# PushAPI-node.js

## Description
[![CI](https://github.com/coveooss/pushapi-nodejs/actions/workflows/ci.yml/badge.svg)](https://github.com/coveooss/pushapi-nodejs/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/coveo-pushapi-cli)](https://www.npmjs.com/package/coveo-pushapi-cli)

This project is a simple wrapper to help sending JSON payloads using Coveo Cloud V2 PushAPI.
Command line tool to push JSON payloads to a Coveo [Push source](https://docs.coveo.com/en/68/cloud-v2-developers/push-api) (or a Catalog source, through the Stream API).

## Available documentation
## Requirements

The code in this project uses the Push API from the Coveo Cloud V2 Platform. You can use the following resource for more information and get started:
- Node.js **24** or newer (see `.nvmrc`).
- No runtime dependencies: HTTP calls use Node's built-in `fetch`, and arguments are parsed with `node:util.parseArgs`.

- Push API documentation: [Use the Push API](https://docs.coveo.com/en/68/cloud-v2-developers/push-api)
## Install

## Contributing
```bash
npm install --global coveo-pushapi-cli
```

- Branch or Fork
- Pull Request
- And... that's pretty much it!
## Usage

## How-to build
The first time you run `pushapi` in a folder, it helps you create the config file `.pushapi-config.json` by asking for your source ID, organization ID, platform and API key. The file is created with `0600` permissions since it holds your API key. Don't commit it.

1. `git clone` this project.
1. `npm install` to get the dependencies (request, fs).
```bash
# Push one JSON payload file. Don't forget to set DocumentId in the payloads.
pushapi file.json

## CLI command
# Push one file, then delete every item of the source that is older than 24 hours.
# Typical when you create/update all valid items and want the leftovers gone.
Comment thread
jdevost marked this conversation as resolved.
Resolved
pushapi file.json -d 24

Install it with NPM:
# Push every .json file of a folder, batched into as few requests as possible.
pushapi folder

```bash
npm i coveo-pushapi-cli --global
# Dry run: build the batch files (.pushapi.buffer.N) locally, without pushing anything.
pushapi folder --dry-run

pushapi --help
```

## How-to run
| Option | Description |
| --- | --- |
| `-d, --deleteOlderThan <hours>` | Delete source items older than `<hours>` before pushing. |
| `-D, --dry-run` | Create the batch files without pushing them. |
| `--help`, `--version` | Show help / version. |

The first time you run in one folder, it will help you to set up your config `.pushapi-config.json` by asking about your `sourceId` and your `API key`.
Uploads accept a mix of these JSON formats, including within the same folder:

- Push one JSON payload file:
- an array of items
- a Push API payload: `{ "AddOrUpdate": [ ... ] }`
- a single item object

`pushapi file.json`
See the `samples/` folder for examples.

where `file.json` is the path to a Push API payload (JSON) file. Don't forget to set `DocumentId` in the payloads.
### Config file

- Push one JSON payload file, but delete source item older than 24 hours:
```json
{
"org": "your-org-id",
"source": "your-source-id",
"apiKey": "xx--your-api-key--abc",
"platform": "push.cloud.coveo.com",
"useStreamApi": false,
"debug": false
}
```

`pushapi file.json -d 24`
| Key | Description |
| --- | --- |
| `org` | Coveo organization ID. |
| `source` | Push (or Catalog) source ID. |
| `apiKey` | API key with the `Edit` privilege on the source. |
| `platform` | Push API host. Defaults to `push.cloud.coveo.com`. Use `pushdev.`, `pushstg.` or `pushhipaa.` for the other platforms. |
| `useStreamApi` | `true` for a Catalog source: payloads go through the [Stream API](https://docs.coveo.com/en/p4eb0129/) instead of the Push API. |
| `debug` | `true` to log every request and its response code. |

to use typically when you create/update all valid items, you want to delete all other items that are older.
## What does it do?

- Push all JSON payload files from one folder:
The tool does a minimal validation of the payload (every document needs a `DocumentId`), then runs these API calls:

`pushapi folder`
1. Change the source state to `REBUILD`
2. Get a file container
3. Upload the payload to the file container
4. Send the batch command to process the file container
5. Change the source state back to `IDLE` (even if a previous step failed)

where `folder` is the path to a folder containing multiple Push API JSON payload files.
Folder uploads accept a mix of these JSON formats in the same folder:
- an array of items
- a Push API payload with `AddOrUpdate`
- a single item object
With `useStreamApi`, it instead opens a stream, uploads one chunk per batch, and closes the stream.

## What does it do?
### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | Missing config file, missing input file, or invalid payload. |
| `2` to `5` | Invalid config: missing config / `apiKey` / `org` / `source`. |
| `10` | A request to the platform failed. The response body is printed. |

The helper does a minimal validation on the payload, then executes these API calls:
## Programmatic use

1. Change source state to REBUILD
2. Get a File container
3. Upload the payload to the File container
4. Sends the batch command to process the File container
5. Change the source state back to IDLE
The package is an ES module. It can also be loaded with `require()` on Node 24+.

> **Note**
> You can add this attribute `"debug": true` in your `.pushapi-config.json` to see these requests and their response code.
```js
import { main } from 'coveo-pushapi-cli';

## Dependencies
// Reads .pushapi-config.json from the current folder.
await main('folder-with-payloads', { deleteOlderThan: 24, dryRun: false });
```

## Development

```bash
git clone https://github.com/coveooss/pushapi-nodejs.git
cd pushapi-nodejs
npm install
npm test # node --test
npm run lint # eslint
```

- Node.js
- Node modules: `fs`, `request`
Tests run against a mocked `fetch`; nothing is pushed to Coveo. CI runs lint and tests on every Node release line declared in `engines`.

## Contributing

- Branch or fork
- Pull request
- And... that's pretty much it!

## Authors

- Jérôme Devost ([GitHub profile](https://github.com/jdevost))

## License

[Apache-2.0](LICENSE)
119 changes: 91 additions & 28 deletions client-global.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,93 @@
#!/usr/bin/env node
const { main, Config } = require('./push.js');
const fs = require('fs');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');

const argv = yargs(hideBin(process.argv))
.usage('\nUsage: $0 <File_or_Folder> [options]')
.example('$0 file1.json', 'Upload a single file to a Push Source')
.example('$0 folder2', 'Upload all .json files from a folder to a Push Source')
.example('$0 folder3 -d 2', 'Sends a deleteOlderThan 2 hours before pushing new data')
.alias('d', 'deleteOlderThan')
.default('d', null)
.describe('d', 'Set the deleteOlderThan delay (in hours)')
.alias('D', 'dry-run')
.boolean('D')
.describe('D', 'Dry run - creates the batch files, without pushing them')
.demandCommand(1, 'You need to specify a FILE or a FOLDER\n')
.help()
.parse();

const FILE_OR_FOLDER = argv._[0];

let configFile = `${process.cwd()}/.pushapi-config.json`;
if (!fs.existsSync(configFile)) {
console.warn(`\n\tCouldn't load ${configFile} file`);
Config.createConfig(configFile, () => main(FILE_OR_FOLDER, argv));
} else {
return main(FILE_OR_FOLDER, argv);
import { existsSync } from 'node:fs';
import path from 'node:path';
import { parseArgs } from 'node:util';

import pkg from './package.json' with { type: 'json' };
import { main, reportError, Config } from './push.js';
import { CONFIG_FILE_NAME } from './src/Config.js';

const HELP = `
Usage: pushapi <File_or_Folder> [options]

Options:
-d, --deleteOlderThan <hours> Set the deleteOlderThan delay (in hours)
-D, --dry-run Dry run - creates the batch files, without pushing them
-h, --help Show help
-v, --version Show version number

Examples:
pushapi file1.json Upload a single file to a Push Source
pushapi folder2 Upload all .json files from a folder to a Push Source
pushapi folder3 -d 2 Sends a deleteOlderThan 2 hours before pushing new data
`;

/** Prints the usage and the error message to stderr, then exits with code 1. */
function usageError(message) {
console.error(HELP);
console.error(`${message}\n`);
process.exit(1);
}

/**
* Parses the command line into `{ fileOrFolder, deleteOlderThan, dryRun }`.
* Exits the process on --help, --version, or invalid arguments.
*/
function parseCommandLine(args) {
let parsed;
try {
parsed = parseArgs({
args,
allowPositionals: true,
options: {
deleteOlderThan: { type: 'string', short: 'd' },
'dry-run': { type: 'boolean', short: 'D', default: false },
help: { type: 'boolean', short: 'h', default: false },
version: { type: 'boolean', short: 'v', default: false },
},
});
} catch (error) {
return usageError(error.message);
}

const { values, positionals } = parsed;
if (values.help) {
console.log(HELP);
process.exit(0);
}
if (values.version) {
console.log(pkg.version);
process.exit(0);
}

if (positionals.length !== 1) {
return usageError(positionals.length === 0 ? 'You need to specify a FILE or a FOLDER' : `Too many arguments: ${positionals.join(' ')}`);
}

let deleteOlderThan = null;
if (values.deleteOlderThan !== undefined) {
deleteOlderThan = Number(values.deleteOlderThan);
if (values.deleteOlderThan.trim() === '' || !Number.isFinite(deleteOlderThan) || deleteOlderThan < 0) {
return usageError(`--deleteOlderThan expects a number of hours, got: ${values.deleteOlderThan}`);
}
}

return { fileOrFolder: positionals[0], deleteOlderThan, dryRun: values['dry-run'] };
}

const { fileOrFolder, ...options } = parseCommandLine(process.argv.slice(2));

try {
const configFile = path.join(process.cwd(), CONFIG_FILE_NAME);
if (!existsSync(configFile)) {
console.warn(`\n\tCouldn't load ${configFile} file`);
const created = await Config.createConfig(configFile);
if (!created) {
process.exit(0);
}
}
await main(fileOrFolder, options);
} catch (error) {
reportError(error);
process.exitCode = typeof error?.exitCode === 'number' ? error.exitCode : 10;
}
Loading
Loading