Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
cec0235
integrating if-unmodified-since
Aug 12, 2025
c35c623
integrating if-unmodified-since into dataset, sample
Aug 18, 2025
5e18837
if-unmodified-since into attachments,datablocks,proposals,datasets
Aug 19, 2025
fd0c9f8
make headerDate from if-unmodified-since more robust
Aug 22, 2025
e0b8f53
Merge branch 'SciCatProject:master' into master
joe-baudisch Aug 29, 2025
d08db24
remove unused import
Aug 29, 2025
f91cf6e
Merge branch 'SciCatProject:master' into master
joe-baudisch Aug 29, 2025
3202914
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 1, 2025
3cd917a
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 2, 2025
6cb52bf
resolve some conversations
Sep 4, 2025
65fbbc5
Merge branch 'master' into master
joe-baudisch Sep 4, 2025
772f2a9
adding test
Sep 5, 2025
fdfd2de
fixing bug by defining controller method without decorator
Sep 5, 2025
e7aea85
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 5, 2025
c7effdb
adding attachments.v4.controller test
Sep 5, 2025
a6f0ccf
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 5, 2025
656905c
adding more tests
Sep 8, 2025
03a1e8a
adding final test with datasets.v4.controller_if-unmodified-since_.sp…
Sep 9, 2025
de6c79a
merge two test-files into one
Sep 9, 2025
5f9fb70
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 9, 2025
8ed8fdb
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 10, 2025
11935c1
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 11, 2025
10a639e
calling extracted logic findByIdAndUpdateInternal from decorated find…
Sep 12, 2025
674d6f7
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 12, 2025
6b98f57
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 14, 2025
ae13ef5
lint fix
Sep 15, 2025
3e89b6b
lint fix
Sep 15, 2025
0962048
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 15, 2025
01718b3
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 16, 2025
4be531f
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 16, 2025
9f8b548
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 18, 2025
38e620a
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 19, 2025
58155f5
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 22, 2025
a8d5d7d
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 23, 2025
cfdbcd3
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 25, 2025
8c7ad78
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 25, 2025
5d78614
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 25, 2025
abf01ae
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 29, 2025
8384ba2
Merge branch 'SciCatProject:master' into master
joe-baudisch Sep 30, 2025
1990822
Merge branch 'SciCatProject:master' into master
joe-baudisch Oct 2, 2025
fc4c9ca
Merge branch 'master' into master
nitrosx Oct 6, 2025
d8279c1
Merge branch 'SciCatProject:master' into master
joe-baudisch Oct 10, 2025
508ddef
Merge branch 'master' into master
joe-baudisch Oct 20, 2025
d234a01
Merge branch 'SciCatProject:master' into master
joe-baudisch Oct 21, 2025
3a9a64d
Merge branch 'SciCatProject:master' into master
joe-baudisch Oct 24, 2025
4cb7df8
Merge branch 'SciCatProject:master' into master
joe-baudisch Oct 30, 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
23 changes: 18 additions & 5 deletions src/attachments/attachments.v4.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
NotFoundException,
Patch,
Put,
HttpCode,
HttpCode, Headers, HttpException,

Check failure on line 17 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Replace `·Headers,` with `⏎··Headers,⏎·`
} from "@nestjs/common";
import {
ApiBearerAuth,
Expand Down Expand Up @@ -365,8 +365,15 @@
async findOneAndUpdate(
@Req() request: Request,
@Param("aid") aid: string,
@Headers() headers: Record<string, string>,
@Body() updateAttachmentDto: PartialUpdateAttachmentV4Dto,
): Promise<OutputAttachmentV4Dto | null> {

Check failure on line 371 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Replace `⏎····const·headerDateString·=·headers['if-unmodified-since'` with `····const·headerDateString·=·headers["if-unmodified-since"`
const headerDateString = headers['if-unmodified-since'];

Check failure on line 372 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Strings must use doublequote
const headerDate = headerDateString && !isNaN(new Date(headerDateString).getTime())

Check failure on line 373 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Insert `⏎·····`
? new Date(headerDateString)

Check failure on line 374 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Insert `··`
: null;

Check failure on line 375 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Insert `··`

const foundAattachment = await this.checkPermissionsForAttachment(
request,
aid,
Expand All @@ -376,10 +383,16 @@
request.headers["content-type"] === "application/merge-patch+json"
? jmp.apply(foundAattachment, updateAttachmentDto)
: updateAttachmentDto;
return this.attachmentsService.findOneAndUpdate(
{ _id: aid },
updateAttachmentDtoForservice,
);


Check failure on line 387 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Delete `⏎`
if (!headerDate || headerDate > foundAattachment.updatedAt) {
return this.attachmentsService.findOneAndUpdate(
{_id: aid},

Check failure on line 390 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Replace `_id:·aid` with `·_id:·aid·`
updateAttachmentDtoForservice,
);
} else {
throw new HttpException("Precondition Failed", HttpStatus.PRECONDITION_FAILED);

Check failure on line 394 in src/attachments/attachments.v4.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Replace `"Precondition·Failed",·HttpStatus.PRECONDITION_FAILED` with `⏎········"Precondition·Failed",⏎········HttpStatus.PRECONDITION_FAILED,⏎······`
}
}

// PUT /attachments/:aid
Expand Down
98 changes: 55 additions & 43 deletions src/datasets/datasets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,11 +1397,18 @@
@Req() request: Request,
@Param("pid") pid: string,
@Body()
@Headers() headers: Record<string, string>,

Check failure on line 1400 in src/datasets/datasets.controller.ts

View workflow job for this annotation

GitHub Actions / eslint

Insert `⏎···`
updateDatasetObsoleteDto:
| PartialUpdateRawDatasetObsoleteDto
| PartialUpdateDerivedDatasetObsoleteDto
| PartialUpdateDatasetDto,
): Promise<OutputDatasetObsoleteDto | null> {

const headerDateString = headers['if-unmodified-since'];
const headerDate = headerDateString && !isNaN(new Date(headerDateString).getTime())
? new Date(headerDateString)
: null;

const foundDataset = await this.datasetsService.findOne({
where: { pid },
});
Expand All @@ -1410,52 +1417,57 @@
throw new NotFoundException();
}

// NOTE: Default validation pipe does not validate union types. So we need custom validation.
let dtoType;
switch (foundDataset.type) {
case DatasetType.Raw:
dtoType = PartialUpdateRawDatasetObsoleteDto;
break;
case DatasetType.Derived:
dtoType = PartialUpdateDerivedDatasetObsoleteDto;
break;
default:
dtoType = PartialUpdateDatasetDto;
break;
}
const validatedUpdateDatasetObsoleteDto =
(await this.validateDatasetObsolete(
updateDatasetObsoleteDto,
dtoType,
)) as
| PartialUpdateRawDatasetObsoleteDto
| PartialUpdateDerivedDatasetObsoleteDto
| PartialUpdateDatasetDto;

// NOTE: We need DatasetClass instance because casl module can not recognize the type from dataset mongo database model. If other fields are needed can be added later.
const datasetInstance =
await this.generateDatasetInstanceForPermissions(foundDataset);
if (!headerDate || headerDate > foundDataset.updatedAt) {

// NOTE: Default validation pipe does not validate union types. So we need custom validation.
let dtoType;
switch (foundDataset.type) {
case DatasetType.Raw:
dtoType = PartialUpdateRawDatasetObsoleteDto;
break;
case DatasetType.Derived:
dtoType = PartialUpdateDerivedDatasetObsoleteDto;
break;
default:
dtoType = PartialUpdateDatasetDto;
break;
}
const validatedUpdateDatasetObsoleteDto =
(await this.validateDatasetObsolete(
updateDatasetObsoleteDto,
dtoType,
)) as
| PartialUpdateRawDatasetObsoleteDto
| PartialUpdateDerivedDatasetObsoleteDto
| PartialUpdateDatasetDto;

// NOTE: We need DatasetClass instance because casl module can not recognize the type from dataset mongo database model. If other fields are needed can be added later.
const datasetInstance =
await this.generateDatasetInstanceForPermissions(foundDataset);

// instantiate the casl matrix for the user
const user: JWTUser = request.user as JWTUser;
const ability = this.caslAbilityFactory.datasetInstanceAccess(user);
// check if he/she can create this dataset
const canUpdate =
ability.can(Action.DatasetUpdateAny, DatasetClass) ||
ability.can(Action.DatasetUpdateOwner, datasetInstance);

if (!canUpdate) {
throw new ForbiddenException("Unauthorized to update this dataset");
}

// instantiate the casl matrix for the user
const user: JWTUser = request.user as JWTUser;
const ability = this.caslAbilityFactory.datasetInstanceAccess(user);
// check if he/she can create this dataset
const canUpdate =
ability.can(Action.DatasetUpdateAny, DatasetClass) ||
ability.can(Action.DatasetUpdateOwner, datasetInstance);
const updateDatasetDto = this.convertObsoleteToCurrentSchema(
validatedUpdateDatasetObsoleteDto,
) as UpdateDatasetDto;

if (!canUpdate) {
throw new ForbiddenException("Unauthorized to update this dataset");
const res = this.convertCurrentToObsoleteSchema(
await this.datasetsService.findByIdAndUpdate(pid, updateDatasetDto),
);
return res;
} else {
throw new HttpException("Precondition Failed", HttpStatus.PRECONDITION_FAILED);
}

const updateDatasetDto = this.convertObsoleteToCurrentSchema(
validatedUpdateDatasetObsoleteDto,
) as UpdateDatasetDto;

const res = this.convertCurrentToObsoleteSchema(
await this.datasetsService.findByIdAndUpdate(pid, updateDatasetDto),
);
return res;
}

// PUT /datasets/:id
Expand Down
36 changes: 24 additions & 12 deletions src/datasets/datasets.v4.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
ForbiddenException,
InternalServerErrorException,
ConflictException,
UsePipes,
UsePipes, Headers, HttpException,
} from "@nestjs/common";
import {
ApiBearerAuth,
Expand Down Expand Up @@ -780,11 +780,18 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
async findByIdAndUpdate(
@Req() request: Request,
@Param("pid") pid: string,
@Headers() headers: Record<string, string>,
@Body()
updateDatasetDto: PartialUpdateDatasetDto,
updateDatasetDto: PartialUpdateDatasetDto,
): Promise<OutputDatasetDto | null> {

const headerDateString = headers['if-unmodified-since'];
const headerDate = headerDateString && !isNaN(new Date(headerDateString).getTime())
? new Date(headerDateString)
: null;

const foundDataset = await this.datasetsService.findOne({
where: { pid },
where: {pid},
});

await this.checkPermissionsForDatasetExtended(
Expand All @@ -809,15 +816,20 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
);
}

const updateDatasetDtoForService =
request.headers["content-type"] === "application/merge-patch+json"
? jmp.apply(foundDataset, updateDatasetDto)
: updateDatasetDto;
const updatedDataset = await this.datasetsService.findByIdAndUpdate(
pid,
updateDatasetDtoForService,
);
return updatedDataset;
if (!headerDate || headerDate > foundDataset.updatedAt) {

const updateDatasetDtoForService =
request.headers["content-type"] === "application/merge-patch+json"
? jmp.apply(foundDataset, updateDatasetDto)
: updateDatasetDto;
const updatedDataset = await this.datasetsService.findByIdAndUpdate(
pid,
updateDatasetDtoForService,
);
return updatedDataset;
} else {
throw new HttpException("Precondition Failed", HttpStatus.PRECONDITION_FAILED);
}
}

// GET /datasets/:id/datasetlifecycle
Expand Down
91 changes: 56 additions & 35 deletions src/instruments/instruments.controller.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
import {
Body,
ConflictException,
Controller,
Delete,
Get,
Post,
Body,
Patch,
Headers,
HttpException,
HttpStatus,
InternalServerErrorException,
NotFoundException,
Param,
Delete,
UseGuards,
Patch,
Post,
Query,
UseInterceptors,
InternalServerErrorException,
ConflictException,
UseGuards,
UseInterceptors
} from "@nestjs/common";
import { MongoError } from "mongodb";
import { InstrumentsService } from "./instruments.service";
import { CreateInstrumentDto } from "./dto/create-instrument.dto";
import { PartialUpdateInstrumentDto } from "./dto/update-instrument.dto";
import {MongoError} from "mongodb";
import {InstrumentsService} from "./instruments.service";
import {CreateInstrumentDto} from "./dto/create-instrument.dto";
import {PartialUpdateInstrumentDto} from "./dto/update-instrument.dto";
import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { PoliciesGuard } from "src/casl/guards/policies.guard";
import { CheckPolicies } from "src/casl/decorators/check-policies.decorator";
import { AppAbility } from "src/casl/casl-ability.factory";
import { Action } from "src/casl/action.enum";
import { Instrument, InstrumentDocument } from "./schemas/instrument.schema";
import { FormatPhysicalQuantitiesInterceptor } from "src/common/interceptors/format-physical-quantities.interceptor";
import { IFilters } from "src/common/interfaces/common.interface";
import {PoliciesGuard} from "src/casl/guards/policies.guard";
import {CheckPolicies} from "src/casl/decorators/check-policies.decorator";
import {AppAbility} from "src/casl/casl-ability.factory";
import {Action} from "src/casl/action.enum";
import {Instrument, InstrumentDocument} from "./schemas/instrument.schema";
import {
FormatPhysicalQuantitiesInterceptor
} from "src/common/interceptors/format-physical-quantities.interceptor";
import {IFilters} from "src/common/interfaces/common.interface";
import {
filterDescription,
filterExample,
replaceLikeOperator,
} from "src/common/utils";
import { CountApiResponse } from "src/common/types";
import {CountApiResponse} from "src/common/types";

@ApiBearerAuth()
@ApiTags("instruments")
Expand Down Expand Up @@ -157,26 +163,41 @@ export class InstrumentsController {
async update(
@Param("id") id: string,
@Body() updateInstrumentDto: PartialUpdateInstrumentDto,
@Headers() headers: Record<string, string>,
): Promise<Instrument | null> {
try {
const instrument = await this.instrumentsService.update(
{ _id: id },
updateInstrumentDto,
);

return instrument;
} catch (error) {

const headerDateString = headers['if-unmodified-since'];
const headerDate = headerDateString && !isNaN(new Date(headerDateString).getTime())
? new Date(headerDateString)
: null;

return this.instrumentsService.findOne({where: {_id: id}}).then((instrument) => {
if (!instrument) {
throw new NotFoundException("Instrument not found");
}

// If header is missing, always update
if (!headerDate) {
return this.instrumentsService.update({_id: id}, updateInstrumentDto);
}

// If header is present, compare with updatedAt
if (!instrument.updatedAt || headerDate > instrument.updatedAt) {
return this.instrumentsService.update({_id: id}, updateInstrumentDto);
} else {
throw new HttpException("Precondition Failed", HttpStatus.PRECONDITION_FAILED);
}
}).then((updatedInstrument) => {
return updatedInstrument;
}).catch((error) => {
if ((error as MongoError).code === 11000) {
throw new ConflictException(
"Instrument with the same unique name already exists",
);
throw new ConflictException("Instrument with the same unique name already exists");
} else {
throw new InternalServerErrorException(
"Something went wrong. Please try again later.",
{ cause: error },
);
throw error; // rethrow other errors (e.g. PreconditionFailed, NotFound)
}
}
});

}

@UseGuards(PoliciesGuard)
Expand Down
12 changes: 8 additions & 4 deletions src/instruments/schemas/instrument.schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { ApiProperty } from "@nestjs/swagger";
import { Document } from "mongoose";
import { v4 as uuidv4 } from "uuid";
import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose";
import {ApiProperty} from "@nestjs/swagger";
import {Document} from "mongoose";
import {v4 as uuidv4} from "uuid";

export type InstrumentDocument = Instrument & Document;

Expand Down Expand Up @@ -74,6 +74,10 @@ export class Instrument {
default: {},
})
customMetadata: Record<string, unknown>;

createdAt?: Date;
updatedAt?: Date;

}

export const InstrumentSchema = SchemaFactory.createForClass(Instrument);
Expand Down
Loading
Loading