diff --git a/api/package-lock.json b/api/package-lock.json index 2d42a9ffe7..07cf9bc710 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1228,11 +1228,6 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, - "adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==" - }, "adm-zip": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.5.tgz", @@ -2007,23 +2002,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, - "cfb": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.1.tgz", - "integrity": "sha512-wT2ScPAFGSVy7CY+aauMezZBnNrfnaLSrxHUHdea+Td/86vrk6ZquggV+ssBR88zNs0OnBkL2+lf9q0K+zVGzQ==", - "requires": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0", - "printj": "~1.3.0" - }, - "dependencies": { - "printj": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz", - "integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==" - } - } - }, "chai": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", @@ -2263,11 +2241,6 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, - "codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" - }, "collection-map": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", @@ -2520,11 +2493,6 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" - }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2574,6 +2542,11 @@ "type": "^1.0.1" } }, + "dayjs": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.8.tgz", + "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==" + }, "db-migrate": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.11.11.tgz", @@ -2992,7 +2965,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { "version": "1.4.4", @@ -3124,7 +3097,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -3398,7 +3371,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "events": { "version": "1.1.1", @@ -4010,11 +3983,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, - "frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==" - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -4027,7 +3995,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fromentries": { "version": "1.3.2", @@ -5054,7 +5022,7 @@ "is-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-dir/-/is-dir-1.0.0.tgz", - "integrity": "sha512-vLwCNpTNkFC5k7SBRxPubhOCryeulkOsSkjbGyZ8eOzZmzMS+hSEO/Kn9ZOVhFNAlRZTFc4ZKql48hESuYUPIQ==" + "integrity": "sha1-QdN/SV/MrMBaR3jWboMCTCkro/8=" }, "is-extendable": { "version": "0.1.1", @@ -8438,14 +8406,6 @@ "resolved": "https://registry.npmjs.org/sql-template-strings/-/sql-template-strings-2.2.2.tgz", "integrity": "sha1-PxFQiiWt384hejBCqdMAwxk7lv8=" }, - "ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "requires": { - "frac": "~1.1.2" - } - }, "ssh2": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.5.4.tgz", @@ -9121,7 +9081,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unset-value": { "version": "1.0.0", @@ -9559,16 +9519,6 @@ } } }, - "wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==" - }, - "word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==" - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -9615,18 +9565,8 @@ "dev": true }, "xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", - "requires": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - } + "version": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz", + "integrity": "sha512-8IfgFctB7fkvqkTGF2MnrDrC6vzE28Wcc1aSbdDQ+4/WFtzfS73YuapbuaPZwGqpR2e0EeDMIrFOJubQVLWFNA==" }, "xml2js": { "version": "0.4.23", diff --git a/api/package.json b/api/package.json index fc5f2a987d..2f740c9128 100644 --- a/api/package.json +++ b/api/package.json @@ -36,6 +36,7 @@ "aws-sdk": "~2.742.0", "axios": "~0.21.4", "clamdjs": "~1.0.2", + "dayjs": "^1.11.8", "db-migrate": "~0.11.11", "db-migrate-pg": "~1.2.2", "express": "~4.17.1", @@ -59,7 +60,7 @@ "utm": "^1.1.1", "uuid": "~8.3.2", "winston": "~3.3.3", - "xlsx": "~0.18.5", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz", "xml2js": "~0.4.23", "zod": "^3.21.4" }, diff --git a/api/src/constants/notifications.ts b/api/src/constants/notifications.ts index 1630871046..7239936058 100644 --- a/api/src/constants/notifications.ts +++ b/api/src/constants/notifications.ts @@ -8,12 +8,3 @@ export const ACCESS_REQUEST_ADMIN_NOTIFICATION_EMAIL: IgcNotifyGenericMessage = main_body2: 'This is an automated message from the BioHub Species Inventory Management System', footer: '' }; - -// Email template for an access request submitter, informing them that their access request has been approved. -export const ACCESS_REQUEST_SUBMITTER_APPROVAL_EMAIL: IgcNotifyGenericMessage = { - subject: 'SIMS: Your request for access has been approved.', - header: 'Your request for access to the Species Inventory Management System has been approved.', - main_body1: `To access the site, `, - main_body2: 'This is an automated message from the BioHub Species Inventory Management System', - footer: '' -}; diff --git a/api/src/json-schema/transformation-schema.test.ts b/api/src/json-schema/transformation-schema.test.ts index e71d9a33c6..b4822b9afe 100644 --- a/api/src/json-schema/transformation-schema.test.ts +++ b/api/src/json-schema/transformation-schema.test.ts @@ -1,287 +1,12 @@ import Ajv from 'ajv'; import { expect } from 'chai'; import { describe } from 'mocha'; -import { submissionTransformationSchema } from './transformation-schema'; - -// Useful online json-schema validator: https://www.jsonschemavalidator.net/ - -describe('submissionTransformationSchema', () => { - const ajv = new Ajv(); - - it('is valid schema', () => { - expect(ajv.validateSchema(submissionTransformationSchema)).to.be.true; - }); -}); - -describe('example submission transformation schema', () => { - const exampleSchema = { - flatten: [ - { - fileName: 'Effort & Site Conditions', - uniqueId: ['Survey Area', 'Block ID/SU ID', 'Stratum'] - }, - { - fileName: 'Observations', - uniqueId: ['Waypoint'], - parent: { - fileName: 'Effort & Site Conditions', - uniqueId: ['Survey Area', 'Block ID/SU ID', 'Stratum'] - } - }, - { - fileName: 'UTM_LatLong', - uniqueId: ['Waypoint'], - parent: { - fileName: 'Observations', - uniqueId: ['Waypoint'] - } - }, - { - fileName: 'Strata Metadata', - uniqueId: ['Stratum'], - parent: { - fileName: 'Observations', - uniqueId: ['Stratum'] - } - } - ], - transform: [ - { - condition: { - if: { - columns: ['Lone Cows'], - not: true - } - }, - transformations: [ - { - fields: { - id: { - columns: ['Survey Area', 'Block ID/SU ID', 'Stratum', 'Waypoint'], - separator: ':' - }, - eventID: { - columns: ['Survey Area', 'Block ID/SU ID', 'Stratum'], - separator: ':' - }, - eventDate: { - columns: ['Date'] - }, - verbatimCoordinatesUTM: { - columns: ['UTM Zone', 'Easting', 'Northing'] - }, - verbatimCoordinatesLatLong: { - columns: ['lat', 'long'] - }, - occurrenceID: { - columns: ['Waypoint'], - unique: 'occ' - }, - individualCount: { - columns: ['Lone Cows'] - }, - vernacularName: { - columns: ['Species'] - }, - lifestage: { - value: 'Adult' - }, - sex: { - value: 'Female' - } - } - } - ] - }, - { - condition: { - if: { - columns: ['Cow W/1 calf'] - } - }, - transformations: [ - { - fields: { - id: { - columns: ['Survey Area', 'Block ID/SU ID', 'Stratum', 'Waypoint'], - separator: ':' - }, - eventID: { - columns: ['Survey Area', 'Block ID/SU ID', 'Stratum'], - separator: ':' - }, - eventDate: { - columns: ['Date'] - }, - verbatimCoordinatesUTM: { - columns: ['UTM Zone', 'Easting', 'Northing'] - }, - verbatimCoordinatesLatLong: { - columns: ['lat', 'long'] - }, - occurrenceID: { - columns: ['Waypoint'], - unique: 'occ' - }, - individualCount: { - columns: ['Cow W/1 calf'] - }, - vernacularName: { - columns: ['Species'] - }, - lifestage: { - value: 'Adult' - }, - sex: { - value: 'Female' - }, - resourceID: { - columns: ['Waypoint'], - unique: 'occ' - }, - relatedResourceID: { - value: '' - }, - relationshipOfResource: { - value: 'mother of' - } - } - }, - { - fields: { - id: { - columns: ['Survey Area', 'Block ID/SU ID', 'Stratum', 'Waypoint'], - separator: ':' - }, - eventID: { - columns: ['Survey Area', 'Block ID/SU ID', 'Stratum'], - separator: ':' - }, - eventDate: { - columns: ['Date'] - }, - verbatimCoordinatesUTM: { - columns: ['UTM Zone', 'Easting', 'Northing'] - }, - verbatimCoordinatesLatLong: { - columns: ['lat', 'long'] - }, - occurrenceID: { - columns: ['Waypoint'], - unique: 'occ' - }, - individualCount: { - value: 1 - }, - vernacularName: { - columns: ['Species'] - }, - lifestage: { - value: 'Yearling' - }, - sex: { - value: 'Unknown' - }, - resourceID: { - columns: ['Waypoint'], - unique: 'occ' - }, - relatedResourceID: { - value: '' - }, - relationshipOfResource: { - value: 'offspring of' - } - } - } - ], - postTransformations: [ - { - relationship: { - spreadColumn: 'individualCount', - uniqueIdColumn: 'occurrenceID' - } - } - ] - } - ], - parse: [ - { - fileName: 'event', - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['eventID'] }, target: 'eventID' }, - { source: { columns: ['eventDate'] }, target: 'eventDate' }, - { - source: { columns: ['verbatimCoordinatesUTM', 'verbatimCoordinatesLatLong'] }, - target: 'verbatimCoordinates' - } - ] - }, - { - fileName: 'occurrence', - conditionalFields: ['individualCount'], - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['occurrenceID'] }, target: 'occurrenceID' }, - { source: { columns: ['individualCount'] }, target: 'individualCount' }, - { source: { columns: ['vernacularName'] }, target: 'associatedTaxa' }, - { source: { columns: ['lifeStage'] }, target: 'lifeStage' }, - { source: { columns: ['sex'] }, target: 'sex' }, - { source: { value: 'Approved' }, target: 'Status' } - ] - }, - { - fileName: 'taxon', - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['occurrenceID'] }, target: 'occurrenceID' }, - { source: { columns: ['vernacularName'] }, target: 'vernacularName' } - ] - }, - { - fileName: 'resourcerelationship', - conditionalFields: ['resourceID'], - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['resourceID'] }, target: 'resourceID' }, - { source: { columns: ['relatedResourceID'] }, target: 'relatedResourceID' }, - { source: { columns: ['relationshipOfResource'] }, target: 'relationshipOfResource' } - ] - }, - { - fileName: 'measurementorfact', - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['eventID'] }, target: 'measurementID' }, - { source: { value: 'Habitat Description' }, target: 'measurementType' }, - { source: { columns: ['effort_habitat_description'] }, target: 'measurementValue' } - ] - }, - { - fileName: 'measurementorfact', - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['occurrenceID'] }, target: 'measurementID' }, - { source: { value: 'Stratum' }, target: 'measurementType' }, - { source: { columns: ['summary_stratum'] }, target: 'measurementValue' } - ] - }, - { - fileName: 'measurementorfact', - columns: [ - { source: { columns: ['id'] }, target: 'id' }, - { source: { columns: ['occurrenceID'] }, target: 'measurementID' }, - { source: { value: 'Activity' }, target: 'measurementType' }, - { source: { columns: ['observation_activity'] }, target: 'measurementValue' } - ] - } - ] - }; +import { transformationConfigJSONSchema } from './transformation-schema'; +describe('transformationJSONSchema', () => { const ajv = new Ajv(); - it('validates against submissionTransformationSchema', () => { - expect(ajv.validate(submissionTransformationSchema, exampleSchema), JSON.stringify(ajv.errors)).to.be.true; + it('is valid json schema', () => { + expect(ajv.validateSchema(transformationConfigJSONSchema)).to.be.true; }); }); diff --git a/api/src/json-schema/transformation-schema.ts b/api/src/json-schema/transformation-schema.ts index ec8ab8d67e..a232b092b4 100644 --- a/api/src/json-schema/transformation-schema.ts +++ b/api/src/json-schema/transformation-schema.ts @@ -1,220 +1,237 @@ -export const submissionTransformationSchema = { - description: 'Occurrence Submission Transformation Schema', +/** + * A JSON-Schema definition for a `TransformSchema`. + */ +export const transformationConfigJSONSchema = { + title: 'Transformation Schema', type: 'object', - required: ['flatten', 'transform', 'parse'], + required: ['templateMeta', 'map', 'dwcMeta'], properties: { - name: { - description: 'The name of the submission file', - type: 'string' + templateMeta: { + type: 'array', + description: + 'Defines the hierarchical structure of the template, which columns represent keys, and the parent-child relationship of the sheets. Used to de-normalize the template data.', + items: { + $ref: '#/$defs/TemplateMetaSchema' + } }, - description: { - description: 'The description of the submission file', - type: 'string' + map: { + type: 'array', + description: + 'Defines the mapping operations that are executed against each flattened row of the template. Used to transform the template data into its corresponding DWC representation.', + items: { + $ref: '#/$defs/MapSchema' + } }, - flatten: { + dwcMeta: { type: 'array', + description: 'Defines the unique keys for each DWC sheet. Used to normalize the DWC data.', items: { - type: 'object', - required: ['fileName', 'uniqueId'], - properties: { - fileName: { + $ref: '#/$defs/DwcMeta' + } + } + }, + $defs: { + TemplateMetaSchema: { + title: 'Sheet Schema', + type: 'object', + required: ['sheetName', 'primaryKey', 'parentKey', 'type', 'foreignKeys'], + properties: { + sheetName: { + type: 'string', + description: 'The name of the template sheet' + }, + primaryKey: { + type: 'array', + description: + 'An array of template column names which combined represent a unique key for rows in this sheet.', + items: { type: 'string' - }, - uniqueId: { - type: 'array', - items: { - type: 'string' - } - }, - parent: { + } + }, + parentKey: { + type: 'array', + description: + 'An array of template column names which combined represent a unique key for the parent row of rows in this sheet.', + items: { + type: 'string' + } + }, + type: { + type: 'string', + enum: ['root', 'leaf', ''] + }, + foreignKeys: { + type: 'array', + items: { type: 'object', + description: 'An array of child template sheet objects.', properties: { - fileName: { - type: 'string' + sheetName: { + type: 'string', + description: 'The name of a child template sheet' }, - uniqueId: { + primaryKey: { type: 'array', + description: + 'An array of template column names which combined represent a unique key for child rows of this sheet.', items: { - type: 'string' + type: 'string', + description: 'A template column name.' } } }, additionalProperties: false } - }, - additionalProperties: false - } + } + }, + additionalProperties: false }, - transform: { - type: 'array', - items: { - type: 'object', - required: ['transformations'], - properties: { - condition: { - type: 'object', - properties: { - if: { + MapSchema: { + title: 'Map Schema', + type: 'object', + required: ['sheetName', 'fields'], + properties: { + sheetName: { + type: 'string', + description: 'The name of the DWC sheet' + }, + condition: { + type: 'object', + description: + 'Defines a condition, which contains one or more checks that must be met in order to proceed processing this `MapSchema` item.', + properties: { + type: { + type: 'string', + enum: ['and', 'or'] + }, + checks: { + type: 'array', + items: { type: 'object', - required: ['columns'], properties: { - columns: { - type: 'array', - items: { - type: 'string' - } - }, - not: { - type: 'boolean' + ifNotEmpty: { + type: 'string' } }, additionalProperties: false } - }, - additionalProperties: false - }, - transformations: { - type: 'array', - items: { - type: 'object', - required: ['fields'], - properties: { - fields: { - type: 'object', - patternProperties: { - '^.*$': { - type: 'object', - oneOf: [ - { - type: 'object', - required: ['columns'], - properties: { - columns: { - type: 'array', - items: { - type: 'string' - } - }, - separator: { - type: 'string' - }, - unique: { - type: 'string' - } - }, - additionalProperties: false - }, - { - type: 'object', - required: ['value'], - properties: { - value: { - type: ['string', 'number'] - } - }, - additionalProperties: false - } - ] - } - }, - additionalProperties: false - } - }, - additionalProperties: false } }, - postTransformations: { - type: 'array', - items: { - anyOf: [ - { - type: 'object', - properties: { - relationship: { - type: 'object', - properties: { - spreadColumn: { - type: 'string' - }, - uniqueIdColumn: { - type: 'string' - } - }, - additionalProperties: false - } - }, - additionalProperties: false + additionalProperties: false + }, + fields: { + type: 'array', + items: { + type: 'object', + properties: { + columnName: { + type: 'string' + }, + columnValue: { + type: 'array', + items: { + $ref: '#/$defs/MapColumnValueSchema' } - ] - } + } + }, + additionalProperties: false } }, - additionalProperties: false - } + add: { + type: 'array', + items: { + $ref: '#/$defs/MapSchema' + } + } + }, + additionalProperties: false }, - parse: { - type: 'array', - items: { - type: 'object', - required: ['fileName', 'columns'], - properties: { - fileName: { + MapColumnValueSchema: { + title: 'MapColumnValueSchema', + type: 'object', + oneOf: [{ required: ['paths'] }, { required: ['static'] }], + properties: { + paths: { + type: 'array', + items: { type: 'string' + } + }, + static: { + type: 'string' + }, + join: { + type: 'string', + description: 'A string used when concatenating columns to create keys.', + default: ':' + }, + postfix: { + type: 'object', + properties: { + paths: { + type: 'array', + items: { + type: 'string' + } + }, + static: { + type: 'string' + } }, - columns: { - type: 'array', - items: { - type: 'object', - properties: { - source: { - oneOf: [ - { - type: 'object', - required: ['columns'], - properties: { - columns: { - type: 'array', - items: { - type: 'string' - } - }, - separator: { - type: 'string' - }, - unique: { - type: 'string' - } - }, - additionalProperties: false - }, - { - type: 'object', - required: ['value'], - properties: { - value: { - type: ['string', 'number'] - } - }, - additionalProperties: false - } - ] - }, - target: { - type: 'string' + additionalProperties: false + }, + condition: { + type: 'object', + description: + 'Defines a condition, which contains one or more checks that must be met in order to proceed processing this `MapColumnValueSchema` item.', + properties: { + type: { + type: 'string', + enum: ['and', 'or'] + }, + checks: { + type: 'array', + items: { + type: 'object', + properties: { + ifNotEmpty: { + type: 'string' + } } - }, - additionalProperties: false + } } }, - conditionalFields: { - type: 'array', - items: { - type: 'string' - } + additionalProperties: false + }, + add: { + type: 'array', + description: + 'An array of additional schemas to add to the process queue. Used to create additional records from within the context of the current schema being processed.', + items: { + $ref: '#/$defs/MapSchema' } + } + }, + additionalProperties: false + }, + DwcMeta: { + title: 'Dwc Schema', + type: 'object', + properties: { + sheetName: { + type: 'string', + description: 'The name of the DWC sheet' }, - additionalProperties: false - } + primaryKey: { + type: 'array', + description: 'An array of DWC column names which combined represent a unique key for rows in this sheet.', + items: { + type: 'string', + description: 'A DWC column name.' + } + } + }, + additionalProperties: false } }, additionalProperties: false diff --git a/api/src/json-schema/validation-schema.test.ts b/api/src/json-schema/validation-schema.test.ts index b4aab63c28..0e607147ad 100644 --- a/api/src/json-schema/validation-schema.test.ts +++ b/api/src/json-schema/validation-schema.test.ts @@ -1,241 +1,12 @@ import Ajv from 'ajv'; import { expect } from 'chai'; import { describe } from 'mocha'; -import { submissionValidationSchema } from './validation-schema'; +import { validationConfigJSONSchema } from './validation-schema'; -// Useful online json-schema validator: https://www.jsonschemavalidator.net/ - -describe('submissionValidationSchema', () => { +describe('validationConfigJSONSchema', () => { const ajv = new Ajv(); it('is valid schema', () => { - expect(ajv.validateSchema(submissionValidationSchema)).to.be.true; - }); -}); - -describe('example submission validation schema', () => { - const exampleSchema2 = { - name: 'Example Submission Validation Schema', - description: 'An example submission validation schema.', - files: [ - { - name: 'event', - description: 'The Event file for this submission. Should contain information about events.', - columns: [ - { - name: 'eventID', - validations: [] - }, - { - name: 'parentEventID', - validations: [] - }, - { - name: 'samplingProtocol', - validations: [] - }, - { - name: 'geodeticDatum', - validations: [] - }, - { - name: 'verbatimCoordinates', - validations: [ - { - column_format_validator: { - reg_exp: '^9N \\d{6} \\d{7}$', - expected_format: '9N 12345 123456' - } - } - ] - }, - { - name: 'verbatimElevation', - validations: [] - }, - { - name: 'coordinateUncertaintyInMeters', - validations: [] - }, - { - name: 'coordinatePrecision', - validations: [] - }, - { - name: 'verbatimLocality', - validations: [] - }, - { - name: 'locationRemarks', - validations: [] - } - ], - validations: [ - { - file_required_columns_validator: { - required_columns: [ - 'eventID', - 'parentEventID', - 'eventDate', - 'samplingProtocol', - 'geodeticDatum', - 'verbatimCoordinates', - 'verbatimElevation', - 'coordinateUncertaintyInMeters', - 'coordinatePrecision', - 'verbatimLocality', - 'locationRemarks' - ] - } - } - ] - }, - { - name: 'occurrence', - description: 'The Occurrence file for this submission. Should contain information about occurrences.', - columns: [ - { - name: 'sex', - desciption: 'The sex of the observed taxon.', - validations: [ - { - column_code_validator: { - name: { - type: 'string' - }, - description: { - type: 'string' - }, - allowed_code_values: [ - { - name: 'male', - description: 'A male' - }, - { - name: 'female', - description: 'A female' - }, - { - name: 'unclassified', - description: 'An unclassified sex' - } - ] - } - } - ] - }, - { - name: 'lifeStage', - desciption: 'The life stage of the observed taxon.', - validations: [ - { - column_code_validator: { - name: { - type: 'string' - }, - description: { - type: 'string' - }, - allowed_code_values: [ - { - name: 'adult', - description: 'An adult' - }, - { - name: 'juvenile', - description: 'A child' - } - ] - } - } - ] - }, - { - name: 'individualCount', - desciption: 'The occurrence count.', - validations: [ - { - column_format_validator: { - name: '', - description: '', - reg_exp: '^\\d.*$', - expected_format: 'A positive integer' - } - } - ] - } - ], - validations: [ - { - file_required_columns_validator: { - name: '', - description: '', - required_columns: [ - 'eventID', - 'occurrenceID', - 'basisOfRecord', - 'type', - 'associatedTaxa', - 'sex', - 'lifeStage', - 'individualCount', - 'identifiedBy', - 'organismQuantity', - 'organismQuantityType' - ] - } - }, - { - file_recommended_columns_validator: { - name: '', - description: '', - recommended_columns: ['occurrenceRemarks'] - } - } - ] - }, - { - name: 'measurementorfact', - description: - 'The Measurement Or Fact file for this submission. Should contain information about measurements and facts.', - columns: [], - validations: [] - }, - { - name: 'resourcerelationship', - description: - 'The Resource Relationship file for this submission. Should contain information about resource relationships.', - columns: [], - validations: [] - }, - { - name: 'taxon', - description: 'The Taxon file for this submission. Should contain information about taxonomy.', - columns: [], - validations: [] - } - ], - validations: [ - { - submission_required_files_validator: { - name: '', - description: '', - required_files: ['event', 'occurrence', 'taxon'] - } - }, - { - mimetype_validator: { - name: '', - description: '', - reg_exps: ['application/zip', 'application/x-zip-compressed', 'application/x-rar-compressed'] - } - } - ] - }; - - const ajv = new Ajv(); - - it('validates against submissionValidationSchema', () => { - expect(ajv.validate(submissionValidationSchema, exampleSchema2), JSON.stringify(ajv.errors)).to.be.true; + expect(ajv.validateSchema(validationConfigJSONSchema)).to.be.true; }); }); diff --git a/api/src/json-schema/validation-schema.ts b/api/src/json-schema/validation-schema.ts index 50ec0a77ff..0441b5afea 100644 --- a/api/src/json-schema/validation-schema.ts +++ b/api/src/json-schema/validation-schema.ts @@ -1,4 +1,4 @@ -export const submissionValidationSchema = { +export const validationConfigJSONSchema = { description: 'Occurrence Submission Validation Schema', type: 'object', properties: { diff --git a/api/src/utils/keycloak-utils.ts b/api/src/utils/keycloak-utils.ts index b7869932b2..ffe74c9af3 100644 --- a/api/src/utils/keycloak-utils.ts +++ b/api/src/utils/keycloak-utils.ts @@ -6,10 +6,10 @@ import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; * * @example getUserGuid({ preferred_username: 'aaabbaaa@idir' }) // => 'aaabbaaa' * - * @param {object} keycloakToken + * @param {Record} keycloakToken * @return {*} {(string | null)} */ -export const getUserGuid = (keycloakToken: object): string | null => { +export const getUserGuid = (keycloakToken: Record): string | null => { const userIdentifier = keycloakToken?.['preferred_username']?.split('@')?.[0]; if (!userIdentifier) { @@ -27,10 +27,10 @@ export const getUserGuid = (keycloakToken: object): string | null => { * @example getUserIdentitySource({ ...token, identity_provider: 'bceidbasic' }) => SYSTEM_IDENTITY_SOURCE.BCEID_BASIC * @example getUserIdentitySource({ preferred_username: 'aaaa@idir' }) => SYSTEM_IDENTITY_SOURCE.IDIR * - * @param {object} keycloakToken + * @param {Record} keycloakToken * @return {*} {SYSTEM_IDENTITY_SOURCE} */ -export const getUserIdentitySource = (keycloakToken: object): SYSTEM_IDENTITY_SOURCE => { +export const getUserIdentitySource = (keycloakToken: Record): SYSTEM_IDENTITY_SOURCE => { const userIdentitySource: string = keycloakToken?.['identity_provider'] || keycloakToken?.['preferred_username']?.split('@')?.[1]; @@ -75,10 +75,10 @@ export const coerceUserIdentitySource = (userIdentitySource: string | null): SYS * * @example getUserIdentifier({ ....token, bceid_username: 'jsmith@idir' }) => 'jsmith' * - * @param {object} keycloakToken + * @param {Record} keycloakToken * @return {*} {(string | null)} */ -export const getUserIdentifier = (keycloakToken: object): string | null => { +export const getUserIdentifier = (keycloakToken: Record): string | null => { const userIdentifier = keycloakToken?.['idir_username'] || keycloakToken?.['bceid_username'] || keycloakToken?.['sims_system_username']; diff --git a/api/src/utils/media/csv/csv-file.ts b/api/src/utils/media/csv/csv-file.ts index 2270a55f67..e5ff6c5438 100644 --- a/api/src/utils/media/csv/csv-file.ts +++ b/api/src/utils/media/csv/csv-file.ts @@ -2,7 +2,7 @@ import xlsx from 'xlsx'; import { SUBMISSION_MESSAGE_TYPE } from '../../../constants/status'; import { safeToLowerCase, safeTrim } from '../../string-utils'; import { IMediaState, MediaValidation } from '../media-file'; -import { getCellValue, getWorksheetRange, replaceCellDates, trimCellWhitespace } from '../xlsx/xlsx-utils'; +import { getWorksheetRange, replaceCellDates, trimCellWhitespace } from '../xlsx/xlsx-utils'; export type CSVWorksheets = { [name: string]: CSVWorksheet }; export type WorkBookValidators = { [name: string]: CSVValidation }; @@ -54,7 +54,7 @@ export class CSVWorksheet { _headers: string[]; _headersLowerCase: string[]; _rows: string[][]; - _rowObjects: object[]; + _rowObjects: Record[]; csvValidation: CSVValidation; @@ -76,6 +76,8 @@ export class CSVWorksheet { * * Note: This is always the first row (index 0) * + * Note: Trims all header values. + * * @return {*} {string[]} * @memberof CSVWorksheet */ @@ -126,6 +128,8 @@ export class CSVWorksheet { * * Note: This does not include the first row (header row). * + * Note: Trims all cell values. + * * @return {*} {string[][]} * @memberof CSVWorksheet */ @@ -156,7 +160,7 @@ export class CSVWorksheet { continue; } - row[j] = getCellValue(trimCellWhitespace(replaceCellDates(cell))); + row[j] = trimCellWhitespace(replaceCellDates(cell)).v; rowHasValues = true; } @@ -172,7 +176,13 @@ export class CSVWorksheet { return this._rows; } - getRowObjects(): object[] { + /** + * Return an array of row objects. + * + * @return {*} {Record[]} + * @memberof CSVWorksheet + */ + getRowObjects(): Record[] { if (!this.worksheet) { return []; } @@ -184,7 +194,7 @@ export class CSVWorksheet { } if (!this._rowObjects.length) { - const rowObjectsArray: object[] = []; + const rowObjectsArray: Record[] = []; const rows = this.getRows(); const headers = this.getHeaders(); @@ -204,24 +214,6 @@ export class CSVWorksheet { return this._rowObjects; } - getCell(headerName: string, rowIndex: number) { - const headerIndex = this.getHeaderIndex(headerName); - - if (headerIndex < 0) { - return undefined; - } - - const rows = this.getRows(); - - const row = rows?.[rowIndex]; - - if (!row || !row.length) { - return undefined; - } - - return row[headerIndex]; - } - /** * Runs all of the given validators on the worksheet, whereby the results of all validations * are stored in `this.csvValidation`. diff --git a/api/src/utils/media/csv/validation/csv-header-validator.test.ts b/api/src/utils/media/csv/validation/csv-header-validator.test.ts index baeec4f489..f1999d9b21 100644 --- a/api/src/utils/media/csv/validation/csv-header-validator.test.ts +++ b/api/src/utils/media/csv/validation/csv-header-validator.test.ts @@ -38,7 +38,7 @@ describe('getDuplicateHeadersValidator', () => { it('adds errors for each header that is a duplicate', () => { const validator = getDuplicateHeadersValidator(); - const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([['Header1', 'Header2', 'Header1', 'Header3', 'Header2', 'Header4']]); + const xlsxWorkSheet = xlsx.utils.aoa_to_sheet([['HEADER1', 'Header2', 'header1', 'Header3', 'Header2', 'Header4']]); const csvWorkSheet = new CSVWorksheet('Sheet1', xlsxWorkSheet); @@ -47,7 +47,7 @@ describe('getDuplicateHeadersValidator', () => { expect(csvWorkSheet.csvValidation.headerErrors).to.eql([ { errorCode: SUBMISSION_MESSAGE_TYPE.DUPLICATE_HEADER, - col: 'Header1', + col: 'header1', message: 'Duplicate Header' }, { diff --git a/api/src/utils/media/csv/validation/csv-header-validator.ts b/api/src/utils/media/csv/validation/csv-header-validator.ts index 504977d211..0eb3ce0937 100644 --- a/api/src/utils/media/csv/validation/csv-header-validator.ts +++ b/api/src/utils/media/csv/validation/csv-header-validator.ts @@ -1,10 +1,13 @@ import { SUBMISSION_MESSAGE_TYPE } from '../../../../constants/status'; -import { safeToLowerCase, safeTrim } from '../../../string-utils'; +import { safeToLowerCase } from '../../../string-utils'; import { CSVValidator } from '../csv-file'; /** * Adds an error for each header that is not unique. * + * Note: This check is case insensitive. In order to avoid confusion, its best not to allow headers with the same + * spelling and different casing. + * * @return {*} {CSVValidator} */ export const getDuplicateHeadersValidator = (): CSVValidator => { @@ -18,7 +21,8 @@ export const getDuplicateHeadersValidator = (): CSVValidator => { const seenHeaders: string[] = []; for (const header of headers) { - if (seenHeaders.includes(header)) { + if (seenHeaders.includes(safeToLowerCase(header))) { + // This header was already seen, therefore there is a duplicate header, add an error message csvWorksheet.csvValidation.addHeaderErrors([ { errorCode: SUBMISSION_MESSAGE_TYPE.DUPLICATE_HEADER, @@ -27,7 +31,7 @@ export const getDuplicateHeadersValidator = (): CSVValidator => { } ]); } else { - seenHeaders.push(header); + seenHeaders.push(safeToLowerCase(header)); } } @@ -63,7 +67,9 @@ export const hasRequiredHeadersValidator = (config?: FileRequiredHeaderValidator const headersLowerCase = csvWorksheet.getHeadersLowerCase(); for (const requiredHeader of config.file_required_columns_validator.required_columns) { + // For each required header, check if there exists a matching header if (!headersLowerCase.includes(safeToLowerCase(requiredHeader))) { + // The array of headers does not include this required header, add an error csvWorksheet.csvValidation.addHeaderErrors([ { errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_REQUIRED_HEADER, @@ -87,7 +93,7 @@ export type FileRecommendedHeaderValidatorConfig = { }; /** - * For a specified set of recommended columns, adds a warning if the column is not present in the csv. + * For a specified set of recommended columns, adds an error if the column is not present in the csv. * * @param {FileRecommendedHeaderValidatorConfig} [config] * @return {*} {CSVValidator} @@ -99,12 +105,14 @@ export const hasRecommendedHeadersValidator = (config?: FileRecommendedHeaderVal } if (!config.file_recommended_columns_validator.recommended_columns.length) { + // No recommended columns return csvWorksheet; } const headersLowerCase = csvWorksheet.getHeadersLowerCase(); if (!headersLowerCase?.length) { + // There are no headers at all, add warnings for all recommended headers csvWorksheet.csvValidation.addHeaderWarnings( config.file_recommended_columns_validator.recommended_columns.map((recommendedHeader) => { return { @@ -119,7 +127,9 @@ export const hasRecommendedHeadersValidator = (config?: FileRecommendedHeaderVal } for (const recommendedHeader of config.file_recommended_columns_validator.recommended_columns) { + // For each recommended header, check if there exists a matching header if (!headersLowerCase.includes(safeToLowerCase(recommendedHeader))) { + // The array of headers does not include this recommended header, add an error csvWorksheet.csvValidation.addHeaderWarnings([ { errorCode: SUBMISSION_MESSAGE_TYPE.MISSING_RECOMMENDED_HEADER, @@ -143,7 +153,7 @@ export type FileValidHeadersValidatorConfig = { }; /** - * Adds a warning if a column in the csv does not match a value in a specified set of valid columns. + * Adds a error if a column in the csv does not match a value in a specified set of valid columns. * * @param {FileValidHeadersValidatorConfig} [config] * @return {*} {CSVValidator} @@ -155,17 +165,16 @@ export const getValidHeadersValidator = (config?: FileValidHeadersValidatorConfi } if (!config.file_valid_columns_validator.valid_columns.length) { + // No valid columns to compare against return csvWorksheet; } const headers = csvWorksheet.getHeaders(); for (const header of headers) { - if ( - !config.file_valid_columns_validator.valid_columns - .map(safeToLowerCase) - .includes(safeToLowerCase(safeTrim(header))) - ) { + // For each header, check if it exists in the array of valid column headers + if (!config.file_valid_columns_validator.valid_columns.map(safeToLowerCase).includes(safeToLowerCase(header))) { + // This header does not exist in the array of valid headers, add an error csvWorksheet.csvValidation.addHeaderWarnings([ { errorCode: SUBMISSION_MESSAGE_TYPE.UNKNOWN_HEADER, diff --git a/api/src/utils/media/csv/validation/csv-row-validator.ts b/api/src/utils/media/csv/validation/csv-row-validator.ts index e44fdbfa2f..28da9218b4 100644 --- a/api/src/utils/media/csv/validation/csv-row-validator.ts +++ b/api/src/utils/media/csv/validation/csv-row-validator.ts @@ -24,9 +24,10 @@ export const getRequiredFieldsValidator = (config?: RequiredFieldsValidatorConfi rows.forEach((row, rowIndex) => { const columnIndex = headersLowerCase.indexOf(safeToLowerCase(config.columnName)); - // if column does not exist, return if (columnIndex < 0) { - return csvWorksheet; + // If column does not exist, return + // It is the job of a separate validation rule to ensure required columns exist + return; } const rowValueForColumn = row[columnIndex]; @@ -83,15 +84,17 @@ export const getCodeValueFieldsValidator = (config?: ColumnCodeValidatorConfig): rows.forEach((row, rowIndex) => { const columnIndex = headersLowerCase.indexOf(safeToLowerCase(config.columnName)); - // if column does not exist, return if (columnIndex < 0) { - return csvWorksheet; + // If column does not exist, return + // It is the job of a separate validation rule to ensure required columns exist + return; } const rowValueForColumn = row[columnIndex]; if (rowValueForColumn === undefined || rowValueForColumn === null || rowValueForColumn === '') { - // cell is empty, use the getRequiredFieldsValidator to assert required fields - return csvWorksheet; + // If cell is empty, return + // It is the job of a separate validation rule to ensure non-empty (required) cells + return; } // compare allowed code values as lowercase strings @@ -149,16 +152,18 @@ export const getValidRangeFieldsValidator = (config?: ColumnRangeValidatorConfig rows.forEach((row, rowIndex) => { const columnIndex = headersLowerCase.indexOf(safeToLowerCase(config.columnName)); - // if column does not exist, return if (columnIndex < 0) { - return csvWorksheet; + // If column does not exist, return + // It is the job of a separate validation rule to ensure required columns exist + return; } const rowValueForColumn = Number(row[columnIndex]); if (rowValueForColumn === undefined || rowValueForColumn === null) { - // cell is empty, use the getRequiredFieldsValidator to assert required fields - return csvWorksheet; + // If cell is empty, return + // It is the job of a separate validation rule to ensure non-empty (required) cells + return; } if (isNaN(rowValueForColumn) && typeof row[columnIndex] === 'string') { @@ -250,13 +255,16 @@ export const getNumericFieldsValidator = (config?: ColumnNumericValidatorConfig) rows.forEach((row, rowIndex) => { const columnIndex = headersLowerCase.indexOf(safeToLowerCase(config.columnName)); - // if column does not exist, return if (columnIndex < 0) { - return csvWorksheet; + // If column does not exist, return + // It is the job of a separate validation rule to ensure required columns exist + return; } - if (row[columnIndex] === undefined || row[columnIndex] === null) { - return csvWorksheet; + if (row[columnIndex] === undefined || row[columnIndex] === null || row[columnIndex] === '') { + // If cell is empty, return + // It is the job of a separate validation rule to ensure non-empty (required) cells + return; } const rowValueForColumn = Number(row[columnIndex]); @@ -313,15 +321,17 @@ export const getValidFormatFieldsValidator = (config?: ColumnFormatValidatorConf rows.forEach((row, rowIndex) => { const columnIndex = headersLowerCase.indexOf(safeToLowerCase(config.columnName)); - // if column does not exist, return if (columnIndex < 0) { - return csvWorksheet; + // If column does not exist, return + // It is the job of a separate validation rule to ensure required columns exist } const rowValueForColumn = row[columnIndex]; if (rowValueForColumn === undefined || rowValueForColumn === null) { - return csvWorksheet; + // If cell is empty, return + // It is the job of a separate validation rule to ensure non-empty (required) cells + return; } const regexFlags = config.column_format_validator.reg_exp_flags ?? ''; diff --git a/api/src/utils/media/media-file.ts b/api/src/utils/media/media-file.ts index 64e6b96c14..2ab548620a 100644 --- a/api/src/utils/media/media-file.ts +++ b/api/src/utils/media/media-file.ts @@ -33,11 +33,11 @@ export class MediaFile implements IMediaFile { * @memberof MediaFile */ get name(): string { - const lastPeriodindex = this.fileName.lastIndexOf('.'); + const lastPeriodIndex = this.fileName.lastIndexOf('.'); - if (lastPeriodindex >= 0) { + if (lastPeriodIndex >= 0) { // strip out the file extension, if it exists - return this.fileName.substring(0, lastPeriodindex); + return this.fileName.substring(0, lastPeriodIndex); } else { return this.fileName; } diff --git a/api/src/utils/media/validation/file-type-and-content-validator.ts b/api/src/utils/media/validation/file-type-and-content-validator.ts index cbceca2f9d..1b48f6c3ef 100644 --- a/api/src/utils/media/validation/file-type-and-content-validator.ts +++ b/api/src/utils/media/validation/file-type-and-content-validator.ts @@ -3,6 +3,11 @@ import { DWCArchive, DWCArchiveValidator } from '../dwc/dwc-archive-file'; import { MediaValidator } from '../media-file'; import { XLSXCSV, XLSXCSVValidator } from '../xlsx/xlsx-file'; +/** + * Return a validator function that checks if the file is empty. + * + * @return {*} {MediaValidator} + */ export const getFileEmptyValidator = (): MediaValidator => { return (mediaFile) => { if (!mediaFile.buffer || !mediaFile.buffer.byteLength) { @@ -21,6 +26,12 @@ export type MimetypeValidatorConfig = { }; }; +/** + * Return a validator function that checks the mimetype of the file. + * + * @param {MimetypeValidatorConfig} [config] + * @return {*} {(DWCArchiveValidator | XLSXCSVValidator)} + */ export const getFileMimeTypeValidator = (config?: MimetypeValidatorConfig): DWCArchiveValidator | XLSXCSVValidator => { return (file: any) => { if (!config) { @@ -55,6 +66,12 @@ export type SubmissionRequiredFilesValidatorConfig = { }; }; +/** + * Return a validator function that checks that the file contains all required files. + * + * @param {SubmissionRequiredFilesValidatorConfig} [config] + * @return {*} {(DWCArchiveValidator | XLSXCSVValidator)} + */ export const getRequiredFilesValidator = ( config?: SubmissionRequiredFilesValidatorConfig ): DWCArchiveValidator | XLSXCSVValidator => { @@ -79,6 +96,13 @@ export const getRequiredFilesValidator = ( }; }; +/** + * Check that the DWCArchive contains all required files. + * + * @param {DWCArchive} dwcArchive + * @param {SubmissionRequiredFilesValidatorConfig} config + * @return {*} + */ const checkRequiredFieldsInDWCArchive = (dwcArchive: DWCArchive, config: SubmissionRequiredFilesValidatorConfig) => { // If there are no files in the archive, then add errors for all required files if (!dwcArchive.rawFile.mediaFiles || !dwcArchive.rawFile.mediaFiles.length) { @@ -100,6 +124,13 @@ const checkRequiredFieldsInDWCArchive = (dwcArchive: DWCArchive, config: Submiss }); }; +/** + * Check that the XLSX workbook contains all required sheets. + * + * @param {XLSXCSV} xlsxCsv + * @param {SubmissionRequiredFilesValidatorConfig} config + * @return {*} + */ const checkRequiredFieldsInXLSXCSV = (xlsxCsv: XLSXCSV, config: SubmissionRequiredFilesValidatorConfig) => { // If there are no sheets in the excel file, then add errors for all required sheets diff --git a/api/src/utils/media/validation/validation-schema-parser.ts b/api/src/utils/media/validation/validation-schema-parser.ts index 3e252de6fa..5715ad2685 100644 --- a/api/src/utils/media/validation/validation-schema-parser.ts +++ b/api/src/utils/media/validation/validation-schema-parser.ts @@ -88,9 +88,9 @@ export const ValidationRulesRegistry = { }; export class ValidationSchemaParser { - validationSchema: object; + validationSchema: Record; - constructor(validationSchema: string | object) { + constructor(validationSchema: string | Record) { if (typeof validationSchema === 'string') { this.validationSchema = this.parseJson(validationSchema); } else { @@ -229,15 +229,15 @@ export class ValidationSchemaParser { return rules; } - getSubmissionValidationSchemas(): object[] { + getSubmissionValidationSchemas(): Record[] { return JSONPath({ json: this.validationSchema, path: this.getSubmissionValidationsJsonPath() })?.[0] || []; } - getWorkbookValidationSchemas(): object[] { + getWorkbookValidationSchemas(): Record[] { return JSONPath({ json: this.validationSchema, path: this.getWorkbookValidationsJsonPath() })?.[0] || []; } - getFileValidationSchemas(fileName: string): object[] { + getFileValidationSchemas(fileName: string): Record[] { let validationSchemas = JSONPath({ json: this.validationSchema, path: this.getFileValidationsJsonPath(fileName) })?.[0] || []; @@ -247,11 +247,11 @@ export class ValidationSchemaParser { return validationSchemas; } - getDefaultFileValidationSchemas(): object[] { + getDefaultFileValidationSchemas(): Record[] { return JSONPath({ json: this.validationSchema, path: this.getDefaultFileValidationsJsonPath() })?.[0] || []; } - getColumnValidationSchemas(fileName: string, columnName: string): object[] { + getColumnValidationSchemas(fileName: string, columnName: string): Record[] { const filevalidationSchemas = JSONPath({ json: this.validationSchema, path: this.getFileValidationsJsonPath(fileName) })?.[0] || []; @@ -268,7 +268,7 @@ export class ValidationSchemaParser { return columnValidationSchemas; } - getDefaultColumnValidationSchemas(columnName: string): object[] { + getDefaultColumnValidationSchemas(columnName: string): Record[] { return ( JSONPath({ json: this.validationSchema, path: this.getDefaultColumnValidationsJsonpath(columnName) })?.[0] || [] ); @@ -320,7 +320,7 @@ export class ValidationSchemaParser { return `$.defaultFile.columns[?(@.name == "${columnName}")].validations`; } - parseJson(json: any): object { + parseJson(json: any): Record { try { return JSON.parse(json); } catch { diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transform-json-path-queries.ts b/api/src/utils/media/xlsx/transformation/xlsx-transform-json-path-queries.ts deleted file mode 100644 index 677fd2c974..0000000000 --- a/api/src/utils/media/xlsx/transformation/xlsx-transform-json-path-queries.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - DWCColumnName, - JSONPathString, - MapFieldSchema, - TemplateColumnName, - TemplateSheetName -} from './xlsx-transform-schema-parser'; - -/** - * Get a json path query string that fetches one or more values within an element where `_name=`. - * - * @param {TemplateSheetName} templateSheetName - * @param {TemplateColumnName[]} templateColumnNames - * @return {*} {JSONPathString} - */ -export const getValuesByName = ( - templateSheetName: TemplateSheetName, - templateColumnNames: TemplateColumnName[] -): JSONPathString => `$..[?(@._name === '${templateSheetName}')]..['${templateColumnNames.join(',')}']`; - -/** - * Create a DWC map `MapFieldSchema` object from a static value. - * - * @param {DWCColumnName} dwcColumnName - * @param {string} staticValue - * @return {*} {MapFieldSchema} - */ -export const createValueField = (dwcColumnName: DWCColumnName, staticValue: string): MapFieldSchema => { - return { - columnName: dwcColumnName, - columnValue: [ - { - static: staticValue - } - ] - }; -}; - -/** - * Create a DWC map `MapFieldSchema` object from a single JSONPathString. - * - * @param {DWCColumnName} dwcColumnName - * @param {TemplateSheetName} templateSheetName - * @param {TemplateColumnName[]} templateSheetColumns - * @return {*} {MapFieldSchema} - */ -export const createPathField = ( - dwcColumnName: DWCColumnName, - templateSheetName: TemplateSheetName, - templateSheetColumns: TemplateColumnName[] -): MapFieldSchema => { - return { - columnName: dwcColumnName, - columnValue: [ - { - paths: templateSheetColumns.map((item) => getValuesByName(templateSheetName, [item])) - } - ] - }; -}; diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transform-schema-parser.ts b/api/src/utils/media/xlsx/transformation/xlsx-transform-schema-parser.ts index c1af9e0d55..ed38ecc7e6 100644 --- a/api/src/utils/media/xlsx/transformation/xlsx-transform-schema-parser.ts +++ b/api/src/utils/media/xlsx/transformation/xlsx-transform-schema-parser.ts @@ -7,14 +7,35 @@ export type DWCSheetName = string; export type DWCColumnName = string; export type JSONPathString = string; - export type ConditionSchema = { type: 'and' | 'or'; + /** + * One or more checks. + * + * @type {IfNotEmptyCheck[]} + */ checks: IfNotEmptyCheck[]; + /** + * Logically `not` the result of the condition. + * + * @type {boolean} + */ + not?: boolean; }; export type IfNotEmptyCheck = { + /** + * `true` if the `JSONPathString` value, when processed, results in a non-empty non-null value. + * + * @type {JSONPathString} + */ ifNotEmpty: JSONPathString; + /** + * Logically `not` the result of this check. + * + * @type {boolean} + */ + not?: boolean; }; export type TemplateMetaForeignKeySchema = { @@ -22,6 +43,14 @@ export type TemplateMetaForeignKeySchema = { primaryKey: string[]; }; +/** + * - `root`: indicates the node is the top level root node (only 1 node can be type `root`). + * - `leaf`: indicates the node is a leaf node. A node that is type `leaf` will prevent the hierarchical + * parsing from progressing further, into this nodes children (if any). + * - ``: indicates a regular node, with no particular special considerations. + */ +export type TemplateMetaSchemaType = 'root' | 'leaf' | ''; + export type TemplateMetaSchema = { /** * The name of the template sheet. @@ -35,8 +64,23 @@ export type TemplateMetaSchema = { * @type {string[]} */ primaryKey: string[]; + /** + * An array of json path query strings. + * + * @type {string[]} + */ parentKey: string[]; - type: 'root' | 'leaf' | ''; + /** + * The type of the node. + * + * @type {TemplateMetaSchemaType} + */ + type: TemplateMetaSchemaType; + /** + * An array of linked child nodes. + * + * @type {TemplateMetaForeignKeySchema[]} + */ foreignKeys: TemplateMetaForeignKeySchema[]; }; @@ -47,8 +91,9 @@ export type MapColumnValuePostfixSchema = { * If multiple query strings are provided, they will be fetched in order, and the first one that returns a non-empty * value will be used. * - * A single query string may return one value, or an array of values. + * A single JSON path string may return one value, or an array of values. * + * @see {@link RowObject} for special known fields that may be used in these `JSONPathString` values. * @type {JSONPathString[]} */ paths?: JSONPathString[]; @@ -69,16 +114,17 @@ export type MapColumnValuePostfixSchema = { export type MapColumnValueSchema = { /** - * An array of json path query strings. + * An array of json path strings. * - * If multiple query strings are provided, they will be fetched in order, and the first one that returns a non-empty - * value will be used. + * If multiple JSON path strings are provided, they will be fetched in order, and the first one that returns a + * non-empty value will be used. * - * A single query string may return one value, or an array of values. + * A single JSON path string may return one value, or an array of values. * * If an array of values is returned, they will be joined using the specified `join` string. * If `join` string is not specified, a colon `:` will be used as the default `join` string. * + * @see {@link RowObject} for special known fields that may be used in these `JSONPathString` values. * @type {JSONPathString[]} */ paths?: JSONPathString[]; diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transform-schema.test.ts b/api/src/utils/media/xlsx/transformation/xlsx-transform-schema.test.ts deleted file mode 100644 index 9ba0e7ee5a..0000000000 --- a/api/src/utils/media/xlsx/transformation/xlsx-transform-schema.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Ajv from 'ajv'; -import { expect } from 'chai'; -import { describe } from 'mocha'; -import { transformationConfigJSONSchema } from './xlsx-transform-schema'; - -describe('transformationJSONSchema', () => { - const ajv = new Ajv(); - - it('is valid json schema', () => { - expect(ajv.validateSchema(transformationConfigJSONSchema)).to.be.true; - }); -}); diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transform-schema.ts b/api/src/utils/media/xlsx/transformation/xlsx-transform-schema.ts deleted file mode 100644 index a232b092b4..0000000000 --- a/api/src/utils/media/xlsx/transformation/xlsx-transform-schema.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * A JSON-Schema definition for a `TransformSchema`. - */ -export const transformationConfigJSONSchema = { - title: 'Transformation Schema', - type: 'object', - required: ['templateMeta', 'map', 'dwcMeta'], - properties: { - templateMeta: { - type: 'array', - description: - 'Defines the hierarchical structure of the template, which columns represent keys, and the parent-child relationship of the sheets. Used to de-normalize the template data.', - items: { - $ref: '#/$defs/TemplateMetaSchema' - } - }, - map: { - type: 'array', - description: - 'Defines the mapping operations that are executed against each flattened row of the template. Used to transform the template data into its corresponding DWC representation.', - items: { - $ref: '#/$defs/MapSchema' - } - }, - dwcMeta: { - type: 'array', - description: 'Defines the unique keys for each DWC sheet. Used to normalize the DWC data.', - items: { - $ref: '#/$defs/DwcMeta' - } - } - }, - $defs: { - TemplateMetaSchema: { - title: 'Sheet Schema', - type: 'object', - required: ['sheetName', 'primaryKey', 'parentKey', 'type', 'foreignKeys'], - properties: { - sheetName: { - type: 'string', - description: 'The name of the template sheet' - }, - primaryKey: { - type: 'array', - description: - 'An array of template column names which combined represent a unique key for rows in this sheet.', - items: { - type: 'string' - } - }, - parentKey: { - type: 'array', - description: - 'An array of template column names which combined represent a unique key for the parent row of rows in this sheet.', - items: { - type: 'string' - } - }, - type: { - type: 'string', - enum: ['root', 'leaf', ''] - }, - foreignKeys: { - type: 'array', - items: { - type: 'object', - description: 'An array of child template sheet objects.', - properties: { - sheetName: { - type: 'string', - description: 'The name of a child template sheet' - }, - primaryKey: { - type: 'array', - description: - 'An array of template column names which combined represent a unique key for child rows of this sheet.', - items: { - type: 'string', - description: 'A template column name.' - } - } - }, - additionalProperties: false - } - } - }, - additionalProperties: false - }, - MapSchema: { - title: 'Map Schema', - type: 'object', - required: ['sheetName', 'fields'], - properties: { - sheetName: { - type: 'string', - description: 'The name of the DWC sheet' - }, - condition: { - type: 'object', - description: - 'Defines a condition, which contains one or more checks that must be met in order to proceed processing this `MapSchema` item.', - properties: { - type: { - type: 'string', - enum: ['and', 'or'] - }, - checks: { - type: 'array', - items: { - type: 'object', - properties: { - ifNotEmpty: { - type: 'string' - } - }, - additionalProperties: false - } - } - }, - additionalProperties: false - }, - fields: { - type: 'array', - items: { - type: 'object', - properties: { - columnName: { - type: 'string' - }, - columnValue: { - type: 'array', - items: { - $ref: '#/$defs/MapColumnValueSchema' - } - } - }, - additionalProperties: false - } - }, - add: { - type: 'array', - items: { - $ref: '#/$defs/MapSchema' - } - } - }, - additionalProperties: false - }, - MapColumnValueSchema: { - title: 'MapColumnValueSchema', - type: 'object', - oneOf: [{ required: ['paths'] }, { required: ['static'] }], - properties: { - paths: { - type: 'array', - items: { - type: 'string' - } - }, - static: { - type: 'string' - }, - join: { - type: 'string', - description: 'A string used when concatenating columns to create keys.', - default: ':' - }, - postfix: { - type: 'object', - properties: { - paths: { - type: 'array', - items: { - type: 'string' - } - }, - static: { - type: 'string' - } - }, - additionalProperties: false - }, - condition: { - type: 'object', - description: - 'Defines a condition, which contains one or more checks that must be met in order to proceed processing this `MapColumnValueSchema` item.', - properties: { - type: { - type: 'string', - enum: ['and', 'or'] - }, - checks: { - type: 'array', - items: { - type: 'object', - properties: { - ifNotEmpty: { - type: 'string' - } - } - } - } - }, - additionalProperties: false - }, - add: { - type: 'array', - description: - 'An array of additional schemas to add to the process queue. Used to create additional records from within the context of the current schema being processed.', - items: { - $ref: '#/$defs/MapSchema' - } - } - }, - additionalProperties: false - }, - DwcMeta: { - title: 'Dwc Schema', - type: 'object', - properties: { - sheetName: { - type: 'string', - description: 'The name of the DWC sheet' - }, - primaryKey: { - type: 'array', - description: 'An array of DWC column names which combined represent a unique key for rows in this sheet.', - items: { - type: 'string', - description: 'A DWC column name.' - } - } - }, - additionalProperties: false - } - }, - additionalProperties: false -}; diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transform-utils.ts b/api/src/utils/media/xlsx/transformation/xlsx-transform-utils.ts index 277d68e28a..968df6ba36 100644 --- a/api/src/utils/media/xlsx/transformation/xlsx-transform-utils.ts +++ b/api/src/utils/media/xlsx/transformation/xlsx-transform-utils.ts @@ -76,10 +76,10 @@ export function getCombinations>(obj: O * * @param {Record[]} arrayOfObjects * @param {string[]} keys - * @return {*} {object[]} + * @return {*} {Record[]} * @memberof XLSXTransform */ -export function filterDuplicateKeys(arrayOfObjects: Record[], keys: string[]): object[] { +export function filterDuplicateKeys(arrayOfObjects: Record[], keys: string[]): Record[] { const keyValues: [string, any][] = arrayOfObjects.map((value) => { const key = keys.map((k) => value[k]).join('|'); return [key, value]; diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transform.ts b/api/src/utils/media/xlsx/transformation/xlsx-transform.ts index 8c2fd066d1..24769c72d3 100644 --- a/api/src/utils/media/xlsx/transformation/xlsx-transform.ts +++ b/api/src/utils/media/xlsx/transformation/xlsx-transform.ts @@ -10,11 +10,14 @@ import XLSXTransformSchemaParser, { JSONPathString, TemplateColumnName, TemplateMetaSchema, + TemplateMetaSchemaType, TemplateSheetName, TransformSchema } from './xlsx-transform-schema-parser'; import { filterDuplicateKeys, getCombinations } from './xlsx-transform-utils'; +export type NonObjectPrimitive = string | number | null | boolean; + /** * Defines a type that indicates a `Partial` value, but with some exceptions. * @@ -39,16 +42,62 @@ import { filterDuplicateKeys, getCombinations } from './xlsx-transform-utils'; */ type AtLeast = Partial & Pick; -export type NonObjectPrimitive = string | number | null | boolean; - +/** + * Contains information about a single row, including information about its parent row and/or child row(s). + * + * It also includes calculated fields that are often used repeatedly, where re-calculation would be impossible or + * inefficient. + */ export type RowObject = { + /** + * The row data. + * + * @type {{ [key: string]: NonObjectPrimitive }} + */ _data: { [key: string]: NonObjectPrimitive }; + /** + * The name of the source file/sheet. + * + * @type {string} + */ _name: string; + /** + * The key for this row. + * + * @type {string} + */ _key: string; + /** + * The key of the parent row, if there is one. + * + * Note: All row objects will have a parent, unless they are `_type='root'` + * + * @type {string} + */ _parentKey: string; - _type: 'root' | 'leaf' | ''; + /** + * The type of the row object. + * + * @type {TemplateMetaSchemaType} + */ + _type: TemplateMetaSchemaType; + /** + * The index of the row from the source file. + * + * @type {number} + */ _row: number; + /** + * The keys of all connected child rows, if any. + * + * @type {string[]} + */ _childKeys: string[]; + /** + * The child row objects, if any. + * + * @type {RowObject[]} + */ _children: RowObject[]; }; @@ -155,7 +204,10 @@ export class XLSXTransform { _key: primaryKey, _parentKey: parentKey, _type: templateMetaSchema.type, - _row: i, + // add 2 to _row: while the transform array index starts at 0, actual csv data (excel) starts at 1. And with the + // header row removed, the first real data rows start at 2. This _row value does not have to match the data row + // precisely, but it is convenient if they do as it better aligns with a humans understanding of the data. + _row: i + 2, _childKeys: childKeys || [], _children: [] }); @@ -448,7 +500,9 @@ export class XLSXTransform { if (Array.isArray(postfixPathValues)) { // postfix value is the concatenation of multiple values postfixValue = - (postfixPathValues.length && postfixPathValues.join(columnValueItem.join ?? ':')) || ''; + (postfixPathValues.length && + postfixPathValues.flat(Infinity).join(columnValueItem.join ?? ':')) || + ''; } else { // postfix value is a single value postfixValue = postfixPathValues || ''; @@ -515,22 +569,40 @@ export class XLSXTransform { } } + let result = false; + if (condition.type === 'or') { - return conditionsMet.has(true); + // condition passes if at least 1 check passes (logical `or`) + result = conditionsMet.has(true); + } else { + // condition passes if no check fails (logical `and`) + result = !conditionsMet.has(false); } - return !conditionsMet.has(false); + if (condition.not) { + // if `true`, negate the result of the condition (logical `not`) + result = !result; + } + + return result; } _processIfNotEmptyCondition(check: IfNotEmptyCheck, rowObjects: RowObject[]): boolean { const pathValues = this._processPaths([check.ifNotEmpty], rowObjects); - if (!pathValues?.length) { - // condition failed - return false; + let result = false; + + if (pathValues?.length) { + // path is not empty + result = true; + } + + if (check.not) { + // if `true`, negate the result of the condition (logical `not`) + result = !result; } - return true; + return result; } _processPaths(paths: JSONPathString[], json: JSONPathOptions['json']): string | string[] | string[][] { diff --git a/api/src/utils/media/xlsx/validation/xlsx-validation.ts b/api/src/utils/media/xlsx/validation/xlsx-validation.ts index 97c3691c96..2d44d51fd7 100644 --- a/api/src/utils/media/xlsx/validation/xlsx-validation.ts +++ b/api/src/utils/media/xlsx/validation/xlsx-validation.ts @@ -53,10 +53,10 @@ export const getParentChildKeyMatchValidator = (config?: ParentChildKeyMatchVali /** * Encodes the column values for a worksheet at a given row into a string, which is used for comparison with another worksheet - * @param {object} rowObject A record reflecting a row in a tbale + * @param {Record} rowObject A record reflecting a row in a tbale * @returns {*} {string} The row objected encoded as a string */ - const serializer = (rowObject: object): string => { + const serializer = (rowObject: Record): string => { return ( filteredColumnNames // Retrieve the value from each column diff --git a/api/src/utils/media/xlsx/xlsx-file.ts b/api/src/utils/media/xlsx/xlsx-file.ts index c3ce15e527..c57cca033c 100644 --- a/api/src/utils/media/xlsx/xlsx-file.ts +++ b/api/src/utils/media/xlsx/xlsx-file.ts @@ -23,7 +23,7 @@ export class XLSXCSV { this.mediaValidation = new MediaValidation(this.rawFile.fileName); this.workbook = new CSVWorkBook( - // See https://www.npmjs.com/package/xlsx#parsing-options for details on parsing options + // See https://docs.sheetjs.com/docs/api/parse-options for details on parsing options xlsx.read(this.rawFile.buffer, { cellDates: true, cellNF: true, cellHTML: false, ...options }) ); } @@ -111,4 +111,7 @@ export class XLSXCSV { export type XLSXCSVValidator = (xlsxCsv: XLSXCSV) => XLSXCSV; -export type XLSXCSVTransformer = { pivot: string; transform: (xlsxCsv: XLSXCSV, modifiers?: object) => XLSXCSV }; +export type XLSXCSVTransformer = { + pivot: string; + transform: (xlsxCsv: XLSXCSV, modifiers?: Record) => XLSXCSV; +}; diff --git a/api/src/utils/media/xlsx/xlsx-utils.ts b/api/src/utils/media/xlsx/xlsx-utils.ts index 8def99bf7e..eb1934bd07 100644 --- a/api/src/utils/media/xlsx/xlsx-utils.ts +++ b/api/src/utils/media/xlsx/xlsx-utils.ts @@ -1,3 +1,4 @@ +import dayjs from 'dayjs'; import xlsx, { CellObject } from 'xlsx'; import { safeTrim } from '../../string-utils'; @@ -51,6 +52,7 @@ export function prepareWorksheetCells(worksheet: xlsx.WorkSheet) { let cell: CellObject = worksheet[coord]; if (!cell || !cell.v) { + // Cell is null or has no raw value continue; } @@ -61,29 +63,114 @@ export function prepareWorksheetCells(worksheet: xlsx.WorkSheet) { } } +/** + * Trims whitespace from the value of a string type cell. + * Trims whitespace from the formatted text value of a cell, if present. + * + * @export + * @param {CellObject} cell + * @return {*} + */ export function trimCellWhitespace(cell: CellObject) { - // check and clean raw strings - if (cell.t === 's') { + if (isStringCell(cell)) { + // check and clean raw strings cell.v = safeTrim(cell.v); } - // check and clean formatted strings if (cell.w) { + // check and clean formatted strings cell.w = safeTrim(cell.w); } return cell; } +/** + * Attempts to update the cells value with a formatted date or time value if the cell is a date type cell that has a + * date or time format. + * + * @see https://docs.sheetjs.com/docs/csf/cell for details on cell fields + * @export + * @param {CellObject} cell + * @return {*} + */ export function replaceCellDates(cell: CellObject) { - if (cell.t === 'd' && cell.v instanceof Date) { - cell.v = (cell.v as Date).toISOString(); + if (!isDateCell(cell)) { + return cell; + } + + const cellDate = dayjs(cell.v as any); + + if (!cellDate.isValid()) { + return cell; + } + + if (isDateFormatCell(cell)) { + const DateFormat = 'YYYY-MM-DD'; + cell.v = cellDate.format(DateFormat); + return cell; + } + + if (isTimeFormatCell(cell)) { + const TimeFormat = 'HH:mm'; + cell.v = cellDate.format(TimeFormat); + return cell; } return cell; } -export function getCellValue(cell: CellObject) { - // See https://www.npmjs.com/package/xlsx#cell-object for details on cell fields - return cell.v; +/** + * Checks if the cell has type string. + * + * @export + * @param {CellObject} cell + * @return {*} {boolean} `true` if the cell has type string, `false` otherwise. + */ +export function isStringCell(cell: CellObject): boolean { + return cell.t === 's'; +} + +/** + * Checks if the cell has type date. + * + * @export + * @param {CellObject} cell + * @return {*} {boolean} `true` if the cell has type date, `false` otherwise. + */ +export function isDateCell(cell: CellObject): boolean { + return cell.t === 'd'; +} + +/** + * Checks if the cell has a format, and if the format is likely a date format. + * + * @export + * @param {CellObject} cell + * @return {*} {boolean} `true` if the cell has a date format, `false` otherwise. + */ +export function isDateFormatCell(cell: CellObject): boolean { + if (!cell.z) { + return false; + } + + // format contains `d` and/or `y` which are values only used in date formats + return String(cell.z).includes('d') || String(cell.z).includes('y'); +} + +/** + * Checks if the cell has a format, and if the format is likely a time format. + * + * @export + * @param {CellObject} cell + * @return {*} {boolean} `true` if the cell has a time format, `false` otherwise. + */ +export function isTimeFormatCell(cell: CellObject): boolean { + if (!cell.z) { + // Not a date cell and/or has no date format + return false; + } + + // format contains `h` and/or `ss` which are values only used in time formats, or date formats that include time + return String(cell.z).includes('h') || String(cell.z).includes('ss'); } diff --git a/app/package-lock.json b/app/package-lock.json index 4d788deecc..ea3f4ab62f 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -5121,7 +5121,7 @@ "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==" + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "ansi-escapes": { "version": "4.3.2", @@ -5257,12 +5257,12 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==" + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { "version": "3.1.6", @@ -5362,7 +5362,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "ast-types-flow": { "version": "0.0.7", @@ -5379,12 +5379,12 @@ "async-foreach": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==" + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "at-least-node": { "version": "1.0.0", @@ -5420,7 +5420,7 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.12.0", @@ -5773,7 +5773,7 @@ "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { "tweetnacl": "^0.14.3" } @@ -5805,7 +5805,7 @@ "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "requires": { "inherits": "~2.0.0" } @@ -5979,7 +5979,7 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==" + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" }, "camelcase-css": { "version": "2.0.1", @@ -5990,7 +5990,7 @@ "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "requires": { "camelcase": "^2.0.0", "map-obj": "^1.0.0" @@ -6022,7 +6022,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chalk": { "version": "2.4.2", @@ -6115,7 +6115,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", @@ -6162,7 +6162,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collect-v8-coverage": { "version": "1.0.1", @@ -6181,7 +6181,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colord": { "version": "2.9.3", @@ -6283,7 +6283,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concaveman": { "version": "1.2.1", @@ -6311,7 +6311,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "content-disposition": { "version": "0.5.4", @@ -6339,7 +6339,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "core-js": { "version": "3.30.2", @@ -6382,7 +6382,7 @@ "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha512-eZ+m1WNhSZutOa/uRblAc9Ut5MQfukFrFMtPSm3bZCA888NmMd5AWXWdgRZ80zd+pTk1P2JrGjg9pUPTvl2PWQ==", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", "requires": { "lru-cache": "^4.0.1", "which": "^1.2.9" @@ -6400,7 +6400,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" } } }, @@ -6730,7 +6730,7 @@ "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "requires": { "array-find-index": "^1.0.1" } @@ -6744,7 +6744,7 @@ "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { "assert-plus": "^1.0.0" } @@ -6771,7 +6771,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decimal.js": { "version": "10.4.3", @@ -6836,22 +6836,22 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==" + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "detect-newline": { "version": "3.1.0", @@ -7074,7 +7074,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -7083,7 +7083,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { "version": "3.1.9", @@ -7119,7 +7119,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "enhanced-resolve": { "version": "5.14.1", @@ -7278,12 +7278,12 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "2.0.0", @@ -8011,7 +8011,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "eventemitter3": { "version": "4.0.7", @@ -8143,7 +8143,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "3.1.3", @@ -8333,7 +8333,7 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -8383,7 +8383,7 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "fork-ts-checker-webpack-plugin": { "version": "6.5.3", @@ -8580,7 +8580,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-extra": { "version": "10.1.0", @@ -8610,7 +8610,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.3.2", @@ -8655,7 +8655,7 @@ "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -8683,7 +8683,7 @@ "geojson-equality": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", - "integrity": "sha512-TqG8YbqizP3EfwP5Uw4aLu6pKkg6JQK9uq/XZ1lXQntvTHD1BBKJWhNpJ2M0ax6TuWMP3oyx6Oq7FCIfznrgpQ==", + "integrity": "sha1-oXE3TvBD5dR5eZWEC65GSOB1LXI=", "requires": { "deep-equal": "^1.0.0" } @@ -8719,7 +8719,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==" + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" }, "get-stream": { "version": "6.0.1", @@ -8740,7 +8740,7 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { "assert-plus": "^1.0.0" } @@ -8900,7 +8900,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.5", @@ -8928,7 +8928,7 @@ "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "requires": { "ansi-regex": "^2.0.0" } @@ -8942,7 +8942,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-property-descriptors": { "version": "1.0.0", @@ -8973,7 +8973,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "he": { "version": "1.2.0", @@ -9186,7 +9186,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -9196,7 +9196,7 @@ "https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, "https-proxy-agent": { @@ -9263,7 +9263,7 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, "immer": { "version": "9.0.21", @@ -9312,7 +9312,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -9368,7 +9368,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-bigint": { "version": "1.0.4", @@ -9446,7 +9446,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" } @@ -9478,7 +9478,7 @@ "is-in-browser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", - "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" }, "is-map": { "version": "2.0.2", @@ -9623,12 +9623,12 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, "is-weakmap": { "version": "2.0.1", @@ -9667,17 +9667,17 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -12829,7 +12829,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { "version": "16.7.0", @@ -12920,7 +12920,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json5": { "version": "2.2.3", @@ -13123,7 +13123,7 @@ "leaflet-fullscreen": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/leaflet-fullscreen/-/leaflet-fullscreen-1.0.2.tgz", - "integrity": "sha512-1Yxm8RZg6KlKX25+hbP2H/wnOAphH7hFcvuADJFb4QZTN7uOSN9Hsci5EZpow8vtNej9OGzu59Jxmn+0qKOO9Q==" + "integrity": "sha1-CcYcS6xF9jsu4Sav2H5c2XZQ/Bs=" }, "leaflet.locatecontrol": { "version": "0.76.1", @@ -13173,7 +13173,7 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -13221,7 +13221,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" } } }, @@ -13276,7 +13276,7 @@ "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -13343,7 +13343,7 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==" + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" }, "mdn-data": { "version": "2.0.4", @@ -13354,7 +13354,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memfs": { "version": "3.5.1", @@ -13373,7 +13373,7 @@ "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -13390,7 +13390,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "merge-stream": { "version": "2.0.0", @@ -13407,7 +13407,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "mgrs": { "version": "1.0.0", @@ -13647,7 +13647,7 @@ "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==" + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" } } }, @@ -13689,12 +13689,12 @@ "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==" + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -13706,14 +13706,14 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==" + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" } } }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "requires": { "abbrev": "1" } @@ -13786,7 +13786,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nwsapi": { "version": "2.2.5", @@ -13802,7 +13802,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-hash": { "version": "3.0.0", @@ -13907,7 +13907,7 @@ "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", "requires": { "ee-first": "1.1.1" } @@ -13921,7 +13921,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } @@ -13963,12 +13963,12 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==" + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", @@ -14093,7 +14093,7 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "requires": { "pinkie-promise": "^2.0.0" } @@ -14101,7 +14101,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -14117,7 +14117,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "path-type": { "version": "4.0.0", @@ -14127,7 +14127,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "picocolors": { "version": "1.0.0", @@ -14143,17 +14143,17 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==" + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "requires": { "pinkie": "^2.0.0" } @@ -15172,7 +15172,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { "version": "1.9.0", @@ -15690,7 +15690,7 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -15712,7 +15712,7 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -15937,7 +15937,7 @@ "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "requires": { "is-finite": "^1.0.0" } @@ -15984,7 +15984,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-from-string": { "version": "2.0.2", @@ -16294,7 +16294,7 @@ "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha512-dYE8LhncfBUar6POCxMTm0Ln+erjeczqEvCJib5/7XNkdw1FkUGgwMPY360FY0FgPWQxHWCx29Jl3oejyGLM9Q==", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", "requires": { "js-base64": "^2.1.8", "source-map": "^0.4.2" @@ -16303,7 +16303,7 @@ "source-map": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "requires": { "amdefine": ">=0.0.4" } @@ -16451,7 +16451,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "setprototypeof": { "version": "1.2.0", @@ -16494,7 +16494,7 @@ "lru-cache": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ==" + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" } } }, @@ -16546,7 +16546,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-js": { "version": "1.0.2", @@ -16722,7 +16722,7 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "stdout-stream": { "version": "1.4.1", @@ -16777,7 +16777,7 @@ "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -16862,7 +16862,7 @@ "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } @@ -16870,7 +16870,7 @@ "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "requires": { "is-utf8": "^0.2.0" } @@ -17256,7 +17256,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-regex-range": { "version": "5.0.1", @@ -17275,7 +17275,7 @@ "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" }, "tough-cookie": { "version": "2.5.0", @@ -17298,7 +17298,7 @@ "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==" + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" }, "true-case-path": { "version": "1.0.3", @@ -17366,7 +17366,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { "safe-buffer": "^5.0.1" } @@ -17374,7 +17374,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.4.0", @@ -17489,7 +17489,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unquote": { "version": "1.1.1", @@ -17546,7 +17546,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", @@ -17569,7 +17569,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "8.3.2", @@ -17612,12 +17612,12 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -18333,7 +18333,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0", @@ -18358,7 +18358,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "3.0.3", @@ -18381,7 +18381,7 @@ "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", "dev": true }, "xml-name-validator": { @@ -18444,7 +18444,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { "version": "3.1.0",