Fixing UI for 2.1 data changes

This commit is contained in:
Thaum Rystra
2024-03-23 16:02:28 +02:00
parent 293deaa592
commit 359d645f6b
40 changed files with 2142 additions and 510 deletions

View File

@@ -11,6 +11,7 @@
"meteortesting",
"nearley",
"ngraph",
"ostrio",
"uncomputed",
"walkdown"
]

View File

@@ -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<MetadataType> {
extension: string;
meta: MetadataType;
path: string;
size: number;
type: string;
}
class FileObj<MetadataType> {
_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<MetadataType>;
};
mime: string;
'mime-type': string;
}
class FileRef<MetadataType> extends FileObj<MetadataType> {
remove: (callback?: (error: Meteor.Error) => void) => void;
link: (version?: string, location?: string) => string;
get: (property?: string) => any;
fetch: () => Array<FileObj<MetadataType>>;
with: () => FileCursor<MetadataType>;
}
interface FileData<MetadataType> {
size: number;
type: string;
mime: string;
'mime-type': string;
ext: string;
extension: string;
name: string;
meta: MetadataType;
}
interface FilesCollectionConfig<MetadataType> {
storagePath?: string | ((fileObj: FileObj<MetadataType>) => string);
collection?: Mongo.Collection<FileObj<MetadataType>>;
collectionName?: string;
continueUploadTTL?: string;
ddp?: object;
cacheControl?: string;
responseHeaders?: { [x: string]: string } | ((responseCode?: string, fileRef?: FileRef<MetadataType>, versionRef?: Version<MetadataType>, version?: string) => { [x: string]: string });
throttle?: number | boolean;
downloadRoute?: string;
schema?: SimpleSchemaDefinition;
chunkSize?: number;
namingFunction?: (fileObj: FileObj<MetadataType>) => string;
permissions?: number;
parentDirPermissions?: number;
integrityCheck?: boolean;
strict?: boolean;
downloadCallback?: (this: ContextHTTP & ContextUser, fileObj: FileObj<MetadataType>) => boolean;
protected?: boolean | ((this: ContextHTTP & ContextUser, fileObj: FileObj<MetadataType>) => boolean | number);
public?: boolean;
onBeforeUpload?: (this: ContextUpload & ContextUser, fileData: FileData<MetadataType>) => boolean | string;
onBeforeRemove?: (this: ContextUser, cursor: Mongo.Cursor<FileObj<MetadataType>>) => boolean;
onInitiateUpload?: (this: ContextUpload & ContextUser, fileData: FileData<MetadataType>) => void;
onAfterUpload?: (fileRef: FileRef<MetadataType>) => any;
onAfterRemove?: (files: ReadonlyArray<FileObj<MetadataType>>) => any;
onbeforeunloadMessage?: string | (() => string);
allowClientCode?: boolean;
debug?: boolean;
interceptDownload?: (http: object, fileRef: FileRef<MetadataType>, version: string) => boolean;
}
interface SearchOptions<MetadataType, TransformAdditions> {
sort?: Mongo.SortSpecifier;
skip?: number;
limit?: number;
fields?: Mongo.FieldSpecifier;
reactive?: boolean;
transform?: (fileObj: FileObj<MetadataType>) => FileObj<MetadataType> & TransformAdditions;
}
interface InsertOptions<MetadataType> {
file: File | object | string;
fileId?: string;
fileName?: string;
isBase64?: boolean;
meta?: MetadataType;
transport?: 'ddp' | 'http';
ddp?: object;
onStart?: (error: Meteor.Error, fileData: FileData<MetadataType>) => any;
onUploaded?: (error: Meteor.Error, fileRef: FileRef<MetadataType>) => any;
onAbort?: (fileData: FileData<MetadataType>) => any;
onError?: (error: Meteor.Error, fileData: FileData<MetadataType>) => any;
onProgress?: (progress: number, fileData: FileData<MetadataType>) => any;
onBeforeUpload?: (fileData: FileData<MetadataType>) => any;
chunkSize?: number | 'dynamic';
allowWebWorkers?: boolean;
type?: string;
}
interface LoadOptions<MetadataType> {
fileName: string;
meta?: MetadataType;
type?: string;
size?: number;
userId?: string;
fileId?: string;
}
class FileUpload {
file: File;
onPause: ReactiveVar<boolean>;
progress: ReactiveVar<number>;
estimateTime: ReactiveVar<number>;
estimateSpeed: ReactiveVar<number>;
state: ReactiveVar<'active' | 'paused' | 'aborted' | 'completed'>;
pause(): void;
continue(): void;
toggle(): void;
pipe(): void;
start(): void;
on(event: string, callback: () => void): void;
}
class FileCursor<MetadataType> extends FileRef<MetadataType> { }
class FilesCursor<MetadataType, TransformAdditions> extends Mongo.Cursor<FileObj<MetadataType>> {
cursor: Mongo.Cursor<FileObj<MetadataType>>; // Refers to base cursor? Why is this existing?
get(): Array<FileCursor<MetadataType> & TransformAdditions>;
hasNext(): boolean;
next(): FileCursor<MetadataType> & TransformAdditions;
hasPrevious(): boolean;
previous(): FileCursor<MetadataType> & TransformAdditions;
first(): FileCursor<MetadataType> & TransformAdditions;
last(): FileCursor<MetadataType> & TransformAdditions;
remove(callback?: (err: object) => void): void;
each(callback: (cursor: FileCursor<MetadataType> & TransformAdditions) => void): void;
current(): object | undefined;
}
class FilesCollection<MetadataType = { [x: string]: any }> {
collection: Mongo.Collection<FileObj<MetadataType>>;
schema: SimpleSchemaDefinition;
constructor(config: FilesCollectionConfig<MetadataType>)
/**
* 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<TransformAdditions = {}>(
selector?: Mongo.Selector<Partial<FileObj<MetadataType>>>,
options?: SearchOptions<MetadataType, TransformAdditions>
): FilesCursor<MetadataType, TransformAdditions>;
/**
* 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<TransformAdditions = {}>(
selector?: Mongo.Selector<Partial<FileObj<MetadataType>>> | string,
options?: SearchOptions<MetadataType, TransformAdditions>
): FileCursor<MetadataType> & TransformAdditions;
insert(settings: InsertOptions<MetadataType>, autoStart?: boolean): FileUpload;
remove(select: Mongo.Selector<FileObj<MetadataType>> | string, callback?: (error: Meteor.Error) => void): FilesCollection<MetadataType>;
update(select: Mongo.Selector<FileObj<MetadataType>> | string, modifier: Mongo.Modifier<FileObj<MetadataType>>, options?: {
multi?: boolean;
upsert?: boolean;
arrayFilters?: Array<{ [identifier: string]: any }>;
}, callback?: (error: Meteor.Error, insertedCount: number) => void): FilesCollection<MetadataType>;
link(fileRef: FileRef<MetadataType>, version?: string): string;
allow(options: Mongo.AllowDenyOptions): void;
deny(options: Mongo.AllowDenyOptions): void;
denyClient(): void;
on(event: string, callback: (fileRef: FileRef<MetadataType>) => void): void;
unlink(fileRef: FileRef<MetadataType>, version?: string): FilesCollection<MetadataType>;
addFile(path: string, opts: LoadOptions<MetadataType>, callback?: (err: any, fileRef: FileRef<MetadataType>) => any, proceedAfterUpload?: boolean): FilesCollection<MetadataType>;
load(url: string, opts: LoadOptions<MetadataType>, callback?: (err: object, fileRef: FileRef<MetadataType>) => any, proceedAfterUpload?: boolean): FilesCollection<MetadataType>;
write(buffer: Buffer, opts: LoadOptions<MetadataType>, callback?: (err: object, fileRef: FileRef<MetadataType>) => any, proceedAfterUpload?: boolean): FilesCollection<MetadataType>;
}
}

15
app/imports/@types/vue-meteor.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue';
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
meteor?: any;
}
}
declare module 'vue/types/vue' {
interface Vue {
$subscribe: (name: string, params: any[]) => void;
$autorun: (fn: () => void) => number;
$subReady: Record<string, boolean>;
}
}

View File

@@ -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');
}
});
}

View File

@@ -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<any> = 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

View File

@@ -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));

View File

@@ -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<EngineAction>('actions');

View File

@@ -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.');
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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',

View File

@@ -8,12 +8,12 @@ interface CreatureProperty {
}
export default class CreatureComputation {
originalPropsById: object;
propsById: object;
propsWithTag: object;
scope: object;
originalPropsById: Record<string, CreatureProperty>;
propsById: Record<string, CreatureProperty>;
propsWithTag: Record<string, string[]>;
scope: Record<string, any>;
props: Array<CreatureProperty>;
dependencyGraph: Graph;
dependencyGraph: Graph<any, string>;
errors: Array<object>;
creature: object;
variables: object;

View File

@@ -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) {

View File

@@ -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<CreatureProperty>, computation: CreatureComputation, forest: Forest<CreatureProperty>
) {
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');
});
}

View File

@@ -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 = [];

View File

@@ -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);
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -0,0 +1,14 @@
import { TreeDoc } from '/imports/api/parenting/ChildSchema';
import { TreeNode } from '/imports/api/parenting/parentingFunctions';
export default function walkDown<T extends TreeDoc>(
trees: TreeNode<T>[], callback: (node: TreeNode<T>, stack: TreeNode<T>[]) => any
) {
const stack = [...trees];
while (stack.length) {
const node = stack.pop();
if (!node) return;
callback(node, stack);
stack.push(...node.children);
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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<S3Metadata> & {
readJSONFile?: (file: FileObj<S3Metadata>) => Promise<any>
};
/* 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<S3Metadata>({
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<FileObj<S3Metadata>>, 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<S3Metadata>, 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<typeof s3.getObject>[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<S3Metadata>) {
// 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<S3Metadata>) {
const collection: S3FilesCollection = new FilesCollection<S3Metadata>({
collectionName,
storagePath,
onBeforeUpload,

View File

@@ -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<TreeDoc> {
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<TreeDoc>[], orphanIds: string[] }
export type Forest<T extends TreeDoc> = {
trees: TreeNode<T>[],
nodeIndex: { [_id: string]: TreeNode<T> },
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<TreeDoc>[], 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<T extends TreeDoc>(docs: T[]): Forest<T> {
// Collect all the docs in a dict by id
const nodesById = <{ [_id: string]: TreeNode<TreeDoc> }>{};
const nodeIndex = <{ [_id: string]: TreeNode<T> }>{};
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<TreeDoc>[] = [];
const trees: TreeNode<T>[] = [];
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<T extends TreeDoc>(docs: T[]): Forest<T> {
// 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<T>(reverse([...docs]));
const { trees, orphanIds } = forest;
const stack = [...forest];
const stack = [...trees];
const visitedNodes = new Set();
const visitedChildren = new Set();
let count = 1;

View File

@@ -42,7 +42,6 @@
<script lang="js">
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
import Actions, { runAction } from '/imports/api/engine/actions/ActionEngine';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
@@ -80,10 +79,7 @@ export default {
},
methods: {
async apply(stepThrough) {
await runAction.callAsync({
actionId: this.actionId,
stepThrough
});
throw new Error('Not implemented')
},
cancel() {
this.$store.dispatch('popDialogStack');

View File

@@ -151,7 +151,7 @@
creatureId: this.creatureId,
noBackdropClose: true,
},
callback(result){
async callback(result){
if (!result){
return 'insert-creature-property-fab';
}
@@ -164,7 +164,7 @@
revealFab(fab);
let creatureProperty = result;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
let id = await insertProperty.callAsync({creatureProperty, parentRef});
return forcedType ? id : `tree-node-${id}`;
}
}

View File

@@ -30,7 +30,7 @@
</v-icon>
<span
v-if="noLinks"
:key="prop._id"
:key="prop._id + '-no-links'"
>
<tree-node-view
:model="prop"
@@ -53,8 +53,9 @@
</template>
<script lang="js">
import { fetchDocByRef } from '/imports/api/parenting/parentingFunctions';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
import { Mongo } from 'meteor/mongo';
export default {
components: {
@@ -75,11 +76,11 @@
embedded: Boolean,
},
computed:{
props(){
return this.model.ancestors
.slice(1)
.map(ref => fetchDocByRef(ref))
.filter(prop => (this.collection !== 'creatureProperties' || prop.type !== 'propertySlot'));
props() {
return Mongo.Collection.get(this.collection).find({
...getFilter.ancestors(this.model),
...this.collection === 'creatureProperties' && { type: { $ne: 'propertySlot' } }
});
},
},
methods: {

View File

@@ -52,20 +52,21 @@
/>
</v-fade-transition>
</template>
<div
v-if="!embedded"
slot="actions"
class="layout"
>
<v-spacer />
<v-btn
text
color="accent"
@click="$store.dispatch('popDialogStack')"
<template #actions>
<div
v-if="!embedded"
class="layout"
>
Close
</v-btn>
</div>
<v-spacer />
<v-btn
text
color="accent"
@click="$store.dispatch('popDialogStack')"
>
Close
</v-btn>
</div>
</template>
</dialog-base>
</template>
@@ -129,12 +130,7 @@ export default {
computed: {
creature(){
if (!this.model) return;
let nearestCreatureAncestor = findLast(
this.model.ancestors,
ref => ref.collection === 'creatures'
);
if (!nearestCreatureAncestor) return;
return Creatures.findOne(nearestCreatureAncestor.id);
return Creatures.findOne(this.model.root.id);
},
creatureId(){
return this.creature && this.creature._id;

View File

@@ -6,27 +6,27 @@
<slot
name="replace-toolbar"
:flat="!offsetTop"
/>
<v-toolbar
v-if="!$scopedSlots['replace-toolbar']"
:color="computedColor"
:dark="isDark"
:light="!isDark"
class="base-dialog-toolbar"
:flat="!offsetTop"
>
<v-toolbar
:color="computedColor"
:dark="isDark"
:light="!isDark"
class="base-dialog-toolbar"
:flat="!offsetTop"
<v-btn
icon
@click="back"
>
<v-btn
icon
@click="back"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<slot name="toolbar" />
<slot
slot="extension"
name="toolbar-extension"
/>
</v-toolbar>
</slot>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<slot name="toolbar" />
<slot
slot="extension"
name="toolbar-extension"
/>
</v-toolbar>
<div
v-if="$slots['unwrapped-content']"
id="base-dialog-body"

View File

@@ -64,9 +64,9 @@
</template>
<script lang="js">
import numberToSignedString from '../../../../../api/utility/numberToSignedString';
import doCheck from '/imports/api/engine/action/methods/doCheck';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import doCheck from '/imports/api/engine/actions/doCheck';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
export default {

View File

@@ -47,10 +47,10 @@
</template>
<script lang="js">
import numberToSignedString from '../../../../../api/utility/numberToSignedString';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import doCheck from '/imports/api/engine/actions/doCheck';
import doCheck from '/imports/api/engine/action/methods/doCheck.js';
import {snackbar} from '/imports/client/ui/components/snackbars/SnackbarQueue';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
export default {
components: {

View File

@@ -32,8 +32,8 @@
<script lang="js">
import SpellSlotListTile from '/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue';
import doCastSpell from '/imports/api/engine/actions/doCastSpell';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import doCastSpell from '/imports/api/engine/action/methods/doCastSpell';
export default {
components: {

View File

@@ -58,11 +58,11 @@
</template>
<script lang="js">
import numberToSignedString from '../../../../../api/utility/numberToSignedString';
import ProficiencyIcon from '/imports/client/ui/properties/shared/ProficiencyIcon.vue';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import doCheck from '/imports/api/engine/actions/doCheck';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import doCheck from '/imports/api/engine/action/methods/doCheck';
export default {
components: {

View File

@@ -125,7 +125,7 @@ import AttributeConsumedView from '/imports/client/ui/properties/components/acti
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty';
import doCastSpell from '/imports/api/engine/actions/doCastSpell';
import doCastSpell from '/imports/api/engine/action/methods/doCastSpell.js';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
export default {
@@ -254,4 +254,3 @@ export default {
height: 40px;
}
</style>
../../../../api/engine/action/methods/doCastSpell

View File

@@ -40,6 +40,7 @@
v-bind="$attrs"
>
<slot>
default slot content
<template v-if="value !== undefined">
{{ valueText }}
</template>

View File

@@ -8,6 +8,8 @@ export default function migrate2To3(archive) {
if (prop.parent?.collection === 'creatureProperties') {
prop.parentId = prop.parent.id;
}
prop.left = prop.order;
prop.right = prop.order;
} catch (e) {
console.warn('Property migration 2 -> 3 failed: ', { propId: prop._id, error: e.message || e.reason || e.toString() });
}

View File

@@ -54,7 +54,7 @@ Meteor.publish('singleCharacter', function (creatureId) {
_creatureId: creatureId,
}),
CreatureProperties.find({
'ancestors.id': creatureId,
'root.id': creatureId,
}),
CreatureLogs.find({
creatureId,

1984
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "dicecloud",
"version": "2.1.0",
"description": "Unofficial Online Realtime D&D 5e App",
"description": "Online Realtime TTRPG Engine",
"license": "GPL-3.0",
"repository": {
"type": "git",
@@ -22,12 +22,12 @@
"npm": "6.13.x"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.523.0",
"@babel/runtime": "^7.23.9",
"@chenfengyuan/vue-countdown": "^1.1.5",
"@tozd/vue-observer-utils": "^0.5.0",
"@types/meteor": "^2.9.8",
"alea": "^1.0.1",
"aws-sdk": "^2.1561.0",
"bcrypt": "^5.1.1",
"chroma-js": "^2.4.2",
"css-box-shadow": "^1.0.0-3",
@@ -58,17 +58,18 @@
"speakingurl": "^14.0.1",
"three": "^0.156.1",
"vivagraphjs": "^0.12.0",
"vue": "2.6.10",
"vue": "2.6.14",
"vue-meteor-tracker": "^2.0.0",
"vue-reactive-provide": "^0.3.0",
"vue-router": "^3.6.5",
"vuedraggable": "^2.23.2",
"vuetify": "^2.7.2",
"vuetify": "2.6.15",
"vuetify-upload-button": "^2.0.2",
"vuex": "^3.1.3"
},
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/lodash": "^4.14.202",
"@types/mocha": "^10.0.6",
"@types/simpl-schema": "^1.12.7",
"@typescript-eslint/eslint-plugin": "^5.62.0",

17
package-lock.json generated
View File

@@ -1,17 +0,0 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@types/lodash": {
"version": "4.14.202",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
"dev": true
},
"@types/nearley": {
"version": "2.11.5",
"resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz",
"integrity": "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA=="
}
}
}