Skip to content

Commit

Permalink
Set correct path for uploaded file in sim config (#1054)
Browse files Browse the repository at this point in the history
* Set correct path for uploaded file in sim config

* Make eslint happy

* Undo unintentional change

* Provide type for filenames

* Add unit test and fix bug found by unit test

* Alphabetically sort keys for eslint

* Add PUBLIC_AERIE_FILE_STORE_PREFIX env var

* Reduce use of continue where simple if statement suffices

* Rename filenames to generatedFilenames and pathsToReplace
  • Loading branch information
mattdailis authored and JosephVolosin committed Oct 21, 2024
1 parent e204cd4 commit eba8119
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 18 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ORIGIN=http://localhost:3000
PUBLIC_AERIE_FILE_STORE_PREFIX=/usr/src/app/merlin_file_store/
PUBLIC_GATEWAY_CLIENT_URL=http://localhost:9000
PUBLIC_GATEWAY_SERVER_URL=http://localhost:9000
PUBLIC_HASURA_CLIENT_URL=http://localhost:8080/v1/graphql
Expand Down
19 changes: 10 additions & 9 deletions docs/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

This document provides detailed information about environment variables for Aerie UI.

| Name | Description | Type | Default |
| ------------------------------ | --------------------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ORIGIN` | Url of where the UI is served from. See the [Svelte Kit Adapter Node docs][svelte-kit-adapter-node-docs]. | `string` | http://localhost |
| `PUBLIC_GATEWAY_CLIENT_URL` | Url of the Gateway as called from the client (i.e. web browser) | `string` | http://localhost:9000 |
| `PUBLIC_GATEWAY_SERVER_URL` | Url of the Gateway as called from the server (i.e. Node.js container) | `string` | http://localhost:9000 |
| `PUBLIC_HASURA_CLIENT_URL` | Url of Hasura as called from the client (i.e. web browser) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_SERVER_URL` | Url of Hasura as called from the server (i.e. Node.js container) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_WEB_SOCKET_URL` | Url of Hasura called to establish a web-socket connection from the client | `string` | ws://localhost:8080/v1/graphql |
| `PUBLIC_LOGIN_PAGE` | Set to `enabled` to turn on login page. Otherwise set to `disabled` to turn off login page. | `string` | enabled |
| Name | Description | Type | Default |
| -------------------------------- | --------------------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ORIGIN` | Url of where the UI is served from. See the [Svelte Kit Adapter Node docs][svelte-kit-adapter-node-docs]. | `string` | http://localhost |
| `PUBLIC_AERIE_FILE_STORE_PREFIX` | Prefix to prepend to files uploaded through simulation configuration. | `string` | /usr/src/app/merlin_file_store/ |
| `PUBLIC_GATEWAY_CLIENT_URL` | Url of the Gateway as called from the client (i.e. web browser) | `string` | http://localhost:9000 |
| `PUBLIC_GATEWAY_SERVER_URL` | Url of the Gateway as called from the server (i.e. Node.js container) | `string` | http://localhost:9000 |
| `PUBLIC_HASURA_CLIENT_URL` | Url of Hasura as called from the client (i.e. web browser) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_SERVER_URL` | Url of Hasura as called from the server (i.e. Node.js container) | `string` | http://localhost:8080/v1/graphql |
| `PUBLIC_HASURA_WEB_SOCKET_URL` | Url of Hasura called to establish a web-socket connection from the client | `string` | ws://localhost:8080/v1/graphql |
| `PUBLIC_LOGIN_PAGE` | Set to `enabled` to turn on login page. Otherwise set to `disabled` to turn off login page. | `string` | enabled |
4 changes: 2 additions & 2 deletions src/components/simulation/SimulationPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
arguments: newArgumentsMap,
};
effects.updateSimulation($plan, newSimulation, user, newFiles);
effects.updateSimulation($plan, newSimulation, user, newFiles, $plan.model.parameters.parameters);
}
}
Expand All @@ -149,7 +149,7 @@
arguments: newArguments,
};
effects.updateSimulation($plan, newSimulation, user, newFiles);
effects.updateSimulation($plan, newSimulation, user, newFiles, $plan.model.parameters.parameters);
}
}
Expand Down
47 changes: 46 additions & 1 deletion src/utilities/effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import * as Errors from '../stores/errors';
import type { User } from '../types/app';
import type { Model } from '../types/model';
import type { ArgumentsMap, ParametersMap } from '../types/parameter';
import type { Plan } from '../types/plan';
import effects from './effects';
import effects, { replacePaths } from './effects';
import * as Modals from './modal';
import * as Requests from './requests';

Expand Down Expand Up @@ -310,4 +311,48 @@ describe('Handle modal and requests in effects', () => {
);
});
});

describe('replacePaths', () => {
it('should find and replace all matching paths in sim config', async () => {
const modelParameters: ParametersMap = {
parameter0: { order: 0, schema: { type: 'int' } },
parameter1: { order: 1, schema: { type: 'path' } },
parameter2: { order: 2, schema: { items: { x: { type: 'boolean' }, y: { type: 'path' } }, type: 'struct' } },
parameter3: {
order: 3,
schema: { items: { type: 'variant', variants: [{ key: 'A', label: 'A' }] }, type: 'series' },
},
parameter4: { order: 4, schema: { items: { type: 'path' }, type: 'series' } },
};
const simArgs: ArgumentsMap = {
parameter0: 1,
parameter1: 'abcdefg',
parameter2: {
x: true,
y: 'hijklmnop',
},
parameter3: ['A'],
parameter4: ['qrstuvwxyz', 'zyxwvut'],
};
const filenames = {
abcdefg: 'path1',
hijklmnop: 'path2',
qrstuvwxyz: 'path3',
zyxwvut: 'path4',
};

const res = replacePaths(modelParameters, simArgs, filenames);

expect(res).toEqual({
parameter0: 1,
parameter1: 'path1',
parameter2: {
x: true,
y: 'path2',
},
parameter3: ['A'],
parameter4: ['path3', 'path4'],
});
});
});
});
90 changes: 84 additions & 6 deletions src/utilities/effects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { env } from '$env/dynamic/public';
import type { CommandDictionary as AmpcsCommandDictionary } from '@nasa-jpl/aerie-ampcs';
import { get } from 'svelte/store';
import { SearchParameters } from '../enums/searchParameters';
Expand Down Expand Up @@ -54,10 +55,13 @@ import type { Extension, ExtensionPayload } from '../types/extension';
import type { Model, ModelInsertInput, ModelSchema, ModelSlim } from '../types/model';
import type { DslTypeScriptResponse, TypeScriptFile } from '../types/monaco';
import type {
Argument,
ArgumentsMap,
EffectiveArguments,
Parameter,
ParameterValidationError,
ParameterValidationResponse,
ParametersMap,
} from '../types/parameter';
import type {
PermissibleQueriesMap,
Expand Down Expand Up @@ -93,6 +97,7 @@ import type {
SchedulingSpecGoalInsertInput,
SchedulingSpecInsertInput,
} from '../types/scheduling';
import type { ValueSchema } from '../types/schema';
import type {
CommandDictionary,
GetSeqJsonResponse,
Expand Down Expand Up @@ -4107,18 +4112,41 @@ const effects = {
simulationSetInput: Simulation,
user: User | null,
newFiles: File[] = [],
modelParameters: ParametersMap | null = null,
): Promise<void> {
try {
if (!queryPermissions.UPDATE_SIMULATION(user, plan)) {
throwPermissionError('update this simulation');
}

const ids = await effects.uploadFiles(newFiles, user);
const original_filename_to_id: Record<string, number> = {};
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (id !== null) {
original_filename_to_id[newFiles[i].name] = id;
}
}

// The aerie gateway mangles the names of uploaded files to ensure uniqueness.
// Here, we use the ids of the files we just uploaded to look up the generated filenames
const generatedFilenames: Record<string, string> = {};
for (const newFile of newFiles) {
const id = original_filename_to_id[newFile.name];
const response = (await reqHasura<[{ name: string }]>(gql.GET_UPLOADED_FILENAME, { id }, user))[
'uploaded_file'
];
if (response !== null) {
generatedFilenames[newFile.name] = `${env.PUBLIC_AERIE_FILE_STORE_PREFIX}${response[0]['name']}`;
}
}

const data = await reqHasura<Pick<Simulation, 'id'>>(
gql.UPDATE_SIMULATION,
{
id: simulationSetInput.id,
simulation: {
arguments: simulationSetInput.arguments,
arguments: replacePaths(modelParameters, simulationSetInput.arguments, generatedFilenames),
simulation_end_time: simulationSetInput?.simulation_end_time ?? null,
simulation_start_time: simulationSetInput?.simulation_start_time ?? null,
simulation_template_id: simulationSetInput?.template?.id ?? null,
Expand All @@ -4127,7 +4155,6 @@ const effects = {
user,
);
if (data.updateSimulation !== null) {
await effects.uploadFiles(newFiles, user);
showSuccessToast('Simulation Updated Successfully');
} else {
throw Error(`Unable to update simulation with ID: "${simulationSetInput.id}"`);
Expand Down Expand Up @@ -4266,15 +4293,16 @@ const effects = {
}
},

async uploadFiles(files: File[], user: User | null): Promise<boolean> {
async uploadFiles(files: File[], user: User | null): Promise<(number | null)[]> {
try {
const ids = [];
for (const file of files) {
await effects.uploadFile(file, user);
ids.push(await effects.uploadFile(file, user));
}
return true;
return ids;
} catch (e) {
catchError(e as Error);
return false;
return [];
}
},

Expand Down Expand Up @@ -4359,4 +4387,54 @@ const effects = {
},
};

/**
* Traverses the given simulation arguments and does a "find and replace", replacing any paths that match the keys of `pathsToReplace` with the corresponding values.
*
* @param modelParameters The type definitions of the mission model parameters. Used to determine which parameters have type 'path'.
* @param simArgs The full simulation arguments, which are assumed to conform to the above type definition.
* @param pathsToReplace A map from old paths to new paths. Any occurrences of old paths in simArgs will be replaced with new paths.
* @returns
*/
export function replacePaths(
modelParameters: ParametersMap | null,
simArgs: ArgumentsMap,
pathsToReplace: Record<string, string>,
): ArgumentsMap {
if (modelParameters === null) {
return simArgs;
}
const result: ArgumentsMap = {};
for (const parameterName in modelParameters) {
const parameter: Parameter = modelParameters[parameterName];
const arg: Argument = simArgs[parameterName];
if (arg !== undefined) {
result[parameterName] = replacePathsHelper(parameter.schema, arg, pathsToReplace);
}
}
return result;
}

function replacePathsHelper(schema: ValueSchema, arg: Argument, pathsToReplace: Record<string, string>) {
switch (schema.type) {
case 'path':
if (arg in pathsToReplace) {
return pathsToReplace[arg];
} else {
return arg;
}
case 'struct':
return (function () {
const res: Argument = {};
for (const key in schema.items) {
res[key] = replacePathsHelper(schema.items[key], arg[key], pathsToReplace);
}
return res;
})();
case 'series':
return arg.map((x: Argument) => replacePathsHelper(schema.items, x, pathsToReplace));
default:
return arg;
}
}

export default effects;
8 changes: 8 additions & 0 deletions src/utilities/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,14 @@ const gql = {
}
`,

GET_UPLOADED_FILENAME: `#graphql
query GetUploadedFileName($id: Int!) {
uploaded_file(where: { id: { _eq: $id }}) {
name
}
}
`,

GET_USER_SEQUENCE: `#graphql
query GetUserSequence($id: Int!) {
userSequence: user_sequence_by_pk(id: $id) {
Expand Down

0 comments on commit eba8119

Please sign in to comment.