Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1635 Add example experiments creation #377

Merged
merged 32 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3c2081f
Add get example experiments endpoint
cosa65 Jun 20, 2022
73953b6
Add return of sample file ids in Sample.getSamples
cosa65 Jun 21, 2022
5965731
Merge branch 'master' into 1635-add-example-experiments-creation
cosa65 Jun 21, 2022
b987194
Add experiment clone endpoint
cosa65 Jun 21, 2022
ffb177a
Cleanup
cosa65 Jun 21, 2022
f61c436
Minor readbilitity improvement
cosa65 Jun 22, 2022
4899bc4
Fix merge conflicts
cosa65 Jun 22, 2022
79f7c3f
Update snapshots
cosa65 Jun 22, 2022
010d4a1
Fix tests and minor refactoreing
cosa65 Jun 22, 2022
6c573c4
Update model/experiment tests
cosa65 Jun 22, 2022
1ab8b6b
Merge branch 'master' into 1635-add-example-experiments-creation
cosa65 Jun 22, 2022
3934bd7
Update experimentController.js
cosa65 Jun 22, 2022
fb5da33
Update experimentController.js
cosa65 Jun 22, 2022
d02bd4d
Add samplesSubsetIds optional body in clone endpoint
cosa65 Jun 22, 2022
8dd4956
Rename
cosa65 Jun 22, 2022
9796c2a
Refactor way in which we get the sample ids
cosa65 Jun 22, 2022
5d3390d
Add model/Sample copyTo tests
cosa65 Jun 22, 2022
1c0ca31
Add getAllExampleExperiments test
cosa65 Jun 22, 2022
0985cbb
Add route tests
cosa65 Jun 22, 2022
10cd9f7
Add some logs
cosa65 Jun 22, 2022
dd3157e
Add tests for experimentController
cosa65 Jun 22, 2022
252bf91
Add experimentController.cloneExperiment tests
cosa65 Jun 22, 2022
b31ecbc
Add check on any metadata tracks existing before doing queries based …
cosa65 Jun 22, 2022
6aa021b
Update tests
cosa65 Jun 22, 2022
6174104
Change endpoints based on pols comments
cosa65 Jun 23, 2022
5d6c841
Update tests
cosa65 Jun 23, 2022
61f91ea
Rename to fromSamples
cosa65 Jun 23, 2022
63b282d
Rename
cosa65 Jun 23, 2022
efd9f6c
Fix comment
cosa65 Jun 23, 2022
c9426d4
Refactor
cosa65 Jun 23, 2022
c2b71d2
Rename
cosa65 Jun 23, 2022
9040c9a
Update experimentController.js
cosa65 Jun 23, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions src/api.v2/controllers/experimentController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,42 @@ const { OK, NotFoundError } = require('../../utils/responses');
const sqlClient = require('../../sql/sqlClient');

const getExperimentBackendStatus = require('../helpers/backendStatus/getExperimentBackendStatus');
const Sample = require('../model/Sample');

const logger = getLogger('[ExperimentController] - ');

const getAllExperiments = async (req, res) => {
const { user: { sub: userId } } = req;
logger.log(`Getting all experiments for user: ${userId}`);

const data = await new Experiment().getAllExperiments(userId);

logger.log(`Finished getting all experiments for user: ${userId}, length: ${data.length}`);
res.json(data);
};

const getAllExampleExperiments = async (req, res) => {
cosa65 marked this conversation as resolved.
Show resolved Hide resolved
logger.log('Getting example experiments');

const data = await new Experiment().getAllExampleExperiments();

logger.log(`Finished getting example experiments, length: ${data.length}`);
res.json(data);
};

const getExperiment = async (req, res) => {
const { params: { experimentId } } = req;

logger.log(`Getting experiment ${experimentId}`);

const data = await new Experiment().getExperimentData(experimentId);

logger.log(`Finished getting experiment ${experimentId}`);

res.json(data);
};

const createExperiment = async (req, res) => {
const { params: { experimentId }, user, body } = req;
const { name, description } = body;

logger.log('Creating experiment');

await sqlClient.get().transaction(async (trx) => {
Expand Down Expand Up @@ -68,7 +77,6 @@ const deleteExperiment = async (req, res) => {
throw new NotFoundError(`Experiment ${experimentId} not found`);
}


logger.log(`Finished deleting experiment ${experimentId}`);
res.json(OK());
};
Expand Down Expand Up @@ -129,11 +137,51 @@ const downloadData = async (req, res) => {
logger.log(`Providing download link for download ${downloadType} for experiment ${experimentId}`);

const downloadLink = await new Experiment().getDownloadLink(experimentId, downloadType);

logger.log(`Finished providing download link for download ${downloadType} for experiment ${experimentId}`);
res.json(downloadLink);
};


const cloneExperiment = async (req, res) => {
const getAllSampleIds = async (experimentId) => {
const { samplesOrder } = await new Experiment().findById(experimentId).first();
return samplesOrder;
};

const {
params: { experimentId: fromExperimentId },
body: { samplesSubsetIds = await getAllSampleIds(fromExperimentId) },
cosa65 marked this conversation as resolved.
Show resolved Hide resolved
user: { sub: userId },
} = req;

logger.log(`Creating experiment to clone ${fromExperimentId} to`);

let toExperimentId;

await sqlClient.get().transaction(async (trx) => {
toExperimentId = await new Experiment(trx).createCopy(fromExperimentId);
await new UserAccess(trx).createNewExperimentPermissions(userId, toExperimentId);
});

logger.log(`Cloning experiment ${fromExperimentId} into ${toExperimentId}`);

const clonedSamplesOrder = await new Sample()
.copyTo(fromExperimentId, toExperimentId, samplesSubsetIds);

await new Experiment().updateById(
toExperimentId,
{ samples_order: JSON.stringify(clonedSamplesOrder) },
);

logger.log(`Finished cloning experiment ${fromExperimentId}, new expeirment's id is ${toExperimentId}`);
cosa65 marked this conversation as resolved.
Show resolved Hide resolved

res.json(toExperimentId);
};

module.exports = {
getAllExperiments,
getAllExampleExperiments,
getExperiment,
createExperiment,
updateProcessingConfig,
Expand All @@ -143,4 +191,5 @@ module.exports = {
getProcessingConfig,
getBackendStatus,
downloadData,
cloneExperiment,
};
5 changes: 4 additions & 1 deletion src/api.v2/helpers/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ const allowedResources = {
'socket',
'/experiments/(?<experimentId>.*)/plots-tables/(?<plotUuid>.*)',
'/experiments/(?<experimentId>.*)/cellSets',
'/experiments/(?<experimentId>.*)/clone',
],
[VIEWER]: [
'/experiments/(?<experimentId>.*)/clone',
],
[VIEWER]: [],
};
const isRoleAuthorized = (role, resource, method) => {
// if no valid role is provided, return not authorized
Expand Down
1 change: 0 additions & 1 deletion src/api.v2/helpers/worker/getWorkResults.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ const getWorkResults = async (experimentId, ETag) => {
const formattedExperimentId = formatExperimentId(experimentId);

await validateTagMatching(formattedExperimentId, params);
console.log('GETTING WORK RESULTS IWTH PARAMS ', params);
logger.log(`Found worker results for experiment: ${experimentId}, Etag: ${ETag}`);

const signedUrl = getSignedUrl('getObject', params);
Expand Down
40 changes: 34 additions & 6 deletions src/api.v2/model/Experiment.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const _ = require('lodash');
const { v4: uuidv4 } = require('uuid');

const BasicModel = require('./BasicModel');
const sqlClient = require('../../sql/sqlClient');
Expand All @@ -12,6 +13,7 @@ const config = require('../../config');
const getLogger = require('../../utils/getLogger');
const bucketNames = require('../helpers/s3/bucketNames');
const { getSignedUrl } = require('../../utils/aws/s3');
const constants = require('../../utils/constants');

const logger = getLogger('[ExperimentModel] - ');

Expand Down Expand Up @@ -43,20 +45,26 @@ class Experiment extends BasicModel {
];

const aliasedExperimentFields = fields.map((field) => `e.${field}`);
function mainQuery() {
this.select([...aliasedExperimentFields, 'm.key'])

const result = await collapseKeyIntoArray(
cosa65 marked this conversation as resolved.
Show resolved Hide resolved
this.sql.select([...aliasedExperimentFields, 'm.key'])
.from(tableNames.USER_ACCESS)
.where('user_id', userId)
.join(`${tableNames.EXPERIMENT} as e`, 'e.id', `${tableNames.USER_ACCESS}.experiment_id`)
.leftJoin(`${tableNames.METADATA_TRACK} as m`, 'e.id', 'm.experiment_id')
.as('mainQuery');
}

const result = await collapseKeyIntoArray(mainQuery, [...fields], 'key', 'metadataKeys', this.sql);
.as('mainQuery'), [...fields],
'key',
'metadataKeys',
this.sql,
);

return result;
}

async getAllExampleExperiments() {
return this.getAllExperiments(constants.PUBLIC_ACCESS_ID);
}

async getExperimentData(experimentId) {
function mainQuery() {
this.select('*')
Expand Down Expand Up @@ -98,6 +106,26 @@ class Experiment extends BasicModel {
return result;
}

async createCopy(fromExperimentId) {
const toExperimentId = uuidv4().replace(/-/g, '');

const { sql } = this;

await sql
.insert(
sql(tableNames.EXPERIMENT)
.select(
sql.raw('? as id', [toExperimentId]),
'name',
'description',
)
.where({ id: fromExperimentId }),
)
.into(sql.raw(`${tableNames.EXPERIMENT} (id, name, description)`));

return toExperimentId;
}

// Sets samples_order as an array that has the sample in oldPosition moved to newPosition
async updateSamplePosition(
experimentId, oldPosition, newPosition,
Expand Down
74 changes: 74 additions & 0 deletions src/api.v2/model/Sample.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
const _ = require('lodash');

const { v4: uuidv4 } = require('uuid');

const BasicModel = require('./BasicModel');
const sqlClient = require('../../sql/sqlClient');

Expand Down Expand Up @@ -38,6 +42,11 @@ class Sample extends BasicModel {
const sampleFileFields = ['sample_file_type', 'size', 'upload_status', 's3_path'];
const sampleFileFieldsWithAlias = sampleFileFields.map((field) => `sf.${field}`);
const fileObjectFormatted = sampleFileFields.map((field) => [`'${field}'`, field]);

// Add sample file id (needs to go separate to avoid conflict with sample id)
sampleFileFieldsWithAlias.push('sf.id as sf_id');
fileObjectFormatted.push(['\'id\'', 'sf_id']);
aerlaut marked this conversation as resolved.
Show resolved Hide resolved

const sampleFileObject = `jsonb_object_agg(sample_file_type,json_build_object(${fileObjectFormatted})) as files`;
const fileNamesQuery = sql.select(['id', sql.raw(sampleFileObject)])
.from(sql.select([...sampleFileFieldsWithAlias, 's.id'])
Expand Down Expand Up @@ -81,6 +90,71 @@ class Sample extends BasicModel {
);
});
}

/**
* Creates copy samples from one experiment to another one
cosa65 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param {*} fromExperimentId
* @param {*} toExperimentId
*/
async copyTo(fromExperimentId, toExperimentId, samplesOrder) {
const result = await this.getSamples(fromExperimentId);
cosa65 marked this conversation as resolved.
Show resolved Hide resolved

const newSampleIds = [];

const metadataTrackKeys = Object.keys(result[0].metadata);

const sampleRows = [];
const sampleFileMapRows = [];
const metadataValueMapRows = [];

await this.sql.transaction(async (trx) => {
let metadataTracks = [];
if (metadataTrackKeys.length > 0) {
metadataTracks = await trx(tableNames.METADATA_TRACK)
.insert(metadataTrackKeys.map((key) => ({ experiment_id: toExperimentId, key })))
.returning(['id', 'key']);
}

// Copy each sample in order so
// the new samples we create follow the same order
samplesOrder.forEach((fromSampleId) => {
const sample = result.find(({ id }) => id === fromSampleId);

const toSampleId = uuidv4();

newSampleIds.push(toSampleId);

sampleRows.push({
id: toSampleId,
experiment_id: toExperimentId,
name: sample.name,
sample_technology: sample.sampleTechnology,
});

Object.entries(sample.files).forEach(([, file]) => {
sampleFileMapRows.push({
sample_id: toSampleId,
sample_file_id: file.id,
});
});

Object.entries(sample.metadata).forEach(([key, value]) => {
const { id } = _.find(metadataTracks, ({ key: currentKey }) => currentKey === key);
metadataValueMapRows.push({ metadata_track_id: id, sample_id: toSampleId, value });
});
});

await trx(tableNames.SAMPLE).insert(sampleRows);
await trx(tableNames.SAMPLE_TO_SAMPLE_FILE_MAP).insert(sampleFileMapRows);

if (metadataValueMapRows.length > 0) {
await trx(tableNames.SAMPLE_IN_METADATA_TRACK_MAP).insert(metadataValueMapRows);
}
});

return newSampleIds;
}
}

module.exports = Sample;
9 changes: 7 additions & 2 deletions src/api.v2/model/UserAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const selectableProps = [
];

const getLogger = require('../../utils/getLogger');
const constants = require('../../utils/constants');

const logger = getLogger('[UserAccessModel] - ');

Expand Down Expand Up @@ -91,11 +92,15 @@ class UserAccess extends BasicModel {
await this.grantAccess(userId, experimentId, AccessRole.OWNER);
}


async canAccessExperiment(userId, experimentId, url, method) {
const result = await this.sql(tableNames.USER_ACCESS)
.first()
// Check if user has access
.where({ experiment_id: experimentId, user_id: userId })
.from(tableNames.USER_ACCESS);
// Or if it is a public access experiment
.orWhere({ experiment_id: experimentId, user_id: constants.PUBLIC_ACCESS_ID })
.from(tableNames.USER_ACCESS)
.first();

// If there is no entry for this user and role, then user definitely doesn't have access
if (_.isNil(result)) {
Expand Down
2 changes: 2 additions & 0 deletions src/api.v2/model/__mocks__/Experiment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ const stub = {
updateSamplePosition: jest.fn(),
updateProcessingConfig: jest.fn(),
getProcessingConfig: jest.fn(),
createCopy: jest.fn(),
addSample: jest.fn(),
deleteSample: jest.fn(),
getDownloadLink: jest.fn(),
getAllExampleExperiments: jest.fn(),
...BasicModel,
};

Expand Down
1 change: 1 addition & 0 deletions src/api.v2/model/__mocks__/Sample.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const BasicModel = require('./BasicModel')();
const stub = {
getSamples: jest.fn(),
setNewFile: jest.fn(),
copyTo: jest.fn(),
...BasicModel,
};

Expand Down
13 changes: 11 additions & 2 deletions src/api.v2/routes/experiment.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const {
createExperiment, getExperiment, patchExperiment, deleteExperiment,
updateSamplePosition, getAllExperiments,
getAllExperiments, getAllExampleExperiments,
createExperiment, getExperiment, patchExperiment, deleteExperiment, cloneExperiment,
getProcessingConfig, updateProcessingConfig,
updateSamplePosition,
getBackendStatus, downloadData,
} = require('../controllers/experimentController');

Expand All @@ -12,6 +13,10 @@ module.exports = {
expressAuthenticationOnlyMiddleware,
(req, res, next) => getAllExperiments(req, res).catch(next),
],
'experiment#getAllExampleExperiments': [
expressAuthenticationOnlyMiddleware,
(req, res, next) => getAllExampleExperiments(req, res).catch(next),
],
'experiment#getExperiment': [
expressAuthorizationMiddleware,
(req, res, next) => getExperiment(req, res).catch(next),
Expand Down Expand Up @@ -48,4 +53,8 @@ module.exports = {
expressAuthorizationMiddleware,
(req, res, next) => downloadData(req, res).catch(next),
],
'experiment#clone': [
expressAuthorizationMiddleware,
(req, res, next) => cloneExperiment(req, res).catch(next),
],
};
Loading