diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 98b1fd7..97737d4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ combo_definitions.json @utaninja @zelixir25 /translations @LunaUrsa /types/ @LunaUrsa .gitignore @LunaUrsa +package-lock.json @LunaUrsa package.json @LunaUrsa README.md @LunaUrsa tsconfig.json @LunaUrsa \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e242012..3582e4d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,9 +2,6 @@ name: Validate changes on: pull_request: - branches: - - main - - development types: - opened - synchronize @@ -29,3 +26,14 @@ jobs: run: npx ts-node ./scripts/combosToDrugs.ts --github-check env: CI: true + + - name: Assign Pull Request + uses: actions/github-script@v5 + with: + script: | + github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + assignees: ['LunaUrsa'] + }) \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e98c9b4..f219586 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -652,6 +652,7 @@ "tsbuildinfo", "Tyramine", "uneccessary", + "utaninja", "Valproic", "Vapaatalo", "vapourised", @@ -666,6 +667,7 @@ "Xanax", "xaxis", "yage", + "zelixir", "zyban", "αphp", "αpvp" diff --git a/drugs.json b/drugs.json index 049082b..20366c8 100644 --- a/drugs.json +++ b/drugs.json @@ -32049,7 +32049,7 @@ ], "dose": "Intravenously Light: 10-30mg Common: 30-60mg Strong: 60-100mg | Note: Do not take this as gospel. Please read ~experiences if you use this substance outside of a hospital setting.", "duration": "Intravenous: 10-20 minutes.", - "experiences": "https://www.reddit.com/r/Drugs/comments/56n5vq/michael_jackson_the_addict/d8l0qe7 (PM Sleep/Hypnos if you'd like more information)", + "experiences": "https://www.reddit.com/r/Drugs/comments/56n5vq/michael_jackson_the_addict/d8l0qe7", "onset": "Intravenous: 0-2 minutes.", "summary": "A very short acting sedative that is usually given at the start of general anesthesia, and for maintenance of the prior. It should never be used outside of a medical setting. With that in mind, if you do plan to use this drug recreational, please have a very experienced friend with you that has the skills to insert a cannula, and monitor you very closely." } diff --git a/package-lock.json b/package-lock.json index 8a0306f..aed9752 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "tripsit_drug_db", "version": "1.0.2", "license": "ISC", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1" + }, "devDependencies": { "@types/node": "^20.11.10", "ts-node": "^10.9.2", @@ -105,6 +109,37 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -126,12 +161,38 @@ "node": ">=0.3.1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -194,6 +255,14 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 872dc0c..eef4089 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "tripsit_drug_db", "version": "1.0.2", "description": "TripSit's Drug Database", - "main": "index.ts", + "main": "./scripts/index.ts", "scripts": { "compare": "ts-node ./scripts/combosToDrugs.ts" }, @@ -28,6 +28,8 @@ "homepage": "https://github.com/TripSit/drugs#readme", "types": "types/drugs.d.ts", "devDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", "@types/node": "^20.11.10", "ts-node": "^10.9.2", "typescript": "^5.3.3" diff --git a/schemas/combo_definitions-schema.json b/schemas/combo_definitions-schema.json index d95c68c..7b50d1b 100644 --- a/schemas/combo_definitions-schema.json +++ b/schemas/combo_definitions-schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", "items": { "$ref": "#/definitions/ComboDefinition" diff --git a/schemas/combos-schema.json b/schemas/combos-schema.json index a185f86..830eaf3 100644 --- a/schemas/combos-schema.json +++ b/schemas/combos-schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/Combos", "definitions": { "Combos": { diff --git a/schemas/drugs-schema.json b/schemas/drugs-schema.json index f8ada3c..516cde8 100644 --- a/schemas/drugs-schema.json +++ b/schemas/drugs-schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "additionalProperties": { "$ref": "#/definitions/Drug" @@ -55,10 +55,7 @@ "type": "object", "additionalProperties": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "https" - ] + "format": "uri" } }, "dose_note": { @@ -305,33 +302,15 @@ "properties": { "experiences": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "https" - ], - "qt-uri-extensions": [ - ".shtml" - ] + "format": "uri" }, "pihkal": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "https" - ], - "qt-uri-extensions": [ - ".shtml" - ] + "format": "uri" }, "tihkal": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "https" - ], - "qt-uri-extensions": [ - ".shtml" - ] + "format": "uri" } }, "required": [ @@ -381,14 +360,7 @@ }, "experiences": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "https" - ], - "qt-uri-extensions": [ - ".php", - ".shtml" - ] + "format": "uri" }, "warning": { "type": "string" @@ -413,10 +385,7 @@ }, "wiki": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http" - ] + "format": "uri" }, "mdma": { "type": "string" @@ -462,34 +431,18 @@ }, "molecule": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http" - ], - "qt-uri-extensions": [ - ".jpg", - ".png" - ] + "format": "uri" }, "vaporization": { "type": "string" }, "calculator": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http" - ] + "format": "uri" }, "chart": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http" - ], - "qt-uri-extensions": [ - ".png" - ] + "format": "uri" }, "Oral": { "type": "string" @@ -569,65 +522,35 @@ "_general": { "type": "array", "items": { - "type": "string", - "qt-uri-protocols": [ - "http", - "https" - ] + "type": "string" } }, "dose": { "type": "array", "items": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http" - ], - "qt-uri-extensions": [ - ".full" - ] + "format": "uri" } }, "duration": { "type": "array", "items": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http" - ], - "qt-uri-extensions": [ - ".full" - ] + "format": "uri" } }, "bioavailability": { "type": "array", "items": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "https" - ] + "format": "uri" } }, "legality": { "type": "array", "items": { "type": "string", - "format": "uri", - "qt-uri-protocols": [ - "http", - "https" - ], - "qt-uri-extensions": [ - ".asp", - ".aspx", - ".cfm", - ".html", - ".pdf" - ] + "format": "uri" } }, "onset": { diff --git a/scripts/combosToDrugs.ts b/scripts/combosToDrugs.ts index 61602db..6ecadce 100644 --- a/scripts/combosToDrugs.ts +++ b/scripts/combosToDrugs.ts @@ -28,92 +28,150 @@ import { ComboData, Combos, Interactions } from '../types/combos'; import drugsData from '../drugs.json'; import combosData from '../combos.json'; import { log } from 'console'; +import * as fsSync from 'fs'; +import Ajv, { JSONSchemaType } from 'ajv'; +import addFormats from 'ajv-formats'; const drugData = drugsData as { [key: string]: Drug }; +console.log(`Drugs ${Object.keys(drugData).length}`); const comboData = combosData as { [key in keyof Combos]: Interactions }; +console.log(`Combos ${Object.keys(combosData).length}`); -enum WildcardDrugs { - "2c-t-x" = "2c-t-x", - "2c-x" = "2c-x", - "5-meo-xxt" = "5-meo-xxt", - dox = "dox", -} +function schemaAlphabetized(): boolean { + let alphabetized = true; -const wildcardDrugs: (keyof Combos)[] = [ - '2c-t-x', - '2c-x', - '5-meo-xxt', - 'dox', -] - -enum CategoryDrugs { - amphetamines = "amphetamines", - benzodiazepines = "benzodiazepines", - maois = "maois", - nbomes = "nbomes", - opioids = "opioids", - ssris = "ssris", - "ghb/gbl" = "ghb/gbl", -} + // Function to check if an object's keys are alphabetized + function isAlphabetized(object: Record): boolean { + const keys = Object.keys(object); + for (let i = 0; i < keys.length - 1; i++) { + // Using localeCompare with numeric option + if (keys[i].localeCompare(keys[i + 1], undefined, { numeric: true, sensitivity: 'base' }) > 0) { + console.log(`Key ${keys[i]} is greater than ${keys[i + 1]}`); + console.log(`Keys: ${keys}`); + return false; // If a key is greater than the next one, it's not alphabetized + } + } + return true; + } -const categoryDrugs: (keyof Combos)[] = [ - 'amphetamines', - 'benzodiazepines', - 'maois', - 'nbomes', - 'opioids', - 'ssris', - 'ghb/gbl', -] - -function isCategoryCombo(comboName: keyof Combos): comboName is keyof typeof CategoryDrugs { - return categoryDrugs.includes(comboName as any); -} + // Function to recursively check each object in the JSON + function checkObject(obj: Record): boolean { + if (!isAlphabetized(obj)) { + return false; + } + for (const key of Object.keys(obj)) { + if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { + if (!checkObject(obj[key])) { + return false; // Recursively check nested objects + } + } + } + return true; + } -function isWildcardCombo(comboName: keyof Combos): comboName is keyof typeof WildcardDrugs { - return wildcardDrugs.includes(comboName as any); -} + // Check the drugs.json file for alphabetization + if (!checkObject(drugData)) { + console.error('Drugs.json is not alphabetized!'); + alphabetized = false; + } else { + console.error('Drugs.json is alphabetized!'); + } -// Function to check if an object's keys are alphabetized -function isAlphabetized(object: Record): boolean { - const keys = Object.keys(object); - for (let i = 0; i < keys.length - 1; i++) { - // Using localeCompare with numeric option - if (keys[i].localeCompare(keys[i + 1], undefined, {numeric: true, sensitivity: 'base'}) > 0) { - console.log(`Key ${keys[i]} is greater than ${keys[i + 1]}`); - console.log(`Keys: ${keys}`); - return false; // If a key is greater than the next one, it's not alphabetized - } + // Check the combos.json file for alphabetization + if (!checkObject(comboData)) { + console.error('Combos.json is not alphabetized!'); + alphabetized = false; + } else { + console.error('Combos.json is alphabetized!'); } - return true; + + return alphabetized; } -// Function to recursively check each object in the JSON -function checkObject(obj: Record): boolean { - if (!isAlphabetized(obj)) { - return false; +function schemaValidated(): boolean { + let valid = true; + // Initialize AJV + const ajv = new Ajv(); + addFormats(ajv); + + // Load and parse the schema and data + const drugsSchema: JSONSchemaType = JSON.parse(fsSync.readFileSync( + path.join(__dirname, '..', 'schemas', 'drugs-schema.json'), 'utf8')); + + const drugsValidate = ajv.compile(drugsSchema); + const drugsValidated = drugsValidate(drugData); + if (!drugsValidated) { + log(`Drugs.json is not valid!`) + log(drugsValidate.errors); + valid = false; + } else { + log(`Drugs.json is valid!`) } - for (const key of Object.keys(obj)) { - if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) { - if (!checkObject(obj[key])) { - return false; // Recursively check nested objects - } - } + + const comboSchema: JSONSchemaType = JSON.parse(fsSync.readFileSync( + path.join(__dirname, '..', 'schemas', 'combos-schema.json'), 'utf8')); + + const comboValidate = ajv.compile(comboSchema); + const comboValidated = comboValidate(comboData); + if (!comboValidated) { + log(`Combos.json is not valid!`) + log(comboValidate.errors); + valid = false; + } else { + log(`Combos.json is valid!`) } - return true; + return valid; } -export default async function compareData(): Promise{ - console.log(`Drugs ${Object.keys(drugData).length}`); - console.log(`Combos ${Object.keys(combosData).length}`); - let dataChanged = false; +async function compareData(): Promise { + enum WildcardDrugs { + "2c-t-x" = "2c-t-x", + "2c-x" = "2c-x", + "5-meo-xxt" = "5-meo-xxt", + dox = "dox", + } + + const wildcardDrugs: (keyof Combos)[] = [ + '2c-t-x', + '2c-x', + '5-meo-xxt', + 'dox', + ] + + enum CategoryDrugs { + amphetamines = "amphetamines", + benzodiazepines = "benzodiazepines", + maois = "maois", + nbomes = "nbomes", + opioids = "opioids", + ssris = "ssris", + "ghb/gbl" = "ghb/gbl", + } + + const categoryDrugs: (keyof Combos)[] = [ + 'amphetamines', + 'benzodiazepines', + 'maois', + 'nbomes', + 'opioids', + 'ssris', + 'ghb/gbl', + ] - // for (const [comboKey, comboEntry] of Object.entries(comboData)) { + function isCategoryCombo(comboName: keyof Combos): comboName is keyof typeof CategoryDrugs { + return categoryDrugs.includes(comboName as any); + } + + function isWildcardCombo(comboName: keyof Combos): comboName is keyof typeof WildcardDrugs { + return wildcardDrugs.includes(comboName as any); + } + + let dataMatches = true; Object.entries(comboData).forEach(async ([comboKey, comboEntry]) => { const drugAName = comboKey as keyof Combos; @@ -127,8 +185,7 @@ export default async function compareData(): Promise{ return; } - console.log(`@ ${drugAName}`); - // for (const [interactionKey, interactionEntry] of Object.entries(comboEntry)) { + // console.log(`@ ${drugAName}`); Object.entries(comboEntry).forEach(async ([interactionKey, interactionEntry]) => { const drugBName = interactionKey as keyof Interactions; const interaction = interactionEntry as ComboData; @@ -143,21 +200,21 @@ export default async function compareData(): Promise{ note: interaction.note, sources: interaction.sources, }; - dataChanged = true; + dataMatches = false; log(`+ ${drugBName} + ${drugAName}`); } - + if (JSON.stringify(comboData[drugBName][drugAName]) != JSON.stringify(interaction)) { // If the status does not match, update it log(`~ ${drugAName} + ${drugBName}`); log(`drugACombo: ${JSON.stringify(interaction)}`); log(`drugBCombo: ${JSON.stringify(comboData[drugBName][drugAName])}`); comboData[drugBName][drugAName] = interaction; - dataChanged = true; + dataMatches = false; } // Drugs.json stuff - + // If drugA doesn't exist in the drugs.json file, create it // The pretty_name may not always be correct if (!drugData[drugAName]) { @@ -169,14 +226,14 @@ export default async function compareData(): Promise{ [drugBName]: interaction as Combo } }; - dataChanged = true; + dataMatches = false; log(`+ ${drugAName} entry in drugs.json`); log(`${JSON.stringify(drugData[drugAName])}`); } // If drugA exists, but doesn't have a combos section, create it if (!drugData[drugAName].combos) { drugData[drugAName].combos = {}; - dataChanged = true; + dataMatches = false; console.log(`+ ${drugAName}.combos`); } @@ -188,10 +245,10 @@ export default async function compareData(): Promise{ // If the combo interaction does not exist, create it if (!drugACombos[drugBName]) { drugACombos[drugBName] = interaction as Combo; - dataChanged = true; + dataMatches = false; log(`+ ${drugBName} + ${drugAName}`); } - + // If the entry exists, but the data is different, update it if (JSON.stringify(drugACombos[drugBName]) !== JSON.stringify(interaction)) { log(`~ ${drugAName} + ${drugBName}`); @@ -199,50 +256,50 @@ export default async function compareData(): Promise{ log(`drugs.json: ${JSON.stringify(drugACombos[drugBName])}`); // Update the drugBInfo with the new data from interaction drugACombos[drugBName] = interaction as Combo; - dataChanged = true; + dataMatches = false; } }); - }); - - // Check the drugs.json file for alphabetization - if (!checkObject(drugData)) { - console.error('Drugs.json is not alphabetized!'); - dataChanged = true; - } - - // Check the combos.json file for alphabetization - if (!checkObject(comboData)) { - console.error('Combos.json is not alphabetized!'); - dataChanged = true; - } - - return dataChanged; + return dataMatches; } // If the command is 'npx ts-node ./scripts/combosToDrugs.ts --github-check' then it will do a check to see if the data has changed if (process.argv.slice(2).includes('--github-check')) { - compareData() - .then(async (dataChanged) => { - if (dataChanged) { - console.error(`Changes were made, unable to merge!`); - process.exit(1); - } - log('No changes were made, able to merge!'); - }) - .catch((err) => { - console.error(err); - }); + console.log('GitHub Check'); + if (!schemaValidated()) { + console.error('Schema is not valid!'); + process.exit(1); + } + if (!schemaAlphabetized()) { + console.error('Schema is not alphabetized!'); + process.exit(1); + } + if (!compareData()) { + console.error('Data does not match!'); + process.exit(1); + } + console.log('Data matches, you can commit and merge!'); + process.exit(0); } else { + console.log('Local Check'); + if (!schemaValidated()) { + console.error('Schema is not valid!'); + process.exit(1); + } + if (!schemaAlphabetized()) { + console.error('Schema is not alphabetized!'); + process.exit(1); + } + compareData() - .then(async (dataChanged) => { - if (dataChanged) { - await fs.writeFile(path.resolve(__dirname, '../drugs.json'), JSON.stringify(drugData, null, 2)); - await fs.writeFile(path.resolve(__dirname, '../combos.json'), JSON.stringify(comboData, null, 2)); - console.log('Updated files!'); + .then(async (dataMatches) => { + if (dataMatches) { + console.log('No changes were made'); return; } - console.log('No changes were made'); + await fs.writeFile(path.resolve(__dirname, '../drugs.json'), JSON.stringify(drugData, null, 2)); + await fs.writeFile(path.resolve(__dirname, '../combos.json'), JSON.stringify(comboData, null, 2)); + console.log('Updated files!'); }) .catch((err) => { console.error(err);