diff --git a/app/api/entities.v2/contracts/EntitiesDataSource.ts b/app/api/entities.v2/contracts/EntitiesDataSource.ts index 4a9195a18b..5f637a26e7 100644 --- a/app/api/entities.v2/contracts/EntitiesDataSource.ts +++ b/app/api/entities.v2/contracts/EntitiesDataSource.ts @@ -1,5 +1,5 @@ import { ResultSet } from 'api/common.v2/contracts/ResultSet'; -import { Entity, MetadataValue } from '../model/Entity'; +import { Entity, EntityMetadata, MetadataValue } from '../model/Entity'; type MarkAsChangedCriteria = { template: string } | { sharedId: string }; type MarkAsChangedData = { property: string } | { properties: string[] }; @@ -8,7 +8,11 @@ export type MarkAsChangedItems = MarkAsChangedCriteria & MarkAsChangedData; export interface EntitiesDataSource { updateObsoleteMetadataValues( id: Entity['_id'], - values: Record + values: Record + ): Promise; + updateMetadataValues( + id: Entity['_id'], + values: Record ): Promise; entitiesExist(sharedIds: string[]): Promise; getByIds(sharedIds: string[], language?: string): ResultSet; diff --git a/app/api/entities.v2/database/MongoEntitiesDataSource.ts b/app/api/entities.v2/database/MongoEntitiesDataSource.ts index 48498120fc..3a2de5c661 100644 --- a/app/api/entities.v2/database/MongoEntitiesDataSource.ts +++ b/app/api/entities.v2/database/MongoEntitiesDataSource.ts @@ -1,15 +1,16 @@ import { ResultSet } from 'api/common.v2/contracts/ResultSet'; import { MongoDataSource } from 'api/common.v2/database/MongoDataSource'; +import { MongoIdHandler } from 'api/common.v2/database/MongoIdGenerator'; import { MongoResultSet } from 'api/common.v2/database/MongoResultSet'; import { MongoTransactionManager } from 'api/common.v2/database/MongoTransactionManager'; -import { MongoIdHandler } from 'api/common.v2/database/MongoIdGenerator'; +import entities from 'api/entities/entities'; import { MongoSettingsDataSource } from 'api/settings.v2/database/MongoSettingsDataSource'; import { MongoTemplatesDataSource } from 'api/templates.v2/database/MongoTemplatesDataSource'; import { Db } from 'mongodb'; import { EntitiesDataSource } from '../contracts/EntitiesDataSource'; +import { Entity, EntityMetadata, MetadataValue } from '../model/Entity'; import { EntityMappers } from './EntityMapper'; import { EntityDBO, EntityJoinTemplate } from './schemas/EntityTypes'; -import { Entity, MetadataValue } from '../model/Entity'; export class MongoEntitiesDataSource extends MongoDataSource @@ -123,9 +124,30 @@ export class MongoEntitiesDataSource return new MongoResultSet(result, entity => entity.sharedId); } + // eslint-disable-next-line class-methods-use-this + async updateMetadataValues( + id: Entity['_id'], + values: Record + ) { + // This is using V1 so that it gets denormalized to speed up development + // this is a hack and should be changed as soon as we finish AT + const entityToModify = await entities.getById(id); + if (!entityToModify) { + throw new Error(`entity does not exists: ${id}`); + } + + Object.entries(values).forEach(([propertyName, metadataValues]) => { + entityToModify.metadata = entityToModify.metadata || {}; + // @ts-ignore + entityToModify.metadata[propertyName] = metadataValues; + }); + + await entities.save(entityToModify, { user: {}, language: entityToModify.language }); + } + async updateObsoleteMetadataValues( id: Entity['_id'], - values: Record + values: Record ): Promise { const stream = this.createBulkStream(); diff --git a/app/api/entities.v2/model/Entity.ts b/app/api/entities.v2/model/Entity.ts index 5b4749a427..6491c59c08 100644 --- a/app/api/entities.v2/model/Entity.ts +++ b/app/api/entities.v2/model/Entity.ts @@ -1,5 +1,7 @@ +type MetadataValue = unknown; + type BaseMetadataValue = { - value: unknown; + value: MetadataValue; label: string; }; @@ -8,9 +10,9 @@ type InheritedResultValue = BaseMetadataValue & { inheritedType: string; }; -type MetadataValue = BaseMetadataValue | InheritedResultValue; +type EntityMetadata = BaseMetadataValue | InheritedResultValue; -type Metadata = Record; +type Metadata = Record; export class Entity { readonly _id: string; @@ -45,4 +47,5 @@ export class Entity { this.obsoleteMetadata = obsoleteMetadata ?? []; } } -export type { Metadata, MetadataValue }; + +export type { Metadata, EntityMetadata, MetadataValue }; diff --git a/app/api/entities.v2/services/EntityRelationshipsUpdateService.ts b/app/api/entities.v2/services/EntityRelationshipsUpdateService.ts index ef040328c2..fccedb5c7f 100644 --- a/app/api/entities.v2/services/EntityRelationshipsUpdateService.ts +++ b/app/api/entities.v2/services/EntityRelationshipsUpdateService.ts @@ -3,7 +3,7 @@ import { Template } from 'api/templates.v2/model/Template'; import { RelationshipProperty } from 'api/templates.v2/model/RelationshipProperty'; import { RelationshipsDataSource } from 'api/relationships.v2/contracts/RelationshipsDataSource'; import { TemplatesDataSource } from 'api/templates.v2/contracts/TemplatesDataSource'; -import { Entity, MetadataValue } from '../model/Entity'; +import { Entity, EntityMetadata } from '../model/Entity'; import { EntitiesDataSource } from '../contracts/EntitiesDataSource'; export class EntityRelationshipsUpdateService { @@ -40,7 +40,7 @@ export class EntityRelationshipsUpdateService { private async transformToDenormalizedData( property: RelationshipProperty, queryResult: Entity[] - ): Promise { + ): Promise { return Promise.all( queryResult.map(async entity => ({ value: entity.sharedId, @@ -65,7 +65,7 @@ export class EntityRelationshipsUpdateService { await this.entitiesDataSource.getByIds(sharedIds).forEach(async entity => { template = await this.findTemplate(template, entity.template); - const metadataToUpdate: Record = {}; + const metadataToUpdate: Record = {}; await Promise.all( template.properties.map(async property => { diff --git a/app/api/externalIntegrations.v2/automaticTranslation/SaveEntityTranslations.ts b/app/api/externalIntegrations.v2/automaticTranslation/SaveEntityTranslations.ts new file mode 100644 index 0000000000..06671d0f44 --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/SaveEntityTranslations.ts @@ -0,0 +1,69 @@ +import { EntitiesDataSource } from 'api/entities.v2/contracts/EntitiesDataSource'; +import { TemplatesDataSource } from 'api/templates.v2/contracts/TemplatesDataSource'; +import { ATTranslationResultValidator } from './contracts/ATTranslationResultValidator'; +import { InvalidInputDataFormat } from './errors/generateATErrors'; +import { TranslationResult } from './types/TranslationResult'; + +export class SaveEntityTranslations { + static AITranslatedText = '(AI translated)'; + + private entitiesDS: EntitiesDataSource; + + private templatesDS: TemplatesDataSource; + + private validator: ATTranslationResultValidator; + + constructor( + templatesDS: TemplatesDataSource, + entitiesDS: EntitiesDataSource, + validator: ATTranslationResultValidator + ) { + this.entitiesDS = entitiesDS; + this.templatesDS = templatesDS; + this.validator = validator; + } + + async execute(translationResult: TranslationResult | unknown) { + if (!this.validator.validate(translationResult)) { + throw new InvalidInputDataFormat(this.validator.getErrors()[0]); + } + + const [, entitySharedId, propertyId] = translationResult.key; + + const property = await this.getProperty(entitySharedId, propertyId); + + const entities = this.entitiesDS.getByIds([entitySharedId]); + + await entities.forEach(async oneEntity => { + const translation = translationResult.translations.find( + t => t.language === oneEntity.language + ); + if (translation) { + await this.entitiesDS.updateMetadataValues(oneEntity._id, { + [property.name]: [ + { value: `${SaveEntityTranslations.AITranslatedText} ${translation.text}` }, + ], + }); + } + }); + } + + private async getProperty(entitySharedId: string, propertyId: string) { + const entity = await this.entitiesDS.getByIds([entitySharedId]).first(); + if (!entity) { + throw new Error('Entity does not exists'); + } + + const template = await this.templatesDS.getById(entity.template); + if (!template) { + throw new Error('Template does not exists'); + } + + const property = template.properties.find(p => p.id === propertyId); + + if (!property) { + throw new Error('Property does not exists'); + } + return property; + } +} diff --git a/app/api/externalIntegrations.v2/automaticTranslation/contracts/ATTranslationResultValidator.ts b/app/api/externalIntegrations.v2/automaticTranslation/contracts/ATTranslationResultValidator.ts new file mode 100644 index 0000000000..f368e257c3 --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/contracts/ATTranslationResultValidator.ts @@ -0,0 +1,6 @@ +import { TranslationResult } from '../types/TranslationResult'; + +export interface ATTranslationResultValidator { + getErrors(): string[]; + validate(data: unknown): data is TranslationResult; +} diff --git a/app/api/externalIntegrations.v2/automaticTranslation/infrastructure/AJVTranslationResultValidator.ts b/app/api/externalIntegrations.v2/automaticTranslation/infrastructure/AJVTranslationResultValidator.ts new file mode 100644 index 0000000000..f27daaff91 --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/infrastructure/AJVTranslationResultValidator.ts @@ -0,0 +1,40 @@ +import { Ajv } from 'ajv'; +import { JTDSchemaType } from 'ajv/dist/core'; +import { TranslationResult } from '../types/TranslationResult'; +import { ATTranslationResultValidator } from '../contracts/ATTranslationResultValidator'; + +const schema: JTDSchemaType = { + additionalProperties: false, + properties: { + key: { elements: { type: 'string' } }, + text: { type: 'string' }, + language_from: { type: 'string' }, + languages_to: { elements: { type: 'string' } }, + translations: { + elements: { + properties: { + text: { type: 'string' }, + language: { type: 'string' }, + success: { type: 'boolean' }, + error_message: { type: 'string' }, + }, + }, + }, + }, +}; + +export class AJVTranslationResultValidator implements ATTranslationResultValidator { + private errors: string[] = []; + + getErrors() { + return this.errors; + } + + validate(data: unknown) { + const ajv = new Ajv({ strict: false }); + const validate = ajv.compile(schema); + const result = validate(data); + this.errors = validate.errors ? validate.errors?.map(e => JSON.stringify(e.params)) : []; + return result; + } +} diff --git a/app/api/externalIntegrations.v2/automaticTranslation/specs/SaveEntityTranslations.spec.ts b/app/api/externalIntegrations.v2/automaticTranslation/specs/SaveEntityTranslations.spec.ts new file mode 100644 index 0000000000..fd7f7c8a29 --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/specs/SaveEntityTranslations.spec.ts @@ -0,0 +1,160 @@ +import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults'; +import { DefaultEntitiesDataSource } from 'api/entities.v2/database/data_source_defaults'; +import { DefaultTemplatesDataSource } from 'api/templates.v2/database/data_source_defaults'; +import { getFixturesFactory } from 'api/utils/fixturesFactory'; +import { testingEnvironment } from 'api/utils/testingEnvironment'; +import testingDB, { DBFixture } from 'api/utils/testing_db'; +import { LanguageISO6391 } from 'shared/types/commonTypes'; +import { SaveEntityTranslations } from '../SaveEntityTranslations'; +import { InvalidInputDataFormat } from '../errors/generateATErrors'; +import { AJVTranslationResultValidator } from '../infrastructure/AJVTranslationResultValidator'; +import { TranslationResult } from '../types/TranslationResult'; + +const factory = getFixturesFactory(); + +beforeEach(async () => { + const fixtures = { + templates: [ + factory.template('template1', [ + { + _id: factory.id('propertyName'), + name: 'propertyName', + type: 'text', + label: 'Prop 1', + }, + ]), + ], + entities: [ + ...factory.entityInMultipleLanguages(['en', 'pt', 'es'], 'entity', 'template1', { + propertyName: [{ value: 'original text' }], + }), + ], + settings: [ + { + languages: [ + { label: 'en', key: 'en' as LanguageISO6391, default: true }, + { label: 'pt', key: 'pt' as LanguageISO6391 }, + { label: 'es', key: 'es' as LanguageISO6391 }, + ], + }, + ], + }; + await testingEnvironment.setUp(fixtures); +}); + +afterAll(async () => { + await testingEnvironment.tearDown(); +}); + +describe('GenerateAutomaticTranslationConfig', () => { + let saveEntityTranslations: SaveEntityTranslations; + + beforeEach(() => { + const transactionManager = DefaultTransactionManager(); + saveEntityTranslations = new SaveEntityTranslations( + DefaultTemplatesDataSource(transactionManager), + DefaultEntitiesDataSource(transactionManager), + new AJVTranslationResultValidator() + ); + }); + + it('should validate input has proper shape at runtime', async () => { + const invalidConfig = { invalid_prop: true }; + await expect(saveEntityTranslations.execute(invalidConfig)).rejects.toEqual( + new InvalidInputDataFormat('{"additionalProperty":"invalid_prop"}') + ); + }); + + it(`should save entity with translated text with prepended "${SaveEntityTranslations.AITranslatedText}"`, async () => { + const translationResult: TranslationResult = { + key: ['tenant', 'entity', factory.idString('propertyName')], + text: 'original text', + language_from: 'en', + languages_to: ['es', 'pt'], + translations: [ + { text: 'texto original', language: 'es', success: true, error_message: '' }, + { text: 'texto original (pt)', language: 'pt', success: true, error_message: '' }, + ], + }; + + await saveEntityTranslations.execute(translationResult); + + const entities = + (await testingDB.mongodb?.collection('entities').find({ sharedId: 'entity' }).toArray()) || + []; + + expect(entities.find(e => e.language === 'es')).toMatchObject({ + metadata: { + propertyName: [{ value: `${SaveEntityTranslations.AITranslatedText} texto original` }], + }, + }); + + expect(entities.find(e => e.language === 'pt')).toMatchObject({ + metadata: { + propertyName: [{ value: `${SaveEntityTranslations.AITranslatedText} texto original (pt)` }], + }, + }); + + expect(entities.find(e => e.language === 'en')).toMatchObject({ + metadata: { propertyName: [{ value: 'original text' }] }, + }); + }); + + it('should denormalize text property on related entities', async () => { + const fixtures: DBFixture = { + settings: [ + { + languages: [{ label: 'es', key: 'es' as LanguageISO6391, default: true }], + }, + ], + templates: [ + factory.template('templateA', [factory.inherit('relationship', 'templateB', 'text')]), + factory.template('templateB', [factory.property('text')]), + ], + entities: [ + factory.entity('A1', 'templateA', { + relationship: [factory.metadataValue('B1')], + }), + factory.entity('B1', 'templateB', {}, { title: 'B1title' }), + ], + }; + + await testingEnvironment.setUp(fixtures); + + const translationResult: TranslationResult = { + key: ['tenant', 'B1', factory.idString('text')], + text: 'texto original', + language_from: 'es', + languages_to: ['en'], + translations: [{ text: 'original text', language: 'en', success: true, error_message: '' }], + }; + + await saveEntityTranslations.execute(translationResult); + + const entities = (await testingDB.mongodb?.collection('entities').find().toArray()) || []; + + expect(entities).toMatchObject([ + { + sharedId: 'A1', + metadata: { + relationship: [ + { + value: 'B1', + label: 'B1title', + inheritedValue: [ + { value: `${SaveEntityTranslations.AITranslatedText} original text` }, + ], + }, + ], + }, + }, + { + title: 'B1title', + sharedId: 'B1', + metadata: { + text: [{ value: `${SaveEntityTranslations.AITranslatedText} original text` }], + }, + }, + ]); + }); +}); diff --git a/app/api/externalIntegrations.v2/automaticTranslation/types/TranslationResult.ts b/app/api/externalIntegrations.v2/automaticTranslation/types/TranslationResult.ts new file mode 100644 index 0000000000..a53b8b3b95 --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/types/TranslationResult.ts @@ -0,0 +1,12 @@ +export interface TranslationResult { + key: string[]; + text: string; + language_from: string; + languages_to: string[]; + translations: { + text: string; + language: string; + success: boolean; + error_message: string; + }[]; +} diff --git a/package.json b/package.json index 3f84a1e2a1..90710e5520 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.185.0-rc7", + "version": "1.186.0-rc1", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react" diff --git a/yarn.lock b/yarn.lock index 90271ce23e..4293ecbf6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5213,7 +5213,12 @@ dependencies: "@types/react" "*" -"@types/lodash@^4.14.167", "@types/lodash@^4.14.170", "@types/lodash@^4.17.7": +"@types/lodash@^4.14.167", "@types/lodash@^4.14.170": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + +"@types/lodash@^4.17.7": version "4.17.7" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== @@ -6475,7 +6480,7 @@ babel-jest@^29.7.0: graceful-fs "^4.2.9" slash "^3.0.0" -babel-loader@9.2.1, babel-loader@^9.1.3: +babel-loader@9.2.1: version "9.2.1" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== @@ -6483,6 +6488,14 @@ babel-loader@9.2.1, babel-loader@^9.1.3: find-cache-dir "^4.0.0" schema-utils "^4.0.0" +babel-loader@^9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" + integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== + dependencies: + find-cache-dir "^4.0.0" + schema-utils "^4.0.0" + babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" @@ -6686,6 +6699,24 @@ bn.js@^5.0.0, bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + body-parser@1.20.3, body-parser@^1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -7124,9 +7155,9 @@ chokidar@^3.4.0, chokidar@^3.5.2, chokidar@^3.5.3: fsevents "~2.3.2" chokidar@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.0.tgz#4d603963e5dd762dc5c7bb1cb5664e53a3002225" - integrity sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA== + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== dependencies: readdirp "^4.0.1" @@ -9668,7 +9699,44 @@ express-session@1.18.0: safe-buffer "5.2.1" uid-safe "~2.1.5" -express@^4.17.3, express@^4.18.2, express@^4.19.2: +express@^4.17.3, express@^4.18.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +express@^4.19.2: version "4.21.0" resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== @@ -9893,7 +9961,12 @@ filelist@^1.0.4: dependencies: minimatch "^5.0.1" -filesize@^10.0.12, filesize@^10.1.4: +filesize@^10.0.12: + version "10.1.4" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.4.tgz#184f256063a201f08b6e6b3cc47d21b60f5b8d89" + integrity sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg== + +filesize@^10.1.4: version "10.1.6" resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.6.tgz#31194da825ac58689c0bce3948f33ce83aabd361" integrity sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w== @@ -9905,6 +9978,19 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + finalhandler@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" @@ -10275,7 +10361,12 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -10455,7 +10546,18 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^10.0.0, glob@^10.3.12: +glob@^10.0.0: + version "10.3.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.4.tgz#c85c9c7ab98669102b6defda76d35c5b1ef9766f" + integrity sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.0.3" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^10.3.12: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -11719,6 +11821,15 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^2.0.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.1.tgz#ce2effa4c458e053640e61938865a5b5fae98456" + integrity sha512-4iSY3Bh1Htv+kLhiiZunUhQ+OYXIn0ze3ulq8JeWrFKmhPAJSySV2+kdtRh2pGcCeF0s6oR8Oc+pYZynJj4t8A== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -12255,7 +12366,12 @@ jimp@^0.10.3: core-js "^3.4.1" regenerator-runtime "^0.13.3" -jiti@^1.20.0, jiti@^1.21.0: +jiti@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + +jiti@^1.21.0: version "1.21.6" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== @@ -12272,9 +12388,9 @@ joi@^17.6.0: "@sideway/pinpoint" "^2.0.0" jotai@^2.9.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.9.3.tgz#abcae49a737cd50e3144a6c9eb39840db077c727" - integrity sha512-IqMWKoXuEzWSShjd9UhalNsRGbdju5G2FrqNLQJT+Ih6p41VNYe2sav5hnwQx4HJr25jq9wRqvGSWGviGG6Gjw== + version "2.10.0" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.10.0.tgz#7483b81ab21ba28778f04b29368728f25efe4e89" + integrity sha512-8W4u0aRlOIwGlLQ0sqfl/c6+eExl5D8lZgAUolirZLktyaj4WnxO/8a0HEPmtriQAB6X5LMhXzZVmw02X0P0qQ== jpeg-js@^0.3.4: version "0.3.7" @@ -13050,6 +13166,10 @@ merge-deep@^3.0.3: clone-deep "^0.2.4" kind-of "^3.0.2" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" @@ -13187,7 +13307,7 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.4: +minimatch@^9.0.1, minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== @@ -13538,9 +13658,9 @@ nodemailer@^6.9.14: integrity sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ== nodemon@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.6.tgz#15bd79dca6849aa27b2689d1dbe02dc28bcc1a1c" - integrity sha512-C8ymJbXpTTinxjWuMfMxw0rZhTn/r7ypSGldQyqPEgDEaVwAthqC0aodsMwontnAInN9TuPwRLeBoyhmfv+iSA== + version "3.1.7" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.7.tgz#07cb1f455f8bece6a499e0d72b5e029485521a54" + integrity sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ== dependencies: chokidar "^3.5.2" debug "^4" @@ -14180,7 +14300,7 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" -path-scurry@^1.11.1, path-scurry@^1.6.1: +path-scurry@^1.10.1, path-scurry@^1.11.1, path-scurry@^1.6.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -14193,6 +14313,10 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + path-to-regexp@^2.2.1: version "2.4.0" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz" @@ -14268,7 +14392,12 @@ phin@^2.9.1: resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.3.tgz#f9b6ac10a035636fb65dfc576aaaa17b8743125c" integrity sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA== -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picocolors@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== @@ -14671,7 +14800,16 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.38, postcss@^8.4.41: +postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.33, postcss@^8.4.38: + version "8.4.41" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681" + integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + +postcss@^8.4.41: version "8.4.47" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== @@ -14919,6 +15057,13 @@ qs@6.10.4: dependencies: side-channel "^1.0.4" +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@6.13.0, qs@^6.10.0, qs@^6.11.0, qs@^6.11.2, qs@^6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -16000,9 +16145,9 @@ sass-loader@16.0.1: neo-async "^2.6.2" sass@^1.77.8: - version "1.79.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.1.tgz#0c572e8f09cc4095c27077567f78dbb9b3dceeb2" - integrity sha512-+mA7svoNKeL0DiJqZGeR/ZGUu8he4I8o3jyUcOFyo4eBJrwNgIMmAEwCMo/N2Y3wdjOBcRzoNxZIOtrtMX8EXg== + version "1.79.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.3.tgz#7811b000eb68195fe51dea89177e73e7ef7f546f" + integrity sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA== dependencies: chokidar "^4.0.0" immutable "^4.0.0" @@ -16065,6 +16210,25 @@ semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -16100,6 +16264,16 @@ serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + serve-static@1.16.2: version "1.16.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" @@ -16373,7 +16547,12 @@ socket.io@^2.2.0: socket.io-client "2.5.0" socket.io-parser "~3.4.0" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -16561,16 +16740,8 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16665,14 +16836,8 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18113,7 +18278,8 @@ world-countries@5.0.0: resolved "https://registry.yarnpkg.com/world-countries/-/world-countries-5.0.0.tgz#6f75ebcce3d5224d84e9117eaf0d75a7726b6501" integrity sha512-wAfOT9Y5i/xnxNOdKJKXdOCw9Q3yQLahBUeuRol+s+o20F6h2a4tLEbJ1lBCYwEQ30Sf9Meqeipk1gib3YwF5w== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -18131,15 +18297,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"