diff --git a/.vscode/settings.json b/.vscode/settings.json index f844d69f..adf32bcd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "meteortesting", "nearley", "ngraph", + "ostrio", "uncomputed", "walkdown" ] diff --git a/app/imports/@types/meteor-ostrio-files.d.ts b/app/imports/@types/meteor-ostrio-files.d.ts new file mode 100644 index 00000000..09a7bd18 --- /dev/null +++ b/app/imports/@types/meteor-ostrio-files.d.ts @@ -0,0 +1,244 @@ +declare module 'meteor/ostrio:files' { + import { Meteor } from 'meteor/meteor'; + import { Mongo } from 'meteor/mongo'; + import { ReactiveVar } from 'meteor/reactive-var'; + import { SimpleSchemaDefinition } from 'simpl-schema'; + import * as http from 'http'; + import { IncomingMessage } from 'connect'; + + interface Params { + _id: string; + query: { [key: string]: string }; + name: string; + version: string; + } + + interface ContextHTTP { + request: IncomingMessage; + response: http.ServerResponse; + params: Params; + } + + interface ContextUser { + userId: string; + user: () => Meteor.User; + } + + interface ContextUpload { + file: object; + /** On server only. */ + chunkId?: number; + /** On server only. */ + eof?: boolean; + } + + interface Version { + extension: string; + meta: MetadataType; + path: string; + size: number; + type: string; + } + + class FileObj { + _id: string; + size: number; + name: string; + type: string; + path: string; + isVideo: boolean; + isAudio: boolean; + isImage: boolean; + isText: boolean; + isJSON: boolean; + isPDF: boolean; + ext?: string; + extension?: string; + extensionWithDot: string; + _storagePath: string; + _downloadRoute: string; + _collectionName: string; + public?: boolean; + meta?: MetadataType; + userId?: string; + updatedAt?: Date; + versions: { + [propName: string]: Version; + }; + mime: string; + 'mime-type': string; + } + + class FileRef extends FileObj { + remove: (callback?: (error: Meteor.Error) => void) => void; + link: (version?: string, location?: string) => string; + get: (property?: string) => any; + fetch: () => Array>; + with: () => FileCursor; + } + + interface FileData { + size: number; + type: string; + mime: string; + 'mime-type': string; + ext: string; + extension: string; + name: string; + meta: MetadataType; + } + + interface FilesCollectionConfig { + storagePath?: string | ((fileObj: FileObj) => string); + collection?: Mongo.Collection>; + collectionName?: string; + continueUploadTTL?: string; + ddp?: object; + cacheControl?: string; + responseHeaders?: { [x: string]: string } | ((responseCode?: string, fileRef?: FileRef, versionRef?: Version, version?: string) => { [x: string]: string }); + throttle?: number | boolean; + downloadRoute?: string; + schema?: SimpleSchemaDefinition; + chunkSize?: number; + namingFunction?: (fileObj: FileObj) => string; + permissions?: number; + parentDirPermissions?: number; + integrityCheck?: boolean; + strict?: boolean; + downloadCallback?: (this: ContextHTTP & ContextUser, fileObj: FileObj) => boolean; + protected?: boolean | ((this: ContextHTTP & ContextUser, fileObj: FileObj) => boolean | number); + public?: boolean; + onBeforeUpload?: (this: ContextUpload & ContextUser, fileData: FileData) => boolean | string; + onBeforeRemove?: (this: ContextUser, cursor: Mongo.Cursor>) => boolean; + onInitiateUpload?: (this: ContextUpload & ContextUser, fileData: FileData) => void; + onAfterUpload?: (fileRef: FileRef) => any; + onAfterRemove?: (files: ReadonlyArray>) => any; + onbeforeunloadMessage?: string | (() => string); + allowClientCode?: boolean; + debug?: boolean; + interceptDownload?: (http: object, fileRef: FileRef, version: string) => boolean; + } + + interface SearchOptions { + sort?: Mongo.SortSpecifier; + skip?: number; + limit?: number; + fields?: Mongo.FieldSpecifier; + reactive?: boolean; + transform?: (fileObj: FileObj) => FileObj & TransformAdditions; + } + + interface InsertOptions { + file: File | object | string; + fileId?: string; + fileName?: string; + isBase64?: boolean; + meta?: MetadataType; + transport?: 'ddp' | 'http'; + ddp?: object; + onStart?: (error: Meteor.Error, fileData: FileData) => any; + onUploaded?: (error: Meteor.Error, fileRef: FileRef) => any; + onAbort?: (fileData: FileData) => any; + onError?: (error: Meteor.Error, fileData: FileData) => any; + onProgress?: (progress: number, fileData: FileData) => any; + onBeforeUpload?: (fileData: FileData) => any; + chunkSize?: number | 'dynamic'; + allowWebWorkers?: boolean; + type?: string; + } + + interface LoadOptions { + fileName: string; + meta?: MetadataType; + type?: string; + size?: number; + userId?: string; + fileId?: string; + } + + class FileUpload { + file: File; + onPause: ReactiveVar; + progress: ReactiveVar; + estimateTime: ReactiveVar; + estimateSpeed: ReactiveVar; + state: ReactiveVar<'active' | 'paused' | 'aborted' | 'completed'>; + pause(): void; + continue(): void; + toggle(): void; + pipe(): void; + start(): void; + on(event: string, callback: () => void): void; + } + + class FileCursor extends FileRef { } + + class FilesCursor extends Mongo.Cursor> { + cursor: Mongo.Cursor>; // Refers to base cursor? Why is this existing? + + get(): Array & TransformAdditions>; + hasNext(): boolean; + next(): FileCursor & TransformAdditions; + hasPrevious(): boolean; + previous(): FileCursor & TransformAdditions; + first(): FileCursor & TransformAdditions; + last(): FileCursor & TransformAdditions; + remove(callback?: (err: object) => void): void; + each(callback: (cursor: FileCursor & TransformAdditions) => void): void; + current(): object | undefined; + } + + class FilesCollection { + collection: Mongo.Collection>; + schema: SimpleSchemaDefinition; + + constructor(config: FilesCollectionConfig) + + /** + * Find and return Cursor for matching documents. + * + * @param selector [[http://docs.meteor.com/api/collections.html#selectors | Mongo-Style selector]] + * @param options [[http://docs.meteor.com/api/collections.html#sortspecifiers | Mongo-Style selector Options]] + * + * @template TransformAdditions Additional properties provided by transforming a document with options.tranform(). + * Note that removing fields with a transform function is not currently supported as this may break + * functions defined on a FileRef or FileCursor. + */ + find( + selector?: Mongo.Selector>>, + options?: SearchOptions + ): FilesCursor; + + /** + * Finds the first document that matches the selector, as ordered by sort and skip options. + * + * @param selector [[http://docs.meteor.com/api/collections.html#selectors | Mongo-Style selector]] + * @param options [[http://docs.meteor.com/api/collections.html#sortspecifiers | Mongo-Style selector Options]] + * + * @template TransformAdditions Additional properties provided by transforming a document with options.tranform(). + * Note that removing fields with a transform function is not currently supported as this may break + * functions defined on a FileRef or FileCursor. + */ + findOne( + selector?: Mongo.Selector>> | string, + options?: SearchOptions + ): FileCursor & TransformAdditions; + + insert(settings: InsertOptions, autoStart?: boolean): FileUpload; + remove(select: Mongo.Selector> | string, callback?: (error: Meteor.Error) => void): FilesCollection; + update(select: Mongo.Selector> | string, modifier: Mongo.Modifier>, options?: { + multi?: boolean; + upsert?: boolean; + arrayFilters?: Array<{ [identifier: string]: any }>; + }, callback?: (error: Meteor.Error, insertedCount: number) => void): FilesCollection; + link(fileRef: FileRef, version?: string): string; + allow(options: Mongo.AllowDenyOptions): void; + deny(options: Mongo.AllowDenyOptions): void; + denyClient(): void; + on(event: string, callback: (fileRef: FileRef) => void): void; + unlink(fileRef: FileRef, version?: string): FilesCollection; + addFile(path: string, opts: LoadOptions, callback?: (err: any, fileRef: FileRef) => any, proceedAfterUpload?: boolean): FilesCollection; + load(url: string, opts: LoadOptions, callback?: (err: object, fileRef: FileRef) => any, proceedAfterUpload?: boolean): FilesCollection; + write(buffer: Buffer, opts: LoadOptions, callback?: (err: object, fileRef: FileRef) => any, proceedAfterUpload?: boolean): FilesCollection; + } +} \ No newline at end of file diff --git a/app/imports/@types/vue-meteor.d.ts b/app/imports/@types/vue-meteor.d.ts new file mode 100644 index 00000000..744b2dd5 --- /dev/null +++ b/app/imports/@types/vue-meteor.d.ts @@ -0,0 +1,15 @@ +import Vue from 'vue'; + +declare module 'vue/types/options' { + interface ComponentOptions { + meteor?: any; + } +} + +declare module 'vue/types/vue' { + interface Vue { + $subscribe: (name: string, params: any[]) => void; + $autorun: (fn: () => void) => number; + $subReady: Record; + } +} \ No newline at end of file diff --git a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js index fffb5a73..d1d497e3 100644 --- a/app/imports/api/creature/archive/methods/verifyArchiveSafety.js +++ b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js @@ -21,14 +21,8 @@ export default function verifyArchiveSafety({ meta, creature, properties, experi } }); properties.forEach(prop => { - if (meta.schemaVersion.schemaVersion >= 3) { - if (prop.root?.id !== creatureId) { - throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); - } - } else { - if (prop.ancestors?.[0]?.id !== creatureId) { - throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); - } + if (prop.root?.id !== creatureId) { + throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); } }); } diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.ts b/app/imports/api/creature/creatureProperties/CreatureProperties.ts index fcf8d490..c346e8c6 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.ts +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.ts @@ -10,10 +10,11 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS'; // TODO make this a union type of all CreatureProperty types const CreatureProperties: Mongo.Collection = new Mongo.Collection('creatureProperties'); -export interface CreatureProperty { +export interface CreatureProperty extends TreeDoc { _id: string _migrationError?: string tags: string[] + type: string disabled?: boolean icon?: { name: string diff --git a/app/imports/api/docs/Docs.js b/app/imports/api/docs/Docs.js index ceba1b37..4cfa992c 100644 --- a/app/imports/api/docs/Docs.js +++ b/app/imports/api/docs/Docs.js @@ -79,6 +79,8 @@ if (Meteor.isClient) { } else if (Meteor.isServer) { Meteor.startup(() => { if (!Docs.findOne()) { + console.warn('Default documents must be updated to new parenting format'); + return; Assets.getText('docs/defaultDocs.json', (error, string) => { const docs = JSON.parse(string) docs.forEach(doc => Docs.insert(doc)); diff --git a/app/imports/api/engine/action/EngineActions.ts b/app/imports/api/engine/action/EngineActions.ts index 5a9f809f..2f7e33d9 100644 --- a/app/imports/api/engine/action/EngineActions.ts +++ b/app/imports/api/engine/action/EngineActions.ts @@ -1,6 +1,7 @@ import SimpleSchema from 'simpl-schema'; import TaskResult from './tasks/TaskResult'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema'; +import { Mongo } from 'meteor/mongo'; const EngineActions = new Mongo.Collection('actions'); diff --git a/app/imports/api/engine/action/functions/writeChangedAction.ts b/app/imports/api/engine/action/functions/writeChangedAction.ts new file mode 100644 index 00000000..89123610 --- /dev/null +++ b/app/imports/api/engine/action/functions/writeChangedAction.ts @@ -0,0 +1,5 @@ +import { EngineAction } from '/imports/api/engine/action/EngineActions'; + +export async function writeChangedAction(originalAction: EngineAction, action: EngineAction) { + console.warn('writeChangedAction not implemented.'); +} diff --git a/app/imports/api/engine/action/methods/doCastSpell.js b/app/imports/api/engine/action/methods/doCastSpell.js index 8efd7ba8..c2e1f957 100644 --- a/app/imports/api/engine/action/methods/doCastSpell.js +++ b/app/imports/api/engine/action/methods/doCastSpell.js @@ -8,8 +8,6 @@ import { import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty'; -import { doActionWork } from '/imports/api/engine/actions/doAction'; -import ActionContext from '/imports/api/engine/actions/ActionContext'; // TODO Migrate this to the new action engine @@ -48,6 +46,8 @@ const doAction = new ValidatedMethod({ timeInterval: 5000, }, run({ spellId, slotId, ritual, targetIds = [], scope = {} }) { + console.warn('Do cast spell not implemented'); + return; // Get action context let spell = CreatureProperties.findOne(spellId); const creatureId = spell.root.id; diff --git a/app/imports/api/engine/action/methods/doCheck.js b/app/imports/api/engine/action/methods/doCheck.js index 3d60d1e6..efdb352d 100644 --- a/app/imports/api/engine/action/methods/doCheck.js +++ b/app/imports/api/engine/action/methods/doCheck.js @@ -24,6 +24,8 @@ const doCheck = new ValidatedMethod({ timeInterval: 5000, }, run({ propId, scope }) { + console.warn('do check not implemented'); + return; const prop = CreatureProperties.findOne(propId); if (!prop) throw new Meteor.Error('not-found', 'The property was not found'); const creatureId = prop.root.id; diff --git a/app/imports/api/engine/action/methods/runAction.ts b/app/imports/api/engine/action/methods/runAction.ts index 281cfc28..54a2e047 100644 --- a/app/imports/api/engine/action/methods/runAction.ts +++ b/app/imports/api/engine/action/methods/runAction.ts @@ -3,6 +3,9 @@ import SimpleSchema from 'simpl-schema'; import EngineActions from '/imports/api/engine/action/EngineActions'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions'; import { getCreature } from '/imports/api/engine/loadCreatures'; +import { EJSON } from 'meteor/ejson'; +import { applyAction } from '/imports/api/engine/action/functions/applyAction'; +import { writeChangedAction } from '../functions/writeChangedAction'; export const runAction = new ValidatedMethod({ name: 'actions.runAction', diff --git a/app/imports/api/engine/computation/CreatureComputation.ts b/app/imports/api/engine/computation/CreatureComputation.ts index 2f252927..b4b4fa0c 100644 --- a/app/imports/api/engine/computation/CreatureComputation.ts +++ b/app/imports/api/engine/computation/CreatureComputation.ts @@ -8,12 +8,12 @@ interface CreatureProperty { } export default class CreatureComputation { - originalPropsById: object; - propsById: object; - propsWithTag: object; - scope: object; + originalPropsById: Record; + propsById: Record; + propsWithTag: Record; + scope: Record; props: Array; - dependencyGraph: Graph; + dependencyGraph: Graph; errors: Array; creature: object; variables: object; diff --git a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts index f698d6f0..4d8772f1 100644 --- a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts +++ b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.ts @@ -27,7 +27,7 @@ function isActive(prop: CreatureProperty): boolean { return true; } -function childrenActive(prop): boolean { +function childrenActive(prop: CreatureProperty): boolean { // Children of disabled properties are always inactive if (prop.disabled) return false; switch (prop.type) { diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts similarity index 61% rename from app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js rename to app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts index aca928f7..06361748 100644 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.ts @@ -1,7 +1,12 @@ import walkDown from '/imports/api/engine/computation/utility/walkdown'; import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies'; +import { Forest, TreeNode } from '/imports/api/parenting/parentingFunctions'; +import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties'; +import CreatureComputation from '/imports/api/engine/computation/CreatureComputation'; -export default function computeToggleDependencies(node, dependencyGraph, computation, forest) { +export default function computeToggleDependencies( + node: TreeNode, computation: CreatureComputation, forest: Forest +) { const prop = node.doc // Only for toggles if (prop.type !== 'toggle') return; @@ -12,11 +17,11 @@ export default function computeToggleDependencies(node, dependencyGraph, computa const target = forest.nodeIndex[targetId]; if (!target) return; target.doc._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(target.doc._id, prop._id, 'toggle'); + computation.dependencyGraph.addLink(target.doc._id, prop._id, 'toggle'); walkDown(target.children, child => { // The child nodes depend on the toggle child.doc._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); + computation.dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); }); }); } @@ -27,6 +32,6 @@ export default function computeToggleDependencies(node, dependencyGraph, computa walkDown(node.children, child => { // The child nodes depend on the toggle child.doc._computationDetails.toggleAncestors.push(prop); - dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); + computation.dependencyGraph.addLink(child.doc._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/engine/computation/buildComputation/linkInventory.js b/app/imports/api/engine/computation/buildComputation/linkInventory.js index faa2674c..6213834b 100644 --- a/app/imports/api/engine/computation/buildComputation/linkInventory.js +++ b/app/imports/api/engine/computation/buildComputation/linkInventory.js @@ -4,7 +4,7 @@ */ export default function linkInventory(forest, dependencyGraph) { // The stack of properties to still navigate - const stack = [...forest]; + const stack = [...forest.trees]; // The current containers we are inside of const containerStack = []; diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.ts similarity index 89% rename from app/imports/api/engine/computation/buildCreatureComputation.js rename to app/imports/api/engine/computation/buildCreatureComputation.ts index 3e5d415b..d9c4bd2f 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.ts @@ -1,5 +1,5 @@ -import { applyNestedSetProperties, calculateNestedSetOperations } from '/imports/api/parenting/parentingFunctions'; -import { DenormalisedOnlyCreaturePropertySchema as denormSchema } +import { applyNestedSetProperties } from '/imports/api/parenting/parentingFunctions'; +import { CreatureProperty, DenormalisedOnlyCreaturePropertySchema as denormSchema } from '/imports/api/creature/creatureProperties/CreatureProperties'; import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures'; import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex'; @@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields'; * computed toggles */ -export default function buildCreatureComputation(creatureId) { +export default function buildCreatureComputation(creatureId: string) { const creature = getCreature(creatureId); const variables = getVariables(creatureId); const properties = getProperties(creatureId); @@ -37,7 +37,9 @@ export default function buildCreatureComputation(creatureId) { return computation; } -export function buildComputationFromProps(properties, creature, variables) { +export function buildComputationFromProps( + properties: CreatureProperty[], creature, variables +) { const computation = new CreatureComputation(properties, creature, variables); // Dependency graph where edge(a, b) means a depends on b @@ -89,13 +91,13 @@ export function buildComputationFromProps(properties, creature, variables) { const forest = applyNestedSetProperties(properties); // Walk the property trees computing things that need to be inherited - walkDown(forest, node => { + walkDown(forest.trees, node => { computeInactiveStatus(node); }); // Inactive status must be complete for the whole tree before toggle deps // are calculated - walkDown(forest, node => { - computeToggleDependencies(node, dependencyGraph, computation, forest); + walkDown(forest.trees, node => { + computeToggleDependencies(node, computation, forest); computeSlotQuantityFilled(node, dependencyGraph); }); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js index cc762ae1..ceb006f1 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js @@ -1,7 +1,7 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import clean from '../../utility/cleanProp.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import clean from '../../utility/cleanProp.testFn'; export default async function () { const computation = buildComputationFromProps(testProperties); diff --git a/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js index ac19f8da..21b4897e 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computePointBuys.testFn.js @@ -1,7 +1,7 @@ -import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation'; import { assert } from 'chai'; -import computeCreatureComputation from '../../computeCreatureComputation.js'; -import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn.js'; +import computeCreatureComputation from '../../computeCreatureComputation'; +import { propsFromForest } from '/imports/api/properties/tests/propTestBuilder.testFn'; export default async function () { const computation = buildComputationFromProps(testProperties); diff --git a/app/imports/api/engine/computation/utility/walkdown.js b/app/imports/api/engine/computation/utility/walkdown.js deleted file mode 100644 index 0efad425..00000000 --- a/app/imports/api/engine/computation/utility/walkdown.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function walkDown(tree, callback){ - let stack = [...tree]; - while(stack.length){ - let node = stack.pop(); - callback(node, stack); - stack.push(...node.children); - } -} diff --git a/app/imports/api/engine/computation/utility/walkdown.ts b/app/imports/api/engine/computation/utility/walkdown.ts new file mode 100644 index 00000000..f0dd5e54 --- /dev/null +++ b/app/imports/api/engine/computation/utility/walkdown.ts @@ -0,0 +1,14 @@ +import { TreeDoc } from '/imports/api/parenting/ChildSchema'; +import { TreeNode } from '/imports/api/parenting/parentingFunctions'; + +export default function walkDown( + trees: TreeNode[], callback: (node: TreeNode, stack: TreeNode[]) => any +) { + const stack = [...trees]; + while (stack.length) { + const node = stack.pop(); + if (!node) return; + callback(node, stack); + stack.push(...node.children); + } +} diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index 86793165..0a685924 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -30,7 +30,6 @@ async function computeComputation(computation, creatureId) { logError.location = e.stack.split('\n')[1]; } console.error(logError); - throw e; } finally { checkPropertyCount(computation) writeErrors(creatureId, computation.errors); diff --git a/app/imports/api/engine/loadCreatures.ts b/app/imports/api/engine/loadCreatures.ts index d23dcdd1..a514c434 100644 --- a/app/imports/api/engine/loadCreatures.ts +++ b/app/imports/api/engine/loadCreatures.ts @@ -52,7 +52,7 @@ export function getSingleProperty(creatureId: string, propertyId: string) { return prop; } -export function getProperties(creatureId) { +export function getProperties(creatureId: string): CreatureProperty[] { const creature = loadedCreatures.get(creatureId); if (creature) { const props = Array.from(creature.properties.values()); @@ -94,7 +94,7 @@ export function getPropertiesOfType(creatureId, propType) { return props; } -export function getCreature(creatureId) { +export function getCreature(creatureId: string) { const loadedCreature = loadedCreatures.get(creatureId); const loadedCreatureDoc = loadedCreature?.creature; if (loadedCreatureDoc) { @@ -106,7 +106,7 @@ export function getCreature(creatureId) { return creature; } -export function getVariables(creatureId) { +export function getVariables(creatureId: string) { const loadedCreature = loadedCreatures.get(creatureId); const loadedVariables = loadedCreature?.variables; if (loadedVariables) { diff --git a/app/imports/api/files/server/s3FileStorage.js b/app/imports/api/files/server/s3FileStorage.ts similarity index 73% rename from app/imports/api/files/server/s3FileStorage.js rename to app/imports/api/files/server/s3FileStorage.ts index 27fbed67..5b698945 100644 --- a/app/imports/api/files/server/s3FileStorage.js +++ b/app/imports/api/files/server/s3FileStorage.ts @@ -2,9 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { each, clone } from 'lodash'; import { Random } from 'meteor/random'; -import { FilesCollection } from 'meteor/ostrio:files'; +import { FileObj, FileRef, FilesCollection, FilesCollectionConfig } from 'meteor/ostrio:files'; import stream from 'stream'; -import S3 from 'aws-sdk/clients/s3'; +import { S3 } from '@aws-sdk/client-s3'; /* See fs-extra and graceful-fs NPM packages */ /* For better i/o performance */ @@ -21,26 +21,32 @@ Meteor.settings.useS3 = !!( s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.endpoint ); -const bound = Meteor.bindEnvironment((callback) => { +const bound = Meteor.bindEnvironment((callback: () => any) => { return callback(); }); let createS3FilesCollection; +type S3Metadata = { + pipePath: string, +} + +type S3FilesCollection = FilesCollection & { + readJSONFile?: (file: FileObj) => Promise +}; + /* Check settings existence in `Meteor.settings` */ /* This is the best practice for app security */ if (Meteor.settings.useS3) { // Create a new S3 object const s3 = new S3({ - accessKeyId: s3Conf.key, - secretAccessKey: s3Conf.secret, + credentials: { + accessKeyId: s3Conf.key, + secretAccessKey: s3Conf.secret, + }, endpoint: s3Conf.endpoint, - sslEnabled: true, // optional - maxRetries: 10, - httpOptions: { - timeout: 12000, - agent: false - } + tls: true, + maxAttempts: 10, }); createS3FilesCollection = function ({ @@ -50,8 +56,15 @@ if (Meteor.settings.useS3) { onAfterUpload, debug,// = !Meteor.isProduction, allowClientCode = false, + }: { + collectionName: string, + storagePath: string, + onBeforeUpload: (...args: any[]) => any, + onAfterUpload: (...args: any[]) => any, + debug: boolean, + allowClientCode?: boolean, }) { - const collection = new FilesCollection({ + const filesCollection: S3FilesCollection = new FilesCollection({ collectionName, storagePath, onBeforeUpload, @@ -80,31 +93,35 @@ if (Meteor.settings.useS3) { Key: filePath, Body: fs.createReadStream(vRef.path), ContentType: vRef.type, - }, (error) => { + }, (error: Error) => { bound(() => { if (error) { - console.error(error); - } else { - // Update FilesCollection with link to the file at AWS - const upd = { $set: {} }; - upd['$set']['versions.' + version + '.meta.pipePath'] = filePath; - - this.collection.update({ - _id: fileRef._id - }, upd, (updError) => { - if (updError) { - console.error(updError); - } else { - // Unlink original files from FS after successful upload to AWS:S3 - this.unlink(this.collection.findOne(fileRef._id), version); - } - }); + return console.error(error); } + // Update FilesCollection with link to the file at AWS + // any should actually be Mongo.Modifier>, but the types aren't quite set up + // Right for mongo modifiers on version.meta + const upd: any = { + $set: { + [`versions.${version}.meta.pipePath`]: filePath + } + }; + + filesCollection.collection.update({ + _id: fileRef._id + }, upd, undefined, (updError: any) => { + if (updError) { + console.error(updError); + } else { + // Unlink original files from FS after successful upload to AWS:S3 + filesCollection.unlink(filesCollection.findOne(fileRef._id), version); + } + }); }); }); }); }, - interceptDownload(http, fileRef, version) { + interceptDownload(http: any, fileRef: FileRef, version: string) { // Intercept access to the file // And redirect request to AWS:S3 let path; @@ -122,20 +139,20 @@ if (Meteor.settings.useS3) { // and to keep original file name, content-type, // content-disposition, chunked "streaming" and cache-control // we're using low-level .serve() method - const opts = { + const opts: Parameters[0] = { Bucket: s3Conf.bucket, Key: path }; if (http.request.headers.range) { const vRef = fileRef.versions[version]; - let range = clone(http.request.headers.range); + const range = clone(http.request.headers.range); const array = range.split(/bytes=([0-9]*)-([0-9]*)/); const start = parseInt(array[1]); let end = parseInt(array[2]); if (isNaN(end)) { // Request data from AWS:S3 by small chunks - end = (start + this.chunkSize) - 1; + end = (start + (this.chunkSize || 0)) - 1; if (end >= vRef.size) { end = vRef.size - 1; } @@ -173,8 +190,8 @@ if (Meteor.settings.useS3) { allowClientCode, }); // Intercept FilesCollection's remove method to remove file from AWS:S3 - const _origRemove = collection.remove; - collection.remove = function (search) { + const _origRemove = filesCollection.remove; + filesCollection.remove = function (search) { const cursor = this.collection.find(search); cursor.forEach((fileRef) => { each(fileRef.versions, (vRef) => { @@ -183,7 +200,7 @@ if (Meteor.settings.useS3) { s3.deleteObject({ Bucket: s3Conf.bucket, Key: vRef.meta.pipePath, - }, (error) => { + }, (error: any) => { bound(() => { if (error) { console.error(error); @@ -195,18 +212,19 @@ if (Meteor.settings.useS3) { }); //remove original file from database - _origRemove.call(this, search); + return _origRemove.call(this, search); }; - collection.readJSONFile = async function (file) { + filesCollection.readJSONFile = async function (file: FileObj) { // If there is the pipepath, use s3 to get the file if (file?.versions?.original?.meta?.pipePath) { const path = file.versions.original.meta.pipePath; const data = await s3.getObject({ Bucket: s3Conf.bucket, Key: path - }).promise(); - return JSON.parse(data.Body.toString('utf-8')); + }); + if (!data.Body) return; + return JSON.parse(data.Body.toString()); } else { // Otherwise use the normal filesystem const fileString = await fsp.readFile(file.path, 'utf8'); @@ -214,7 +232,7 @@ if (Meteor.settings.useS3) { } }; - return collection; + return filesCollection; } } else { createS3FilesCollection = function ({ @@ -224,8 +242,8 @@ if (Meteor.settings.useS3) { onAfterUpload, debug,// = !Meteor.isProduction, allowClientCode = false, - }) { - const collection = new FilesCollection({ + }: FilesCollectionConfig) { + const collection: S3FilesCollection = new FilesCollection({ collectionName, storagePath, onBeforeUpload, diff --git a/app/imports/api/parenting/parentingFunctions.ts b/app/imports/api/parenting/parentingFunctions.ts index 1623961a..758aaf13 100644 --- a/app/imports/api/parenting/parentingFunctions.ts +++ b/app/imports/api/parenting/parentingFunctions.ts @@ -2,6 +2,7 @@ import { chain, reverse, set } from 'lodash'; import { TreeDoc, treeDocFields, Reference } from '/imports/api/parenting/ChildSchema'; import { getProperties } from '/imports/api/engine/loadCreatures'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties'; +import { Mongo } from 'meteor/mongo'; export function getCollectionByName(name: string): Mongo.Collection { const collection = Mongo.Collection.get(name) @@ -140,9 +141,9 @@ export function filterToForest( }); // Get the doc ancestors - let ancestors: object[] = []; + let ancestors: FilteredDoc[] = []; if (filter && includeFilteredDocAncestors) { - ancestors = collection.find(getFilter.ancestorsOfAll(docs), collectionOptions).map(doc => { + ancestors = collection.find(getFilter.ancestorsOfAll(docs), collectionOptions).map((doc: FilteredDoc) => { // Mark that the nodes are ancestors of the found nodes doc._ancestorOfMatchedDocument = true; return doc; @@ -172,7 +173,12 @@ export function filterToForest( return docsToForest(nodes); } -type ForestAndOrphans = { forest: TreeNode[], orphanIds: string[] } +export type Forest = { + trees: TreeNode[], + nodeIndex: { [_id: string]: TreeNode }, + orphanIds: string[], +} + /** * Takes a complete set of documents and builds a forest using just their `.parentIds` * Uses `.left` for sibling order within a parent only. @@ -182,31 +188,35 @@ type ForestAndOrphans = { forest: TreeNode[], orphanIds: string[] } * @returns forest: An array of tree nodes that each contain a document and its children. * orphans: an array of the same, but their parents weren't in the input array */ -export function docsToForestByParentId(docs: TreeDoc[]): ForestAndOrphans { +export function docsToForestByParentId(docs: T[]): Forest { // Collect all the docs in a dict by id - const nodesById = <{ [_id: string]: TreeNode }>{}; + const nodeIndex = <{ [_id: string]: TreeNode }>{}; docs.forEach(doc => { - nodesById[doc._id] = { doc, children: [] }; + nodeIndex[doc._id] = { doc, children: [] }; }); // Assign the docs to their parent or the forest or orphanage - const forest: TreeNode[] = []; + const trees: TreeNode[] = []; const orphanIds: string[] = []; docs.forEach(doc => { - const node = nodesById[doc._id]; + const node = nodeIndex[doc._id]; if (!doc.parentId) { // Root is parent - forest.push(node); - } else if (nodesById[doc.parentId]) { + trees.push(node); + } else if (nodeIndex[doc.parentId]) { // Parent is found - nodesById[doc.parentId].children.push(node); + nodeIndex[doc.parentId].children.push(node); } else { // Parent is missing, unset it, and store orphan node.doc.parentId = undefined; orphanIds.push(node.doc._id); - forest.push(node); + trees.push(node); } }); - return { forest, orphanIds }; + return { + trees, + orphanIds, + nodeIndex + }; } export const getFilter = { @@ -700,7 +710,7 @@ export async function rebuildCreatureNestedSets(creatureId) { * @returns */ export function calculateNestedSetOperations(docs: TreeDoc[]) { - const { forest: stack, orphanIds } = docsToForestByParentId(reverse(docs)); + const { trees: stack, orphanIds } = docsToForestByParentId(reverse(docs)); const removeMissingParentsOp = orphanIds.length ? { updateMany: { filter: { _id: { $in: orphanIds } }, @@ -763,11 +773,12 @@ export function calculateNestedSetOperations(docs: TreeDoc[]) { * @param docs An array of documents that share a common root. Must already be sorted by `.left` in ascending order * @returns The documents as a forest of tree nodes */ -export function applyNestedSetProperties(docs: TreeDoc[]) { +export function applyNestedSetProperties(docs: T[]): Forest { // Walk around the tree numbering left on the way down and right on the way up like so: - const { forest, orphanIds } = docsToForestByParentId(reverse([...docs])); + const forest = docsToForestByParentId(reverse([...docs])); + const { trees, orphanIds } = forest; - const stack = [...forest]; + const stack = [...trees]; const visitedNodes = new Set(); const visitedChildren = new Set(); let count = 1; diff --git a/app/imports/client/ui/creature/actions/ActionDialog.vue b/app/imports/client/ui/creature/actions/ActionDialog.vue index a2768178..334b5571 100644 --- a/app/imports/client/ui/creature/actions/ActionDialog.vue +++ b/app/imports/client/ui/creature/actions/ActionDialog.vue @@ -42,7 +42,6 @@