Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
92bde05
Adding 'external link' Class to old and current "output dataset" API.
GBirkel Jul 24, 2025
806533d
Merge branch 'SciCatProject:master' into templated-external-links-v2
GBirkel Aug 4, 2025
1baa254
Test version of virtual (derived) value for external links.
GBirkel Aug 7, 2025
ddeceeb
Applying some test templates, in a draft data structure
GBirkel Aug 13, 2025
1cd8986
Adding a config json file and section for link templates.
GBirkel Aug 14, 2025
58e1b48
Moving the virtual field creation up to the module so we can get acce…
GBirkel Aug 14, 2025
a419d1d
Minor commentary and cleanup.
GBirkel Aug 14, 2025
e0a77ca
Merge branch 'refs/heads/master' into templated-external-links-v2
GBirkel Aug 14, 2025
fa3d6f3
Too much nesting. :D
GBirkel Aug 15, 2025
d1d1544
Lint.
GBirkel Aug 15, 2025
9afc970
Sorry linter, we actually need this for the anonymous filter/map func…
GBirkel Aug 15, 2025
42fee6a
Mentioning datasetExternalLinkTemplates.json in the development instr…
GBirkel Aug 18, 2025
023bfd7
Just a bit of code cleanup.
GBirkel Aug 28, 2025
9183bcf
Adding v3 and v4 endpoints for /:pid/externallinks , calling a common…
GBirkel Aug 28, 2025
ce6e498
Removing the injected virtual field code for dataset links.
GBirkel Aug 28, 2025
ea96c52
The list of external links is no longer returned as a field in the da…
GBirkel Aug 28, 2025
b04c6fa
Merge branch 'refs/heads/master' into templated-external-links-v2
GBirkel Sep 3, 2025
c570748
This is no longer true. :D
GBirkel Sep 3, 2025
b76cce2
OutputAttachmentV3Dto is not a schema (it has no Mongo representation…
GBirkel Sep 3, 2025
0e8304e
Merge branch 'refs/heads/master' into templated-external-links-v2
GBirkel Sep 4, 2025
bf27ced
Merge branch 'refs/heads/master' into templated-external-links-v2
GBirkel Sep 8, 2025
c650365
Lint
GBirkel Sep 8, 2025
12d5e25
Upgrading dependency to latest release to test SDK generation
GBirkel Sep 8, 2025
846ff80
Removing imports that are no longer used.
GBirkel Sep 8, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/node_modules
functionalAccounts.json
datasetTypes.json
datasetExternalLinkTemplates.json
proposalTypes.json
loggers.json
jobConfig.json
Expand Down
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ Thank you for your interest in contributing to our project!
7. _Optional_ Add [loggers.json](#loggers-configuration) file to the root folder and configure multiple loggers.
8. _Optional_ Add [proposalTypes.json](#proposal-types-configuration) file to the root folder and configure the proposal types.
9. _Optional_ Add [datasetTypes.json](#dataset-types-configuration) file to the root folder and configure the dataset types.
10. `npm run start:dev`
11. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.
12. To be able to run the e2e tests with the same setup as in the Github actions you will need to run `npm run prepare:local` and after that run `npm run start:dev`. This will start all needed containers and copy some configuration to the right place.
10. _Optional_ Add [datasetExternalLinkTemplates.json](#dataset-external-link-templates-configuration) file to the root folder and configure the external link types.
11. `npm run start:dev`
12. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.
13. To be able to run the e2e tests with the same setup as in the Github actions you will need to run `npm run prepare:local` and after that run `npm run start:dev`. This will start all needed containers and copy some configuration to the right place.

## Develop in a container using the docker-compose.dev file

Expand All @@ -57,11 +58,12 @@ Thank you for your interest in contributing to our project!
5. _Optional_ Mount [loggers.json](#loggers-configuration) file to a volume in the container to configure multiple loggers.
6. _Optional_ Mount [proposalTypes.json](#proposal-types-configuration) file to a volume in the container to configure the proposal types.
7. _Optional_ Mount [datasetTypes.json](#dataset-types-configuration) file to a volume in the container to configure the dataset types.
8. _Optional_ Change the container env variables.
9. _Optional_ Create the file test/config/.env.override to override ENV vars that are used when running the tests.
10. Attach to the container.
11. `npm run start:dev`
12. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.
8. _Optional_ Mount [datasetExternalLinkTemplates.json](#dataset-external-link-templates-configuration) file to a volume in the container to configure the external link types.
9. _Optional_ Change the container env variables.
10. _Optional_ Create the file test/config/.env.override to override ENV vars that are used when running the tests.
11. Attach to the container.
12. `npm run start:dev`
13. Go to http://localhost:3000/explorer to get an overview of available endpoints and database schemas.

## Test the app

Expand Down Expand Up @@ -113,16 +115,26 @@ The `loggers.example.json` file in the root directory showcases the example of c

### Proposal types configuration

Providing a file called _proposalTypes.json_ at the root of the project, locally or in the container, will be automatically loaded into the application configuration service under property called `proposalTypes` and used for validation against proposal creation and update.
If a file called _proposalTypes.json_ is provided at the root of the project, locally or in the container, it will be automatically loaded into the application configuration service under the property `proposalTypes`.

This content is used for validation against proposal creation and update.

The `proposalTypes.json.example` file in the root directory showcases the example of configuration structure for proposal types.
The file `proposalTypes.example.json` contains an example.

### Dataset types configuration

When providing a file called _datasetTypes.json_ at the root of the project, locally or in the container, it will be automatically loaded into the application configuration service under property called `datasetTypes` and used for validation against dataset creation and update. The types `Raw` and `Derived` are always valid dataset types by default.
If a file called _datasetTypes.json_ is provided at the root of the project, locally or in the container, it will be automatically loaded into the application configuration service under the property `datasetTypes`.

The `datasetTypes.example.json` file in the root directory showcases an example of configuration structure for dataset types.

### Dataset external link templates configuration

If a file called _datasetExternalLinkTemplates.json_ is provided at at the root of the project, locally or in the container, it will be automatically loaded into the application configuration service under the property `datasetExternalLinkTemplates`.

The content is used to create links to external websites from individual datasets, based on criteria applied to the dataset metadata.

The file `datasetExternalLinkTemplates.example.json` contains an example.

### Published data configuration

Providing a file called _publishedDataConfig.json_ at the root of the project, locally or in the container, will be automatically loaded into the application configuration service under property called `publishedDataConfig`. It will be used for published data metadata form generation in the frontend and metadata validation in publication and registration of the published data.
Expand Down
14 changes: 14 additions & 0 deletions datasetExternalLinkTemplates.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"title": "Franzviewer II",
"url_template": "https://franz.site.com/franzviewer?id=${dataset.pid}",
"description_template": "View ${dataset.numberOfFiles} files in Franz' own personal viewer",
"filter": "(dataset.type == 'derived') && dataset.owner.includes('Franz')"
},
{
"title": "High Beam-Energy View",
"url_template": "https://beamviewer.beamline.net/highenergy?id=${dataset.pid}",
"description_template": "The high-energy beamviewer (value ${dataset.scientificMetadata?.beamEnergy?.value}) at beamCo",
"filter": "(dataset.scientificMetadata?.beamEnergy?.value > 20)"
}
]
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scicat-backend-next",
"version": "4.5.0",
"version": "4.23.0",
"description": "scicat-backend-next",
"author": "",
"private": true,
Expand Down
5 changes: 5 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const configuration = () => {
};
const jsonConfigMap: { [key: string]: object | object[] | boolean } = {
datasetTypes: {},
datasetExternalLinkTemplates: [],
proposalTypes: {},
};
const jsonConfigFileList: { [key: string]: string } = {
Expand All @@ -80,6 +81,9 @@ const configuration = () => {
process.env.FRONTEND_THEME_FILE || "./src/config/frontend.theme.json",
loggers: process.env.LOGGERS_CONFIG_FILE || "loggers.json",
datasetTypes: process.env.DATASET_TYPES_FILE || "datasetTypes.json",
datasetExternalLinkTemplates:
process.env.DATASET_EXTERNAL_LINK_TEMPLATES_FILE ||
"datasetExternalLinkTemplates.json",
proposalTypes: process.env.PROPOSAL_TYPES_FILE || "proposalTypes.json",
metricsConfig: process.env.METRICS_CONFIG_FILE || "metricsConfig.json",
publishedDataConfig:
Expand Down Expand Up @@ -402,6 +406,7 @@ const configuration = () => {
policyRetentionShiftInYears: process.env.POLICY_RETENTION_SHIFT ?? -1,
},
datasetTypes: jsonConfigMap.datasetTypes,
datasetExternalLinkTemplates: jsonConfigMap.datasetExternalLinkTemplates,
proposalTypes: jsonConfigMap.proposalTypes,
frontendConfig: jsonConfigMap.frontendConfig,
frontendTheme: jsonConfigMap.frontendTheme,
Expand Down
111 changes: 71 additions & 40 deletions src/datasets/datasets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { PartialUpdateDatablockDto } from "src/datablocks/dto/update-datablock.d
import { Datablock } from "src/datablocks/schemas/datablock.schema";
import { LogbooksService } from "src/logbooks/logbooks.service";
import { Logbook } from "src/logbooks/schemas/logbook.schema";
import { ExternalLinkClass } from "./schemas/externallink.class";
import { CreateDatasetOrigDatablockDto } from "src/origdatablocks/dto/create-dataset-origdatablock";
import { CreateOrigDatablockDto } from "src/origdatablocks/dto/create-origdatablock.dto";
import { UpdateOrigDatablockDto } from "src/origdatablocks/dto/update-origdatablock.dto";
Expand Down Expand Up @@ -912,9 +913,9 @@ export class DatasetsController {
outputDatasets = datasets.map((dataset) =>
this.convertCurrentToObsoleteSchema(dataset),
);
await Promise.all(
outputDatasets.map(async (dataset) => {
if (includeFilters) {
if (includeFilters) {
await Promise.all(
outputDatasets.map(async (dataset) => {
await Promise.all(
includeFilters.map(async ({ relation }) => {
switch (relation) {
Expand Down Expand Up @@ -946,13 +947,9 @@ export class DatasetsController {
}
}),
);
} else {
/* eslint-disable @typescript-eslint/no-unused-expressions */
// TODO: check the eslint error "Expected an assignment or function call and instead saw an expression"
dataset;
}
}),
);
}),
);
}
}
return outputDatasets as OutputDatasetObsoleteDto[];
}
Expand Down Expand Up @@ -1207,8 +1204,7 @@ export class DatasetsController {
@Get("/findOne")
@ApiOperation({
summary: "It returns the first dataset found.",
description:
"It returns the first dataset of the ones that matches the filter provided. The list returned can be modified by providing a filter.",
description: "Returns the first dataset that matches the provided filters.",
})
@ApiQuery({
name: "filter",
Expand Down Expand Up @@ -1243,35 +1239,30 @@ export class DatasetsController {

if (outputDataset) {
const includeFilters = mergedFilters.include ?? [];
await Promise.all(
includeFilters.map(async ({ relation }) => {
switch (relation) {
case "attachments": {
outputDataset.attachments = await this.attachmentsService.findAll(
{
where: {
datasetId: outputDataset.pid,
},
},
);
break;
}
case "origdatablocks": {
outputDataset.origdatablocks =
await this.origDatablocksService.findAll({
where: { datasetId: outputDataset.pid },
});
break;
if (includeFilters) {
await Promise.all(
includeFilters.map(async ({ relation }) => {
switch (relation) {
case "attachments": {
outputDataset.attachments =
await this.attachmentsService.findAll({
where: {
datasetId: outputDataset.pid,
},
});
break;
}
case "origdatablocks": {
outputDataset.origdatablocks =
await this.origDatablocksService.findAll({
where: { datasetId: outputDataset.pid },
});
break;
}
}
case "datablocks": {
outputDataset.datablocks = await this.datablocksService.findAll({
where: { datasetId: outputDataset.pid },
});
break;
}
}
}),
);
}),
);
}
}
return outputDataset;
}
Expand Down Expand Up @@ -1823,6 +1814,46 @@ export class DatasetsController {
return await this.convertCurrentToObsoleteSchema(outputDatasetDto);
}

// GET /datasets/:id/externallinks
@UseGuards(PoliciesGuard)
@CheckPolicies(
"datasets",
(ability: AppAbility) =>
ability.can(Action.DatasetRead, DatasetClass) ||
ability.can(Action.DatasetReadOnePublic, DatasetClass),
)
@Get("/:pid/externallinks")
@ApiOperation({
summary: "Returns dataset external links.",
description:
"Returns the applicable external links for the dataset with the given pid.",
})
@ApiParam({
name: "pid",
description: "Id of the dataset to return external links",
type: String,
})
@ApiResponse({
status: HttpStatus.OK,
type: ExternalLinkClass,
isArray: true,
description: "A list of exernal link objects.",
})
async findExternalLinksById(
@Req() request: Request,
@Param("pid") id: string,
) {
const links = await this.datasetsService.findExternalLinksById(id);

await this.checkPermissionsForDatasetExtended(
request,
id,
Action.DatasetRead,
);

return links;
}

// GET /datasets/:id/thumbnail
@UseGuards(PoliciesGuard)
@CheckPolicies(
Expand Down
48 changes: 48 additions & 0 deletions src/datasets/datasets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
PartialUpdateDatasetWithHistoryDto,
UpdateDatasetDto,
} from "./dto/update-dataset.dto";
import { ExternalLinkClass } from "./schemas/externallink.class";
import { IDatasetFields } from "./interfaces/dataset-filters.interface";
import { DatasetClass, DatasetDocument } from "./schemas/dataset.schema";
import {
Expand Down Expand Up @@ -395,6 +396,53 @@ export class DatasetsService {
throw new NotFoundException(error);
}
}

async findExternalLinksById(id: string): Promise<ExternalLinkClass[]> {
const thisDataSet = await this.findOneComplete({
where: { pid: id },
include: [DatasetLookupKeysEnum.all],
});

if (!thisDataSet) {
// no luck. we need to create a new dataset
throw new NotFoundException(`Dataset #${id} not found`);
}

interface ExternalLinkTemplateConfig {
title: string;
url_template: string;
description_template: string;
filter: string;
}

const templates: ExternalLinkTemplateConfig[] | undefined =
this.configService.get("datasetExternalLinkTemplates");
if (!templates) {
return [];
}

return templates
.filter((d) => {
const filterFn = new Function("dataset", `return (${d.filter});`);
return filterFn(thisDataSet);
})
.map((d) => {
const urlFn = new Function(
"dataset",
`return (\`${d.url_template}\`);`,
);
const descriptionFn = new Function(
"dataset",
`return (\`${d.description_template}\`);`,
);
return {
url: urlFn(thisDataSet),
title: d.title,
description: descriptionFn(thisDataSet),
};
});
}

// Get metadata keys
async metadataKeys(
filters: IFilters<DatasetDocument, IDatasetFields>,
Expand Down
Loading
Loading