Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
4 changes: 2 additions & 2 deletions .github/actions/integration-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ runs:
npm pkg set "overrides.@cap-js/sqlite=^1.0.0"
npm pkg set "overrides.@cap-js/hana=^1.0.0"
npm pkg set "overrides.@cap-js/db-service=^1.0.0"
cd test/incidents-app
cd test/bookshop
npm pkg set "dependencies.@cap-js/hana=^1.0.0"
npm pkg set "dependencies.@cap-js/db-service=^1.0.0"
cd ../..
Expand All @@ -86,7 +86,7 @@ runs:
- name: Create HDI Container
shell: bash
run: cf create-service hana hdi-shared cap-js-print-hana-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }}
- run: cd test/incidents-app/ && cds deploy --to hana:cap-js-print-hana-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }}
- run: cd test/bookshop/ && cds deploy --to hana:cap-js-print-hana-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }}
shell: bash
# Bind against BTP services
- run: cds bind db -2 cap-js-print-hana-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}-$NODE_VERSION_HANA-${{ matrix.cds-version }} -o package.json
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: |
npm install
npm run lint
cd test/incidents-app && npm install
cd test/bookshop && npm install
cd ../..
npm run test

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm i -g @sap/cds-dk@${{ matrix.cds-version }}
- run: npm i
- run: cd test/incidents-app && npm i
- run: cd test/bookshop && npm i
- run: npm run test
integration-tests:
runs-on: ubuntu-latest
Expand Down
103 changes: 40 additions & 63 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,79 +22,54 @@ Usage of this plugin requires a valid subscription of the [SAP Print Service](ht

To use this plugin to print documents there are two main steps:

1. Add required annotations to your CDS model:
a. Entity
b. Action
2. Configure print queues to select from available options. (Optional, but recommended)
1. Make sure your CDS model is modelled correctly
2. Annotate your CDS model with `@PDF.Printable`

### Annotations in CDS model
### Assumptions of your model

#### Entity
- The attribute you want to print is of type `LargeBinary`
- This attribute has the annotation `@Core.ContentDisposition: fileName`, where `fileName` is the attribute that specifies the file name
- TODO: Your entity only has one `LargeBinary` attribute to print

First of all, the entity needs to be annotated to define the content and the name of the document to be printed.
### Annotations in CDS model

```cds
entity Incidents : cuid {
@print.fileContent
file : LargeBinary @Core.MediaType: 'application/pdf';
@print.fileName
fileName : String;
}
To use the print plugin, simply annotate your entity with `@PDF.Printable`:

```cds
@PDF.Printable
entity Books as projection on my.Books;
```

- `@print.fileContent`: Annotates the field containing the document content to be printed.
- `@print.fileName`: Annotates the field containing the name of the document
This annotation does the following things in the background:

#### Annotation of actions
- Adds an action `print` to the annotated entity.
- This action is added to the UI and a handler is generated to process the print request.
- An entity `PrintServiceQueues` is added to the service to provide available print queues in a value help.

Sending a print request works via bound actions annotated with `@print`. The parameter of the action are used to define the print job details.
## Manual usage

```cds
service IncidentService {
entity Incidents as projection on db.Incidents actions {

@print
action printIncidentFile(
@Common: {
ValueListWithFixedValues,
ValueList: {
$Type: 'Common.ValueListType',
CollectionPath: 'Queues',
Parameters: [{
$Type: 'Common.ValueListParameterInOut',
LocalDataProperty: qnameID,
ValueListProperty: 'ID'
}]
},
Label: 'Print Queues',
}
@print.queue
qnameID: String,
@print.numberOfCopies
@UI.ParameterDefaultValue : 1
copies: Integer
);
};
}
```
You can also use the print service to print documents manually, i.e. without the `@PDF.Printable` annotations and generated actions and handlers. For this, you can use the `cds.connect.to`-API of CAP to connect to the print service and invoke the `print` action manually.

- `@print`: Annotates the action that triggers the print job.
- `@print.queue`: Annotates the parameter specifying the print queue. It is recommended to use a value help for this parameter to select from available print queues. See TOOD
- `@print.numberOfCopies`: Annotates the parameter specifying the number of copies to print
```javascript
const printService = cds.connect.to("PrintService");

### Queues
await printService.send("print", {
qname: "Printer_Queue_Name",
numberOfCopies: 1,
docsToPrint: [
{
fileName: "file_name.pdf",
content: "<base64-encoded-pdf-content>",
isMainDocument: true,
},
],
});

Every print request needs to specify a print queue it is send to. It is recommended to provide a value help for the print queue selection. To enbale this, define an entity as projection on the `Queues` entity provided by the print service. When this projection is in place, the plugin automatically provides the available print queues coming from the print service.

```cds
using {sap.print as sp} from '@cap-js/print';

service IncidentService {
entity Queues as projection on sp.Queues;
}
const queues = await printService.get("/Queues");
```

It is possible that for LargeBinaries, that you get from the database, the content is provided as a stream. In this case, the stream needs to be converted to base64 before passing it to the print service. For an example, have a look at the sample application in `test/bookshop/`

## Local Development

When running the application locally, i.e. `cds watch`, the print service is mocked. This mock implementation prints the print job details to the console instead of sending it to the actual print service. It also provides a number of sample print queues for selection.
Expand All @@ -105,18 +80,20 @@ You can also run the application locally with a binding to the cloud print servi

### Local

To set up local hybrid integration tests, run the following. The service key is created automatically.
As the `hybrid` profile of the plugin uses SAP HANA Cloud to execute integration tests in CI, a profile `local` is added that uses can be used to execute the application locally with a binding to the cloud print service.

```bash
# Once as setup
cds bind -2 <print-service-instance-name>
# Run the tests
cds bind --exec npm run test
cd test/bookshop
cds bind -2 <print-service-instance-name> -4 local
# Run the application (from the root)
npm run watch-sample:local

```

### CI

For CI, the hybrid integration tests are automatically run against a SAP Print Service instance created for testing purposes.
For CI, the hybrid integration tests are automatically run against a SAP Print Service instance and a SAP HANA Cloud instance created for testing purposes.

## Support, Feedback, Contributing

Expand Down
113 changes: 33 additions & 80 deletions cds-plugin.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,50 @@
const cds = require("@sap/cds");

const PRINT = "@print";
const PRINT_NUMBER_OF_COPIES = "@print.numberOfCopies";
const PRINT_QUEUE = "@print.queue";
const PRINT_FILE_NAME = "@print.fileName";
const PRINT_FILE_CONTENT = "@print.fileContent";
const enhanceModel = require("./lib/enhance-csn");

const QUEUE_ENTITY_NAME = "sap.print.Queues";
const PRINT_ACTION = "@PDF.Printable.Action";
const QUEUE_ENTITY = "@PDF.Printable.QueueEntity";
const COPIES_ELEMENT = "copies";
const QUEUE_ELEMENT = "qnameID";

cds.on("compile.for.runtime", (csn) => {
enhanceModel(csn);
});
cds.on("compile.to.edmx", (csn) => {
enhanceModel(csn);
});
cds.on("compile.to.dbx", (csn) => {
enhanceModel(csn);
});

cds.once("served", async () => {
const printer = await cds.connect.to("print");
const printer = await cds.connect.to("PrintService");
for (let srv of cds.services) {
// Iterate over all entities in the service
for (let entity of srv.entities) {
const queueEntities = [];
if (entity.projection?.from.ref[0] === QUEUE_ENTITY_NAME) {
if (entity[QUEUE_ENTITY]) {
queueEntities.push(entity);
}
if (queueEntities.length > 0) {
srv.prepend(() => {
srv.on("READ", queueEntities, async (req) => {
const queues = await printer.getQueues();

return applyOdataRequestOptions(queues, req);
req.target = printer.entities.Queues;
req.query.SELECT.from.ref[0] = "PrintService.Queues";
return await printer.run(req.query);
});
});
}

if (!entity.actions) continue;

for (const action of entity.actions) {
if (action[PRINT]) {
const { numberOfCopiesAttribute, queueIDAttribute, fileNameAttribute, contentAttribute } =
getPrintParamsAttributeFromAction(entity, action);
if (action[PRINT_ACTION]) {
const { fileNameAttribute, contentAttribute } = getPrintParamsAttributeFromEntity(entity);

srv.on(action.name, entity, async (req) => {
const numberOfCopies = req.data[numberOfCopiesAttribute];
const queueID = req.data[queueIDAttribute];
const numberOfCopies = req.data[COPIES_ELEMENT];
const queueID = req.data[QUEUE_ELEMENT];

const object = await SELECT.one
.from(req.subject)
Expand All @@ -55,7 +63,7 @@ cds.once("served", async () => {
return Buffer.concat(chunks).toString("base64");
};
try {
await printer.print({
await printer.send("print", {
qname: queueID,
numberOfCopies: numberOfCopies,
docsToPrint: [
Expand All @@ -81,73 +89,18 @@ cds.once("served", async () => {
}
});

function getPrintParamsAttributeFromAction(entity, action) {
const copiesElement = Object.values(action.params).find((el) => el[PRINT_NUMBER_OF_COPIES]);
const queueElement = Object.values(action.params).find((el) => el[PRINT_QUEUE]);
function getPrintParamsAttributeFromEntity(entity) {
const content = Object.values(entity.elements).find((el) => el.type === "cds.LargeBinary");
const fileNameAttribute = content["@Core.ContentDisposition"]["="];

const fileName = Object.values(entity.elements).find((el) => el[PRINT_FILE_NAME]);
const content = Object.values(entity.elements).find((el) => el[PRINT_FILE_CONTENT]);

if ((!copiesElement || !queueElement, !fileName || !content)) {
cds.error(
`Print action ${action.name} is missing required annotations. Make sure @print.numberOfCopies, @print.queue are present in the action and @print.fileName and @print.fileContent are present in the entity.`,
if (!content) return cds.error("No large binary content found in the entity for printing.");
if (!fileNameAttribute)
return cds.error(
"No file name provided. Please add @Core.ContentDisposition to the content element.",
);
}

return {
numberOfCopiesAttribute: copiesElement.name,
queueIDAttribute: queueElement.name,
fileNameAttribute: fileName.name,
fileNameAttribute,
contentAttribute: content.name,
};
}

// only works for entities as projections on queue entity defined in index.cds
// Use case: provide VH to FE as SAP Print service provides a REST endpoint to get queues
function applyOdataRequestOptions(queues, req) {
let result = queues;

const {
$search: search,
$filter: filter,
$skip: skip,
$top: top,
$count: count,
$orderby: orderby,
} = req._.req.query;

if (filter) {
// only allow filtering by ID eq '<value>', as only VH should be supported by now
const filterParts = filter.split(" ");
if (filterParts.length !== 3 || filterParts[0] !== "ID" || filterParts[1] !== "eq") {
return req.reject(400, "Invalid $filter format. Expected format: 'ID eq <value>'");
}

const filterValue = filterParts[2].replace(/^'(.*)'$/, "$1");
result = result.filter((queue) => queue.ID === filterValue);
} else if (search) {
const searchTerm = search.toLowerCase();
result = result.filter((queue) => queue.ID.toLowerCase().includes(searchTerm));
result = result.sort((a, b) => a.ID.localeCompare(b.ID));
}

if (orderby) {
const [field, direction] = orderby.split(" ");
if (field !== "ID") {
return req.reject(400, "Invalid $orderby field. Only 'ID' is supported.");
}
result = result.sort((a, b) => {
const comparison = a.ID.localeCompare(b.ID);
return direction === "desc" ? -comparison : comparison;
});
}

const skipNumber = parseInt(skip || "0", 10);
const topNumber = parseInt(top || result.length, 10);
result = result.slice(skipNumber, skipNumber + topNumber);

if (count) {
result.$count = result.length;
}
return result;
}
8 changes: 0 additions & 8 deletions index.cds

This file was deleted.

Loading