From c049b2272344f631f637f948ee860bfdbdc9af98 Mon Sep 17 00:00:00 2001 From: Arno Kaimbacher Date: Fri, 19 Sep 2025 14:35:23 +0200 Subject: [PATCH 1/4] - feat: Enhance README with setup instructions, usage, and command documentation - fix: Update API routes to include DOI URL handling and improve route organization - chore: Add ORCID preload rule file and ensure proper registration - docs: Add MIT License to the project for open-source compliance - feat: Implement command to detect and fix missing dataset cross-references - feat: Create command for updating DataCite DOI records with detailed logging and error handling - docs: Add comprehensive documentation for dataset indexing command - docs: Create detailed documentation for DataCite update command with usage examples and error handling --- LICENSE | 22 + app/Controllers/Http/Api/DatasetController.ts | 212 +++- app/Library/Doi/DoiClient.ts | 243 ++++- commands/fix_dataset_cross_references.ts | 317 ++++++ commands/update_datacite.ts | 271 +++++ docs/commands/index-datasets.md | 278 +++++ docs/commands/update-datacite.md | 216 ++++ package-lock.json | 989 +++++++++--------- readme.md | 174 ++- start/routes/api.ts | 18 +- start/rules/orcid.ts | 2 +- 11 files changed, 2187 insertions(+), 555 deletions(-) create mode 100644 LICENSE create mode 100644 commands/fix_dataset_cross_references.ts create mode 100644 commands/update_datacite.ts create mode 100644 docs/commands/index-datasets.md create mode 100644 docs/commands/update-datacite.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..23bb390 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + +MIT License + +Copyright (c) 2025 Tethys Research Repository + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE \ No newline at end of file diff --git a/app/Controllers/Http/Api/DatasetController.ts b/app/Controllers/Http/Api/DatasetController.ts index f6bc648..0e38e34 100644 --- a/app/Controllers/Http/Api/DatasetController.ts +++ b/app/Controllers/Http/Api/DatasetController.ts @@ -1,23 +1,35 @@ import type { HttpContext } from '@adonisjs/core/http'; -// import Person from 'App/Models/Person'; import Dataset from '#models/dataset'; import { StatusCodes } from 'http-status-codes'; // node ace make:controller Author export default class DatasetController { - public async index({}: HttpContext) { - // Select datasets with server_state 'published' or 'deleted' and sort by the last published date - const datasets = await Dataset.query() - .where(function (query) { - query.where('server_state', 'published').orWhere('server_state', 'deleted'); - }) - .preload('titles') - .preload('identifier') - .orderBy('server_date_published', 'desc'); + /** + * GET /api/datasets + * Find all published datasets + */ + public async index({ response }: HttpContext) { + try { + const datasets = await Dataset.query() + .where(function (query) { + query.where('server_state', 'published').orWhere('server_state', 'deleted'); + }) + .preload('titles') + .preload('identifier') + .orderBy('server_date_published', 'desc'); - return datasets; + return response.status(StatusCodes.OK).json(datasets); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: error.message || 'Some error occurred while retrieving datasets.', + }); + } } + /** + * GET /api/dataset + * Find all published datasets + */ public async findAll({ response }: HttpContext) { try { const datasets = await Dataset.query() @@ -33,48 +45,142 @@ export default class DatasetController { } } - public async findOne({ params }: HttpContext) { - const datasets = await Dataset.query() - .where('publish_id', params.publish_id) - .preload('titles') - .preload('descriptions') - .preload('user', (builder) => { - builder.select(['id', 'firstName', 'lastName', 'avatar', 'login']); - }) - .preload('authors', (builder) => { - builder - .select(['id', 'academic_title', 'first_name', 'last_name', 'identifier_orcid', 'status', 'name_type']) - .withCount('datasets', (query) => { - query.as('datasets_count'); - }) - .pivotColumns(['role', 'sort_order']) - .orderBy('pivot_sort_order', 'asc'); - }) - .preload('contributors', (builder) => { - builder - .select(['id', 'academic_title', 'first_name', 'last_name', 'identifier_orcid', 'status', 'name_type']) - .withCount('datasets', (query) => { - query.as('datasets_count'); - }) - .pivotColumns(['role', 'sort_order', 'contributor_type']) - .orderBy('pivot_sort_order', 'asc'); - }) - .preload('subjects') - .preload('coverage') - .preload('licenses') - .preload('references') - .preload('project') - .preload('referenced_by', (builder) => { - builder.preload('dataset', (builder) => { - builder.preload('identifier'); - }); - }) - .preload('files', (builder) => { - builder.preload('hashvalues'); - }) - .preload('identifier') - .firstOrFail(); + /** + * GET /api/dataset/:publish_id + * Find one dataset by publish_id + */ + public async findOne({ response, params }: HttpContext) { + try { + const dataset = await Dataset.query() + .where('publish_id', params.publish_id) + .preload('titles') + .preload('descriptions') // Using 'descriptions' instead of 'abstracts' + .preload('user', (builder) => { + builder.select(['id', 'firstName', 'lastName', 'avatar', 'login']); + }) + .preload('authors', (builder) => { + builder + .select(['id', 'academic_title', 'first_name', 'last_name', 'identifier_orcid', 'status', 'name_type']) + .withCount('datasets', (query) => { + query.as('datasets_count'); + }) + .pivotColumns(['role', 'sort_order']) + .orderBy('pivot_sort_order', 'asc'); + }) + .preload('contributors', (builder) => { + builder + .select(['id', 'academic_title', 'first_name', 'last_name', 'identifier_orcid', 'status', 'name_type']) + .withCount('datasets', (query) => { + query.as('datasets_count'); + }) + .pivotColumns(['role', 'sort_order', 'contributor_type']) + .orderBy('pivot_sort_order', 'asc'); + }) + .preload('subjects') + .preload('coverage') + .preload('licenses') + .preload('references') + .preload('project') + .preload('referenced_by', (builder) => { + builder.preload('dataset', (builder) => { + builder.preload('identifier'); + }); + }) + .preload('files', (builder) => { + builder.preload('hashvalues'); + }) + .preload('identifier') + .first(); // Use first() instead of firstOrFail() to handle not found gracefully - return datasets; + if (!dataset) { + return response.status(StatusCodes.NOT_FOUND).json({ + message: `Cannot find Dataset with publish_id=${params.publish_id}.`, + }); + } + + return response.status(StatusCodes.OK).json(dataset); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: error.message || `Error retrieving Dataset with publish_id=${params.publish_id}.`, + }); + } + } + + /** + * GET /:prefix/:value + * Find dataset by identifier (e.g., https://doi.tethys.at/10.24341/tethys.99.2) + */ + public async findByIdentifier({ response, params }: HttpContext) { + const identifierValue = `${params.prefix}/${params.value}`; + + // Optional: Validate DOI format + if (!identifierValue.match(/^10\.\d+\/[a-zA-Z0-9._-]+\.[0-9]+(?:\.[0-9]+)*$/)) { + return response.status(StatusCodes.BAD_REQUEST).json({ + message: `Invalid DOI format: ${identifierValue}`, + }); + } + + try { + // Method 1: Using subquery with whereIn (most similar to your original) + const dataset = await Dataset.query() + // .whereIn('id', (subQuery) => { + // subQuery.select('dataset_id').from('dataset_identifiers').where('value', identifierValue); + // }) + .whereHas('identifier', (builder) => { + builder.where('value', identifierValue); + }) + .preload('titles') + .preload('descriptions') // Using 'descriptions' instead of 'abstracts' + .preload('user', (builder) => { + builder.select(['id', 'firstName', 'lastName', 'avatar', 'login']); + }) + .preload('authors', (builder) => { + builder + .select(['id', 'academic_title', 'first_name', 'last_name', 'identifier_orcid', 'status', 'name_type']) + .withCount('datasets', (query) => { + query.as('datasets_count'); + }) + .pivotColumns(['role', 'sort_order']) + .wherePivot('role', 'author') + .orderBy('pivot_sort_order', 'asc'); + }) + .preload('contributors', (builder) => { + builder + .select(['id', 'academic_title', 'first_name', 'last_name', 'identifier_orcid', 'status', 'name_type']) + .withCount('datasets', (query) => { + query.as('datasets_count'); + }) + .pivotColumns(['role', 'sort_order', 'contributor_type']) + .wherePivot('role', 'contributor') + .orderBy('pivot_sort_order', 'asc'); + }) + .preload('subjects') + .preload('coverage') + .preload('licenses') + .preload('references') + .preload('project') + .preload('referenced_by', (builder) => { + builder.preload('dataset', (builder) => { + builder.preload('identifier'); + }); + }) + .preload('files', (builder) => { + builder.preload('hashvalues'); + }) + .preload('identifier') + .first(); + + if (!dataset) { + return response.status(StatusCodes.NOT_FOUND).json({ + message: `Cannot find Dataset with identifier=${identifierValue}.`, + }); + } + + return response.status(StatusCodes.OK).json(dataset); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: error.message || `Error retrieving Dataset with identifier=${identifierValue}.`, + }); + } } } diff --git a/app/Library/Doi/DoiClient.ts b/app/Library/Doi/DoiClient.ts index 86151e7..4232ac4 100644 --- a/app/Library/Doi/DoiClient.ts +++ b/app/Library/Doi/DoiClient.ts @@ -1,6 +1,3 @@ -// import { Client } from 'guzzle'; -// import { Log } from '@adonisjs/core/build/standalone'; -// import { DoiInterface } from './interfaces/DoiInterface'; import DoiClientContract from '#app/Library/Doi/DoiClientContract'; import DoiClientException from '#app/exceptions/DoiClientException'; import { StatusCodes } from 'http-status-codes'; @@ -12,14 +9,14 @@ export class DoiClient implements DoiClientContract { public username: string; public password: string; public serviceUrl: string; + public apiUrl: string; constructor() { // const datacite_environment = process.env.DATACITE_ENVIRONMENT || 'debug'; this.username = process.env.DATACITE_USERNAME || ''; this.password = process.env.DATACITE_PASSWORD || ''; this.serviceUrl = process.env.DATACITE_SERVICE_URL || ''; - // this.prefix = process.env.DATACITE_PREFIX || ''; - // this.base_domain = process.env.BASE_DOMAIN || ''; + this.apiUrl = process.env.DATACITE_API_URL || 'https://api.datacite.org'; if (this.username === '' || this.password === '' || this.serviceUrl === '') { const message = 'issing configuration settings to properly initialize DOI client'; @@ -90,4 +87,240 @@ export class DoiClient implements DoiClientContract { throw new DoiClientException(error.response.status, error.response.data); } } + + /** + * Retrieves DOI information from DataCite REST API + * + * @param doiValue The DOI identifier e.g. '10.5072/tethys.999' + * @returns Promise with DOI information or null if not found + */ + public async getDoiInfo(doiValue: string): Promise { + try { + // Use configurable DataCite REST API URL + const dataciteApiUrl = `${this.apiUrl}/dois/${doiValue}`; + const response = await axios.get(dataciteApiUrl, { + headers: { + Accept: 'application/vnd.api+json', + }, + }); + + if (response.status === 200 && response.data.data) { + return { + created: response.data.data.attributes.created, + registered: response.data.data.attributes.registered, + updated: response.data.data.attributes.updated, + published: response.data.data.attributes.published, + state: response.data.data.attributes.state, + url: response.data.data.attributes.url, + metadata: response.data.data.attributes, + }; + } + } catch (error) { + if (error.response?.status === 404) { + logger.debug(`DOI ${doiValue} not found in DataCite`); + return null; + } + + logger.debug(`DataCite REST API failed for ${doiValue}: ${error.message}`); + + // Fallback to MDS API + return await this.getDoiInfoFromMds(doiValue); + } + + return null; + } + + /** + * Fallback method to get DOI info from MDS API + * + * @param doiValue The DOI identifier + * @returns Promise with basic DOI information or null + */ + private async getDoiInfoFromMds(doiValue: string): Promise { + try { + const auth = { + username: this.username, + password: this.password, + }; + + // Get DOI URL + const doiResponse = await axios.get(`${this.serviceUrl}/doi/${doiValue}`, { auth }); + + if (doiResponse.status === 200) { + // Get metadata if available + try { + const metadataResponse = await axios.get(`${this.serviceUrl}/metadata/${doiValue}`, { + auth, + headers: { + Accept: 'application/xml', + }, + }); + + return { + url: doiResponse.data.trim(), + metadata: metadataResponse.data, + created: new Date().toISOString(), // MDS doesn't provide creation dates + registered: new Date().toISOString(), // Use current time as fallback + source: 'mds', + }; + } catch (metadataError) { + // Return basic info even if metadata fetch fails + return { + url: doiResponse.data.trim(), + created: new Date().toISOString(), + registered: new Date().toISOString(), + source: 'mds', + }; + } + } + } catch (error) { + if (error.response?.status === 404) { + logger.debug(`DOI ${doiValue} not found in DataCite MDS`); + return null; + } + + logger.debug(`DataCite MDS API failed for ${doiValue}: ${error.message}`); + } + + return null; + } + + /** + * Checks if a DOI exists in DataCite + * + * @param doiValue The DOI identifier + * @returns Promise True if DOI exists + */ + public async doiExists(doiValue: string): Promise { + const doiInfo = await this.getDoiInfo(doiValue); + return doiInfo !== null; + } + + /** + * Gets the last modification date of a DOI + * + * @param doiValue The DOI identifier + * @returns Promise Last modification date or creation date if never updated, null if not found + */ + public async getDoiLastModified(doiValue: string): Promise { + const doiInfo = await this.getDoiInfo(doiValue); + + if (doiInfo) { + // Use updated date if available, otherwise fall back to created/registered date + const dateToUse = doiInfo.updated || doiInfo.registered || doiInfo.created; + + if (dateToUse) { + logger.debug( + `DOI ${doiValue}: Using ${doiInfo.updated ? 'updated' : doiInfo.registered ? 'registered' : 'created'} date: ${dateToUse}`, + ); + return new Date(dateToUse); + } + } + + return null; + } + + /** + * Makes a DOI unfindable (registered but not discoverable) + * Note: DOIs cannot be deleted, only made unfindable + * await doiClient.makeDoiUnfindable('10.21388/tethys.231'); + * + * @param doiValue The DOI identifier e.g. '10.5072/tethys.999' + * @returns Promise> The http response + */ + public async makeDoiUnfindable(doiValue: string): Promise> { + const auth = { + username: this.username, + password: this.password, + }; + + try { + // First, check if DOI exists + const exists = await this.doiExists(doiValue); + if (!exists) { + throw new DoiClientException(404, `DOI ${doiValue} not found`); + } + + // Delete the DOI URL mapping to make it unfindable + // This removes the URL but keeps the metadata registered + const response = await axios.delete(`${this.serviceUrl}/doi/${doiValue}`, { auth }); + + // Response Codes for DELETE /doi/{doi} + // 200 OK: operation successful + // 401 Unauthorized: no login + // 403 Forbidden: login problem, quota exceeded + // 404 Not Found: DOI does not exist + if (response.status !== 200) { + const message = `Unexpected DataCite MDS response code ${response.status}`; + logger.error(message); + throw new DoiClientException(response.status, message); + } + + logger.info(`DOI ${doiValue} successfully made unfindable`); + return response; + } catch (error) { + logger.error(`Failed to make DOI ${doiValue} unfindable: ${error.message}`); + if (error instanceof DoiClientException) { + throw error; + } + throw new DoiClientException(error.response?.status || 500, error.response?.data || error.message); + } + } + + /** + * Makes a DOI findable again by re-registering the URL + * await doiClient.makeDoiFindable( + * '10.21388/tethys.231', + * 'https://doi.dev.tethys.at/10.21388/tethys.231' + * ); + * + * @param doiValue The DOI identifier e.g. '10.5072/tethys.999' + * @param landingPageUrl The landing page URL + * @returns Promise> The http response + */ + public async makeDoiFindable(doiValue: string, landingPageUrl: string): Promise> { + const auth = { + username: this.username, + password: this.password, + }; + + try { + // Re-register the DOI with its URL to make it findable again + const response = await axios.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, { auth }); + + // Response Codes for PUT /doi/{doi} + // 201 Created: operation successful + // 400 Bad Request: request body must be exactly two lines: DOI and URL + // 401 Unauthorized: no login + // 403 Forbidden: login problem, quota exceeded + // 412 Precondition failed: metadata must be uploaded first + if (response.status !== 201) { + const message = `Unexpected DataCite MDS response code ${response.status}`; + logger.error(message); + throw new DoiClientException(response.status, message); + } + + logger.info(`DOI ${doiValue} successfully made findable again`); + return response; + } catch (error) { + logger.error(`Failed to make DOI ${doiValue} findable: ${error.message}`); + if (error instanceof DoiClientException) { + throw error; + } + throw new DoiClientException(error.response?.status || 500, error.response?.data || error.message); + } + } + + /** + * Gets the current state of a DOI (draft, registered, findable) + * const state = await doiClient.getDoiState('10.21388/tethys.231'); + * console.log(`Current state: ${state}`); // 'findable' + * + * @param doiValue The DOI identifier + * @returns Promise The DOI state or null if not found + */ + public async getDoiState(doiValue: string): Promise { + const doiInfo = await this.getDoiInfo(doiValue); + return doiInfo?.state || null; + } } diff --git a/commands/fix_dataset_cross_references.ts b/commands/fix_dataset_cross_references.ts new file mode 100644 index 0000000..2662e25 --- /dev/null +++ b/commands/fix_dataset_cross_references.ts @@ -0,0 +1,317 @@ +/* +|-------------------------------------------------------------------------- +| node ace make:command fix-dataset-cross-references +| DONE: create commands/fix_dataset_cross_references.ts +|-------------------------------------------------------------------------- +*/ +import { BaseCommand, flags } from '@adonisjs/core/ace'; +import type { CommandOptions } from '@adonisjs/core/types/ace'; +import Dataset from '#models/dataset'; +import DatasetReference from '#models/dataset_reference'; +// import env from '#start/env'; + +interface MissingCrossReference { + sourceDatasetId: number; + targetDatasetId: number; + sourcePublishId: number | null; + targetPublishId: number | null; + referenceType: string; + relation: string; + doi: string | null; + reverseRelation: string; +} + +export default class DetectMissingCrossReferences extends BaseCommand { + static commandName = 'detect:missing-cross-references'; + static description = 'Detect missing bidirectional cross-references between versioned datasets'; + + public static needsApplication = true; + + @flags.boolean({ alias: 'f', description: 'Fix missing cross-references automatically' }) + public fix: boolean = false; + + @flags.boolean({ alias: 'v', description: 'Verbose output' }) + public verbose: boolean = false; + + public static options: CommandOptions = { + startApp: true, + staysAlive: false, + }; + + async run() { + this.logger.info('πŸ” Detecting missing cross-references...'); + + try { + const missingReferences = await this.findMissingCrossReferences(); + + if (missingReferences.length === 0) { + this.logger.success('All cross-references are properly linked!'); + return; + } + + this.logger.warning(`Found ${missingReferences.length} missing cross-reference(s):`); + + for (const missing of missingReferences) { + this.logger.info( + `Dataset ${missing.sourceDatasetId} references ${missing.targetDatasetId}, but reverse reference is missing`, + ); + + if (this.verbose) { + this.logger.info(` - Reference type: ${missing.referenceType}`); + this.logger.info(` - Relation: ${missing.relation}`); + this.logger.info(` - DOI: ${missing.doi}`); + } + } + + if (this.fix) { + await this.fixMissingReferences(missingReferences); + this.logger.success('All missing cross-references have been fixed!'); + } else { + this.printMissingReferencesList(missingReferences); + this.logger.info('πŸ’‘ Run with --fix flag to automatically create missing cross-references'); + } + } catch (error) { + this.logger.error('Error detecting missing cross-references:', error); + process.exit(1); + } + } + private async findMissingCrossReferences(): Promise { + const missingReferences: { + sourceDatasetId: number; + targetDatasetId: number; + sourcePublishId: number | null; + targetPublishId: number | null; + referenceType: string; + relation: string; + doi: string | null; + reverseRelation: string; + }[] = []; + + this.logger.info('πŸ“Š Querying dataset references...'); + + // Find all references that point to Tethys datasets (DOI or URL containing tethys DOI) + // Only from datasets that are published + const tethysReferences = await DatasetReference.query() + .whereIn('type', ['DOI', 'URL']) + .where((query) => { + query.where('value', 'like', '%doi.org/10.24341/tethys.%').orWhere('value', 'like', '%tethys.at/dataset/%'); + }) + .preload('dataset', (datasetQuery) => { + datasetQuery.where('server_state', 'published'); + }) + .whereHas('dataset', (datasetQuery) => { + datasetQuery.where('server_state', 'published'); + }); + + this.logger.info(`πŸ”— Found ${tethysReferences.length} Tethys references from published datasets`); + + let processedCount = 0; + for (const reference of tethysReferences) { + processedCount++; + + if (this.verbose && processedCount % 10 === 0) { + this.logger.info(`πŸ“ˆ Processed ${processedCount}/${tethysReferences.length} references...`); + } + + // Extract dataset publish_id from DOI or URL + const targetDatasetPublish = this.extractDatasetPublishIdFromReference(reference.value); + + if (!targetDatasetPublish) { + if (this.verbose) { + this.logger.warning(`⚠️ Could not extract publish ID from: ${reference.value}`); + } + continue; + } + + // Check if target dataset exists and is published + const targetDataset = await Dataset.query() + .where('publish_id', targetDatasetPublish) + .where('server_state', 'published') + .first(); + + if (!targetDataset) { + if (this.verbose) { + this.logger.warning(`⚠️ Target dataset with publish_id ${targetDatasetPublish} not found or not published`); + } + continue; + } + + // Ensure we have a valid source dataset with proper preloading + if (!reference.dataset) { + this.logger.warning(`⚠️ Source dataset ${reference.document_id} not properly loaded, skipping...`); + continue; + } + + // Check if reverse reference exists + const reverseReferenceExists = await this.checkReverseReferenceExists( + targetDataset.id, + reference.document_id, + reference.relation, + ); + + if (!reverseReferenceExists) { + missingReferences.push({ + sourceDatasetId: reference.document_id, + targetDatasetId: targetDataset.id, + sourcePublishId: reference.dataset.publish_id || null, + targetPublishId: targetDataset.publish_id || null, + referenceType: reference.type, + relation: reference.relation, + doi: reference.value, + reverseRelation: this.getReverseRelation(reference.relation), + }); + } + } + + this.logger.info(`βœ… Processed all ${processedCount} references`); + return missingReferences; + } + + private extractDatasetPublishIdFromReference(value: string): number | null { + // Extract from DOI: https://doi.org/10.24341/tethys.107 -> 107 + const doiMatch = value.match(/10\.24341\/tethys\.(\d+)/); + if (doiMatch) { + return parseInt(doiMatch[1]); + } + + // Extract from URL: https://tethys.at/dataset/107 -> 107 + const urlMatch = value.match(/tethys\.at\/dataset\/(\d+)/); + if (urlMatch) { + return parseInt(urlMatch[1]); + } + + return null; + } + + private async checkReverseReferenceExists( + sourceDatasetId: number, + targetDatasetId: number, + originalRelation: string, + ): Promise { + const reverseRelation = this.getReverseRelation(originalRelation); + + // Only check for reverse references where the source dataset is also published + const reverseReference = await DatasetReference.query() + .where('document_id', sourceDatasetId) + .where('related_document_id', targetDatasetId) + .where('relation', reverseRelation) + .whereHas('dataset', (datasetQuery) => { + datasetQuery.where('server_state', 'published'); + }) + .first(); + + return !!reverseReference; + } + + private getReverseRelation(relation: string): string { + const relationMap: Record = { + IsNewVersionOf: 'IsPreviousVersionOf', + IsPreviousVersionOf: 'IsNewVersionOf', + + IsVersionOf: 'HasVersion', + HasVersion: 'IsVersionOf', + + Compiles: 'IsCompiledBy', + IsCompiledBy: 'Compiles', + + IsVariantFormOf: 'IsOriginalFormOf', + IsOriginalFormOf: 'IsVariantFormOf', + + IsPartOf: 'HasPart', + HasPart: 'IsPartOf', + + IsSupplementTo: 'IsSupplementedBy', + IsSupplementedBy: 'IsSupplementTo', + + Continues: 'IsContinuedBy', + IsContinuedBy: 'Continues', + }; + + // to catch relation types like 'compiles' or 'IsVariantFormOf' that are not in the map mark reverse as 'HasVersion' + return relationMap[relation] || 'HasVersion'; // Default fallback + } + + private printMissingReferencesList(missingReferences: MissingCrossReference[]) { + console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'); + console.log('β”‚ MISSING CROSS-REFERENCES REPORT β”‚'); + console.log('β”‚ (Published Datasets Only) β”‚'); + console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'); + console.log(); + + missingReferences.forEach((missing, index) => { + console.log( + `${index + 1}. Dataset ${missing.sourceDatasetId} (Publish ID: ${missing.sourcePublishId}) β†’ Dataset ${missing.targetDatasetId} (Publish ID: ${missing.targetPublishId})`, + ); + console.log(` β”œβ”€ Current relation: "${missing.relation}"`); + console.log(` β”œβ”€ Missing reverse relation: "${missing.reverseRelation}"`); + console.log(` β”œβ”€ Reference type: ${missing.referenceType}`); + console.log(` └─ DOI/URL: ${missing.doi}`); + console.log(); + }); + + console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'); + console.log(`β”‚ SUMMARY: ${missingReferences.length} missing reverse reference(s) detected β”‚`); + console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'); + } + + private async fixMissingReferences(missingReferences: MissingCrossReference[]) { + this.logger.info('πŸ”§ Creating missing cross-references in database...'); + + let fixedCount = 0; + let errorCount = 0; + + for (const [index, missing] of missingReferences.entries()) { + try { + // Get the source dataset to create proper reference - ensure it's published + const sourceDataset = await Dataset.query() + .where('id', missing.sourceDatasetId) + .where('server_state', 'published') + .preload('identifier') + .first(); + + if (!sourceDataset) { + this.logger.warning(`⚠️ Source dataset ${missing.sourceDatasetId} not found or not published, skipping...`); + errorCount++; + continue; + } + + // Create the reverse reference + const reverseReference = new DatasetReference(); + reverseReference.document_id = missing.targetDatasetId; + reverseReference.related_document_id = missing.sourceDatasetId; + reverseReference.type = 'DOI'; + reverseReference.relation = missing.reverseRelation; + + // Use the source dataset's DOI for the value + if (sourceDataset.identifier?.value) { + reverseReference.value = `https://doi.org/${sourceDataset.identifier.value}`; + } else { + // Fallback to dataset URL if no DOI + reverseReference.value = `https://tethys.at/dataset/${sourceDataset.publish_id || missing.sourceDatasetId}`; + } + + // Use the source dataset's main title for the label + reverseReference.label = sourceDataset.mainTitle || `Dataset ${missing.sourceDatasetId}`; + + await reverseReference.save(); + fixedCount++; + + if (this.verbose) { + this.logger.info( + `βœ… [${index + 1}/${missingReferences.length}] Created reverse reference: Dataset ${missing.targetDatasetId} -> ${missing.sourceDatasetId}`, + ); + } else if ((index + 1) % 10 === 0) { + this.logger.info(`πŸ“ˆ Fixed ${fixedCount}/${missingReferences.length} references...`); + } + } catch (error) { + this.logger.error( + `❌ Error creating reverse reference for datasets ${missing.targetDatasetId} -> ${missing.sourceDatasetId}:`, + error, + ); + errorCount++; + } + } + + this.logger.info(`πŸ“Š Fix completed: ${fixedCount} created, ${errorCount} errors`); + } +} diff --git a/commands/update_datacite.ts b/commands/update_datacite.ts new file mode 100644 index 0000000..9280f95 --- /dev/null +++ b/commands/update_datacite.ts @@ -0,0 +1,271 @@ +/* +|-------------------------------------------------------------------------- +| node ace make:command update-datacite +| DONE: create commands/update_datacite.ts +|-------------------------------------------------------------------------- +*/ +import { BaseCommand, flags } from '@adonisjs/core/ace'; +import { CommandOptions } from '@adonisjs/core/types/ace'; +import Dataset from '#models/dataset'; +import { DoiClient } from '#app/Library/Doi/DoiClient'; +import DoiClientException from '#app/exceptions/DoiClientException'; +import Index from '#app/Library/Utils/Index'; +import env from '#start/env'; +import logger from '@adonisjs/core/services/logger'; +import { DateTime } from 'luxon'; +import { getDomain } from '#app/utils/utility-functions'; + +export default class UpdateDatacite extends BaseCommand { + static commandName = 'update:datacite'; + static description = 'Update DataCite DOI records for published datasets'; + + public static needsApplication = true; + + @flags.number({ alias: 'p', description: 'Specific publish_id to update' }) + public publish_id: number; + + @flags.boolean({ alias: 'f', description: 'Force update all records regardless of modification date' }) + public force: boolean = false; + + @flags.boolean({ alias: 'd', description: 'Dry run - show what would be updated without making changes' }) + public dryRun: boolean = false; + + @flags.boolean({ alias: 's', description: 'Show detailed stats for each dataset that needs updating' }) + public stats: boolean = false; + + //example: node ace update:datacite -p 123 --force --dry-run + + public static options: CommandOptions = { + startApp: true, // Whether to boot the application before running the command + stayAlive: false, // Whether to keep the process alive after the command has executed + }; + + async run() { + logger.info('Starting DataCite update process...'); + + const prefix = env.get('DATACITE_PREFIX', ''); + const base_domain = env.get('BASE_DOMAIN', ''); + const apiUrl = env.get('DATACITE_API_URL', 'https://api.datacite.org'); + + if (!prefix || !base_domain) { + logger.error('Missing DATACITE_PREFIX or BASE_DOMAIN environment variables'); + return; + } + + logger.info(`Using DataCite API: ${apiUrl}`); + + const datasets = await this.getDatasets(); + logger.info(`Found ${datasets.length} datasets to process`); + + let updated = 0; + let skipped = 0; + let errors = 0; + + for (const dataset of datasets) { + try { + const shouldUpdate = this.force || (await this.shouldUpdateDataset(dataset)); + + if (this.stats) { + // Stats mode: show detailed information for datasets that need updating + if (shouldUpdate) { + await this.showDatasetStats(dataset); + updated++; + } else { + skipped++; + } + continue; + } + + if (!shouldUpdate) { + logger.info(`Dataset ${dataset.publish_id}: Up to date, skipping`); + skipped++; + continue; + } + + if (this.dryRun) { + logger.info(`Dataset ${dataset.publish_id}: Would update DataCite record (dry run)`); + updated++; + continue; + } + + await this.updateDataciteRecord(dataset, prefix, base_domain); + logger.info(`Dataset ${dataset.publish_id}: Successfully updated DataCite record`); + updated++; + } catch (error) { + logger.error(`Dataset ${dataset.publish_id}: Failed to update - ${error.message}`); + errors++; + } + } + + if (this.stats) { + logger.info(`\nDataCite Stats Summary: ${updated} datasets need updating, ${skipped} are up to date`); + } else { + logger.info(`DataCite update completed. Updated: ${updated}, Skipped: ${skipped}, Errors: ${errors}`); + } + } + + private async getDatasets(): Promise { + const query = Dataset.query() + .preload('identifier') + .preload('xmlCache') + .where('server_state', 'published') + .whereHas('identifier', (identifierQuery) => { + identifierQuery.where('type', 'doi'); + }); + + if (this.publish_id) { + query.where('publish_id', this.publish_id); + } + + return await query.exec(); + } + + private async shouldUpdateDataset(dataset: Dataset): Promise { + try { + // Check if dataset has a DOI identifier (HasOne relationship) + let doiIdentifier = dataset.identifier; + + if (!doiIdentifier) { + // Try to load the relationship if not already loaded + await dataset.load('identifier'); + doiIdentifier = dataset.identifier; + } + + if (!doiIdentifier || doiIdentifier.type !== 'doi') { + logger.warn(`Dataset ${dataset.publish_id}: No DOI identifier found`); + return false; + } + + // Validate dataset modification date + const datasetModified = dataset.server_date_modified; + const now = DateTime.now(); + + if (!datasetModified) { + logger.error(`Dataset ${dataset.publish_id}: server_date_modified is null or undefined`); + return true; // Update anyway if modification date is missing + } + + if (datasetModified > now) { + logger.error( + `Dataset ${dataset.publish_id}: server_date_modified (${datasetModified.toISO()}) is in the future! ` + + `Current time: ${now.toISO()}. This indicates a data integrity issue. Skipping update.`, + ); + return false; // Do not update when modification date is invalid + } + + // Get DOI information from DataCite using DoiClient + const doiClient = new DoiClient(); + const doiLastModified = await doiClient.getDoiLastModified(doiIdentifier.value); + + if (!doiLastModified) { + logger.warn(`Dataset ${dataset.publish_id}: Could not retrieve DOI modification date from DataCite`); + return true; // Update anyway if we can't get DOI info + } + + // Compare dataset modification date with DOI modification date + const doiModified = DateTime.fromJSDate(doiLastModified); + + logger.debug( + `Dataset ${dataset.publish_id}: Dataset modified: ${datasetModified.toISO()}, DOI modified: ${doiModified.toISO()}`, + ); + + // Update if dataset was modified after the DOI record + return datasetModified > doiModified; + } catch (error) { + logger.warn(`Error checking update status for dataset ${dataset.publish_id}: ${error.message}`); + return true; // Update anyway if we can't determine status + } + } + + private async updateDataciteRecord(dataset: Dataset, prefix: string, base_domain: string): Promise { + try { + // Get the DOI identifier (HasOne relationship) + let doiIdentifier = dataset.identifier; + + if (!doiIdentifier) { + await dataset.load('identifier'); + doiIdentifier = dataset.identifier; + } + + if (!doiIdentifier || doiIdentifier.type !== 'doi') { + throw new Error('No DOI identifier found for dataset'); + } + + // Generate XML metadata + const xmlMeta = (await Index.getDoiRegisterString(dataset)) as string; + if (!xmlMeta) { + throw new Error('Failed to generate XML metadata'); + } + + // Construct DOI value and landing page URL + const doiValue = doiIdentifier.value; // Use existing DOI value + const landingPageUrl = `https://doi.${getDomain(base_domain)}/${doiValue}`; + + // Update DataCite record + const doiClient = new DoiClient(); + const dataciteResponse = await doiClient.registerDoi(doiValue, xmlMeta, landingPageUrl); + + if (dataciteResponse?.status === 201) { + // // Update dataset modification date + // dataset.server_date_modified = DateTime.now(); + // await dataset.save(); + + // // Update search index + // const index_name = 'tethys-records'; + // await Index.indexDocument(dataset, index_name); + + logger.debug(`Dataset ${dataset.publish_id}: DataCite record and search index updated successfully`); + } else { + throw new DoiClientException( + dataciteResponse?.status || 500, + `Unexpected DataCite response code: ${dataciteResponse?.status}`, + ); + } + } catch (error) { + if (error instanceof DoiClientException) { + throw error; + } + throw new Error(`Failed to update DataCite record: ${error.message}`); + } + } + + /** + * Shows detailed statistics for a dataset that needs updating + */ + private async showDatasetStats(dataset: Dataset): Promise { + try { + let doiIdentifier = dataset.identifier; + + if (!doiIdentifier) { + await dataset.load('identifier'); + doiIdentifier = dataset.identifier; + } + + const doiValue = doiIdentifier?.value || 'N/A'; + const doiStatus = doiIdentifier?.status || 'N/A'; + const datasetModified = dataset.server_date_modified; + + // Get DOI info from DataCite + const doiClient = new DoiClient(); + const doiLastModified = await doiClient.getDoiLastModified(doiValue); + const doiState = await doiClient.getDoiState(doiValue); + + console.log(` + β”Œβ”€ Dataset ${dataset.publish_id} ─────────────────────────────────────────────────────────────── + β”‚ DOI Value: ${doiValue} + β”‚ DOI Status (DB): ${doiStatus} + β”‚ DOI State (DataCite): ${doiState || 'Unknown'} + β”‚ Dataset Modified: ${datasetModified ? datasetModified.toISO() : 'N/A'} + β”‚ DOI Modified: ${doiLastModified ? DateTime.fromJSDate(doiLastModified).toISO() : 'N/A'} + β”‚ Needs Update: YES - Dataset newer than DOI + └─────────────────────────────────────────────────────────────────────────────────────────────`); + } catch (error) { + console.log(` + β”Œβ”€ Dataset ${dataset.publish_id} ─────────────────────────────────────────────────────────────── + β”‚ DOI Value: ${dataset.identifier?.value || 'N/A'} + β”‚ Error: ${error.message} + β”‚ Needs Update: YES - Error checking status + └─────────────────────────────────────────────────────────────────────────────────────────────`); + } + } +} diff --git a/docs/commands/index-datasets.md b/docs/commands/index-datasets.md new file mode 100644 index 0000000..baecfe6 --- /dev/null +++ b/docs/commands/index-datasets.md @@ -0,0 +1,278 @@ +# Dataset Indexing Command + +AdonisJS Ace command for indexing and synchronizing published datasets with OpenSearch for search functionality. + +## Overview + +The `index:datasets` command processes published datasets and creates/updates corresponding search index documents in OpenSearch. It intelligently compares modification timestamps to only re-index datasets when necessary, optimizing performance while maintaining search index accuracy. + +## Command Syntax + +```bash +node ace index:datasets [options] +``` + +## Options + +| Flag | Alias | Description | +|------|-------|-------------| +| `--publish_id ` | `-p` | Index a specific dataset by publish_id | + +## Usage Examples + +### Basic Operations + +```bash +# Index all published datasets that have been modified since last indexing +node ace index:datasets + +# Index a specific dataset by publish_id +node ace index:datasets --publish_id 231 +node ace index:datasets -p 231 +``` + +## How It Works + +### 1. **Dataset Selection** +The command processes datasets that meet these criteria: +- `server_state = 'published'` - Only published datasets +- Has preloaded `xmlCache` relationship for metadata transformation +- Optionally filtered by specific `publish_id` + +### 2. **Smart Update Detection** +For each dataset, the command: +- Checks if the dataset exists in the OpenSearch index +- Compares `server_date_modified` timestamps +- Only re-indexes if the dataset is newer than the indexed version + +### 3. **Document Processing** +The indexing process involves: +1. **XML Generation**: Creates structured XML from dataset metadata +2. **XSLT Transformation**: Converts XML to JSON using Saxon-JS processor +3. **Index Update**: Updates or creates the document in OpenSearch +4. **Logging**: Records success/failure for each operation + +## Index Structure + +### Index Configuration +- **Index Name**: `tethys-records` +- **Document ID**: Dataset `publish_id` +- **Refresh**: `true` (immediate availability) + +### Document Fields +The indexed documents contain: +- **Metadata Fields**: Title, description, authors, keywords +- **Identifiers**: DOI, publish_id, and other identifiers +- **Temporal Data**: Publication dates, coverage periods +- **Geographic Data**: Spatial coverage information +- **Technical Details**: Data formats, access information +- **Timestamps**: Creation and modification dates + +## Example Output + +### Successful Run +```bash +node ace index:datasets +``` +``` +Found 150 published datasets to process +Dataset with publish_id 231 successfully indexed +Dataset with publish_id 245 is up to date, skipping indexing +Dataset with publish_id 267 successfully indexed +An error occurred while indexing dataset with publish_id 289. Error: Invalid XML metadata +Processing completed: 148 indexed, 1 skipped, 1 error +``` + +### Specific Dataset +```bash +node ace index:datasets --publish_id 231 +``` +``` +Found 1 published dataset to process +Dataset with publish_id 231 successfully indexed +Processing completed: 1 indexed, 0 skipped, 0 errors +``` + +## Update Logic + +The command uses intelligent indexing to avoid unnecessary processing: + +| Condition | Action | Reason | +|-----------|--------|--------| +| Dataset not in index | βœ… Index | New dataset needs indexing | +| Dataset newer than indexed version | βœ… Re-index | Dataset has been updated | +| Dataset same/older than indexed version | ❌ Skip | Already up to date | +| OpenSearch document check fails | βœ… Index | Better safe than sorry | +| Invalid XML metadata | ❌ Skip + Log Error | Cannot process invalid data | + +### Timestamp Comparison +```typescript +// Example comparison logic +const existingModified = DateTime.fromMillis(Number(existingDoc.server_date_modified) * 1000); +const currentModified = dataset.server_date_modified; + +if (currentModified <= existingModified) { + // Skip - already up to date + return false; +} +// Proceed with indexing +``` + +## XML Transformation Process + +### 1. **XML Generation** +```xml + + + + + Research Dataset Title + Dataset description... + + + +``` + +### 2. **XSLT Processing** +The command uses Saxon-JS with a compiled stylesheet (`solr.sef.json`) to transform XML to JSON: +```javascript +const result = await SaxonJS.transform({ + stylesheetText: proc, + destination: 'serialized', + sourceText: xmlString, +}); +``` + +### 3. **Final JSON Document** +```json +{ + "id": "231", + "title": "Research Dataset Title", + "description": "Dataset description...", + "authors": ["Author Name"], + "server_date_modified": 1634567890, + "publish_id": 231 +} +``` + +## Configuration Requirements + +### Environment Variables +```bash +# OpenSearch Configuration +OPENSEARCH_HOST=localhost:9200 + +# For production: +# OPENSEARCH_HOST=your-opensearch-cluster:9200 +``` + +### Required Files +- **XSLT Stylesheet**: `public/assets2/solr.sef.json` - Compiled Saxon-JS stylesheet for XML transformation + +### Database Relationships +The command expects these model relationships: +```typescript +// Dataset model must have: +@hasOne(() => XmlCache, { foreignKey: 'dataset_id' }) +public xmlCache: HasOne +``` + +## Error Handling + +The command handles various error scenarios gracefully: + +### Common Errors and Solutions + +| Error | Cause | Solution | +|-------|-------|----------| +| `XSLT transformation failed` | Invalid XML or missing stylesheet | Check XML structure and stylesheet path | +| `OpenSearch connection error` | Service unavailable | Verify OpenSearch is running and accessible | +| `JSON parse error` | Malformed transformation result | Check XSLT stylesheet output format | +| `Missing xmlCache relationship` | Data integrity issue | Ensure xmlCache exists for dataset | + +### Error Logging +```bash +# Typical error log entry +An error occurred while indexing dataset with publish_id 231. +Error: XSLT transformation failed: Invalid XML structure at line 15 +``` + +## Performance Considerations + +### Batch Processing +- Processes datasets sequentially to avoid overwhelming OpenSearch +- Each dataset is committed individually for reliability +- Failed indexing of one dataset doesn't stop processing others + +### Resource Usage +- **Memory**: XML/JSON transformations require temporary memory +- **Network**: OpenSearch API calls for each dataset +- **CPU**: XSLT transformations are CPU-intensive + +### Optimization Tips +```bash +# Index only recently modified datasets (run regularly) +node ace index:datasets + +# Index specific datasets when needed +node ace index:datasets --publish_id 231 + +# Consider running during off-peak hours for large batches +``` + +## Integration with Other Systems + +### Search Functionality +The indexed documents power: +- **Dataset Search**: Full-text search across metadata +- **Faceted Browsing**: Filter by authors, keywords, dates +- **Geographic Search**: Spatial query capabilities +- **Auto-complete**: Suggest dataset titles and keywords + +### Related Commands +- [`update:datacite`](update-datacite.md) - Often run after indexing to sync DOI metadata +- **Database migrations** - May require re-indexing after schema changes + +### API Integration +The indexed data is consumed by: +- **Search API**: `/api/search` endpoints +- **Browse API**: `/api/datasets` with filtering +- **Recommendations**: Related dataset suggestions + +## Monitoring and Maintenance + +### Regular Tasks +```bash +# Daily indexing (recommended cron job) +0 2 * * * cd /path/to/project && node ace index:datasets + +# Weekly full re-index (if needed) +0 3 * * 0 cd /path/to/project && node ace index:datasets --force +``` + +### Health Checks +- Monitor OpenSearch cluster health +- Check for failed indexing operations in logs +- Verify search functionality is working +- Compare dataset counts between database and index + +### Troubleshooting +```bash +# Check specific dataset indexing +node ace index:datasets --publish_id 231 + +# Verify OpenSearch connectivity +curl -X GET "localhost:9200/_cluster/health" + +# Check index statistics +curl -X GET "localhost:9200/tethys-records/_stats" +``` + +## Best Practices + +1. **Regular Scheduling**: Run the command regularly (daily) to keep the search index current +2. **Monitor Logs**: Watch for transformation errors or OpenSearch issues +3. **Backup Strategy**: Include OpenSearch indices in backup procedures +4. **Resource Management**: Monitor OpenSearch cluster resources during bulk operations +5. **Testing**: Verify search functionality after major indexing operations +6. **Coordination**: Run indexing before DataCite updates when both are needed \ No newline at end of file diff --git a/docs/commands/update-datacite.md b/docs/commands/update-datacite.md new file mode 100644 index 0000000..06041f0 --- /dev/null +++ b/docs/commands/update-datacite.md @@ -0,0 +1,216 @@ +# DataCite Update Command + +AdonisJS Ace command for updating DataCite DOI records for published datasets. + +## Overview + +The `update:datacite` command synchronizes your local dataset metadata with DataCite DOI records. It intelligently compares modification dates to only update records when necessary, reducing unnecessary API calls and maintaining data consistency. + +## Command Syntax + +```bash +node ace update:datacite [options] +``` + +## Options + +| Flag | Alias | Description | +|------|-------|-------------| +| `--publish_id ` | `-p` | Update a specific dataset by publish_id | +| `--force` | `-f` | Force update all records regardless of modification date | +| `--dry-run` | `-d` | Preview what would be updated without making changes | +| `--stats` | `-s` | Show detailed statistics for datasets that need updating | + +## Usage Examples + +### Basic Operations + +```bash +# Update all datasets that have been modified since their DOI was last updated +node ace update:datacite + +# Update a specific dataset +node ace update:datacite --publish_id 231 +node ace update:datacite -p 231 + +# Force update all datasets with DOIs (ignores modification dates) +node ace update:datacite --force +``` + +### Preview and Analysis + +```bash +# Preview what would be updated (dry run) +node ace update:datacite --dry-run + +# Show detailed statistics for datasets that need updating +node ace update:datacite --stats + +# Show stats for a specific dataset +node ace update:datacite --stats --publish_id 231 +``` + +### Combined Options + +```bash +# Dry run for a specific dataset +node ace update:datacite --dry-run --publish_id 231 + +# Show stats for all datasets (including up-to-date ones) +node ace update:datacite --stats --force +``` + +## Command Modes + +### 1. **Normal Mode** (Default) +Updates DataCite records for datasets that have been modified since their DOI was last updated. + +**Example Output:** +``` +Using DataCite API: https://api.test.datacite.org +Found 50 datasets to process +Dataset 231: Successfully updated DataCite record +Dataset 245: Up to date, skipping +Dataset 267: Successfully updated DataCite record +DataCite update completed. Updated: 15, Skipped: 35, Errors: 0 +``` + +### 2. **Dry Run Mode** (`--dry-run`) +Shows what would be updated without making any changes to DataCite. + +**Use Case:** Preview updates before running the actual command. + +**Example Output:** +``` +Dataset 231: Would update DataCite record (dry run) +Dataset 267: Would update DataCite record (dry run) +Dataset 245: Up to date, skipping +DataCite update completed. Updated: 2, Skipped: 1, Errors: 0 +``` + +### 3. **Stats Mode** (`--stats`) +Shows detailed information for each dataset that needs updating, including why it needs updating. + +**Use Case:** Debug synchronization issues, monitor dataset/DOI status, generate reports. + +**Example Output:** +``` +β”Œβ”€ Dataset 231 ───────────────────────────────────────────────────────── +β”‚ DOI Value: 10.21388/tethys.231 +β”‚ DOI Status (DB): findable +β”‚ DOI State (DataCite): findable +β”‚ Dataset Modified: 2024-09-15T10:30:00.000Z +β”‚ DOI Modified: 2024-09-10T08:15:00.000Z +β”‚ Needs Update: YES - Dataset newer than DOI +└─────────────────────────────────────────────────────────────────────── + +β”Œβ”€ Dataset 267 ───────────────────────────────────────────────────────── +β”‚ DOI Value: 10.21388/tethys.267 +β”‚ DOI Status (DB): findable +β”‚ DOI State (DataCite): findable +β”‚ Dataset Modified: 2024-09-18T14:20:00.000Z +β”‚ DOI Modified: 2024-09-16T12:45:00.000Z +β”‚ Needs Update: YES - Dataset newer than DOI +└─────────────────────────────────────────────────────────────────────── + +DataCite Stats Summary: 2 datasets need updating, 48 are up to date +``` + +## Update Logic + +The command uses intelligent update detection: + +1. **Compares modification dates**: Dataset `server_date_modified` vs DOI last modification date from DataCite +2. **Validates data integrity**: Checks for missing or future dates +3. **Handles API failures gracefully**: Updates anyway if DataCite info can't be retrieved +4. **Uses dual API approach**: DataCite REST API (primary) with MDS API fallback + +### When Updates Happen + +| Condition | Action | Reason | +|-----------|--------|--------| +| Dataset modified > DOI modified | βœ… Update | Dataset has newer changes | +| Dataset modified ≀ DOI modified | ❌ Skip | DOI is up to date | +| Dataset date in future | ❌ Skip | Invalid data, needs investigation | +| Dataset date missing | βœ… Update | Can't determine staleness | +| DataCite API error | βœ… Update | Better safe than sorry | +| `--force` flag used | βœ… Update | Override all logic | + +## Environment Configuration + +Required environment variables: + +```bash +# DataCite Credentials +DATACITE_USERNAME=your_username +DATACITE_PASSWORD=your_password + +# API Endpoints (environment-specific) +DATACITE_API_URL=https://api.test.datacite.org # Test environment +DATACITE_SERVICE_URL=https://mds.test.datacite.org # Test MDS + +DATACITE_API_URL=https://api.datacite.org # Production +DATACITE_SERVICE_URL=https://mds.datacite.org # Production MDS + +# Project Configuration +DATACITE_PREFIX=10.21388 # Your DOI prefix +BASE_DOMAIN=tethys.at # Your domain +``` + +## Error Handling + +The command handles various error scenarios: + +- **Invalid modification dates**: Logs errors but continues processing other datasets +- **DataCite API failures**: Falls back to MDS API, then to safe update +- **Missing DOI identifiers**: Skips datasets without DOI identifiers +- **Network issues**: Continues with next dataset after logging error + +## Integration + +The command integrates with: + +- **Dataset Model**: Uses `server_date_modified` for change detection +- **DatasetIdentifier Model**: Reads DOI values and status +- **OpenSearch Index**: Updates search index after DataCite update +- **DoiClient**: Handles all DataCite API interactions + +## Common Workflows + +### Daily Maintenance +```bash +# Update any datasets modified today +node ace update:datacite +``` + +### Pre-Deployment Check +```bash +# Check what would be updated before deployment +node ace update:datacite --dry-run +``` + +### Debugging Sync Issues +```bash +# Investigate why specific dataset isn't syncing +node ace update:datacite --stats --publish_id 231 +``` + +### Full Resync +```bash +# Force update all DOI records (use with caution) +node ace update:datacite --force +``` + +### Monitoring Report +```bash +# Generate sync status report +node ace update:datacite --stats > datacite-sync-report.txt +``` + +## Best Practices + +1. **Regular Updates**: Run daily or after bulk dataset modifications +2. **Test First**: Use `--dry-run` or `--stats` before bulk operations +3. **Monitor Logs**: Check for data integrity warnings +4. **Environment Separation**: Use correct API URLs for test vs production +5. **Rate Limiting**: The command handles DataCite rate limits automatically \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d266b68..5ecb2b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,9 +109,9 @@ } }, "node_modules/@adonisjs/ace": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@adonisjs/ace/-/ace-13.3.0.tgz", - "integrity": "sha512-68dveDFd766p69cBvK/MtOrOP0+YKYLeHspa9KLEWcWk9suPf3pbGkHQ2pwDnvLJxBPHk4932KbbSSzzpGNZGw==", + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@adonisjs/ace/-/ace-13.4.0.tgz", + "integrity": "sha512-7Wq6CpXmQm3m/6fKfzubAadCdiH2kKSni+K8s5KcTIFryKSqW+f06UAPOUwRJWqy80hnVlujAjveIsNJSPeJjA==", "license": "MIT", "dependencies": { "@poppinss/cliui": "^6.4.1", @@ -1018,48 +1018,48 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.888.0.tgz", - "integrity": "sha512-d+mxbE8HxTF5TWAfpSL8yNpR62PfBCO5cXEAvxJq2ftvMlNPwRdyRqRddl2+86nQAnm5y6RChBkFIxr0np/F2g==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.891.0.tgz", + "integrity": "sha512-X5cejGUQjUPnkD5d/SXsh6NAPRvc8JtuF+iEclbFe5OOhxJZG9YemT0V7wqC+taFe0279c89+1qJT19UjXZEeg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.888.0", - "@aws-sdk/credential-provider-node": "3.888.0", - "@aws-sdk/middleware-host-header": "3.887.0", - "@aws-sdk/middleware-logger": "3.887.0", - "@aws-sdk/middleware-recursion-detection": "3.887.0", - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/credential-provider-node": "3.891.0", + "@aws-sdk/middleware-host-header": "3.891.0", + "@aws-sdk/middleware-logger": "3.891.0", + "@aws-sdk/middleware-recursion-detection": "3.891.0", + "@aws-sdk/middleware-user-agent": "3.891.0", + "@aws-sdk/region-config-resolver": "3.890.0", "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-endpoints": "3.891.0", "@aws-sdk/util-user-agent-browser": "3.887.0", - "@aws-sdk/util-user-agent-node": "3.888.0", - "@smithy/config-resolver": "^4.2.1", + "@aws-sdk/util-user-agent-node": "3.891.0", + "@smithy/config-resolver": "^4.2.2", "@smithy/core": "^3.11.0", "@smithy/fetch-http-handler": "^5.2.1", "@smithy/hash-node": "^4.1.1", "@smithy/invalid-dependency": "^4.1.1", "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-endpoint": "^4.2.2", + "@smithy/middleware-retry": "^4.2.3", "@smithy/middleware-serde": "^4.1.1", "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/node-http-handler": "^4.2.1", "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/smithy-client": "^4.6.2", "@smithy/types": "^4.5.0", "@smithy/url-parser": "^4.1.1", "@smithy/util-base64": "^4.1.0", "@smithy/util-body-length-browser": "^4.1.0", "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.1", - "@smithy/util-defaults-mode-node": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-defaults-mode-browser": "^4.1.2", + "@smithy/util-defaults-mode-node": "^4.1.2", + "@smithy/util-endpoints": "^3.1.2", "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", + "@smithy/util-retry": "^4.1.2", "@smithy/util-utf8": "^4.1.0", "@smithy/util-waiter": "^4.1.1", "tslib": "^2.6.2" @@ -1069,47 +1069,47 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.888.0.tgz", - "integrity": "sha512-8CLy/ehGKUmekjH+VtZJ4w40PqDg3u0K7uPziq/4P8Q7LLgsy8YQoHNbuY4am7JU3HWrqLXJI9aaz1+vPGPoWA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.891.0.tgz", + "integrity": "sha512-QMDaD9GhJe7l0KQp3Tt7dzqFCz/H2XuyNjQgvi10nM1MfI1RagmLtmEhZveQxMPhZ/AtohLSK0Tisp/I5tR8RQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.888.0", - "@aws-sdk/middleware-host-header": "3.887.0", - "@aws-sdk/middleware-logger": "3.887.0", - "@aws-sdk/middleware-recursion-detection": "3.887.0", - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/middleware-host-header": "3.891.0", + "@aws-sdk/middleware-logger": "3.891.0", + "@aws-sdk/middleware-recursion-detection": "3.891.0", + "@aws-sdk/middleware-user-agent": "3.891.0", + "@aws-sdk/region-config-resolver": "3.890.0", "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-endpoints": "3.891.0", "@aws-sdk/util-user-agent-browser": "3.887.0", - "@aws-sdk/util-user-agent-node": "3.888.0", - "@smithy/config-resolver": "^4.2.1", + "@aws-sdk/util-user-agent-node": "3.891.0", + "@smithy/config-resolver": "^4.2.2", "@smithy/core": "^3.11.0", "@smithy/fetch-http-handler": "^5.2.1", "@smithy/hash-node": "^4.1.1", "@smithy/invalid-dependency": "^4.1.1", "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-endpoint": "^4.2.2", + "@smithy/middleware-retry": "^4.2.3", "@smithy/middleware-serde": "^4.1.1", "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/node-http-handler": "^4.2.1", "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/smithy-client": "^4.6.2", "@smithy/types": "^4.5.0", "@smithy/url-parser": "^4.1.1", "@smithy/util-base64": "^4.1.0", "@smithy/util-body-length-browser": "^4.1.0", "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.1", - "@smithy/util-defaults-mode-node": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-defaults-mode-browser": "^4.1.2", + "@smithy/util-defaults-mode-node": "^4.1.2", + "@smithy/util-endpoints": "^3.1.2", "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", + "@smithy/util-retry": "^4.1.2", "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, @@ -1118,19 +1118,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.888.0.tgz", - "integrity": "sha512-L3S2FZywACo4lmWv37Y4TbefuPJ1fXWyWwIJ3J4wkPYFJ47mmtUPqThlVrSbdTHkEjnZgJe5cRfxk0qCLsFh1w==", + "version": "3.890.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.890.0.tgz", + "integrity": "sha512-CT+yjhytHdyKvV3Nh/fqBjnZ8+UiQZVz4NMm4LrPATgVSOdfygXHqrWxrPTVgiBtuJWkotg06DF7+pTd5ekLBw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.887.0", "@aws-sdk/xml-builder": "3.887.0", "@smithy/core": "^3.11.0", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/property-provider": "^4.0.5", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.6.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/smithy-client": "^4.6.2", "@smithy/types": "^4.5.0", "@smithy/util-base64": "^4.1.0", "@smithy/util-body-length-browser": "^4.1.0", @@ -1144,14 +1144,14 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.888.0.tgz", - "integrity": "sha512-shPi4AhUKbIk7LugJWvNpeZA8va7e5bOHAEKo89S0Ac8WDZt2OaNzbh/b9l0iSL2eEyte8UgIsYGcFxOwIF1VA==", + "version": "3.890.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.890.0.tgz", + "integrity": "sha512-BtsUa2y0Rs8phmB2ScZ5RuPqZVmxJJXjGfeiXctmLFTxTwoayIK1DdNzOWx6SRMPVc3s2RBGN4vO7T1TwN+ajA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", + "@aws-sdk/core": "3.890.0", "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", + "@smithy/property-provider": "^4.1.1", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1160,18 +1160,18 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.888.0.tgz", - "integrity": "sha512-Jvuk6nul0lE7o5qlQutcqlySBHLXOyoPtiwE6zyKbGc7RVl0//h39Lab7zMeY2drMn8xAnIopL4606Fd8JI/Hw==", + "version": "3.890.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.890.0.tgz", + "integrity": "sha512-0sru3LVwsuGYyzbD90EC/d5HnCZ9PL4O9BA2LYT6b9XceC005Oj86uzE47LXb+mDhTAt3T6ZO0+ZcVQe0DDi8w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", + "@aws-sdk/core": "3.890.0", "@aws-sdk/types": "3.887.0", "@smithy/fetch-http-handler": "^5.2.1", "@smithy/node-http-handler": "^4.2.1", - "@smithy/property-provider": "^4.0.5", + "@smithy/property-provider": "^4.1.1", "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/smithy-client": "^4.6.2", "@smithy/types": "^4.5.0", "@smithy/util-stream": "^4.3.1", "tslib": "^2.6.2" @@ -1181,22 +1181,22 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.888.0.tgz", - "integrity": "sha512-M82ItvS5yq+tO6ZOV1ruaVs2xOne+v8HW85GFCXnz8pecrzYdgxh6IsVqEbbWruryG/mUGkWMbkBZoEsy4MgyA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.891.0.tgz", + "integrity": "sha512-9LOfm97oy2d2frwCQjl53XLkoEYG6/rsNM3Y6n8UtRU3bzGAEjixdIuv3b6Z/Mk/QLeikcQEJ9FMC02DuQh2Yw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/credential-provider-env": "3.888.0", - "@aws-sdk/credential-provider-http": "3.888.0", - "@aws-sdk/credential-provider-process": "3.888.0", - "@aws-sdk/credential-provider-sso": "3.888.0", - "@aws-sdk/credential-provider-web-identity": "3.888.0", - "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/credential-provider-env": "3.890.0", + "@aws-sdk/credential-provider-http": "3.890.0", + "@aws-sdk/credential-provider-process": "3.890.0", + "@aws-sdk/credential-provider-sso": "3.891.0", + "@aws-sdk/credential-provider-web-identity": "3.891.0", + "@aws-sdk/nested-clients": "3.891.0", "@aws-sdk/types": "3.887.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1205,21 +1205,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.888.0.tgz", - "integrity": "sha512-KCrQh1dCDC8Y+Ap3SZa6S81kHk+p+yAaOQ5jC3dak4zhHW3RCrsGR/jYdemTOgbEGcA6ye51UbhWfrrlMmeJSA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.891.0.tgz", + "integrity": "sha512-IjGvQJhpCN512xlT1DFGaPeE1q0YEm/X62w7wHsRpBindW//M+heSulJzP4KPkoJvmJNVu1NxN26/p4uH+M8TQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.888.0", - "@aws-sdk/credential-provider-http": "3.888.0", - "@aws-sdk/credential-provider-ini": "3.888.0", - "@aws-sdk/credential-provider-process": "3.888.0", - "@aws-sdk/credential-provider-sso": "3.888.0", - "@aws-sdk/credential-provider-web-identity": "3.888.0", + "@aws-sdk/credential-provider-env": "3.890.0", + "@aws-sdk/credential-provider-http": "3.890.0", + "@aws-sdk/credential-provider-ini": "3.891.0", + "@aws-sdk/credential-provider-process": "3.890.0", + "@aws-sdk/credential-provider-sso": "3.891.0", + "@aws-sdk/credential-provider-web-identity": "3.891.0", "@aws-sdk/types": "3.887.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1228,15 +1228,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.888.0.tgz", - "integrity": "sha512-+aX6piSukPQ8DUS4JAH344GePg8/+Q1t0+kvSHAZHhYvtQ/1Zek3ySOJWH2TuzTPCafY4nmWLcQcqvU1w9+4Lw==", + "version": "3.890.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.890.0.tgz", + "integrity": "sha512-dWZ54TI1Q+UerF5YOqGiCzY+x2YfHsSQvkyM3T4QDNTJpb/zjiVv327VbSOULOlI7gHKWY/G3tMz0D9nWI7YbA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", + "@aws-sdk/core": "3.890.0", "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1245,17 +1245,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.888.0.tgz", - "integrity": "sha512-b1ZJji7LJ6E/j1PhFTyvp51in2iCOQ3VP6mj5H6f5OUnqn7efm41iNMoinKr87n0IKZw7qput5ggXVxEdPhouA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.891.0.tgz", + "integrity": "sha512-RtF9BwUIZqc/7sFbK6n6qhe0tNaWJQwin89nSeZ1HOsA0Z7TfTOelX8Otd0L5wfeVBMVcgiN3ofqrcZgjFjQjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.888.0", - "@aws-sdk/core": "3.888.0", - "@aws-sdk/token-providers": "3.888.0", + "@aws-sdk/client-sso": "3.891.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/token-providers": "3.891.0", "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1264,15 +1264,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.888.0.tgz", - "integrity": "sha512-7P0QNtsDzMZdmBAaY/vY1BsZHwTGvEz3bsn2bm5VSKFAeMmZqsHK1QeYdNsFjLtegnVh+wodxMq50jqLv3LFlA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.891.0.tgz", + "integrity": "sha512-yq7kzm1sHZ0GZrtS+qpjMUp4ES66UoT1+H2xxrOuAZkvUnkpQq1iSjOgBgJJ9FW1EsDUEmlgn94i4hJTNvm7fg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/nested-clients": "3.891.0", "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1281,9 +1282,9 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.887.0.tgz", - "integrity": "sha512-ulzqXv6NNqdu/kr0sgBYupWmahISHY+azpJidtK6ZwQIC+vBUk9NdZeqQpy7KVhIk2xd4+5Oq9rxapPwPI21CA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.891.0.tgz", + "integrity": "sha512-OYaxbqNDeo/noE7MfYWWQDu86cF/R/bMXdZ2QZwpWpX2yjy8xMwxSg7c/4tEK/OtiDZTKRXXrvPxRxG2+1bnJw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.887.0", @@ -1296,9 +1297,9 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.887.0.tgz", - "integrity": "sha512-YbbgLI6jKp2qSoAcHnXrQ5jcuc5EYAmGLVFgMVdk8dfCfJLfGGSaOLxF4CXC7QYhO50s+mPPkhBYejCik02Kug==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.891.0.tgz", + "integrity": "sha512-azL4mg1H1FLpOAECiFtU+r+9VDhpeF6Vh9pzD4m51BWPJ60CVnyHayeI/0gqPsL60+5l90/b9VWonoA8DvAvpg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.887.0", @@ -1310,9 +1311,9 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.887.0.tgz", - "integrity": "sha512-tjrUXFtQnFLo+qwMveq5faxP5MQakoLArXtqieHphSqZTXm21wDJM73hgT4/PQQGTwgYjDKqnqsE1hvk0hcfDw==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.891.0.tgz", + "integrity": "sha512-n++KwAEnNlvx5NZdIQZnvl2GjSH/YE3xGSqW2GmPB5780tFY5lOYSb1uA+EUzJSVX4oAKAkSPdR2AOW09kzoew==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.887.0", @@ -1326,14 +1327,14 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.888.0.tgz", - "integrity": "sha512-ZkcUkoys8AdrNNG7ATjqw2WiXqrhTvT+r4CIK3KhOqIGPHX0p0DQWzqjaIl7ZhSUToKoZ4Ud7MjF795yUr73oA==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.891.0.tgz", + "integrity": "sha512-xyxIZtR7FunCWymPAxEm61VUq9lruXxWIYU5AIh5rt0av7nXa2ayAAlscQ7ch9jUlw+lbC2PVbw0K/OYrMovuA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", + "@aws-sdk/core": "3.890.0", "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-endpoints": "3.891.0", "@smithy/core": "^3.11.0", "@smithy/protocol-http": "^5.2.1", "@smithy/types": "^4.5.0", @@ -1344,47 +1345,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.888.0.tgz", - "integrity": "sha512-py4o4RPSGt+uwGvSBzR6S6cCBjS4oTX5F8hrHFHfPCdIOMVjyOBejn820jXkCrcdpSj3Qg1yUZXxsByvxc9Lyg==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.891.0.tgz", + "integrity": "sha512-cpol+Yk4T3GXPXbRfUyN2u6tpMEHUxAiesZgrfMm11QGHV+pmzyejJV/QZ0pdJKj5sXKaCr4DCntoJ5iBx++Cw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.888.0", - "@aws-sdk/middleware-host-header": "3.887.0", - "@aws-sdk/middleware-logger": "3.887.0", - "@aws-sdk/middleware-recursion-detection": "3.887.0", - "@aws-sdk/middleware-user-agent": "3.888.0", - "@aws-sdk/region-config-resolver": "3.887.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/middleware-host-header": "3.891.0", + "@aws-sdk/middleware-logger": "3.891.0", + "@aws-sdk/middleware-recursion-detection": "3.891.0", + "@aws-sdk/middleware-user-agent": "3.891.0", + "@aws-sdk/region-config-resolver": "3.890.0", "@aws-sdk/types": "3.887.0", - "@aws-sdk/util-endpoints": "3.887.0", + "@aws-sdk/util-endpoints": "3.891.0", "@aws-sdk/util-user-agent-browser": "3.887.0", - "@aws-sdk/util-user-agent-node": "3.888.0", - "@smithy/config-resolver": "^4.2.1", + "@aws-sdk/util-user-agent-node": "3.891.0", + "@smithy/config-resolver": "^4.2.2", "@smithy/core": "^3.11.0", "@smithy/fetch-http-handler": "^5.2.1", "@smithy/hash-node": "^4.1.1", "@smithy/invalid-dependency": "^4.1.1", "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.1", - "@smithy/middleware-retry": "^4.2.1", + "@smithy/middleware-endpoint": "^4.2.2", + "@smithy/middleware-retry": "^4.2.3", "@smithy/middleware-serde": "^4.1.1", "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/node-http-handler": "^4.2.1", "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/smithy-client": "^4.6.2", "@smithy/types": "^4.5.0", "@smithy/url-parser": "^4.1.1", "@smithy/util-base64": "^4.1.0", "@smithy/util-body-length-browser": "^4.1.0", "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.1", - "@smithy/util-defaults-mode-node": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-defaults-mode-browser": "^4.1.2", + "@smithy/util-defaults-mode-node": "^4.1.2", + "@smithy/util-endpoints": "^3.1.2", "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", + "@smithy/util-retry": "^4.1.2", "@smithy/util-utf8": "^4.1.0", "tslib": "^2.6.2" }, @@ -1393,15 +1394,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.887.0.tgz", - "integrity": "sha512-VdSMrIqJ3yjJb/fY+YAxrH/lCVv0iL8uA+lbMNfQGtO5tB3Zx6SU9LEpUwBNX8fPK1tUpI65CNE4w42+MY/7Mg==", + "version": "3.890.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.890.0.tgz", + "integrity": "sha512-VfdT+tkF9groRYNzKvQCsCGDbOQdeBdzyB1d6hWiq22u13UafMIoskJ1ec0i0H1X29oT6mjTitfnvPq1UiKwzQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.887.0", - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-config-provider": "^4.1.0", "@smithy/util-middleware": "^4.1.1", "tslib": "^2.6.2" }, @@ -1410,16 +1411,16 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.888.0.tgz", - "integrity": "sha512-WA3NF+3W8GEuCMG1WvkDYbB4z10G3O8xuhT7QSjhvLYWQ9CPt3w4VpVIfdqmUn131TCIbhCzD0KN/1VJTjAjyw==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.891.0.tgz", + "integrity": "sha512-n31JDMWhj/53QX33C97+1W63JGtgO8pg1/Tfmv4f9TR2VSGf1rFwYH7cPZ7dVIMmcUBeI2VCVhwUIabGNHw86Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.888.0", - "@aws-sdk/nested-clients": "3.888.0", + "@aws-sdk/core": "3.890.0", + "@aws-sdk/nested-clients": "3.891.0", "@aws-sdk/types": "3.887.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -1441,15 +1442,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.887.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.887.0.tgz", - "integrity": "sha512-kpegvT53KT33BMeIcGLPA65CQVxLUL/C3gTz9AzlU/SDmeusBHX4nRApAicNzI/ltQ5lxZXbQn18UczzBuwF1w==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.891.0.tgz", + "integrity": "sha512-MgxvmHIQJbUK+YquX4bdjDw1MjdBqTRJGHs6iU2KM8nN1ut0bPwvavkq7NrY/wB3ZKKECqmv6J/nw+hYKKUIHA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.887.0", "@smithy/types": "^4.5.0", "@smithy/url-parser": "^4.1.1", - "@smithy/util-endpoints": "^3.1.1", + "@smithy/util-endpoints": "^3.1.2", "tslib": "^2.6.2" }, "engines": { @@ -1481,14 +1482,14 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.888.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.888.0.tgz", - "integrity": "sha512-rSB3OHyuKXotIGfYEo//9sU0lXAUrTY28SUUnxzOGYuQsAt0XR5iYwBAp+RjV6x8f+Hmtbg0PdCsy1iNAXa0UQ==", + "version": "3.891.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.891.0.tgz", + "integrity": "sha512-/mmvVL2PJE2NMTWj9JSY98OISx7yov0mi72eOViWCHQMRYJCN12DY54i1rc4Q/oPwJwTwIrx69MLjVhQ1OZsgw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.888.0", + "@aws-sdk/middleware-user-agent": "3.891.0", "@aws-sdk/types": "3.887.0", - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -2016,9 +2017,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -2032,9 +2033,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -2048,9 +2049,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -2064,9 +2065,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -2080,9 +2081,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -2096,9 +2097,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -2112,9 +2113,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -2128,9 +2129,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -2144,9 +2145,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -2160,9 +2161,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -2176,9 +2177,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -2192,9 +2193,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -2208,9 +2209,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -2224,9 +2225,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -2240,9 +2241,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -2256,9 +2257,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -2272,9 +2273,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -2288,9 +2289,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "cpu": [ "arm64" ], @@ -2304,9 +2305,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -2320,9 +2321,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "cpu": [ "arm64" ], @@ -2336,9 +2337,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -2352,9 +2353,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", "cpu": [ "arm64" ], @@ -2368,9 +2369,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -2384,9 +2385,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -2400,9 +2401,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -2416,9 +2417,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -2511,18 +2512,18 @@ } }, "node_modules/@fontsource/archivo-black": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@fontsource/archivo-black/-/archivo-black-5.2.6.tgz", - "integrity": "sha512-Jxq9D0G9k9OfKvx8tQO9WZ6bfNqcN99UoQ2fJivn1JPkY/vVbK/uzHn2BPj347Vh9oNQBUpKaQ9NoDUKLJPQ/w==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/archivo-black/-/archivo-black-5.2.8.tgz", + "integrity": "sha512-3zNj/o9LzWyDl/UEpY5IOHpAQyUtFr3hQaFS7NSKwCLLkXOfH/CMCt1L2b2Z+OF25OURtOYenCadgAebALz7/A==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" } }, "node_modules/@fontsource/inter": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.6.tgz", - "integrity": "sha512-CZs9S1CrjD0jPwsNy9W6j0BhsmRSQrgwlTNkgQXTsAeDRM42LBRLo3eo9gCzfH4GvV7zpyf78Ozfl773826csw==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -2642,9 +2643,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", - "integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -3018,9 +3019,9 @@ } }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.11.0.tgz", - "integrity": "sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz", + "integrity": "sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3565,9 +3566,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", - "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", + "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", "cpu": [ "arm" ], @@ -3578,9 +3579,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", - "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", + "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", "cpu": [ "arm64" ], @@ -3591,9 +3592,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", - "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", + "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", "cpu": [ "arm64" ], @@ -3604,9 +3605,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", - "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", + "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", "cpu": [ "x64" ], @@ -3617,9 +3618,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", - "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", + "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", "cpu": [ "arm64" ], @@ -3630,9 +3631,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", - "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", + "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", "cpu": [ "x64" ], @@ -3643,9 +3644,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", - "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", + "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", "cpu": [ "arm" ], @@ -3656,9 +3657,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", - "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", + "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", "cpu": [ "arm" ], @@ -3669,9 +3670,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", - "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", + "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", "cpu": [ "arm64" ], @@ -3682,9 +3683,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", - "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", + "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", "cpu": [ "arm64" ], @@ -3694,10 +3695,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", - "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", + "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", "cpu": [ "loong64" ], @@ -3708,9 +3709,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", - "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", + "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", "cpu": [ "ppc64" ], @@ -3721,9 +3722,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", - "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", + "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", "cpu": [ "riscv64" ], @@ -3734,9 +3735,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", - "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", + "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", "cpu": [ "riscv64" ], @@ -3747,9 +3748,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", - "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", + "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", "cpu": [ "s390x" ], @@ -3760,9 +3761,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", - "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", + "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", "cpu": [ "x64" ], @@ -3773,9 +3774,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", - "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", + "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", "cpu": [ "x64" ], @@ -3786,9 +3787,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", - "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", + "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", "cpu": [ "arm64" ], @@ -3799,9 +3800,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", - "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", + "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", "cpu": [ "arm64" ], @@ -3812,9 +3813,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", - "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", + "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", "cpu": [ "ia32" ], @@ -3825,9 +3826,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", - "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", + "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", "cpu": [ "x64" ], @@ -3851,9 +3852,9 @@ "license": "MIT" }, "node_modules/@sindresorhus/is": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", - "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", + "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", "license": "MIT", "engines": { "node": ">=18" @@ -3889,12 +3890,12 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.1.tgz", - "integrity": "sha512-FXil8q4QN7mgKwU2hCLm0ltab8NyY/1RiqEf25Jnf6WLS3wmb11zGAoLETqg1nur2Aoibun4w4MjeN9CMJ4G6A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", + "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/types": "^4.5.0", "@smithy/util-config-provider": "^4.1.0", "@smithy/util-middleware": "^4.1.1", @@ -3905,9 +3906,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.0.tgz", - "integrity": "sha512-Abs5rdP1o8/OINtE49wwNeWuynCu0kme1r4RI3VXVrHr4odVDG7h7mTnw1WXXfN5Il+c25QOnrdL2y56USfxkA==", + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.11.1.tgz", + "integrity": "sha512-REH7crwORgdjSpYs15JBiIWOYjj0hJNC3aCecpJvAlMMaaqL5i2CLb1i6Hc4yevToTKSqslLMI9FKjhugEwALA==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.1.1", @@ -3916,7 +3917,7 @@ "@smithy/util-base64": "^4.1.0", "@smithy/util-body-length-browser": "^4.1.0", "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.1", + "@smithy/util-stream": "^4.3.2", "@smithy/util-utf8": "^4.1.0", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", @@ -3927,12 +3928,12 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.1.tgz", - "integrity": "sha512-1WdBfM9DwA59pnpIizxnUvBf/de18p4GP+6zP2AqrlFzoW3ERpZaT4QueBR0nS9deDMaQRkBlngpVlnkuuTisQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", + "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/property-provider": "^4.1.1", "@smithy/types": "^4.5.0", "@smithy/url-parser": "^4.1.1", @@ -4013,15 +4014,15 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.1.tgz", - "integrity": "sha512-fUTMmQvQQZakXOuKizfu7fBLDpwvWZjfH6zUK2OLsoNZRZGbNUdNSdLJHpwk1vS208jtDjpUIskh+JoA8zMzZg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.3.tgz", + "integrity": "sha512-+1H5A28DeffRVrqmVmtqtRraEjoaC6JVap3xEQdVoBh2EagCVY7noPmcBcG4y7mnr9AJitR1ZAse2l+tEtK5vg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.11.0", + "@smithy/core": "^3.11.1", "@smithy/middleware-serde": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", - "@smithy/shared-ini-file-loader": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "@smithy/url-parser": "^4.1.1", "@smithy/util-middleware": "^4.1.1", @@ -4032,18 +4033,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.1.tgz", - "integrity": "sha512-JzfvjwSJXWRl7LkLgIRTUTd2Wj639yr3sQGpViGNEOjtb0AkAuYqRAHs+jSOI/LPC0ZTjmFVVtfrCICMuebexw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.2.4.tgz", + "integrity": "sha512-amyqYQFewnAviX3yy/rI/n1HqAgfvUdkEhc04kDjxsngAUREKuOI24iwqQUirrj6GtodWmR4iO5Zeyl3/3BwWg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/protocol-http": "^5.2.1", - "@smithy/service-error-classification": "^4.1.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/service-error-classification": "^4.1.2", + "@smithy/smithy-client": "^4.6.3", "@smithy/types": "^4.5.0", "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.1", + "@smithy/util-retry": "^4.1.2", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" @@ -4080,13 +4081,13 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.1.tgz", - "integrity": "sha512-AIA0BJZq2h295J5NeCTKhg1WwtdTA/GqBCaVjk30bDgMHwniUETyh5cP9IiE9VrId7Kt8hS7zvREVMTv1VfA6g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", + "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -4164,9 +4165,9 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.1.tgz", - "integrity": "sha512-Iam75b/JNXyDE41UvrlM6n8DNOa/r1ylFyvgruTUx7h2Uk7vDNV9AAwP1vfL1fOL8ls0xArwEGVcGZVd7IO/Cw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz", + "integrity": "sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.5.0" @@ -4176,9 +4177,9 @@ } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.1.1.tgz", - "integrity": "sha512-YkpikhIqGc4sfXeIbzSj10t2bJI/sSoP5qxLue6zG+tEE3ngOBSm8sO3+djacYvS/R5DfpxN/L9CyZsvwjWOAQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", + "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.5.0", @@ -4208,17 +4209,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.1.tgz", - "integrity": "sha512-WolVLDb9UTPMEPPOncrCt6JmAMCSC/V2y5gst2STWJ5r7+8iNac+EFYQnmvDCYMfOLcilOSEpm5yXZXwbLak1Q==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.3.tgz", + "integrity": "sha512-K27LqywsaqKz4jusdUQYJh/YP2VbnbdskZ42zG8xfV+eovbTtMc2/ZatLWCfSkW0PDsTUXlpvlaMyu8925HsOw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.11.0", - "@smithy/middleware-endpoint": "^4.2.1", + "@smithy/core": "^3.11.1", + "@smithy/middleware-endpoint": "^4.2.3", "@smithy/middleware-stack": "^4.1.1", "@smithy/protocol-http": "^5.2.1", "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.1", + "@smithy/util-stream": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -4315,13 +4316,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.1.tgz", - "integrity": "sha512-hA1AKIHFUMa9Tl6q6y8p0pJ9aWHCCG8s57flmIyLE0W7HcJeYrYtnqXDcGnftvXEhdQnSexyegXnzzTGk8bKLA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.3.tgz", + "integrity": "sha512-5fm3i2laE95uhY6n6O6uGFxI5SVbqo3/RWEuS3YsT0LVmSZk+0eUqPhKd4qk0KxBRPaT5VNT/WEBUqdMyYoRgg==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.1.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/smithy-client": "^4.6.3", "@smithy/types": "^4.5.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -4331,16 +4332,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.1.tgz", - "integrity": "sha512-RGSpmoBrA+5D2WjwtK7tto6Pc2wO9KSXKLpLONhFZ8VyuCbqlLdiDAfuDTNY9AJe4JoE+Cx806cpTQQoQ71zPQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.3.tgz", + "integrity": "sha512-lwnMzlMslZ9GJNt+/wVjz6+fe9Wp5tqR1xAyQn+iywmP+Ymj0F6NhU/KfHM5jhGPQchRSCcau5weKhFdLIM4cA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.2.1", - "@smithy/credential-provider-imds": "^4.1.1", - "@smithy/node-config-provider": "^4.2.1", + "@smithy/config-resolver": "^4.2.2", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/node-config-provider": "^4.2.2", "@smithy/property-provider": "^4.1.1", - "@smithy/smithy-client": "^4.6.1", + "@smithy/smithy-client": "^4.6.3", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -4349,12 +4350,12 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.1.tgz", - "integrity": "sha512-qB4R9kO0SetA11Rzu6MVGFIaGYX3p6SGGGfWwsKnC6nXIf0n/0AKVwRTsYsz9ToN8CeNNtNgQRwKFBndGJZdyw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", + "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.1", + "@smithy/node-config-provider": "^4.2.2", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -4388,12 +4389,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.1.tgz", - "integrity": "sha512-jGeybqEZ/LIordPLMh5bnmnoIgsqnp4IEimmUp5c5voZ8yx+5kAlN5+juyr7p+f7AtZTgvhmInQk4Q0UVbrZ0Q==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.2.tgz", + "integrity": "sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.1.1", + "@smithy/service-error-classification": "^4.1.2", "@smithy/types": "^4.5.0", "tslib": "^2.6.2" }, @@ -4402,9 +4403,9 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.1.tgz", - "integrity": "sha512-khKkW/Jqkgh6caxMWbMuox9+YfGlsk9OnHOYCGVEdYQb/XVzcORXHLYUubHmmda0pubEDncofUrPNniS9d+uAA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz", + "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.2.1", @@ -4908,9 +4909,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.3.tgz", - "integrity": "sha512-gTVM8js2twdtqM+AE2PdGEe9zGQY4UvmFjan9rZcVb6FGdStfjWoWejdmy4CfWVO9rh5MiYQGZloKAGkJt8lMw==", + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -5423,9 +5424,9 @@ } }, "node_modules/@vavite/multibuild/node_modules/@types/node": { - "version": "18.19.124", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.124.tgz", - "integrity": "sha512-hY4YWZFLs3ku6D2Gqo3RchTd9VRCcrjqp/I0mmohYeUVA5Y8eCXKJEasHxLAJVZRJuQogfd1GiJ9lgogBgKeuQ==", + "version": "18.19.127", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.127.tgz", + "integrity": "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -6239,9 +6240,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", - "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -6275,9 +6276,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.2.tgz", - "integrity": "sha512-NvcIedLxrs9llVpX7wI+Jz4Hn9vJQkCPKrTaHIE0sW/Rj1iq6Fzby4NbyTZjQJNoypBXNaG7tEHkTgONZpwgxQ==", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.5.tgz", + "integrity": "sha512-TiU4qUT9jdCuh4aVOG7H1QozyeI2sZRqoRPdqBIaslfNt4WUSanRBueAwl2x5jt4rXBMim3lIN2x6yT8PDi24Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6532,9 +6533,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, "funding": [ { @@ -6552,7 +6553,7 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", "electron-to-chromium": "^1.5.218", "node-releases": "^2.0.21", @@ -6696,9 +6697,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", "dev": true, "funding": [ { @@ -7410,9 +7411,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7620,9 +7621,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", + "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -7866,9 +7867,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.221", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.221.tgz", + "integrity": "sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==", "dev": true, "license": "ISC" }, @@ -7971,9 +7972,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8041,9 +8042,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -8053,32 +8054,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" } }, "node_modules/escalade": { @@ -9139,9 +9140,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", - "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -9526,9 +9527,9 @@ } }, "node_modules/got": { - "version": "14.4.8", - "resolved": "https://registry.npmjs.org/got/-/got-14.4.8.tgz", - "integrity": "sha512-vxwU4HuR0BIl+zcT1LYrgBjM+IJjNElOjCzs0aPgHorQyr/V6H6Y73Sn3r3FOlUffvWD+Q5jtRuGWaXkU8Jbhg==", + "version": "14.4.9", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.9.tgz", + "integrity": "sha512-Dbu075Jwm3QwNCIoCenqkqY8l2gd7e/TanuhMbzZIEsb1mpAneImSusKhZ+XdqqC3S91SDV/1SdWpGXKAlm8tA==", "license": "MIT", "dependencies": { "@sindresorhus/is": "^7.0.1", @@ -10240,9 +10241,9 @@ } }, "node_modules/is-network-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", - "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.2.0.tgz", + "integrity": "sha512-32jdpRpJo8SeL7zOuBJbMLz/VTw9mDpTvcKzzR8DkXWsJbbE60gdiX8YOd0UAV6b8Skt+CMytzfgVVIRFidn0Q==", "dev": true, "license": "MIT", "engines": { @@ -10965,9 +10966,9 @@ } }, "node_modules/memfs": { - "version": "4.39.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.39.0.tgz", - "integrity": "sha512-tFRr2IkSXl2B6IAJsxjHIMTOsfLt9W+8+t2uNxCeQcz4tFqgQR8DYk8hlLH2HsucTctLuoHq3U0G08atyBE3yw==", + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.42.0.tgz", + "integrity": "sha512-RG+4HMGyIVp6UWDWbFmZ38yKrSzblPnfJu0PyPt0hw52KW4PPlPp+HdV4qZBG0hLDuYVnf8wfQT4NymKXnlQjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12130,9 +12131,9 @@ } }, "node_modules/pino": { - "version": "9.9.5", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.5.tgz", - "integrity": "sha512-d1s98p8/4TfYhsJ09r/Azt30aYELRi6NNnZtEbqFw6BoGsdPVf5lKNK3kUwH8BmJJfpTLNuicjUQjaMbd93dVg==", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.10.0.tgz", + "integrity": "sha512-VOFxoNnxICtxaN8S3E73pR66c5MTFC+rwRcNRyHV/bV/c90dXvJqMfjkeRFsGBDXmlUN3LccJQPqGIufnaJePA==", "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0", @@ -12312,10 +12313,20 @@ } }, "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -12323,10 +12334,6 @@ "engines": { "node": "^12 || ^14 || >= 16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.4.21" } @@ -12575,9 +12582,9 @@ } }, "node_modules/pretty-ms": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", - "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13154,9 +13161,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.50.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", - "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", + "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -13169,27 +13176,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.1", - "@rollup/rollup-android-arm64": "4.50.1", - "@rollup/rollup-darwin-arm64": "4.50.1", - "@rollup/rollup-darwin-x64": "4.50.1", - "@rollup/rollup-freebsd-arm64": "4.50.1", - "@rollup/rollup-freebsd-x64": "4.50.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", - "@rollup/rollup-linux-arm-musleabihf": "4.50.1", - "@rollup/rollup-linux-arm64-gnu": "4.50.1", - "@rollup/rollup-linux-arm64-musl": "4.50.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", - "@rollup/rollup-linux-ppc64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-gnu": "4.50.1", - "@rollup/rollup-linux-riscv64-musl": "4.50.1", - "@rollup/rollup-linux-s390x-gnu": "4.50.1", - "@rollup/rollup-linux-x64-gnu": "4.50.1", - "@rollup/rollup-linux-x64-musl": "4.50.1", - "@rollup/rollup-openharmony-arm64": "4.50.1", - "@rollup/rollup-win32-arm64-msvc": "4.50.1", - "@rollup/rollup-win32-ia32-msvc": "4.50.1", - "@rollup/rollup-win32-x64-msvc": "4.50.1", + "@rollup/rollup-android-arm-eabi": "4.50.2", + "@rollup/rollup-android-arm64": "4.50.2", + "@rollup/rollup-darwin-arm64": "4.50.2", + "@rollup/rollup-darwin-x64": "4.50.2", + "@rollup/rollup-freebsd-arm64": "4.50.2", + "@rollup/rollup-freebsd-x64": "4.50.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", + "@rollup/rollup-linux-arm-musleabihf": "4.50.2", + "@rollup/rollup-linux-arm64-gnu": "4.50.2", + "@rollup/rollup-linux-arm64-musl": "4.50.2", + "@rollup/rollup-linux-loong64-gnu": "4.50.2", + "@rollup/rollup-linux-ppc64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-gnu": "4.50.2", + "@rollup/rollup-linux-riscv64-musl": "4.50.2", + "@rollup/rollup-linux-s390x-gnu": "4.50.2", + "@rollup/rollup-linux-x64-gnu": "4.50.2", + "@rollup/rollup-linux-x64-musl": "4.50.2", + "@rollup/rollup-openharmony-arm64": "4.50.2", + "@rollup/rollup-win32-arm64-msvc": "4.50.2", + "@rollup/rollup-win32-ia32-msvc": "4.50.2", + "@rollup/rollup-win32-x64-msvc": "4.50.2", "fsevents": "~2.3.2" } }, diff --git a/readme.md b/readme.md index 0fd7e64..7d032ce 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,8 @@ Welcome to the Tethys Research Repository Backend System! This is the backend co - [Configuration](#configuration) - [Database](#database) - [API Documentation](#api-documentation) +- [Commands](#commands) +- [Documentation](#documentation) - [Contributing](#contributing) - [License](#license) @@ -29,5 +31,175 @@ Before you begin, ensure you have met the following requirements: 1. Clone this repository: ```bash - git clone https://gitea.geologie.ac.at/geolba/tethys.backend.git + git clone git clone https://gitea.geologie.ac.at/geolba/tethys.backend.git + cd tethys-backend ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Configure environment variables (see [Configuration](#configuration)) + +4. Run database migrations: + + ```bash + node ace migration:run + ``` + +5. Start the development server: + + ```bash + npm run dev + ``` + +## Usage + +The Tethys Backend provides RESTful APIs for managing research datasets, user authentication, DOI registration, and search functionality. + +## Configuration + +Copy the `.env.example` file to `.env` and configure the following variables: + +### Database Configuration +```bash +DB_CONNECTION=pg +DB_HOST=localhost +DB_PORT=5432 +DB_USER=your_username +DB_PASSWORD=your_password +DB_DATABASE=tethys_db +``` + +### DataCite Configuration +```bash +# DataCite Credentials +DATACITE_USERNAME=your_datacite_username +DATACITE_PASSWORD=your_datacite_password +DATACITE_PREFIX=10.21388 + +# Environment-specific API endpoints +DATACITE_API_URL=https://api.test.datacite.org # Test environment +DATACITE_SERVICE_URL=https://mds.test.datacite.org # Test MDS + +# For production: +# DATACITE_API_URL=https://api.datacite.org +# DATACITE_SERVICE_URL=https://mds.datacite.org +``` + +### OpenSearch Configuration +```bash +OPENSEARCH_HOST=localhost:9200 +``` + +### Application Configuration +```bash +BASE_DOMAIN=tethys.at +APP_KEY=your_app_key +``` + +## Database + +The system uses PostgreSQL with Lucid ORM. Key models include: + +- **Dataset**: Research dataset metadata +- **DatasetIdentifier**: DOI and other identifiers for datasets +- **User**: User management and authentication +- **XmlCache**: Cached XML metadata + +Run migrations and seeders: + +```bash +# Run migrations +node ace migration:run + +# Run seeders (if available) +node ace db:seed +``` + +## API Documentation + +API endpoints are available for: + +- Dataset management (`/api/datasets`) +- User authentication (`/api/auth`) +- DOI registration (`/api/doi`) +- Search functionality (`/api/search`) + +*Detailed API documentation can be found in the `/docs/api` directory.* + +## Commands + +The system includes several Ace commands for maintenance and data management: + +### Dataset Indexing +```bash +# Index all published datasets to OpenSearch +node ace index:datasets + +# Index a specific dataset +node ace index:datasets --publish_id 123 +``` + +### DataCite DOI Management +```bash +# Update DataCite records for modified datasets +node ace update:datacite + +# Show detailed statistics for datasets needing updates +node ace update:datacite --stats + +# Preview what would be updated (dry run) +node ace update:datacite --dry-run + +# Force update all DOI records +node ace update:datacite --force + +# Update a specific dataset +node ace update:datacite --publish_id 123 +``` + +*For detailed command documentation, see the [Commands Documentation](docs/commands/)* + +## Documentation + +Comprehensive documentation is available in the `/docs` directory: + +- **[Commands Documentation](docs/commands/)** - Detailed guides for Ace commands + - [DataCite Update Command](docs/commands/update-datacite.md) - DOI synchronization and management + - [Dataset Indexing Command](docs/commands/index-datasets.md) - Search index management +- **[API Documentation](docs/api/)** - REST API endpoints and usage +- **[Deployment Guide](docs/deployment/)** - Production deployment instructions +- **[Configuration Guide](docs/configuration/)** - Environment setup and configuration options + +## Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Development Guidelines + +- Follow the existing code style and conventions +- Write tests for new features +- Update documentation for any API changes +- Ensure all commands and migrations work properly + +### Testing Commands + +```bash +# Run tests +npm test + +# Test specific commands +node ace update:datacite --dry-run --publish_id 123 +node ace index:datasets --publish_id 123 +``` + +## License + +This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/start/routes/api.ts b/start/routes/api.ts index 55fccc5..ca3b079 100644 --- a/start/routes/api.ts +++ b/start/routes/api.ts @@ -8,14 +8,24 @@ import AvatarController from '#controllers/Http/Api/AvatarController'; import UserController from '#controllers/Http/Api/UserController'; import CollectionsController from '#controllers/Http/Api/collections_controller'; import { middleware } from '../kernel.js'; -// API + +// Clean DOI URL routes (no /api prefix) + +// API routes with /api prefix router .group(() => { - router.get('clients', [UserController, 'getSubmitters']).as('client.index').use(middleware.auth());; - router.get('authors', [AuthorsController, 'index']).as('author.index').use(middleware.auth());; + router.get('clients', [UserController, 'getSubmitters']).as('client.index').use(middleware.auth()); + router.get('authors', [AuthorsController, 'index']).as('author.index').use(middleware.auth()); router.get('datasets', [DatasetController, 'index']).as('dataset.index'); router.get('persons', [AuthorsController, 'persons']).as('author.persons'); + // This should come BEFORE any other routes that might conflict + router + .get('/dataset/:prefix/:value', [DatasetController, 'findByIdentifier']) + .where('prefix', /^10\.\d+$/) // Match DOI prefix pattern (10.xxxx) + .where('value', /^[a-zA-Z0-9._-]+\.[0-9]+(?:\.[0-9]+)*$/) // Match DOI suffix pattern + .as('dataset.findByIdentifier'); + router.get('/dataset', [DatasetController, 'findAll']).as('dataset.findAll'); router.get('/dataset/:publish_id', [DatasetController, 'findOne']).as('dataset.findOne'); router.get('/sitelinks/:year', [HomeController, 'findDocumentsPerYear']); @@ -35,7 +45,7 @@ router .as('apps.twofactor_backupcodes.create') .use(middleware.auth()); - router.get('collections/:id', [CollectionsController, 'show']).as('collection.show') + router.get('collections/:id', [CollectionsController, 'show']).as('collection.show'); }) // .namespace('App/Controllers/Http/Api') .prefix('api'); diff --git a/start/rules/orcid.ts b/start/rules/orcid.ts index ee60534..b16e504 100644 --- a/start/rules/orcid.ts +++ b/start/rules/orcid.ts @@ -1,7 +1,7 @@ /* |-------------------------------------------------------------------------- | Preloaded File - node ace make:preload rules/orcid -| ❯ Do you want to register the preload file in .adonisrc.ts file? (y/N) Β· true +| Do you want to register the preload file in .adonisrc.ts file? (y/N) Β· true | DONE: create start/rules/orcid.ts | DONE: update adonisrc.ts file |-------------------------------------------------------------------------- From 2f079e6fdd006e88914912da66095b4efab4c662 Mon Sep 17 00:00:00 2001 From: Arno Kaimbacher Date: Fri, 19 Sep 2025 16:26:01 +0200 Subject: [PATCH 2/4] feat: Add optional ORCID identifier to dataset validation also for the subitter npm updates --- .../Http/Submitter/DatasetController.ts | 4 + package-lock.json | 208 +++++++++--------- 2 files changed, 108 insertions(+), 104 deletions(-) diff --git a/app/Controllers/Http/Submitter/DatasetController.ts b/app/Controllers/Http/Submitter/DatasetController.ts index b7bb7a0..a308634 100644 --- a/app/Controllers/Http/Submitter/DatasetController.ts +++ b/app/Controllers/Http/Submitter/DatasetController.ts @@ -235,6 +235,7 @@ export default class DatasetController { .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255).optional().requiredWhen('name_type', '=', 'Personal'), last_name: vine.string().trim().minLength(3).maxLength(255), + identifier_orcid: vine.string().trim().maxLength(255).orcid().optional(), }), ) .minLength(1) @@ -251,6 +252,7 @@ export default class DatasetController { .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255).optional().requiredWhen('name_type', '=', 'Personal'), last_name: vine.string().trim().minLength(3).maxLength(255), + identifier_orcid: vine.string().trim().maxLength(255).orcid().optional(), pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)), }), ) @@ -326,6 +328,7 @@ export default class DatasetController { .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255).optional().requiredWhen('name_type', '=', 'Personal'), last_name: vine.string().trim().minLength(3).maxLength(255), + identifier_orcid: vine.string().trim().maxLength(255).orcid().optional(), }), ) .minLength(1) @@ -342,6 +345,7 @@ export default class DatasetController { .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255).optional().requiredWhen('name_type', '=', 'Personal'), last_name: vine.string().trim().minLength(3).maxLength(255), + identifier_orcid: vine.string().trim().maxLength(255).orcid().optional(), pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)), }), ) diff --git a/package-lock.json b/package-lock.json index 5ecb2b3..eb6179c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2597,13 +2597,13 @@ } }, "node_modules/@inertiajs/core": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.1.6.tgz", - "integrity": "sha512-PvY3QJecHt/Z+X+C+jJOHnBGxvi6YNuX3tLY0oXA2yC8aQZeUHnIoWnQdKgc0KA0CNj3b3sZSxm2ZQs9nMY7/Q==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.1.7.tgz", + "integrity": "sha512-ahBSdNj4d7oqEBr5KcGPVuoyI3JWYKwwLjqhy2O4Jp/UKX1C6W0U/WkpL6NzCapNaNDACBVSc3rqZ/6lY0VbWA==", "license": "MIT", "dependencies": { "@types/lodash-es": "^4.17.12", - "axios": "^1.11.0", + "axios": "^1.12.0", "lodash-es": "^4.17.21", "qs": "^6.9.0" } @@ -2629,12 +2629,12 @@ } }, "node_modules/@inertiajs/vue3": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.1.6.tgz", - "integrity": "sha512-r2tCjCe8kSPaEsbZFphKWnKZQ2/2ZItvrB3HWYCwl0ZS0wY396IsZcCpeJKFhF1xCoAnNYpfZo10dvcRmCipfQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.1.7.tgz", + "integrity": "sha512-Y6sL2lBJ/uJNVRL4FnYKAp7xdoWwgpc4HvlP8RZhm6roHUBcQNZWxxrPqhtBSx5v9vcTeIvu4+TsoFetPSYgdw==", "license": "MIT", "dependencies": { - "@inertiajs/core": "2.1.6", + "@inertiajs/core": "2.1.7", "@types/lodash-es": "^4.17.12", "lodash-es": "^4.17.21" }, @@ -3566,9 +3566,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz", - "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.51.0.tgz", + "integrity": "sha512-VyfldO8T/C5vAXBGIobrAnUE+VJNVLw5z9h4NgSDq/AJZWt/fXqdW+0PJbk+M74xz7yMDRiHtlsuDV7ew6K20w==", "cpu": [ "arm" ], @@ -3579,9 +3579,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz", - "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.51.0.tgz", + "integrity": "sha512-Z3ujzDZgsEVSokgIhmOAReh9SGT2qloJJX2Xo1Q3nPU1EhCXrV0PbpR3r7DWRgozqnjrPZQkLe5cgBPIYp70Vg==", "cpu": [ "arm64" ], @@ -3592,9 +3592,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz", - "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.51.0.tgz", + "integrity": "sha512-T3gskHgArUdR6TCN69li5VELVAZK+iQ4iwMoSMNYixoj+56EC9lTj35rcxhXzIJt40YfBkvDy3GS+t5zh7zM6g==", "cpu": [ "arm64" ], @@ -3605,9 +3605,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz", - "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.51.0.tgz", + "integrity": "sha512-Hh7n/fh0g5UjH6ATDF56Qdf5bzdLZKIbhp5KftjMYG546Ocjeyg15dxphCpH1FFY2PJ2G6MiOVL4jMq5VLTyrQ==", "cpu": [ "x64" ], @@ -3618,9 +3618,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz", - "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.51.0.tgz", + "integrity": "sha512-0EddADb6FBvfqYoxwVom3hAbAvpSVUbZqmR1wmjk0MSZ06hn/UxxGHKRqEQDMkts7XiZjejVB+TLF28cDTU+gA==", "cpu": [ "arm64" ], @@ -3631,9 +3631,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz", - "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.51.0.tgz", + "integrity": "sha512-MpqaEDLo3JuVPF+wWV4mK7V8akL76WCz8ndfz1aVB7RhvXFO3k7yT7eu8OEuog4VTSyNu5ibvN9n6lgjq/qLEQ==", "cpu": [ "x64" ], @@ -3644,9 +3644,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz", - "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.51.0.tgz", + "integrity": "sha512-WEWAGFNFFpvSWAIT3MYvxTkYHv/cJl9yWKpjhheg7ONfB0hetZt/uwBnM3GZqSHrk5bXCDYTFXg3jQyk/j7eXQ==", "cpu": [ "arm" ], @@ -3657,9 +3657,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz", - "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.51.0.tgz", + "integrity": "sha512-9bxtxj8QoAp++LOq5PGDGkEEOpCDk9rOEHUcXadnijedDH8IXrBt6PnBa4Y6NblvGWdoxvXZYghZLaliTCmAng==", "cpu": [ "arm" ], @@ -3670,9 +3670,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz", - "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.51.0.tgz", + "integrity": "sha512-DdqA+fARqIsfqDYkKo2nrWMp0kvu/wPJ2G8lZ4DjYhn+8QhrjVuzmsh7tTkhULwjvHTN59nWVzAixmOi6rqjNA==", "cpu": [ "arm64" ], @@ -3683,9 +3683,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz", - "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.51.0.tgz", + "integrity": "sha512-2XVRNzcUJE1UJua8P4a1GXS5jafFWE+pQ6zhUbZzptOu/70p1F6+0FTi6aGPd6jNtnJqGMjtBCXancC2dhYlWw==", "cpu": [ "arm64" ], @@ -3696,9 +3696,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz", - "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.51.0.tgz", + "integrity": "sha512-R8QhY0kLIPCAVXWi2yftDSpn7Jtejey/WhMoBESSfwGec5SKdFVupjxFlKoQ7clVRuaDpiQf7wNx3EBZf4Ey6g==", "cpu": [ "loong64" ], @@ -3709,9 +3709,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz", - "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.51.0.tgz", + "integrity": "sha512-I498RPfxx9cMv1KTHQ9tg2Ku1utuQm+T5B+Xro+WNu3FzAFSKp4awKfgMoZwjoPgNbaFGINaOM25cQW6WuBhiQ==", "cpu": [ "ppc64" ], @@ -3722,9 +3722,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz", - "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.51.0.tgz", + "integrity": "sha512-o8COudsb8lvtdm9ixg9aKjfX5aeoc2x9KGE7WjtrmQFquoCRZ9jtzGlonujE4WhvXFepTraWzT4RcwyDDeHXjA==", "cpu": [ "riscv64" ], @@ -3735,9 +3735,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz", - "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.51.0.tgz", + "integrity": "sha512-0shJPgSXMdYzOQzpM5BJN2euXY1f8uV8mS6AnrbMcH2KrkNsbpMxWB1wp8UEdiJ1NtyBkCk3U/HfX5mEONBq6w==", "cpu": [ "riscv64" ], @@ -3748,9 +3748,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz", - "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.51.0.tgz", + "integrity": "sha512-L7pV+ny7865jamSCQwyozBYjFRUKaTsPqDz7ClOtJCDu4paf2uAa0mrcHwSt4XxZP2ogFZS9uuitH3NXdeBEJA==", "cpu": [ "s390x" ], @@ -3761,9 +3761,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", - "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.51.0.tgz", + "integrity": "sha512-4YHhP+Rv3T3+H3TPbUvWOw5tuSwhrVhkHHZhk4hC9VXeAOKR26/IsUAT4FsB4mT+kfIdxxb1BezQDEg/voPO8A==", "cpu": [ "x64" ], @@ -3774,9 +3774,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz", - "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.51.0.tgz", + "integrity": "sha512-P7U7U03+E5w7WgJtvSseNLOX1UhknVPmEaqgUENFWfNxNBa1OhExT6qYGmyF8gepcxWSaSfJsAV5UwhWrYefdQ==", "cpu": [ "x64" ], @@ -3787,9 +3787,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz", - "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.51.0.tgz", + "integrity": "sha512-FuD8g3u9W6RPwdO1R45hZFORwa1g9YXEMesAKP/sOi7mDqxjbni8S3zAXJiDcRfGfGBqpRYVuH54Gu3FTuSoEw==", "cpu": [ "arm64" ], @@ -3800,9 +3800,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz", - "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.51.0.tgz", + "integrity": "sha512-zST+FdMCX3QAYfmZX3dp/Fy8qLUetfE17QN5ZmmFGPrhl86qvRr+E9u2bk7fzkIXsfQR30Z7ZRS7WMryPPn4rQ==", "cpu": [ "arm64" ], @@ -3813,9 +3813,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz", - "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.51.0.tgz", + "integrity": "sha512-U+qhoCVAZmTHCmUKxdQxw1jwAFNFXmOpMME7Npt5GTb1W/7itfgAgNluVOvyeuSeqW+dEQLFuNZF3YZPO8XkMg==", "cpu": [ "ia32" ], @@ -3826,9 +3826,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz", - "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.51.0.tgz", + "integrity": "sha512-z6UpFzMhXSD8NNUfCi2HO+pbpSzSWIIPgb1TZsEZjmZYtk6RUIC63JYjlFBwbBZS3jt3f1q6IGfkj3g+GnBt2Q==", "cpu": [ "x64" ], @@ -6276,9 +6276,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.5.tgz", - "integrity": "sha512-TiU4qUT9jdCuh4aVOG7H1QozyeI2sZRqoRPdqBIaslfNt4WUSanRBueAwl2x5jt4rXBMim3lIN2x6yT8PDi24Q==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7867,9 +7867,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.221", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.221.tgz", - "integrity": "sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==", + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", "dev": true, "license": "ISC" }, @@ -10241,9 +10241,9 @@ } }, "node_modules/is-network-error": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.2.0.tgz", - "integrity": "sha512-32jdpRpJo8SeL7zOuBJbMLz/VTw9mDpTvcKzzR8DkXWsJbbE60gdiX8YOd0UAV6b8Skt+CMytzfgVVIRFidn0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", "dev": true, "license": "MIT", "engines": { @@ -13161,9 +13161,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.50.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", - "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.51.0.tgz", + "integrity": "sha512-7cR0XWrdp/UAj2HMY/Y4QQEUjidn3l2AY1wSeZoFjMbD8aOMPoV9wgTFYbrJpPzzvejDEini1h3CiUP8wLzxQA==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -13176,27 +13176,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.50.2", - "@rollup/rollup-android-arm64": "4.50.2", - "@rollup/rollup-darwin-arm64": "4.50.2", - "@rollup/rollup-darwin-x64": "4.50.2", - "@rollup/rollup-freebsd-arm64": "4.50.2", - "@rollup/rollup-freebsd-x64": "4.50.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", - "@rollup/rollup-linux-arm-musleabihf": "4.50.2", - "@rollup/rollup-linux-arm64-gnu": "4.50.2", - "@rollup/rollup-linux-arm64-musl": "4.50.2", - "@rollup/rollup-linux-loong64-gnu": "4.50.2", - "@rollup/rollup-linux-ppc64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-gnu": "4.50.2", - "@rollup/rollup-linux-riscv64-musl": "4.50.2", - "@rollup/rollup-linux-s390x-gnu": "4.50.2", - "@rollup/rollup-linux-x64-gnu": "4.50.2", - "@rollup/rollup-linux-x64-musl": "4.50.2", - "@rollup/rollup-openharmony-arm64": "4.50.2", - "@rollup/rollup-win32-arm64-msvc": "4.50.2", - "@rollup/rollup-win32-ia32-msvc": "4.50.2", - "@rollup/rollup-win32-x64-msvc": "4.50.2", + "@rollup/rollup-android-arm-eabi": "4.51.0", + "@rollup/rollup-android-arm64": "4.51.0", + "@rollup/rollup-darwin-arm64": "4.51.0", + "@rollup/rollup-darwin-x64": "4.51.0", + "@rollup/rollup-freebsd-arm64": "4.51.0", + "@rollup/rollup-freebsd-x64": "4.51.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.51.0", + "@rollup/rollup-linux-arm-musleabihf": "4.51.0", + "@rollup/rollup-linux-arm64-gnu": "4.51.0", + "@rollup/rollup-linux-arm64-musl": "4.51.0", + "@rollup/rollup-linux-loong64-gnu": "4.51.0", + "@rollup/rollup-linux-ppc64-gnu": "4.51.0", + "@rollup/rollup-linux-riscv64-gnu": "4.51.0", + "@rollup/rollup-linux-riscv64-musl": "4.51.0", + "@rollup/rollup-linux-s390x-gnu": "4.51.0", + "@rollup/rollup-linux-x64-gnu": "4.51.0", + "@rollup/rollup-linux-x64-musl": "4.51.0", + "@rollup/rollup-openharmony-arm64": "4.51.0", + "@rollup/rollup-win32-arm64-msvc": "4.51.0", + "@rollup/rollup-win32-ia32-msvc": "4.51.0", + "@rollup/rollup-win32-x64-msvc": "4.51.0", "fsevents": "~2.3.2" } }, From 4c8cce27da62c72b25faca0262a2f32741b26f23 Mon Sep 17 00:00:00 2001 From: Arno Kaimbacher Date: Fri, 19 Sep 2025 17:21:21 +0200 Subject: [PATCH 3/4] feat: Update form field labels from "Main Title Language*" to "Main Description Language*" for clarity --- resources/js/Pages/Editor/Dataset/Edit.vue | 2 +- resources/js/Pages/Submitter/Dataset/Create.vue | 2 +- resources/js/Pages/Submitter/Dataset/Edit.vue | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/js/Pages/Editor/Dataset/Edit.vue b/resources/js/Pages/Editor/Dataset/Edit.vue index e33e5dd..9a2385c 100644 --- a/resources/js/Pages/Editor/Dataset/Edit.vue +++ b/resources/js/Pages/Editor/Dataset/Edit.vue @@ -163,7 +163,7 @@ - - Date: Fri, 26 Sep 2025 12:19:35 +0200 Subject: [PATCH 4/4] feat: Enhance ClamAV Docker entrypoint and configuration - Updated docker-entrypoint.sh to improve ClamAV service initialization and logging. - Added checks for ClamAV and freshclam daemon status. - Optimized freshclam configuration for container usage, including logging to stdout and setting database directory. - Introduced caching mechanism for enabled file extensions in vinejs_provider.ts to reduce database queries. - Implemented a new command to list datasets needing DataCite DOI updates, with options for verbose output, count only, and IDs only. - Updated package dependencies to include p-limit and pino-pretty. - finalized ace command 'detect:missing-cross-references' --- Dockerfile | 77 ++--- adonisrc.ts | 6 +- commands/fix_dataset_cross_references.ts | 183 ++++++++---- commands/list_updatable_datacite.ts | 346 +++++++++++++++++++++++ commands/update_datacite.ts | 39 ++- docker-entrypoint.sh | 88 +++--- freshclam.conf | 222 ++------------- package-lock.json | 39 +-- package.json | 3 +- providers/vinejs_provider.ts | 172 +++++++---- 10 files changed, 745 insertions(+), 430 deletions(-) create mode 100644 commands/list_updatable_datacite.ts diff --git a/Dockerfile b/Dockerfile index a5d1263..0d2f959 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,55 +1,61 @@ ################## First Stage - Creating base ######################### # Created a variable to hold our node base image -ARG NODE_IMAGE=node:22-bookworm-slim +ARG NODE_IMAGE=node:22-trixie-slim FROM $NODE_IMAGE AS base + # Install dumb-init and ClamAV, and perform ClamAV database update -RUN apt update \ - && apt-get install -y dumb-init clamav clamav-daemon nano \ +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + dumb-init \ + clamav \ + clamav-daemon \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* \ # Creating folders and changing ownerships - && mkdir -p /home/node/app && chown node:node /home/node/app \ + && mkdir -p /home/node/app \ && mkdir -p /var/lib/clamav \ && mkdir /usr/local/share/clamav \ - && chown -R node:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav \ - # permissions && mkdir /var/run/clamav \ - && chown node:clamav /var/run/clamav \ - && chmod 750 /var/run/clamav -# ----------------------------------------------- -# --- ClamAV & FeshClam ------------------------- -# ----------------------------------------------- -# RUN \ -# chmod 644 /etc/clamav/freshclam.conf && \ -# freshclam && \ -# mkdir /var/run/clamav && \ - # chown -R clamav:root /var/run/clamav + && mkdir -p /var/log/clamav \ + && mkdir -p /tmp/clamav-logs \ + + # Set ownership and permissions + && chown node:node /home/node/app \ + # && chown -R node:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav /var/run/clamav \ + && chown -R clamav:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav /var/run/clamav /var/log/clamav \ + && chmod 755 /tmp/clamav-logs \ + && chmod 750 /var/run/clamav \ + && chmod 755 /var/lib/clamav \ + && chmod 755 /var/log/clamav \ + # Add node user to clamav group and allow sudo for clamav commands + && usermod -a -G clamav node \ + && chmod g+w /var/run/clamav /var/lib/clamav /var/log/clamav /tmp/clamav-logs -# # initial update of av databases -# RUN freshclam -# Configure Clam AV... -COPY --chown=node:clamav ./*.conf /etc/clamav/ +# Configure ClamAV - copy config files before switching user +# COPY --chown=node:clamav ./*.conf /etc/clamav/ +COPY --chown=clamav:clamav ./*.conf /etc/clamav/ + +# Copy entrypoint script +COPY --chown=node:node docker-entrypoint.sh /home/node/app/docker-entrypoint.sh +RUN chmod +x /home/node/app/docker-entrypoint.sh + +ENV TZ="Europe/Vienna" -# # permissions -# RUN mkdir /var/run/clamav && \ -# chown node:clamav /var/run/clamav && \ -# chmod 750 /var/run/clamav # Setting the working directory WORKDIR /home/node/app # Changing the current active user to "node" + +# Download initial ClamAV database as root before switching users +USER root +RUN freshclam --quiet || echo "Initial database download failed - will retry at runtime" + USER node -# initial update of av databases -RUN freshclam - -# VOLUME /var/lib/clamav -COPY --chown=node:clamav docker-entrypoint.sh /home/node/app/docker-entrypoint.sh -RUN chmod +x /home/node/app/docker-entrypoint.sh -ENV TZ="Europe/Vienna" - - +# Initial update of AV databases (moved after USER directive) +# RUN freshclam || true ################## Second Stage - Installing dependencies ########## @@ -70,14 +76,13 @@ ENV NODE_ENV=production # We run "node ace build" to build the app (dist folder) for production RUN node ace build --ignore-ts-errors # RUN node ace build --production -# RUN node ace build --ignore-ts-errors ################## Final Stage - Production ######################### # In this final stage, we will start running the application FROM base AS production # Here, we include all the required environment variables -# ENV NODE_ENV=production +ENV NODE_ENV=production # ENV PORT=$PORT # ENV HOST=0.0.0.0 @@ -91,4 +96,4 @@ COPY --chown=node:node --from=build /home/node/app/build . EXPOSE 3333 ENTRYPOINT ["/home/node/app/docker-entrypoint.sh"] # Run the command to start the server using "dumb-init" -CMD [ "dumb-init", "node", "bin/server.js" ] \ No newline at end of file +CMD [ "node", "bin/server.js" ] \ No newline at end of file diff --git a/adonisrc.ts b/adonisrc.ts index de94c63..e42693a 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -30,9 +30,9 @@ export default defineConfig({ () => import('#start/rules/unique'), () => import('#start/rules/translated_language'), () => import('#start/rules/unique_person'), - () => import('#start/rules/file_length'), - () => import('#start/rules/file_scan'), - () => import('#start/rules/allowed_extensions_mimetypes'), + // () => import('#start/rules/file_length'), + // () => import('#start/rules/file_scan'), + // () => import('#start/rules/allowed_extensions_mimetypes'), () => import('#start/rules/dependent_array_min_length'), () => import('#start/rules/referenceValidation'), () => import('#start/rules/valid_mimetype'), diff --git a/commands/fix_dataset_cross_references.ts b/commands/fix_dataset_cross_references.ts index 2662e25..248fefd 100644 --- a/commands/fix_dataset_cross_references.ts +++ b/commands/fix_dataset_cross_references.ts @@ -6,6 +6,7 @@ */ import { BaseCommand, flags } from '@adonisjs/core/ace'; import type { CommandOptions } from '@adonisjs/core/types/ace'; +import { DateTime } from 'luxon'; import Dataset from '#models/dataset'; import DatasetReference from '#models/dataset_reference'; // import env from '#start/env'; @@ -15,6 +16,8 @@ interface MissingCrossReference { targetDatasetId: number; sourcePublishId: number | null; targetPublishId: number | null; + sourceDoi: string | null; + targetDoi: string | null; referenceType: string; relation: string; doi: string | null; @@ -33,30 +36,58 @@ export default class DetectMissingCrossReferences extends BaseCommand { @flags.boolean({ alias: 'v', description: 'Verbose output' }) public verbose: boolean = false; + @flags.number({ alias: 'p', description: 'Filter by specific publish_id (source or target dataset)' }) + public publish_id?: number; + + // example: node ace detect:missing-cross-references --verbose -p 227 //if you want to filter by specific publish_id with details + // example: node ace detect:missing-cross-references --verbose + // example: node ace detect:missing-cross-references --fix -p 227 //if you want to filter by specific publish_id and fix it + // example: node ace detect:missing-cross-references + public static options: CommandOptions = { startApp: true, staysAlive: false, }; + // Define the allowed relations that we want to process + private readonly ALLOWED_RELATIONS = ['IsNewVersionOf', 'IsPreviousVersionOf', 'IsVariantFormOf', 'IsOriginalFormOf']; + async run() { this.logger.info('πŸ” Detecting missing cross-references...'); + this.logger.info(`πŸ“‹ Processing only these relations: ${this.ALLOWED_RELATIONS.join(', ')}`); + + if (this.publish_id) { + this.logger.info(`Filtering by publish_id: ${this.publish_id}`); + } try { const missingReferences = await this.findMissingCrossReferences(); if (missingReferences.length === 0) { - this.logger.success('All cross-references are properly linked!'); + const filterMsg = this.publish_id ? ` for publish_id ${this.publish_id}` : ''; + this.logger.success(`All cross-references are properly linked for the specified relations${filterMsg}!`); return; } - this.logger.warning(`Found ${missingReferences.length} missing cross-reference(s):`); + const filterMsg = this.publish_id ? ` (filtered by publish_id ${this.publish_id})` : ''; + this.logger.warning(`Found ${missingReferences.length} missing cross-reference(s)${filterMsg}:`); - for (const missing of missingReferences) { - this.logger.info( - `Dataset ${missing.sourceDatasetId} references ${missing.targetDatasetId}, but reverse reference is missing`, - ); + // Show brief list if not verbose mode + if (!this.verbose) { + for (const missing of missingReferences) { + const sourceDoi = missing.sourceDoi ? ` DOI: ${missing.sourceDoi}` : ''; + const targetDoi = missing.targetDoi ? ` DOI: ${missing.targetDoi}` : ''; - if (this.verbose) { + this.logger.info( + `Dataset ${missing.sourceDatasetId} (Publish ID: ${missing.sourcePublishId}${sourceDoi}) ${missing.relation} Dataset ${missing.targetDatasetId} (Publish ID: ${missing.targetPublishId}${targetDoi}) β†’ missing reverse: ${missing.reverseRelation}`, + ); + } + } else { + // Verbose mode - show detailed info + for (const missing of missingReferences) { + this.logger.info( + `Dataset ${missing.sourceDatasetId} references ${missing.targetDatasetId}, but reverse reference is missing`, + ); this.logger.info(` - Reference type: ${missing.referenceType}`); this.logger.info(` - Relation: ${missing.relation}`); this.logger.info(` - DOI: ${missing.doi}`); @@ -67,20 +98,28 @@ export default class DetectMissingCrossReferences extends BaseCommand { await this.fixMissingReferences(missingReferences); this.logger.success('All missing cross-references have been fixed!'); } else { - this.printMissingReferencesList(missingReferences); + if (this.verbose) { + this.printMissingReferencesList(missingReferences); + } this.logger.info('πŸ’‘ Run with --fix flag to automatically create missing cross-references'); + if (this.publish_id) { + this.logger.info(`🎯 Currently filtering by publish_id: ${this.publish_id}`); + } } } catch (error) { this.logger.error('Error detecting missing cross-references:', error); process.exit(1); } } + private async findMissingCrossReferences(): Promise { const missingReferences: { sourceDatasetId: number; targetDatasetId: number; sourcePublishId: number | null; targetPublishId: number | null; + sourceDoi: string | null; + targetDoi: string | null; referenceType: string; relation: string; doi: string | null; @@ -90,22 +129,32 @@ export default class DetectMissingCrossReferences extends BaseCommand { this.logger.info('πŸ“Š Querying dataset references...'); // Find all references that point to Tethys datasets (DOI or URL containing tethys DOI) - // Only from datasets that are published - const tethysReferences = await DatasetReference.query() + // Only from datasets that are published AND only for allowed relations + const tethysReferencesQuery = DatasetReference.query() .whereIn('type', ['DOI', 'URL']) + .whereIn('relation', this.ALLOWED_RELATIONS) // Only process allowed relations .where((query) => { query.where('value', 'like', '%doi.org/10.24341/tethys.%').orWhere('value', 'like', '%tethys.at/dataset/%'); }) .preload('dataset', (datasetQuery) => { - datasetQuery.where('server_state', 'published'); + datasetQuery.preload('identifier'); }) .whereHas('dataset', (datasetQuery) => { datasetQuery.where('server_state', 'published'); }); + if (typeof this.publish_id === 'number') { + tethysReferencesQuery.whereHas('dataset', (datasetQuery) => { + datasetQuery.where('publish_id', this.publish_id as number); + }); + } - this.logger.info(`πŸ”— Found ${tethysReferences.length} Tethys references from published datasets`); + const tethysReferences = await tethysReferencesQuery.exec(); + + this.logger.info(`πŸ”— Found ${tethysReferences.length} Tethys references from published datasets (allowed relations only)`); let processedCount = 0; + let skippedCount = 0; + for (const reference of tethysReferences) { processedCount++; @@ -113,6 +162,15 @@ export default class DetectMissingCrossReferences extends BaseCommand { this.logger.info(`πŸ“ˆ Processed ${processedCount}/${tethysReferences.length} references...`); } + // Double-check that this relation is in our allowed list (safety check) + if (!this.ALLOWED_RELATIONS.includes(reference.relation)) { + skippedCount++; + if (this.verbose) { + this.logger.info(`⏭️ Skipping relation "${reference.relation}" - not in allowed list`); + } + continue; + } + // Extract dataset publish_id from DOI or URL const targetDatasetPublish = this.extractDatasetPublishIdFromReference(reference.value); @@ -127,6 +185,7 @@ export default class DetectMissingCrossReferences extends BaseCommand { const targetDataset = await Dataset.query() .where('publish_id', targetDatasetPublish) .where('server_state', 'published') + .preload('identifier') .first(); if (!targetDataset) { @@ -145,25 +204,31 @@ export default class DetectMissingCrossReferences extends BaseCommand { // Check if reverse reference exists const reverseReferenceExists = await this.checkReverseReferenceExists( targetDataset.id, - reference.document_id, + // reference.document_id, reference.relation, ); if (!reverseReferenceExists) { - missingReferences.push({ - sourceDatasetId: reference.document_id, - targetDatasetId: targetDataset.id, - sourcePublishId: reference.dataset.publish_id || null, - targetPublishId: targetDataset.publish_id || null, - referenceType: reference.type, - relation: reference.relation, - doi: reference.value, - reverseRelation: this.getReverseRelation(reference.relation), - }); + const reverseRelation = this.getReverseRelation(reference.relation); + if (reverseRelation) { + // Only add if we have a valid reverse relation + missingReferences.push({ + sourceDatasetId: reference.document_id, + targetDatasetId: targetDataset.id, + sourcePublishId: reference.dataset.publish_id || null, + targetPublishId: targetDataset.publish_id || null, + referenceType: reference.type, + relation: reference.relation, + doi: reference.value, + reverseRelation: reverseRelation, + sourceDoi: reference.dataset.identifier ? reference.dataset.identifier.value : null, + targetDoi: targetDataset.identifier ? targetDataset.identifier.value : null, + }); + } } } - this.logger.info(`βœ… Processed all ${processedCount} references`); + this.logger.info(`βœ… Processed ${processedCount} references (${skippedCount} skipped due to relation filtering)`); return missingReferences; } @@ -183,64 +248,47 @@ export default class DetectMissingCrossReferences extends BaseCommand { return null; } - private async checkReverseReferenceExists( - sourceDatasetId: number, - targetDatasetId: number, - originalRelation: string, - ): Promise { + private async checkReverseReferenceExists(targetDatasetId: number, originalRelation: string): Promise { const reverseRelation = this.getReverseRelation(originalRelation); + if (!reverseRelation) { + return true; // If no reverse relation is defined, consider it as "exists" to skip processing + } + // Only check for reverse references where the source dataset is also published const reverseReference = await DatasetReference.query() - .where('document_id', sourceDatasetId) + // We don't filter by source document_id here to find any incoming reference from any published dataset + // .where('document_id', sourceDatasetId) .where('related_document_id', targetDatasetId) .where('relation', reverseRelation) - .whereHas('dataset', (datasetQuery) => { - datasetQuery.where('server_state', 'published'); - }) .first(); return !!reverseReference; } - private getReverseRelation(relation: string): string { + private getReverseRelation(relation: string): string | null { const relationMap: Record = { IsNewVersionOf: 'IsPreviousVersionOf', IsPreviousVersionOf: 'IsNewVersionOf', - - IsVersionOf: 'HasVersion', - HasVersion: 'IsVersionOf', - - Compiles: 'IsCompiledBy', - IsCompiledBy: 'Compiles', - IsVariantFormOf: 'IsOriginalFormOf', IsOriginalFormOf: 'IsVariantFormOf', - - IsPartOf: 'HasPart', - HasPart: 'IsPartOf', - - IsSupplementTo: 'IsSupplementedBy', - IsSupplementedBy: 'IsSupplementTo', - - Continues: 'IsContinuedBy', - IsContinuedBy: 'Continues', }; - // to catch relation types like 'compiles' or 'IsVariantFormOf' that are not in the map mark reverse as 'HasVersion' - return relationMap[relation] || 'HasVersion'; // Default fallback + // Only return reverse relation if it exists in our map, otherwise return null + return relationMap[relation] || null; } private printMissingReferencesList(missingReferences: MissingCrossReference[]) { console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'); console.log('β”‚ MISSING CROSS-REFERENCES REPORT β”‚'); - console.log('β”‚ (Published Datasets Only) β”‚'); + console.log('β”‚ (Published Datasets Only - Filtered Relations) β”‚'); console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'); console.log(); missingReferences.forEach((missing, index) => { console.log( - `${index + 1}. Dataset ${missing.sourceDatasetId} (Publish ID: ${missing.sourcePublishId}) β†’ Dataset ${missing.targetDatasetId} (Publish ID: ${missing.targetPublishId})`, + `${index + 1}. Dataset ${missing.sourceDatasetId} (Publish ID: ${missing.sourcePublishId} Identifier: ${missing.sourceDoi}) + ${missing.relation} Dataset ${missing.targetDatasetId} (Publish ID: ${missing.targetPublishId} Identifier: ${missing.targetDoi})`, ); console.log(` β”œβ”€ Current relation: "${missing.relation}"`); console.log(` β”œβ”€ Missing reverse relation: "${missing.reverseRelation}"`); @@ -251,6 +299,7 @@ export default class DetectMissingCrossReferences extends BaseCommand { console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'); console.log(`β”‚ SUMMARY: ${missingReferences.length} missing reverse reference(s) detected β”‚`); + console.log(`β”‚ Processed relations: ${this.ALLOWED_RELATIONS.join(', ')} β”‚`); console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'); } @@ -262,27 +311,37 @@ export default class DetectMissingCrossReferences extends BaseCommand { for (const [index, missing] of missingReferences.entries()) { try { - // Get the source dataset to create proper reference - ensure it's published + // Get both source and target datasets const sourceDataset = await Dataset.query() .where('id', missing.sourceDatasetId) .where('server_state', 'published') .preload('identifier') .first(); + const targetDataset = await Dataset.query().where('id', missing.targetDatasetId).where('server_state', 'published').first(); + if (!sourceDataset) { this.logger.warning(`⚠️ Source dataset ${missing.sourceDatasetId} not found or not published, skipping...`); errorCount++; continue; } - // Create the reverse reference + if (!targetDataset) { + this.logger.warning(`⚠️ Target dataset ${missing.targetDatasetId} not found or not published, skipping...`); + errorCount++; + continue; + } + + // Create the reverse reference using the referenced_by relationship + // Example: If Dataset 297 IsNewVersionOf Dataset 144 + // We create an incoming reference for Dataset 144 that shows Dataset 297 IsPreviousVersionOf it const reverseReference = new DatasetReference(); - reverseReference.document_id = missing.targetDatasetId; - reverseReference.related_document_id = missing.sourceDatasetId; + // Don't set document_id - this creates an incoming reference via related_document_id + reverseReference.related_document_id = missing.targetDatasetId; // 144 (dataset receiving the incoming reference) reverseReference.type = 'DOI'; reverseReference.relation = missing.reverseRelation; - // Use the source dataset's DOI for the value + // Use the source dataset's DOI for the value (what's being referenced) if (sourceDataset.identifier?.value) { reverseReference.value = `https://doi.org/${sourceDataset.identifier.value}`; } else { @@ -293,12 +352,16 @@ export default class DetectMissingCrossReferences extends BaseCommand { // Use the source dataset's main title for the label reverseReference.label = sourceDataset.mainTitle || `Dataset ${missing.sourceDatasetId}`; + // Also save 'server_date_modified' on target dataset to trigger any downstream updates (e.g. search index) + targetDataset.server_date_modified = DateTime.now(); + await targetDataset.save(); + await reverseReference.save(); fixedCount++; if (this.verbose) { this.logger.info( - `βœ… [${index + 1}/${missingReferences.length}] Created reverse reference: Dataset ${missing.targetDatasetId} -> ${missing.sourceDatasetId}`, + `βœ… [${index + 1}/${missingReferences.length}] Created reverse reference: Dataset ${missing.sourceDatasetId} -> ${missing.targetDatasetId} (${missing.reverseRelation})`, ); } else if ((index + 1) % 10 === 0) { this.logger.info(`πŸ“ˆ Fixed ${fixedCount}/${missingReferences.length} references...`); diff --git a/commands/list_updatable_datacite.ts b/commands/list_updatable_datacite.ts new file mode 100644 index 0000000..aed411a --- /dev/null +++ b/commands/list_updatable_datacite.ts @@ -0,0 +1,346 @@ +/* +|-------------------------------------------------------------------------- +| node ace make:command list-updateable-datacite +| DONE: create commands/list_updeatable_datacite.ts +|-------------------------------------------------------------------------- +*/ +import { BaseCommand, flags } from '@adonisjs/core/ace'; +import { CommandOptions } from '@adonisjs/core/types/ace'; +import Dataset from '#models/dataset'; +import { DoiClient } from '#app/Library/Doi/DoiClient'; +import env from '#start/env'; +import logger from '@adonisjs/core/services/logger'; +import { DateTime } from 'luxon'; +import pLimit from 'p-limit'; + +export default class ListUpdateableDatacite extends BaseCommand { + static commandName = 'list:updateable-datacite'; + static description = 'List all datasets that need DataCite DOI updates'; + + public static needsApplication = true; + + // private chunkSize = 100; // Set chunk size for pagination + + @flags.boolean({ alias: 'v', description: 'Verbose output showing detailed information' }) + public verbose: boolean = false; + + @flags.boolean({ alias: 'c', description: 'Show only count of updatable datasets' }) + public countOnly: boolean = false; + + @flags.boolean({ alias: 'i', description: 'Show only publish IDs (useful for scripting)' }) + public idsOnly: boolean = false; + + @flags.number({ description: 'Chunk size for processing datasets (default: 50)' }) + public chunkSize: number = 50; + + //example: node ace list:updateable-datacite + //example: node ace list:updateable-datacite --verbose + //example: node ace list:updateable-datacite --count-only + //example: node ace list:updateable-datacite --ids-only + //example: node ace list:updateable-datacite --chunk-size 50 + + public static options: CommandOptions = { + startApp: true, + stayAlive: false, + }; + + async run() { + const prefix = env.get('DATACITE_PREFIX', ''); + const base_domain = env.get('BASE_DOMAIN', ''); + + if (!prefix || !base_domain) { + logger.error('Missing DATACITE_PREFIX or BASE_DOMAIN environment variables'); + return; + } + + // Prevent conflicting flags + if ((this.verbose && this.countOnly) || (this.verbose && this.idsOnly)) { + logger.error('Flags --verbose cannot be combined with --count-only or --ids-only'); + return; + } + + const chunkSize = this.chunkSize || 50; + let page = 1; + let hasMoreDatasets = true; + let totalProcessed = 0; + const updatableDatasets: Dataset[] = []; + + if (!this.countOnly && !this.idsOnly) { + logger.info(`Processing datasets in chunks of ${chunkSize}...`); + } + + while (hasMoreDatasets) { + const datasets = await this.getDatasets(page, chunkSize); + + if (datasets.length === 0) { + hasMoreDatasets = false; + break; + } + + if (!this.countOnly && !this.idsOnly) { + logger.info(`Processing chunk ${page} (${datasets.length} datasets)...`); + } + + const chunkUpdatableDatasets = await this.processChunk(datasets); + updatableDatasets.push(...chunkUpdatableDatasets); + totalProcessed += datasets.length; + + page += 1; + if (datasets.length < chunkSize) { + hasMoreDatasets = false; + } + } + + if (!this.countOnly && !this.idsOnly) { + logger.info(`Processed ${totalProcessed} datasets total, found ${updatableDatasets.length} that need updates`); + } + + if (this.countOnly) { + console.log(updatableDatasets.length); + } else if (this.idsOnly) { + updatableDatasets.forEach((dataset) => console.log(dataset.publish_id)); + } else if (this.verbose) { + await this.showVerboseOutput(updatableDatasets); + } else { + this.showSimpleOutput(updatableDatasets); + } + } + + /** + * Processes a chunk of datasets to determine which ones need DataCite updates + * + * This method handles parallel processing of datasets within a chunk, providing + * efficient error handling and filtering of results. + * + * @param datasets - Array of Dataset objects to process + * @returns Promise - Array of datasets that need updates + */ + // private async processChunk(datasets: Dataset[]): Promise { + // // Process datasets in parallel using Promise.allSettled for better error handling + // // + // // Why Promise.allSettled vs Promise.all? + // // - Promise.all fails fast: if ANY promise rejects, the entire operation fails + // // - Promise.allSettled waits for ALL promises: some can fail, others succeed + // // - This is crucial for batch processing where we don't want one bad dataset + // // to stop processing of the entire chunk + // const results = await Promise.allSettled( + // datasets.map(async (dataset) => { + // try { + // // Check if this specific dataset needs a DataCite update + // const needsUpdate = await this.shouldUpdateDataset(dataset); + + // // Return the dataset if it needs update, null if it doesn't + // // This creates a sparse array that we'll filter later + // return needsUpdate ? dataset : null; + // } catch (error) { + // // Error handling for individual dataset checks + // // + // // Log warnings only if we're not in silent modes (count-only or ids-only) + // // This prevents log spam when running automated scripts + // if (!this.countOnly && !this.idsOnly) { + // logger.warn(`Error checking dataset ${dataset.publish_id}: ${error.message}`); + // } + + // // IMPORTANT DECISION: Return the dataset anyway if we can't determine status + // // + // // Why? It's safer to include a dataset that might not need updating + // // than to miss one that actually does need updating. This follows the + // // "fail-safe" principle - if we're unsure, err on the side of caution + // return dataset; + // } + // }), + // ); + + // // Filter and extract results from Promise.allSettled response + // // + // // Promise.allSettled returns an array of objects with this structure: + // // - { status: 'fulfilled', value: T } for successful promises + // // - { status: 'rejected', reason: Error } for failed promises + // // + // // We need to: + // // 1. Only get fulfilled results (rejected ones are already handled above) + // // 2. Filter out null values (datasets that don't need updates) + // // 3. Extract the actual Dataset objects from the wrapper + // return results + // .filter( + // (result): result is PromiseFulfilledResult => + // // Type guard: only include fulfilled results that have actual values + // // This filters out: + // // - Rejected promises (shouldn't happen due to try/catch, but safety first) + // // - Fulfilled promises that returned null (datasets that don't need updates) + // result.status === 'fulfilled' && result.value !== null, + // ) + // .map((result) => result.value!); // Extract the Dataset from the wrapper + // // The ! is safe here because we filtered out null values above + // } + + private async processChunk(datasets: Dataset[]): Promise { + // Limit concurrency to avoid API flooding (e.g., max 5 at once) + const limit = pLimit(5); + + const tasks = datasets.map((dataset) => + limit(async () => { + try { + const needsUpdate = await this.shouldUpdateDataset(dataset); + return needsUpdate ? dataset : null; + } catch (error) { + if (!this.countOnly && !this.idsOnly) { + logger.warn( + `Error checking dataset ${dataset.publish_id}: ${ + error instanceof Error ? error.message : JSON.stringify(error) + }`, + ); + } + // Fail-safe: include dataset if uncertain + return dataset; + } + }), + ); + + const results = await Promise.allSettled(tasks); + + return results + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled' && result.value !== null) + .map((result) => result.value!); + } + + private async getDatasets(page: number, chunkSize: number): Promise { + return await Dataset.query() + .orderBy('publish_id', 'asc') + .preload('identifier') + .preload('xmlCache') + .preload('titles') + .where('server_state', 'published') + .whereHas('identifier', (identifierQuery) => { + identifierQuery.where('type', 'doi'); + }) + .forPage(page, chunkSize); // Get files for the current page + } + + private async shouldUpdateDataset(dataset: Dataset): Promise { + try { + let doiIdentifier = dataset.identifier; + if (!doiIdentifier) { + await dataset.load('identifier'); + doiIdentifier = dataset.identifier; + } + + if (!doiIdentifier || doiIdentifier.type !== 'doi') { + return false; + } + + const datasetModified = + dataset.server_date_modified instanceof DateTime + ? dataset.server_date_modified + : DateTime.fromJSDate(dataset.server_date_modified); + + if (!datasetModified) { + return true; + } + + if (datasetModified > DateTime.now()) { + return false; + } + + const doiClient = new DoiClient(); + const DOI_CHECK_TIMEOUT = 300; // ms + + const doiLastModified = await Promise.race([ + doiClient.getDoiLastModified(doiIdentifier.value), + this.createTimeoutPromise(DOI_CHECK_TIMEOUT), + ]).catch(() => null); + + if (!doiLastModified) { + // If uncertain, better include dataset for update + return true; + } + + const doiModified = DateTime.fromJSDate(doiLastModified); + if (datasetModified > doiModified) { + const diffInSeconds = Math.abs(datasetModified.diff(doiModified, 'seconds').seconds); + const toleranceSeconds = 600; + return diffInSeconds > toleranceSeconds; + } + return false; + } catch (error) { + return true; // safer: include dataset if unsure + } + } + + /** + * Create a timeout promise for API calls + */ + private createTimeoutPromise(timeoutMs: number): Promise { + return new Promise((_, reject) => { + setTimeout(() => reject(new Error(`API call timeout after ${timeoutMs}ms`)), timeoutMs); + }); + } + + private showSimpleOutput(updatableDatasets: Dataset[]): void { + if (updatableDatasets.length === 0) { + console.log('No datasets need DataCite updates.'); + return; + } + + console.log(`\nFound ${updatableDatasets.length} dataset(s) that need DataCite updates:\n`); + + updatableDatasets.forEach((dataset) => { + console.log(`publish_id ${dataset.publish_id} needs update - ${dataset.mainTitle || 'Untitled'}`); + }); + + console.log(`\nTo update these datasets, run:`); + console.log(` node ace update:datacite`); + console.log(`\nOr update specific datasets:`); + console.log(` node ace update:datacite -p `); + } + + private async showVerboseOutput(updatableDatasets: Dataset[]): Promise { + if (updatableDatasets.length === 0) { + console.log('No datasets need DataCite updates.'); + return; + } + + console.log(`\nFound ${updatableDatasets.length} dataset(s) that need DataCite updates:\n`); + + for (const dataset of updatableDatasets) { + await this.showDatasetDetails(dataset); + } + + console.log(`\nSummary: ${updatableDatasets.length} datasets need updates`); + } + + private async showDatasetDetails(dataset: Dataset): Promise { + try { + let doiIdentifier = dataset.identifier; + + if (!doiIdentifier) { + await dataset.load('identifier'); + doiIdentifier = dataset.identifier; + } + + const doiValue = doiIdentifier?.value || 'N/A'; + const datasetModified = dataset.server_date_modified; + + // Get DOI info from DataCite + const doiClient = new DoiClient(); + const doiLastModified = await doiClient.getDoiLastModified(doiValue); + const doiState = await doiClient.getDoiState(doiValue); + + console.log(`β”Œβ”€ Dataset ${dataset.publish_id} ───────────────────────────────────────────────────────────────`); + console.log(`β”‚ Title: ${dataset.mainTitle || 'Untitled'}`); + console.log(`β”‚ DOI: ${doiValue}`); + console.log(`β”‚ DOI State: ${doiState || 'Unknown'}`); + console.log(`β”‚ Dataset Modified: ${datasetModified ? datasetModified.toISO() : 'N/A'}`); + console.log(`β”‚ DOI Modified: ${doiLastModified ? DateTime.fromJSDate(doiLastModified).toISO() : 'N/A'}`); + console.log(`β”‚ Status: NEEDS UPDATE`); + console.log(`└─────────────────────────────────────────────────────────────────────────────────────────────\n`); + } catch (error) { + console.log(`β”Œβ”€ Dataset ${dataset.publish_id} ───────────────────────────────────────────────────────────────`); + console.log(`β”‚ Title: ${dataset.mainTitle || 'Untitled'}`); + console.log(`β”‚ DOI: ${dataset.identifier?.value || 'N/A'}`); + console.log(`β”‚ Error: ${error.message}`); + console.log(`β”‚ Status: NEEDS UPDATE (Error checking)`); + console.log(`└─────────────────────────────────────────────────────────────────────────────────────────────\n`); + } + } +} diff --git a/commands/update_datacite.ts b/commands/update_datacite.ts index 9280f95..7ccb0f0 100644 --- a/commands/update_datacite.ts +++ b/commands/update_datacite.ts @@ -122,58 +122,53 @@ export default class UpdateDatacite extends BaseCommand { private async shouldUpdateDataset(dataset: Dataset): Promise { try { - // Check if dataset has a DOI identifier (HasOne relationship) let doiIdentifier = dataset.identifier; if (!doiIdentifier) { - // Try to load the relationship if not already loaded await dataset.load('identifier'); doiIdentifier = dataset.identifier; } if (!doiIdentifier || doiIdentifier.type !== 'doi') { - logger.warn(`Dataset ${dataset.publish_id}: No DOI identifier found`); return false; } - // Validate dataset modification date const datasetModified = dataset.server_date_modified; const now = DateTime.now(); if (!datasetModified) { - logger.error(`Dataset ${dataset.publish_id}: server_date_modified is null or undefined`); - return true; // Update anyway if modification date is missing + return true; // Update if modification date is missing } if (datasetModified > now) { - logger.error( - `Dataset ${dataset.publish_id}: server_date_modified (${datasetModified.toISO()}) is in the future! ` + - `Current time: ${now.toISO()}. This indicates a data integrity issue. Skipping update.`, - ); - return false; // Do not update when modification date is invalid + return false; // Skip invalid future dates } - // Get DOI information from DataCite using DoiClient + // Check DataCite DOI modification date const doiClient = new DoiClient(); const doiLastModified = await doiClient.getDoiLastModified(doiIdentifier.value); if (!doiLastModified) { - logger.warn(`Dataset ${dataset.publish_id}: Could not retrieve DOI modification date from DataCite`); - return true; // Update anyway if we can't get DOI info + return false; // not Update if we can't get DOI info } - // Compare dataset modification date with DOI modification date const doiModified = DateTime.fromJSDate(doiLastModified); + if (datasetModified > doiModified) { + // if dataset was modified after DOI creation + // Calculate the difference in seconds + const diffInSeconds = Math.abs(datasetModified.diff(doiModified, 'seconds').seconds); - logger.debug( - `Dataset ${dataset.publish_id}: Dataset modified: ${datasetModified.toISO()}, DOI modified: ${doiModified.toISO()}`, - ); + // Define tolerance threshold (60 seconds = 1 minute) + const toleranceSeconds = 60; - // Update if dataset was modified after the DOI record - return datasetModified > doiModified; + // Only update if the difference is greater than the tolerance + // This prevents unnecessary updates for minor timestamp differences + return diffInSeconds > toleranceSeconds; + } else { + return false; // No update needed + } } catch (error) { - logger.warn(`Error checking update status for dataset ${dataset.publish_id}: ${error.message}`); - return true; // Update anyway if we can't determine status + return false; // not update if we can't determine status or other error } } diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 8ef61c7..f932a8d 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,47 +1,61 @@ #!/bin/bash +set -e -# # Run freshclam to update virus definitions -# freshclam +echo "Starting ClamAV services..." -# # Sleep for a few seconds to give ClamAV time to start -# sleep 5 -# # Start the ClamAV daemon -# /etc/init.d/clamav-daemon start - -# bootstrap clam av service and clam av database updater -set -m - -function process_file() { - if [[ ! -z "$1" ]]; then - local SETTING_LIST=$(echo "$1" | tr ',' '\n' | grep "^[A-Za-z][A-Za-z]*=.*$") - local SETTING - - for SETTING in ${SETTING_LIST}; do - # Remove any existing copies of this setting. We do this here so that - # settings with multiple values (e.g. ExtraDatabase) can still be added - # multiple times below - local KEY=${SETTING%%=*} - sed -i $2 -e "/^${KEY} /d" - done - - for SETTING in ${SETTING_LIST}; do - # Split on first '=' - local KEY=${SETTING%%=*} - local VALUE=${SETTING#*=} - echo "${KEY} ${VALUE}" >> "$2" - done +# Try to download database if missing +if [ ! "$(ls -A /var/lib/clamav 2>/dev/null)" ]; then + echo "Downloading ClamAV database (this may take a while)..." + + # Simple freshclam run without complex config + if sg clamav -c "freshclam --datadir=/var/lib/clamav --quiet"; then + echo "βœ“ Database downloaded successfully" + else + echo "⚠ Database download failed - creating minimal setup" + # Create a dummy file so clamd doesn't immediately fail + sg clamav -c "touch /var/lib/clamav/.dummy" fi -} +fi -# process_file "${CLAMD_SETTINGS_CSV}" /etc/clamav/clamd.conf -# process_file "${FRESHCLAM_SETTINGS_CSV}" /etc/clamav/freshclam.conf +# Start freshclam daemon for automatic updates +echo "Starting freshclam daemon for automatic updates..." +sg clamav -c "freshclam -d" & -# start in background -freshclam -d & # /etc/init.d/clamav-freshclam start & -clamd +# Start clamd in background +# Start clamd in foreground (so dumb-init can supervise it) # /etc/init.d/clamav-daemon start & -# change back to CMD of dockerfile -exec "$@" \ No newline at end of file +# Start clamd daemon in background using sg +echo "Starting ClamAV daemon..." +# sg clamav -c "clamd" & +# Use sg to run clamd with proper group permissions +# sg clamav -c "clamd" & +sg clamav -c "clamd --config-file=/etc/clamav/clamd.conf" & + + +# Give services time to start +echo "Waiting for services to initialize..." +sleep 8 + +# simple check +if pgrep clamd > /dev/null; then + echo "βœ“ ClamAV daemon is running" +else + echo "⚠ ClamAV daemon status uncertain, but continuing..." +fi + +# Check if freshclam daemon is running +if pgrep freshclam > /dev/null; then + echo "βœ“ Freshclam daemon is running" +else + echo "⚠ Freshclam daemon status uncertain, but continuing..." +fi + +# # change back to CMD of dockerfile +# exec "$@" + +echo "βœ“ ClamAV setup complete" +echo "Starting main application..." +exec dumb-init -- "$@" \ No newline at end of file diff --git a/freshclam.conf b/freshclam.conf index ee82a23..444a63f 100644 --- a/freshclam.conf +++ b/freshclam.conf @@ -1,229 +1,47 @@ ## -## Example config file for freshclam -## Please read the freshclam.conf(5) manual before editing this file. +## Container-optimized freshclam configuration ## - -# Comment or remove the line below. - -# Path to the database directory. -# WARNING: It must match clamd.conf's directive! -# Default: hardcoded (depends on installation options) +# Database directory DatabaseDirectory /var/lib/clamav -# Path to the log file (make sure it has proper permissions) -# Default: disabled +# Log to stdout for container logging # UpdateLogFile /dev/stdout -# Maximum size of the log file. -# Value of 0 disables the limit. -# You may use 'M' or 'm' for megabytes (1M = 1m = 1048576 bytes) -# and 'K' or 'k' for kilobytes (1K = 1k = 1024 bytes). -# in bytes just don't use modifiers. If LogFileMaxSize is enabled, -# log rotation (the LogRotate option) will always be enabled. -# Default: 1M -#LogFileMaxSize 2M - -# Log time with each message. -# Default: no +# Basic logging settings LogTime yes - -# Enable verbose logging. -# Default: no -LogVerbose yes - -# Use system logger (can work together with UpdateLogFile). -# Default: no +LogVerbose no LogSyslog no -# Specify the type of syslog messages - please refer to 'man syslog' -# for facility names. -# Default: LOG_LOCAL6 -#LogFacility LOG_MAIL - -# Enable log rotation. Always enabled when LogFileMaxSize is enabled. -# Default: no -#LogRotate yes - -# This option allows you to save the process identifier of the daemon -# Default: disabled -#PidFile /var/run/freshclam.pid +# PID file location PidFile /var/run/clamav/freshclam.pid -# By default when started freshclam drops privileges and switches to the -# "clamav" user. This directive allows you to change the database owner. -# Default: clamav (may depend on installation options) -DatabaseOwner node +# Database owner +DatabaseOwner clamav -# Use DNS to verify virus database version. Freshclam uses DNS TXT records -# to verify database and software versions. With this directive you can change -# the database verification domain. -# WARNING: Do not touch it unless you're configuring freshclam to use your -# own database verification domain. -# Default: current.cvd.clamav.net -#DNSDatabaseInfo current.cvd.clamav.net - -# Uncomment the following line and replace XY with your country -# code. See http://www.iana.org/cctld/cctld-whois.htm for the full list. -# You can use db.XY.ipv6.clamav.net for IPv6 connections. +# Mirror settings for Austria DatabaseMirror db.at.clamav.net - -# database.clamav.net is a round-robin record which points to our most -# reliable mirrors. It's used as a fall back in case db.XY.clamav.net is -# not working. DO NOT TOUCH the following line unless you know what you -# are doing. DatabaseMirror database.clamav.net -# How many attempts to make before giving up. -# Default: 3 (per mirror) -#MaxAttempts 5 - # With this option you can control scripted updates. It's highly recommended # to keep it enabled. # Default: yes -#ScriptedUpdates yes - -# By default freshclam will keep the local databases (.cld) uncompressed to -# make their handling faster. With this option you can enable the compression; -# the change will take effect with the next database update. -# Default: no -#CompressLocalDatabase no - -# With this option you can provide custom sources (http:// or file://) for -# database files. This option can be used multiple times. -# Default: no custom URLs -#DatabaseCustomURL http://myserver.com/mysigs.ndb -#DatabaseCustomURL file:///mnt/nfs/local.hdb - -# This option allows you to easily point freshclam to private mirrors. -# If PrivateMirror is set, freshclam does not attempt to use DNS -# to determine whether its databases are out-of-date, instead it will -# use the If-Modified-Since request or directly check the headers of the -# remote database files. For each database, freshclam first attempts -# to download the CLD file. If that fails, it tries to download the -# CVD file. This option overrides DatabaseMirror, DNSDatabaseInfo -# and ScriptedUpdates. It can be used multiple times to provide -# fall-back mirrors. -# Default: disabled -#PrivateMirror mirror1.mynetwork.com -#PrivateMirror mirror2.mynetwork.com +# Update settings +ScriptedUpdates yes # Number of database checks per day. # Default: 12 (every two hours) -#Checks 24 +Checks 12 -# Proxy settings -# Default: disabled -#HTTPProxyServer myproxy.com -#HTTPProxyPort 1234 -#HTTPProxyUsername myusername -#HTTPProxyPassword mypass - -# If your servers are behind a firewall/proxy which applies User-Agent -# filtering you can use this option to force the use of a different -# User-Agent header. -# Default: clamav/version_number -#HTTPUserAgent SomeUserAgentIdString - -# Use aaa.bbb.ccc.ddd as client address for downloading databases. Useful for -# multi-homed systems. -# Default: Use OS'es default outgoing IP address. -#LocalIPAddress aaa.bbb.ccc.ddd - -# Send the RELOAD command to clamd. -# Default: no -#NotifyClamd /path/to/clamd.conf - -# Run command after successful database update. -# Default: disabled -#OnUpdateExecute command - -# Run command when database update process fails. -# Default: disabled -#OnErrorExecute command - -# Run command when freshclam reports outdated version. -# In the command string %v will be replaced by the new version number. -# Default: disabled -#OnOutdatedExecute command - -# Don't fork into background. -# Default: no +# Don't fork (good for containers) Foreground no -# Enable debug messages in libclamav. -# Default: no -#Debug yes +# Connection timeouts +ConnectTimeout 60 +ReceiveTimeout 60 -# Timeout in seconds when connecting to database server. -# Default: 30 -#ConnectTimeout 60 +# Test databases before using them +TestDatabases yes -# Timeout in seconds when reading from database server. -# Default: 30 -#ReceiveTimeout 60 - -# With this option enabled, freshclam will attempt to load new -# databases into memory to make sure they are properly handled -# by libclamav before replacing the old ones. -# Default: yes -#TestDatabases yes - -# When enabled freshclam will submit statistics to the ClamAV Project about -# the latest virus detections in your environment. The ClamAV maintainers -# will then use this data to determine what types of malware are the most -# detected in the field and in what geographic area they are. -# Freshclam will connect to clamd in order to get recent statistics. -# Default: no -#SubmitDetectionStats /path/to/clamd.conf - -# Country of origin of malware/detection statistics (for statistical -# purposes only). The statistics collector at ClamAV.net will look up -# your IP address to determine the geographical origin of the malware -# reported by your installation. If this installation is mainly used to -# scan data which comes from a different location, please enable this -# option and enter a two-letter code (see http://www.iana.org/domains/root/db/) -# of the country of origin. -# Default: disabled -#DetectionStatsCountry country-code - -# This option enables support for our "Personal Statistics" service. -# When this option is enabled, the information on malware detected by -# your clamd installation is made available to you through our website. -# To get your HostID, log on http://www.stats.clamav.net and add a new -# host to your host list. Once you have the HostID, uncomment this option -# and paste the HostID here. As soon as your freshclam starts submitting -# information to our stats collecting service, you will be able to view -# the statistics of this clamd installation by logging into -# http://www.stats.clamav.net with the same credentials you used to -# generate the HostID. For more information refer to: -# http://www.clamav.net/documentation.html#cctts -# This feature requires SubmitDetectionStats to be enabled. -# Default: disabled -#DetectionStatsHostID unique-id - -# This option enables support for Google Safe Browsing. When activated for -# the first time, freshclam will download a new database file (safebrowsing.cvd) -# which will be automatically loaded by clamd and clamscan during the next -# reload, provided that the heuristic phishing detection is turned on. This -# database includes information about websites that may be phishing sites or -# possible sources of malware. When using this option, it's mandatory to run -# freshclam at least every 30 minutes. -# Freshclam uses the ClamAV's mirror infrastructure to distribute the -# database and its updates but all the contents are provided under Google's -# terms of use. See http://www.google.com/transparencyreport/safebrowsing -# and http://www.clamav.net/documentation.html#safebrowsing -# for more information. -# Default: disabled -#SafeBrowsing yes - -# This option enables downloading of bytecode.cvd, which includes additional -# detection mechanisms and improvements to the ClamAV engine. -# Default: enabled -#Bytecode yes - -# Download an additional 3rd party signature database distributed through -# the ClamAV mirrors. -# This option can be used multiple times. -#ExtraDatabase dbname1 -#ExtraDatabase dbname2 +# Enable bytecode signatures +Bytecode yes \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb6179c..f9efdac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,9 @@ "node-2fa": "^2.0.3", "node-exceptions": "^4.0.1", "notiwind": "^2.0.0", + "p-limit": "^7.1.1", "pg": "^8.9.0", + "pino-pretty": "^13.0.0", "qrcode": "^1.5.3", "redis": "^5.0.0", "reflect-metadata": "^0.2.1", @@ -92,7 +94,6 @@ "hot-hook": "^0.4.0", "numeral": "^2.0.6", "pinia": "^3.0.2", - "pino-pretty": "^13.0.0", "postcss-loader": "^8.1.1", "prettier": "^3.4.2", "supertest": "^6.3.3", @@ -7398,7 +7399,6 @@ "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -7904,7 +7904,6 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -8560,7 +8559,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -8633,7 +8631,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, "license": "MIT" }, "node_modules/fast-uri": { @@ -9667,7 +9664,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "dev": true, "license": "MIT" }, "node_modules/hookable": { @@ -10432,7 +10428,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -11159,7 +11154,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11717,15 +11711,15 @@ } }, "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.1.1.tgz", + "integrity": "sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==", "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" + "yocto-queue": "^1.2.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11746,6 +11740,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", @@ -12165,7 +12174,6 @@ "version": "13.1.1", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.1.tgz", "integrity": "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==", - "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.7", @@ -12190,7 +12198,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", - "dev": true, "funding": [ { "type": "github", @@ -12207,7 +12214,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.16" @@ -12659,7 +12665,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", diff --git a/package.json b/package.json index 989a29e..49385ba 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "hot-hook": "^0.4.0", "numeral": "^2.0.6", "pinia": "^3.0.2", - "pino-pretty": "^13.0.0", "postcss-loader": "^8.1.1", "prettier": "^3.4.2", "supertest": "^6.3.3", @@ -115,7 +114,9 @@ "node-2fa": "^2.0.3", "node-exceptions": "^4.0.1", "notiwind": "^2.0.0", + "p-limit": "^7.1.1", "pg": "^8.9.0", + "pino-pretty": "^13.0.0", "qrcode": "^1.5.3", "redis": "^5.0.0", "reflect-metadata": "^0.2.1", diff --git a/providers/vinejs_provider.ts b/providers/vinejs_provider.ts index e4aad4c..568dd3e 100644 --- a/providers/vinejs_provider.ts +++ b/providers/vinejs_provider.ts @@ -6,17 +6,16 @@ import type { ApplicationService } from '@adonisjs/core/types'; import vine, { symbols, BaseLiteralType, Vine } from '@vinejs/vine'; import type { FieldContext, FieldOptions } from '@vinejs/vine/types'; -// import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/types'; import type { MultipartFile } from '@adonisjs/core/bodyparser'; import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser'; import { Request, RequestValidator } from '@adonisjs/core/http'; import MimeType from '#models/mime_type'; - /** * Validation options accepted by the "file" rule */ export type FileRuleValidationOptions = Partial | ((field: FieldContext) => Partial); + /** * Extend VineJS */ @@ -25,6 +24,7 @@ declare module '@vinejs/vine' { myfile(options?: FileRuleValidationOptions): VineMultipartFile; } } + /** * Extend HTTP request class */ @@ -36,19 +36,54 @@ declare module '@adonisjs/core/http' { * Checks if the value is an instance of multipart file * from bodyparser. */ -export function isBodyParserFile(file: MultipartFile | unknown): boolean { +export function isBodyParserFile(file: MultipartFile | unknown): file is MultipartFile { return !!(file && typeof file === 'object' && 'isMultipartFile' in file); } -export async function getEnabledExtensions() { - const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec(); - const extensions = enabledExtensions - .map((extension) => { - return extension.file_extension.split('|'); - }) - .flat(); - return extensions; +/** + * Cache for enabled extensions to reduce database queries + */ +let extensionsCache: string[] | null = null; +let cacheTimestamp = 0; +const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes + +/** + * Get enabled extensions with caching + */ +export async function getEnabledExtensions(): Promise { + const now = Date.now(); + + if (extensionsCache && now - cacheTimestamp < CACHE_DURATION) { + return extensionsCache; + } + + try { + const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec(); + + const extensions = enabledExtensions + .map((extension) => extension.file_extension.split('|')) + .flat() + .map((ext) => ext.toLowerCase().trim()) + .filter((ext) => ext.length > 0); + + extensionsCache = [...new Set(extensions)]; // Remove duplicates + cacheTimestamp = now; + + return extensionsCache; + } catch (error) { + console.error('Error fetching enabled extensions:', error); + return extensionsCache || []; + } } + +/** + * Clear extensions cache + */ +export function clearExtensionsCache(): void { + extensionsCache = null; + cacheTimestamp = 0; +} + /** * VineJS validation rule that validates the file to be an * instance of BodyParser MultipartFile class. @@ -65,6 +100,7 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op // At this point, you can use type assertion to explicitly tell TypeScript that file is of type MultipartFile const validatedFile = file as MultipartFile; const validationOptions = typeof options === 'function' ? options(field) : options; + /** * Set size when it's defined in the options and missing * on the file instance @@ -72,30 +108,29 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op if (validatedFile.sizeLimit === undefined && validationOptions.size) { validatedFile.sizeLimit = validationOptions.size; } + /** * Set extensions when it's defined in the options and missing * on the file instance */ - // if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) { - // validatedFile.allowedExtensions = validationOptions.extnames; - // } - if (validatedFile.allowedExtensions === undefined && validationOptions.extnames !== undefined) { - validatedFile.allowedExtensions = validationOptions.extnames; // await getEnabledExtensions(); - } else if (validatedFile.allowedExtensions === undefined && validationOptions.extnames === undefined) { - validatedFile.allowedExtensions = await getEnabledExtensions(); + if (validatedFile.allowedExtensions === undefined) { + if (validationOptions.extnames !== undefined) { + validatedFile.allowedExtensions = validationOptions.extnames; + } else { + validatedFile.allowedExtensions = await getEnabledExtensions(); + } } - /** - * wieder lΓΆschen - * Set extensions when it's defined in the options and missing - * on the file instance - */ - // if (file.clientNameSizeLimit === undefined && validationOptions.clientNameSizeLimit) { - // file.clientNameSizeLimit = validationOptions.clientNameSizeLimit; - // } + /** * Validate file */ - validatedFile.validate(); + try { + validatedFile.validate(); + } catch (error) { + field.report(`File validation failed: ${error.message}`, 'file.validation_error', field, validationOptions); + return; + } + /** * Report errors */ @@ -107,36 +142,37 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op const MULTIPART_FILE: typeof symbols.SUBTYPE = symbols.SUBTYPE; export class VineMultipartFile extends BaseLiteralType { - [MULTIPART_FILE]: string; - // constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) { - // super(options, [isMultipartFile(validationOptions || {})]); - // this.validationOptions = validationOptions; - // this.#private = true; - // } - - // clone(): this { - // return new VineMultipartFile(this.validationOptions, this.cloneOptions()) as this; - // } - // #private; - // constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation[]); - // clone(): this; - - public validationOptions; + public validationOptions?: FileRuleValidationOptions; // extnames: (18) ['gpkg', 'htm', 'html', 'csv', 'txt', 'asc', 'c', 'cc', 'h', 'srt', 'tiff', 'pdf', 'png', 'zip', 'jpg', 'jpeg', 'jpe', 'xlsx'] // size: '512mb' - // public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation[]) { public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) { - // super(options, validations); super(options, [isMultipartFile(validationOptions || {})]); this.validationOptions = validationOptions; } public clone(): any { - // return new VineMultipartFile(this.validationOptions, this.cloneOptions(), this.cloneValidations()); return new VineMultipartFile(this.validationOptions, this.cloneOptions()); } + + /** + * Set maximum file size + */ + public maxSize(size: string | number): this { + const newOptions = { ...this.validationOptions, size }; + return new VineMultipartFile(newOptions, this.cloneOptions()) as this; + } + + /** + * Set allowed extensions + */ + public extensions(extnames: string[]): this { + const newOptions = { ...this.validationOptions, extnames }; + return new VineMultipartFile(newOptions, this.cloneOptions()) as this; + } + + } export default class VinejsProvider { @@ -155,13 +191,8 @@ export default class VinejsProvider { /** * The container bindings have booted */ - boot(): void { - // VineString.macro('translatedLanguage', function (this: VineString, options: Options) { - // return this.use(translatedLanguageRule(options)); - // }); - - Vine.macro('myfile', function (this: Vine, options) { + Vine.macro('myfile', function (this: Vine, options?: FileRuleValidationOptions) { return new VineMultipartFile(options); }); @@ -175,6 +206,41 @@ export default class VinejsProvider { } return new RequestValidator(this.ctx).validateUsing(...args); }); + + // Ensure MIME validation macros are loaded + this.loadMimeValidationMacros(); + this.loadFileScanMacros(); + this.loadFileLengthMacros(); + } + + /** + * Load MIME validation macros - called during boot to ensure they're available + */ + private async loadMimeValidationMacros(): Promise { + try { + // Dynamically import the MIME validation rule to ensure macros are registered + await import('#start/rules/allowed_extensions_mimetypes'); + } catch (error) { + console.warn('Could not load MIME validation macros:', error); + } + } + + private async loadFileScanMacros(): Promise { + try { + // Dynamically import the MIME validation rule to ensure macros are registered + await import('#start/rules/file_scan'); + } catch (error) { + console.warn('Could not load MIME validation macros:', error); + } + } + + private async loadFileLengthMacros(): Promise { + try { + // Dynamically import the MIME validation rule to ensure macros are registered + await import('#start/rules/file_length'); + } catch (error) { + console.warn('Could not load MIME validation macros:', error); + } } /** @@ -190,5 +256,7 @@ export default class VinejsProvider { /** * Preparing to shutdown the app */ - async shutdown() {} + async shutdown() { + clearExtensionsCache(); + } }